mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +01:00 
			
		
		
		
	Convert upload to kotlin (part 3) (#6104)
* Convert UploadCategoriesFragment to kotlin * Convert UploadBaseFragment to kotlin * Convert UploadItem to kotlin * Convert UploadModel to kotlin * Convert UploadMediaDetailAdapter to kotlin * Convert UploadActivity to kotlin * Convert UploadMediaPresenter to kotlin * Convert UploadMediaDetailFragment to kotlin * Fix NPE that broke uploads
This commit is contained in:
		
							parent
							
								
									6d64357d45
								
							
						
					
					
						commit
						0e735512bb
					
				
					 37 changed files with 3236 additions and 3564 deletions
				
			
		|  | @ -52,7 +52,7 @@ class FileProcessor | |||
|          * Processes filePath coordinates, either from EXIF data or user location | ||||
|          */ | ||||
|         fun processFileCoordinates( | ||||
|             similarImageInterface: SimilarImageInterface, | ||||
|             similarImageInterface: SimilarImageInterface?, | ||||
|             filePath: String?, | ||||
|             inAppPictureLocation: LatLng?, | ||||
|         ): ImageCoordinates { | ||||
|  | @ -146,7 +146,7 @@ class FileProcessor | |||
|          */ | ||||
|         private fun findOtherImages( | ||||
|             fileBeingProcessed: File, | ||||
|             similarImageInterface: SimilarImageInterface, | ||||
|             similarImageInterface: SimilarImageInterface?, | ||||
|         ) { | ||||
|             val oneHundredAndTwentySeconds = 120 * 1000L | ||||
|             // Time when the original image was created | ||||
|  | @ -161,7 +161,7 @@ class FileProcessor | |||
|                 .map { Pair(it, readImageCoordinates(it)) } | ||||
|                 .firstOrNull { it.second?.decimalCoords != null } | ||||
|                 ?.let { fileCoordinatesPair -> | ||||
|                     similarImageInterface.showSimilarImageFragment( | ||||
|                     similarImageInterface?.showSimilarImageFragment( | ||||
|                         fileBeingProcessed.path, | ||||
|                         fileCoordinatesPair.first.absolutePath, | ||||
|                         fileCoordinatesPair.second, | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ import java.util.Locale | |||
|  */ | ||||
| class LanguagesAdapter constructor( | ||||
|     context: Context, | ||||
|     private val selectedLanguages: HashMap<*, String>, | ||||
|     private val selectedLanguages: MutableMap<Int, String>, | ||||
| ) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) { | ||||
|     companion object { | ||||
|         /** | ||||
|  |  | |||
|  | @ -1,986 +0,0 @@ | |||
| package fr.free.nrw.commons.upload; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; | ||||
| import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction; | ||||
| import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE; | ||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; | ||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE; | ||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.ProgressDialog; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.location.Location; | ||||
| import android.location.LocationManager; | ||||
| import android.os.Build; | ||||
| import android.os.Build.VERSION; | ||||
| import android.os.Build.VERSION_CODES; | ||||
| import android.os.Bundle; | ||||
| import android.provider.Settings; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.widget.CheckBox; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.fragment.app.FragmentStatePagerAdapter; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.viewpager.widget.PagerAdapter; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.auth.LoginActivity; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.contributions.ContributionController; | ||||
| import fr.free.nrw.commons.databinding.ActivityUploadBinding; | ||||
| import fr.free.nrw.commons.filepicker.Constants.RequestCodes; | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; | ||||
| 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.LocationServiceManager; | ||||
| import fr.free.nrw.commons.mwapi.UserClient; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.settings.Prefs; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.UploadBaseFragment.Callback; | ||||
| import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; | ||||
| import fr.free.nrw.commons.upload.depicts.DepictsFragment; | ||||
| import fr.free.nrw.commons.upload.license.MediaLicenseFragment; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class UploadActivity extends BaseActivity implements | ||||
|     UploadContract.View, UploadBaseFragment.Callback, ThumbnailsAdapter.OnThumbnailDeletedListener { | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionController contributionController; | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     JsonKvStore directKvStore; | ||||
|     @Inject | ||||
|     UploadContract.UserActionListener presenter; | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
|     @Inject | ||||
|     UserClient userClient; | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
| 
 | ||||
|     private boolean isTitleExpanded = true; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable; | ||||
|     private ProgressDialog progressDialog; | ||||
|     private UploadImageAdapter uploadImagesAdapter; | ||||
|     private List<UploadBaseFragment> fragments; | ||||
|     private UploadCategoriesFragment uploadCategoriesFragment; | ||||
|     private DepictsFragment depictsFragment; | ||||
|     private MediaLicenseFragment mediaLicenseFragment; | ||||
|     private ThumbnailsAdapter thumbnailsAdapter; | ||||
|     BasicKvStore store; | ||||
|     private Place place; | ||||
|     private LatLng prevLocation; | ||||
|     private LatLng currLocation; | ||||
|     private static boolean uploadIsOfAPlace = false; | ||||
|     private boolean isInAppCameraUpload; | ||||
|     private List<UploadableFile> uploadableFiles = Collections.emptyList(); | ||||
|     private int currentSelectedPosition = 0; | ||||
|     /* | ||||
|      Checks for if multiple files selected | ||||
|      */ | ||||
|     private boolean isMultipleFilesSelected = false; | ||||
| 
 | ||||
|     public static final String EXTRA_FILES = "commons_image_exta"; | ||||
|     public static final String LOCATION_BEFORE_IMAGE_CAPTURE = "user_location_before_image_capture"; | ||||
|     public static final String IN_APP_CAMERA_UPLOAD = "in_app_camera_upload"; | ||||
| 
 | ||||
|     /** | ||||
|      * Stores all nearby places found and related users response for | ||||
|      * each place while uploading media | ||||
|      */ | ||||
|     public static HashMap<Place,Boolean> nearbyPopupAnswers; | ||||
| 
 | ||||
|     /** | ||||
|      * A private boolean variable to control whether a permissions dialog should be shown | ||||
|      * when necessary. Initially, it is set to `true`, indicating that the permissions dialog | ||||
|      * should be displayed if permissions are missing and it is first time calling | ||||
|      * `checkStoragePermissions` method. | ||||
|      * This variable is used in the `checkStoragePermissions` method to determine whether to | ||||
|      * show a permissions dialog to the user if the required permissions are not granted. | ||||
|      * If `showPermissionsDialog` is set to `true` and the necessary permissions are missing, | ||||
|      * a permissions dialog will be displayed to request the required permissions. If set | ||||
|      * to `false`, the dialog won't be shown. | ||||
|      * | ||||
|      * @see UploadActivity#checkStoragePermissions() | ||||
|      */ | ||||
|     private boolean showPermissionsDialog = true; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether fragments have been saved. | ||||
|      */ | ||||
|     private boolean isFragmentsSaved = false; | ||||
|      | ||||
|     public static final String keyForCurrentUploadImagesSize = "CurrentUploadImagesSize"; | ||||
|     public static final String storeNameForCurrentUploadImagesSize = "CurrentUploadImageQualities"; | ||||
| 
 | ||||
|     private ActivityUploadBinding binding; | ||||
| 
 | ||||
|     @SuppressLint("CheckResult") | ||||
|     @Override | ||||
|     protected void onCreate(final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
| 
 | ||||
|         binding = ActivityUploadBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
| 
 | ||||
|         /* | ||||
|          If Configuration of device is changed then get the new fragments | ||||
|          created by the system and populate the fragments ArrayList | ||||
|          */ | ||||
|         if (savedInstanceState != null) { | ||||
|             isFragmentsSaved = true; | ||||
|             final List<Fragment> fragmentList = getSupportFragmentManager().getFragments(); | ||||
|             fragments = new ArrayList<>(); | ||||
|             for (final Fragment fragment : fragmentList) { | ||||
|                 fragments.add((UploadBaseFragment) fragment); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|         init(); | ||||
|         binding.rlContainerTitle.setOnClickListener(v -> onRlContainerTitleClicked()); | ||||
|         nearbyPopupAnswers = new HashMap<>(); | ||||
|         //getting the current dpi of the device and if it is less than 320dp i.e. overlapping | ||||
|         //threshold, thumbnails automatically minimizes | ||||
|         final DisplayMetrics metrics = getResources().getDisplayMetrics(); | ||||
|         final float dpi = (metrics.widthPixels)/(metrics.density); | ||||
|         if (dpi<=321) { | ||||
|             onRlContainerTitleClicked(); | ||||
|         } | ||||
|         if (PermissionUtils.hasPermission(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||
|             locationManager.registerLocationManager(); | ||||
|         } | ||||
|         locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); | ||||
|         locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); | ||||
|         store = new BasicKvStore(this, storeNameForCurrentUploadImagesSize); | ||||
|         store.clearAll(); | ||||
|         checkStoragePermissions(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void init() { | ||||
|         initProgressDialog(); | ||||
|         initViewPager(); | ||||
|         initThumbnailsRecyclerView(); | ||||
|         //And init other things you need to | ||||
|     } | ||||
| 
 | ||||
|     private void initProgressDialog() { | ||||
|         progressDialog = new ProgressDialog(this); | ||||
|         progressDialog.setMessage(getString(R.string.please_wait)); | ||||
|         progressDialog.setCancelable(false); | ||||
|     } | ||||
| 
 | ||||
|     private void initThumbnailsRecyclerView() { | ||||
|         binding.rvThumbnails.setLayoutManager(new LinearLayoutManager(this, | ||||
|             LinearLayoutManager.HORIZONTAL, false)); | ||||
|         thumbnailsAdapter = new ThumbnailsAdapter(() -> currentSelectedPosition); | ||||
|         thumbnailsAdapter.setOnThumbnailDeletedListener(this); | ||||
|         binding.rvThumbnails.setAdapter(thumbnailsAdapter); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void initViewPager() { | ||||
|         uploadImagesAdapter = new UploadImageAdapter(getSupportFragmentManager()); | ||||
|         binding.vpUpload.setAdapter(uploadImagesAdapter); | ||||
|         binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { | ||||
|             @Override | ||||
|             public void onPageScrolled(final int position, final float positionOffset, | ||||
|                 final int positionOffsetPixels) { | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onPageSelected(final int position) { | ||||
|                 currentSelectedPosition = position; | ||||
|                 if (position >= uploadableFiles.size()) { | ||||
|                     binding.cvContainerTopCard.setVisibility(View.GONE); | ||||
|                 } else { | ||||
|                     thumbnailsAdapter.notifyDataSetChanged(); | ||||
|                     binding.cvContainerTopCard.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onPageScrollStateChanged(final int state) { | ||||
| 
 | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isLoggedIn() { | ||||
|         return sessionManager.isUserLoggedIn(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onResume() { | ||||
|         super.onResume(); | ||||
|         presenter.onAttachView(this); | ||||
|         if (!isLoggedIn()) { | ||||
|             askUserToLogIn(); | ||||
|         } | ||||
|         checkBlockStatus(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar | ||||
|      * is created to notify the user | ||||
|      */ | ||||
|     protected void checkBlockStatus() { | ||||
|         compositeDisposable.add(userClient.isUserBlockedFromCommons() | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .filter(result -> result) | ||||
|             .subscribe(result -> DialogUtil.showAlertDialog( | ||||
|                 this, | ||||
|                 getString(R.string.block_notification_title), | ||||
|                 getString(R.string.block_notification), | ||||
|                 getString(R.string.ok), | ||||
|                 this::finish))); | ||||
|     } | ||||
| 
 | ||||
|     public void checkStoragePermissions() { | ||||
|         // Check if all required permissions are granted | ||||
|         final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE()); | ||||
|         final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this); | ||||
|         if (hasAllPermissions || hasPartialAccess) { | ||||
|             // All required permissions are granted, so enable UI elements and perform actions | ||||
|             receiveSharedItems(); | ||||
|             binding.cvContainerTopCard.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|             // Permissions are missing | ||||
|             binding.cvContainerTopCard.setVisibility(View.INVISIBLE); | ||||
|             if(showPermissionsDialog){ | ||||
|                 checkPermissionsAndPerformAction(this, | ||||
|                     () -> { | ||||
|                         binding.cvContainerTopCard.setVisibility(View.VISIBLE); | ||||
|                         this.receiveSharedItems(); | ||||
|                     },() -> { | ||||
|                         this.showPermissionsDialog = true; | ||||
|                         this.checkStoragePermissions(); | ||||
|                         }, | ||||
|                     R.string.storage_permission_title, | ||||
|                     R.string.write_storage_permission_rationale_for_image_share, | ||||
|                     getPERMISSIONS_STORAGE()); | ||||
|             } | ||||
|         } | ||||
|         /* If all permissions are not granted and a dialog is already showing on screen | ||||
|          showPermissionsDialog will set to false making it not show dialog again onResume, | ||||
|          but if user Denies any permission showPermissionsDialog will be to true | ||||
|          and permissions dialog will be shown again. | ||||
|          */ | ||||
|         this.showPermissionsDialog = hasAllPermissions ; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onStop() { | ||||
|         // Resetting setImageCancelled to false | ||||
|         setImageCancelled(false); | ||||
|         super.onStop(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void returnToMainActivity() { | ||||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * go to the uploadProgress activity to check the status of uploading | ||||
|      */ | ||||
|     @Override | ||||
|     public void goToUploadProgressActivity() { | ||||
|         startActivity(new Intent(this, UploadProgressActivity.class)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show/Hide the progress dialog | ||||
|      */ | ||||
|     @Override | ||||
|     public void showProgress(final boolean shouldShow) { | ||||
|         if (shouldShow) { | ||||
|             if (!progressDialog.isShowing()) { | ||||
|                 progressDialog.show(); | ||||
|             } | ||||
|         } else { | ||||
|             if (progressDialog != null && !isFinishing()) { | ||||
|                 progressDialog.dismiss(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getIndexInViewFlipper(final UploadBaseFragment fragment) { | ||||
|         return fragments.indexOf(fragment); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getTotalNumberOfSteps() { | ||||
|         return fragments.size(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isWLMUpload() { | ||||
|         return place!=null && place.isMonument(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showMessage(final int messageResourceId) { | ||||
|         ViewUtil.showLongToast(this, messageResourceId); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public List<UploadableFile> getUploadableFiles() { | ||||
|         return uploadableFiles; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showHideTopCard(final boolean shouldShow) { | ||||
|         binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onUploadMediaDeleted(final int index) { | ||||
|         fragments.remove(index);//Remove the corresponding fragment | ||||
|         uploadableFiles.remove(index);//Remove the files from the list | ||||
|         thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter | ||||
|         uploadImagesAdapter.notifyDataSetChanged(); //Notify the ViewPager | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateTopCardTitle() { | ||||
|         binding.tvTopCardTitle.setText(getResources() | ||||
|             .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void makeUploadRequest() { | ||||
|         WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), | ||||
|             ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void askUserToLogIn() { | ||||
|         Timber.d("current session is null, asking user to login"); | ||||
|         ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); | ||||
|         final Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); | ||||
|         startActivity(loginIntent); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(final int requestCode, | ||||
|         @NonNull final String[] permissions, | ||||
|         @NonNull final int[] grantResults) { | ||||
|         boolean areAllGranted = false; | ||||
|         if (requestCode == RequestCodes.STORAGE) { | ||||
|             if (VERSION.SDK_INT >= VERSION_CODES.M) { | ||||
|                 for (int i = 0; i < grantResults.length; i++) { | ||||
|                     final String permission = permissions[i]; | ||||
|                     areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED; | ||||
|                     if (grantResults[i] == PackageManager.PERMISSION_DENIED) { | ||||
|                         final boolean showRationale = shouldShowRequestPermissionRationale(permission); | ||||
|                         if (!showRationale) { | ||||
|                             DialogUtil.showAlertDialog(this, | ||||
|                                 getString(R.string.storage_permissions_denied), | ||||
|                                 getString(R.string.unable_to_share_upload_item), | ||||
|                                 getString(android.R.string.ok), | ||||
|                                 this::finish); | ||||
|                         } else { | ||||
|                             DialogUtil.showAlertDialog(this, | ||||
|                                 getString(R.string.storage_permission_title), | ||||
|                                 getString( | ||||
|                                     R.string.write_storage_permission_rationale_for_image_share), | ||||
|                                 getString(android.R.string.ok), | ||||
|                                 this::checkStoragePermissions); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (areAllGranted) { | ||||
|                     receiveSharedItems(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the flag indicating whether the upload is of a specific place. | ||||
|      * | ||||
|      * @param uploadOfAPlace a boolean value indicating whether the upload is of place. | ||||
|      */ | ||||
|     public static void setUploadIsOfAPlace(final boolean uploadOfAPlace) { | ||||
|         uploadIsOfAPlace = uploadOfAPlace; | ||||
|     } | ||||
| 
 | ||||
|     private void receiveSharedItems() { | ||||
|         final Intent intent = getIntent(); | ||||
|         final String action = intent.getAction(); | ||||
|         if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { | ||||
|             receiveExternalSharedItems(); | ||||
|         } else if (ACTION_INTERNAL_UPLOADS.equals(action)) { | ||||
|             receiveInternalSharedItems(); | ||||
|         } | ||||
| 
 | ||||
|         if (uploadableFiles == null || uploadableFiles.isEmpty()) { | ||||
|             handleNullMedia(); | ||||
|         } else { | ||||
|             //Show thumbnails | ||||
|             if (uploadableFiles.size() > 1){ | ||||
|                 if(!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")){//If there is only file, no need to show the image thumbnails | ||||
|                     showAlertDialogForCategories(); | ||||
|                 } | ||||
|                 if (uploadableFiles.size() > 3 && | ||||
|                     !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")){ | ||||
|                     showAlertForBattery(); | ||||
|                 } | ||||
|                 thumbnailsAdapter.setUploadableFiles(uploadableFiles); | ||||
|             } else { | ||||
|                 binding.llContainerTopCard.setVisibility(View.GONE); | ||||
|             } | ||||
|             binding.tvTopCardTitle.setText(getResources() | ||||
|                 .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); | ||||
| 
 | ||||
| 
 | ||||
|             if(fragments == null){ | ||||
|                 fragments = new ArrayList<>(); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             for (final UploadableFile uploadableFile : uploadableFiles) { | ||||
|                 final UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); | ||||
| 
 | ||||
|                 if (!uploadIsOfAPlace) { | ||||
|                     handleLocation(); | ||||
|                     uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); | ||||
|                     locationManager.unregisterLocationManager(); | ||||
|                 } else { | ||||
|                     uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); | ||||
|                 } | ||||
| 
 | ||||
|                 final UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() { | ||||
|                     @Override | ||||
|                     public void deletePictureAtIndex(final int index) { | ||||
|                         store.putInt(keyForCurrentUploadImagesSize, | ||||
|                             (store.getInt(keyForCurrentUploadImagesSize) - 1)); | ||||
|                         presenter.deletePictureAtIndex(index); | ||||
|                     } | ||||
| 
 | ||||
|                     /** | ||||
|                      * Changes the thumbnail of an UploadableFile at the specified index. | ||||
|                      * This method updates the list of uploadableFiles by replacing the UploadableFile | ||||
|                      * at the given index with a new UploadableFile created from the provided file path. | ||||
|                      * After updating the list, it notifies the RecyclerView's adapter to refresh its data, | ||||
|                      * ensuring that the thumbnail change is reflected in the UI. | ||||
|                      * | ||||
|                      * @param index The index of the UploadableFile to be updated. | ||||
|                      * @param filepath The file path of the new thumbnail image. | ||||
|                      */ | ||||
|                     @Override | ||||
|                     public void changeThumbnail(final int index, final String filepath) { | ||||
|                         uploadableFiles.remove(index); | ||||
|                         uploadableFiles.add(index, new UploadableFile(new File(filepath))); | ||||
|                         binding.rvThumbnails.getAdapter().notifyDataSetChanged(); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onNextButtonClicked(final int index) { | ||||
|                         UploadActivity.this.onNextButtonClicked(index); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onPreviousButtonClicked(final int index) { | ||||
|                         UploadActivity.this.onPreviousButtonClicked(index); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void showProgress(final boolean shouldShow) { | ||||
|                         UploadActivity.this.showProgress(shouldShow); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public int getIndexInViewFlipper(final UploadBaseFragment fragment) { | ||||
|                         return fragments.indexOf(fragment); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public int getTotalNumberOfSteps() { | ||||
|                         return fragments.size(); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public boolean isWLMUpload() { | ||||
|                         return place!=null && place.isMonument(); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 if(isFragmentsSaved){ | ||||
|                     final UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0); | ||||
|                     fragment.setCallback(uploadMediaDetailFragmentCallback); | ||||
|                 }else{ | ||||
|                     uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback); | ||||
|                     fragments.add(uploadMediaDetailFragment); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             //If fragments are not created, create them and add them to the fragments ArrayList | ||||
|             if(!isFragmentsSaved){ | ||||
|                 uploadCategoriesFragment = new UploadCategoriesFragment(); | ||||
|                 if (place != null) { | ||||
|                     final Bundle categoryBundle = new Bundle(); | ||||
|                     categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory()); | ||||
|                     uploadCategoriesFragment.setArguments(categoryBundle); | ||||
|                 } | ||||
| 
 | ||||
|                 uploadCategoriesFragment.setCallback(this); | ||||
| 
 | ||||
|                 depictsFragment = new DepictsFragment(); | ||||
|                 final Bundle placeBundle = new Bundle(); | ||||
|                 placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place); | ||||
|                 depictsFragment.setArguments(placeBundle); | ||||
|                 depictsFragment.setCallback(this); | ||||
| 
 | ||||
|                 mediaLicenseFragment = new MediaLicenseFragment(); | ||||
|                 mediaLicenseFragment.setCallback(this); | ||||
| 
 | ||||
|                 fragments.add(depictsFragment); | ||||
|                 fragments.add(uploadCategoriesFragment); | ||||
|                 fragments.add(mediaLicenseFragment); | ||||
| 
 | ||||
|             }else{ | ||||
|                 for(int i=1;i<fragments.size();i++){ | ||||
|                     fragments.get(i).setCallback(new Callback() { | ||||
|                         @Override | ||||
|                         public void onNextButtonClicked(final int index) { | ||||
|                             if (index < fragments.size() - 1) { | ||||
|                                 binding.vpUpload.setCurrentItem(index + 1, false); | ||||
|                                 fragments.get(index + 1).onBecameVisible(); | ||||
|                                 ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) | ||||
|                                     .scrollToPositionWithOffset((index > 0) ? index-1 : 0, 0); | ||||
|                             } else { | ||||
|                                 presenter.handleSubmit(); | ||||
|                             } | ||||
| 
 | ||||
|                         } | ||||
|                         @Override | ||||
|                         public void onPreviousButtonClicked(final int index) { | ||||
|                             if (index != 0) { | ||||
|                                 binding.vpUpload.setCurrentItem(index - 1, true); | ||||
|                                 fragments.get(index - 1).onBecameVisible(); | ||||
|                                 ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) | ||||
|                                     .scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0); | ||||
|                             } | ||||
|                         } | ||||
|                         @Override | ||||
|                         public void showProgress(final boolean shouldShow) { | ||||
|                             if (shouldShow) { | ||||
|                                 if (!progressDialog.isShowing()) { | ||||
|                                     progressDialog.show(); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (progressDialog != null && !isFinishing()) { | ||||
|                                     progressDialog.dismiss(); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         @Override | ||||
|                         public int getIndexInViewFlipper(final UploadBaseFragment fragment) { | ||||
|                             return fragments.indexOf(fragment); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public int getTotalNumberOfSteps() { | ||||
|                             return fragments.size(); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public boolean isWLMUpload() { | ||||
|                             return place!=null && place.isMonument(); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             uploadImagesAdapter.setFragments(fragments); | ||||
|             binding.vpUpload.setOffscreenPageLimit(fragments.size()); | ||||
| 
 | ||||
|         } | ||||
|         // Saving size of uploadableFiles | ||||
|         store.putInt(keyForCurrentUploadImagesSize, uploadableFiles.size()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Users may uncheck Location tag from the Manage EXIF tags setting any time. | ||||
|      * So, their location must not be shared in this case. | ||||
|      * | ||||
|      */ | ||||
|     private boolean isLocationTagUncheckedInTheSettings() { | ||||
|         final Set<String> prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS); | ||||
|         if (prefExifTags.contains(getString(R.string.exif_tag_location))) { | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. | ||||
|      * Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a> | ||||
|      * | ||||
|      * @param index Index of image to be removed | ||||
|      * @param maxSize Max size of the {@code uploadableFiles} | ||||
|      */ | ||||
|     @Override | ||||
|     public void highlightNextImageOnCancelledImage(final int index, final int maxSize) { | ||||
|         if (binding.vpUpload != null && index < (maxSize)) { | ||||
|             binding.vpUpload.setCurrentItem(index + 1, false); | ||||
|             binding.vpUpload.setCurrentItem(index, false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to check if user has cancelled upload of any image in current upload | ||||
|      * so that location compare doesn't show up again in same upload. | ||||
|      * Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a> | ||||
|      * | ||||
|      * @param isCancelled Is true when user has cancelled upload of any image in current upload | ||||
|      */ | ||||
|     @Override | ||||
|     public void setImageCancelled(final boolean isCancelled) { | ||||
|         final BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled"); | ||||
|         basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculate the difference between current location and | ||||
|      * location recorded before capturing the image | ||||
|      * | ||||
|      */ | ||||
|     private float getLocationDifference(final LatLng currLocation, final LatLng prevLocation) { | ||||
|         if (prevLocation == null) { | ||||
|             return 0.0f; | ||||
|         } | ||||
|         final float[] distance = new float[2]; | ||||
|         Location.distanceBetween( | ||||
|             currLocation.getLatitude(), currLocation.getLongitude(), | ||||
|             prevLocation.getLatitude(), prevLocation.getLongitude(), distance); | ||||
|         return distance[0]; | ||||
|     } | ||||
| 
 | ||||
|     private void receiveExternalSharedItems() { | ||||
|         uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); | ||||
|     } | ||||
| 
 | ||||
|     private void receiveInternalSharedItems() { | ||||
|         final Intent intent = getIntent(); | ||||
| 
 | ||||
|         Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction()); | ||||
| 
 | ||||
|         uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); | ||||
|         isMultipleFilesSelected = uploadableFiles.size() > 1; | ||||
|         Timber.i("Received multiple upload %s", uploadableFiles.size()); | ||||
| 
 | ||||
|         place = intent.getParcelableExtra(PLACE_OBJECT); | ||||
|         prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE); | ||||
|         isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false); | ||||
|         resetDirectPrefs(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns if multiple files selected or not. | ||||
|      */ | ||||
|     public boolean getIsMultipleFilesSelected() { | ||||
|         return isMultipleFilesSelected; | ||||
|     } | ||||
| 
 | ||||
|     public void resetDirectPrefs() { | ||||
|         directKvStore.remove(PLACE_OBJECT); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle null URI from the received intent. | ||||
|      * Current implementation will simply show a toast and finish the upload activity. | ||||
|      */ | ||||
|     private void handleNullMedia() { | ||||
|         ViewUtil.showLongToast(this, R.string.error_processing_image); | ||||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void showAlertDialog(final int messageResourceId, @NonNull final Runnable onPositiveClick) { | ||||
|         DialogUtil.showAlertDialog(this, | ||||
|             "", | ||||
|             getString(messageResourceId), | ||||
|             getString(R.string.ok), | ||||
|             onPositiveClick); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onNextButtonClicked(final int index) { | ||||
|         if (index < fragments.size() - 1) { | ||||
|             binding.vpUpload.setCurrentItem(index + 1, false); | ||||
|             fragments.get(index + 1).onBecameVisible(); | ||||
|             ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) | ||||
|                 .scrollToPositionWithOffset((index > 0) ? index - 1 : 0, 0); | ||||
|             if (index < fragments.size() - 4) { | ||||
|                 // check image quality if next image exists | ||||
|                 presenter.checkImageQuality(index + 1); | ||||
|             } | ||||
|         } else { | ||||
|             presenter.handleSubmit(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPreviousButtonClicked(final int index) { | ||||
|         if (index != 0) { | ||||
|             binding.vpUpload.setCurrentItem(index - 1, true); | ||||
|             fragments.get(index - 1).onBecameVisible(); | ||||
|             ((LinearLayoutManager) binding.rvThumbnails.getLayoutManager()) | ||||
|                 .scrollToPositionWithOffset((index > 3) ? index-2 : 0, 0); | ||||
|             if ((index != 1) && ((index - 1) < uploadableFiles.size())) { | ||||
|                 // Shows the top card if it was hidden because of the last image being deleted and | ||||
|                 // now the user has hit previous button to go back to the media details | ||||
|                 showHideTopCard(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onThumbnailDeleted(final int position) { | ||||
|         presenter.deletePictureAtIndex(position); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The adapter used to show image upload intermediate fragments | ||||
|      */ | ||||
| 
 | ||||
| 
 | ||||
|     private static class UploadImageAdapter extends FragmentStatePagerAdapter { | ||||
|         List<UploadBaseFragment> fragments; | ||||
| 
 | ||||
|         public UploadImageAdapter(final FragmentManager fragmentManager) { | ||||
|             super(fragmentManager); | ||||
|             this.fragments = new ArrayList<>(); | ||||
|         } | ||||
| 
 | ||||
|         public void setFragments(final List<UploadBaseFragment> fragments) { | ||||
|             this.fragments = fragments; | ||||
|             notifyDataSetChanged(); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public Fragment getItem(final int position) { | ||||
|             return fragments.get(position); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getCount() { | ||||
|             return fragments.size(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getItemPosition(@NonNull final Object item) { | ||||
|             return PagerAdapter.POSITION_NONE; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     public void onRlContainerTitleClicked() { | ||||
|         binding.rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE); | ||||
|         isTitleExpanded = !isTitleExpanded; | ||||
|         binding.ibToggleTopCard.setRotation(binding.ibToggleTopCard.getRotation() + 180); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         // Resetting all values in store by clearing them | ||||
|         store.clearAll(); | ||||
|         presenter.onDetachView(); | ||||
|         compositeDisposable.clear(); | ||||
|         fragments = null; | ||||
|         uploadImagesAdapter = null; | ||||
|         if (mediaLicenseFragment != null) { | ||||
|             mediaLicenseFragment.setCallback(null); | ||||
|         } | ||||
|         if (uploadCategoriesFragment != null) { | ||||
|             uploadCategoriesFragment.setCallback(null); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the value of the showPermissionDialog variable. | ||||
|      * | ||||
|      * @return {@code true} if Permission Dialog should be shown, {@code false} otherwise. | ||||
|      */ | ||||
|     public boolean isShowPermissionsDialog() { | ||||
|         return showPermissionsDialog; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the value of the showPermissionDialog variable. | ||||
|      * | ||||
|      * @param showPermissionsDialog {@code true} to indicate to show | ||||
|      * Permissions Dialog if permissions are missing, {@code false} otherwise. | ||||
|      */ | ||||
|     public void setShowPermissionsDialog(final boolean showPermissionsDialog) { | ||||
|         this.showPermissionsDialog = showPermissionsDialog; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Overrides the back button to make sure the user is prepared to lose their progress | ||||
|      */ | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         DialogUtil.showAlertDialog(this, | ||||
|             getString(R.string.back_button_warning), | ||||
|             getString(R.string.back_button_warning_desc), | ||||
|             getString(R.string.back_button_continue), | ||||
|             getString(R.string.back_button_warning), | ||||
|             null, | ||||
|             this::finish | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If the user uploads more than 1 file informs that | ||||
|      * depictions/categories apply to all pictures of a multi upload. | ||||
|      * This method takes no arguments and does not return any value. | ||||
|      * It shows the AlertDialog and continues the flow of uploads. | ||||
|      */ | ||||
|     private void showAlertDialogForCategories() { | ||||
|         UploadMediaPresenter.isCategoriesDialogShowing = true; | ||||
|         // Inflate the custom layout | ||||
|         final LayoutInflater inflater = getLayoutInflater(); | ||||
|         final View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null); | ||||
|         final CheckBox checkBox = view.findViewById(R.id.categories_checkbox); | ||||
|         // Create the alert dialog | ||||
|         final AlertDialog alertDialog = new AlertDialog.Builder(this) | ||||
|             .setView(view) | ||||
|             .setTitle(getString(R.string.multiple_files_depiction_header)) | ||||
|             .setMessage(getString(R.string.multiple_files_depiction)) | ||||
|             .setCancelable(false) | ||||
|             .setPositiveButton("OK", (dialog, which) -> { | ||||
|                 if (checkBox.isChecked()) { | ||||
|                     // Save the user's choice to not show the dialog again | ||||
|                     defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true); | ||||
|                 } | ||||
|                 presenter.checkImageQuality(0); | ||||
| 
 | ||||
|                 UploadMediaPresenter.isCategoriesDialogShowing = false; | ||||
|             }) | ||||
|             .setNegativeButton("", null) | ||||
|             .create(); | ||||
|         alertDialog.show(); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     /** Suggest users to turn battery optimisation off when uploading | ||||
|      * more than a few files. That's because we have noticed that | ||||
|      * many-files uploads have a much higher probability of failing | ||||
|      * than uploads with less files. Show the dialog for Android 6 | ||||
|      * and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS | ||||
|      * intent was added in API level 23 | ||||
|      */ | ||||
|     private void showAlertForBattery(){ | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|                 // When battery-optimisation dialog is shown don't show the image quality dialog | ||||
|                 UploadMediaPresenter.isBatteryDialogShowing = true; | ||||
|                 DialogUtil.showAlertDialog( | ||||
|                     this, | ||||
|                     getString(R.string.unrestricted_battery_mode), | ||||
|                     getString(R.string.suggest_unrestricted_mode), | ||||
|                     getString(R.string.title_activity_settings), | ||||
|                     getString(R.string.cancel), | ||||
|                     () -> { | ||||
|                         /* Since opening the right settings page might be device dependent, using | ||||
|                            https://github.com/WaseemSabir/BatteryPermissionHelper | ||||
|                            directly appeared like a promising idea. | ||||
|                            However, this simply closed the popup and did not make | ||||
|                            the settings page appear on a Pixel as well as a Xiaomi device. | ||||
|                            Used the standard intent instead of using this library as | ||||
|                            it shows a list of all the apps on the device and allows users to | ||||
|                            turn battery optimisation off. | ||||
|                          */ | ||||
|                         final Intent batteryOptimisationSettingsIntent = new Intent( | ||||
|                             Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); | ||||
|                         startActivity(batteryOptimisationSettingsIntent); | ||||
|                         // calling checkImageQuality after battery dialog is interacted with | ||||
|                         // so that 2 dialogs do not pop up simultaneously | ||||
| 
 | ||||
|                         UploadMediaPresenter.isBatteryDialogShowing = false; | ||||
|                     }, | ||||
|                     () -> { | ||||
|                         UploadMediaPresenter.isBatteryDialogShowing = false; | ||||
|                     } | ||||
|                 ); | ||||
|                 defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     /** | ||||
|      * If the permission for Location is turned on and certain | ||||
|      * conditions are met, returns current location of the user. | ||||
|      */ | ||||
|     private void handleLocation(){ | ||||
|         final LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( | ||||
|             this, locationManager, null); | ||||
|         if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||
|             currLocation = locationManager.getLastLocation(); | ||||
|         } | ||||
| 
 | ||||
|         if (currLocation != null) { | ||||
|             final float locationDifference = getLocationDifference(currLocation, prevLocation); | ||||
|             final boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); | ||||
|                     /* Remove location if the user has unchecked the Location EXIF tag in the | ||||
|                        Manage EXIF Tags setting or turned "Record location for in-app shots" off. | ||||
|                        Also, location information is discarded if the difference between | ||||
|                        current location and location recorded just before capturing the image | ||||
|                        is greater than 100 meters */ | ||||
|             if (isLocationTagUnchecked || locationDifference > 100 | ||||
|                 || !defaultKvStore.getBoolean("inAppCameraLocationPref") | ||||
|                 || !isInAppCameraUpload) { | ||||
|                 currLocation = null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										947
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										947
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,947 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.ProgressDialog | ||||
| import android.content.DialogInterface | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.location.Location | ||||
| import android.location.LocationManager | ||||
| import android.os.Build.VERSION | ||||
| import android.os.Build.VERSION_CODES | ||||
| import android.os.Bundle | ||||
| import android.provider.Settings | ||||
| import android.view.View | ||||
| import android.widget.CheckBox | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentManager | ||||
| import androidx.fragment.app.FragmentStatePagerAdapter | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.viewpager.widget.ViewPager.OnPageChangeListener | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.auth.LoginActivity | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.contributions.ContributionController | ||||
| import fr.free.nrw.commons.databinding.ActivityUploadBinding | ||||
| import fr.free.nrw.commons.filepicker.Constants.RequestCodes | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| 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.LocationServiceManager | ||||
| import fr.free.nrw.commons.mwapi.UserClient | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.settings.Prefs | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import fr.free.nrw.commons.upload.ThumbnailsAdapter.OnThumbnailDeletedListener | ||||
| import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment | ||||
| import fr.free.nrw.commons.upload.depicts.DepictsFragment | ||||
| import fr.free.nrw.commons.upload.license.MediaLicenseFragment | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | ||||
| 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.PermissionUtils.hasPartialAccess | ||||
| import fr.free.nrw.commons.utils.PermissionUtils.hasPermission | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.Callback, | ||||
|     OnThumbnailDeletedListener { | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var contributionController: ContributionController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     @field:Named("default_preferences") | ||||
|     var directKvStore: JsonKvStore? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var presenter: UploadContract.UserActionListener? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var sessionManager: SessionManager? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var userClient: UserClient? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var locationManager: LocationServiceManager? = null | ||||
| 
 | ||||
|     private var isTitleExpanded = true | ||||
| 
 | ||||
|     private var progressDialog: ProgressDialog? = null | ||||
|     private var uploadImagesAdapter: UploadImageAdapter? = null | ||||
|     private var fragments: MutableList<UploadBaseFragment>? = null | ||||
|     private var uploadCategoriesFragment: UploadCategoriesFragment? = null | ||||
|     private var depictsFragment: DepictsFragment? = null | ||||
|     private var mediaLicenseFragment: MediaLicenseFragment? = null | ||||
|     private var thumbnailsAdapter: ThumbnailsAdapter? = null | ||||
|     var store: BasicKvStore? = null | ||||
|     private var place: Place? = null | ||||
|     private var prevLocation: LatLng? = null | ||||
|     private var currLocation: LatLng? = null | ||||
|     private var isInAppCameraUpload = false | ||||
|     private var uploadableFiles: MutableList<UploadableFile> = mutableListOf() | ||||
|     private var currentSelectedPosition = 0 | ||||
| 
 | ||||
|     /** | ||||
|      * Returns if multiple files selected or not. | ||||
|      */ | ||||
|     /* | ||||
|          Checks for if multiple files selected | ||||
|          */ | ||||
|     var isMultipleFilesSelected: Boolean = false | ||||
|         private set | ||||
| 
 | ||||
|     /** | ||||
|      * Get the value of the showPermissionDialog variable. | ||||
|      * | ||||
|      * @return `true` if Permission Dialog should be shown, `false` otherwise. | ||||
|      */ | ||||
|     /** | ||||
|      * Set the value of the showPermissionDialog variable. | ||||
|      * | ||||
|      * @param showPermissionsDialog `true` to indicate to show | ||||
|      * Permissions Dialog if permissions are missing, `false` otherwise. | ||||
|      */ | ||||
|     /** | ||||
|      * A private boolean variable to control whether a permissions dialog should be shown | ||||
|      * when necessary. Initially, it is set to `true`, indicating that the permissions dialog | ||||
|      * should be displayed if permissions are missing and it is first time calling | ||||
|      * `checkStoragePermissions` method. | ||||
|      * This variable is used in the `checkStoragePermissions` method to determine whether to | ||||
|      * show a permissions dialog to the user if the required permissions are not granted. | ||||
|      * If `showPermissionsDialog` is set to `true` and the necessary permissions are missing, | ||||
|      * a permissions dialog will be displayed to request the required permissions. If set | ||||
|      * to `false`, the dialog won't be shown. | ||||
|      * | ||||
|      * @see UploadActivity.checkStoragePermissions | ||||
|      */ | ||||
|     var isShowPermissionsDialog: Boolean = true | ||||
| 
 | ||||
|     /** | ||||
|      * Whether fragments have been saved. | ||||
|      */ | ||||
|     private var isFragmentsSaved = false | ||||
| 
 | ||||
|     override val totalNumberOfSteps: Int | ||||
|         get() = fragments!!.size | ||||
| 
 | ||||
|     override val isWLMUpload: Boolean | ||||
|         get() = place != null && place!!.isMonument | ||||
| 
 | ||||
|     /** | ||||
|      * Users may uncheck Location tag from the Manage EXIF tags setting any time. | ||||
|      * So, their location must not be shared in this case. | ||||
|      * | ||||
|      */ | ||||
|     private val isLocationTagUncheckedInTheSettings: Boolean | ||||
|         get() { | ||||
|             val prefExifTags: Set<String> = | ||||
|                 defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) | ||||
|             return !prefExifTags.contains(getString(R.string.exif_tag_location)) | ||||
|         } | ||||
| 
 | ||||
|     private var _binding: ActivityUploadBinding? = null | ||||
|     private val binding: ActivityUploadBinding get() = _binding!! | ||||
| 
 | ||||
|     @SuppressLint("CheckResult") | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| 
 | ||||
|         _binding = ActivityUploadBinding.inflate(layoutInflater) | ||||
|         setContentView(binding.root) | ||||
| 
 | ||||
|         /* | ||||
|          If Configuration of device is changed then get the new fragments | ||||
|          created by the system and populate the fragments ArrayList | ||||
|          */ | ||||
|         if (savedInstanceState != null) { | ||||
|             isFragmentsSaved = true | ||||
|             fragments = mutableListOf<UploadBaseFragment>().apply { | ||||
|                 supportFragmentManager.fragments.forEach { fragment -> | ||||
|                     add(fragment as UploadBaseFragment) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         init() | ||||
|         binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() } | ||||
|         nearbyPopupAnswers = mutableMapOf() | ||||
|         //getting the current dpi of the device and if it is less than 320dp i.e. overlapping | ||||
|         //threshold, thumbnails automatically minimizes | ||||
|         val metrics = resources.displayMetrics | ||||
|         val dpi = (metrics.widthPixels) / (metrics.density) | ||||
|         if (dpi <= 321) { | ||||
|             onRlContainerTitleClicked() | ||||
|         } | ||||
|         if (hasPermission(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION))) { | ||||
|             locationManager!!.registerLocationManager() | ||||
|         } | ||||
|         locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) | ||||
|         locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) | ||||
|         store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply { | ||||
|             clearAll() | ||||
|         } | ||||
|         checkStoragePermissions() | ||||
|     } | ||||
| 
 | ||||
|     private fun init() { | ||||
|         initProgressDialog() | ||||
|         initViewPager() | ||||
|         initThumbnailsRecyclerView() | ||||
|         //And init other things you need to | ||||
|     } | ||||
| 
 | ||||
|     private fun initProgressDialog() { | ||||
|         progressDialog = ProgressDialog(this) | ||||
|         progressDialog!!.setMessage(getString(R.string.please_wait)) | ||||
|         progressDialog!!.setCancelable(false) | ||||
|     } | ||||
| 
 | ||||
|     private fun initThumbnailsRecyclerView() { | ||||
|         binding.rvThumbnails.layoutManager = LinearLayoutManager( | ||||
|             this, | ||||
|             LinearLayoutManager.HORIZONTAL, false | ||||
|         ) | ||||
|         thumbnailsAdapter = ThumbnailsAdapter { currentSelectedPosition } | ||||
|         thumbnailsAdapter!!.onThumbnailDeletedListener = this | ||||
|         binding.rvThumbnails.adapter = thumbnailsAdapter | ||||
|     } | ||||
| 
 | ||||
|     private fun initViewPager() { | ||||
|         uploadImagesAdapter = UploadImageAdapter(supportFragmentManager) | ||||
|         binding.vpUpload.adapter = uploadImagesAdapter | ||||
|         binding.vpUpload.addOnPageChangeListener(object : OnPageChangeListener { | ||||
|             override fun onPageScrolled( | ||||
|                 position: Int, positionOffset: Float, | ||||
|                 positionOffsetPixels: Int | ||||
|             ) = Unit | ||||
| 
 | ||||
|             override fun onPageSelected(position: Int) { | ||||
|                 currentSelectedPosition = position | ||||
|                 if (position >= uploadableFiles!!.size) { | ||||
|                     binding.cvContainerTopCard.visibility = View.GONE | ||||
|                 } else { | ||||
|                     thumbnailsAdapter!!.notifyDataSetChanged() | ||||
|                     binding.cvContainerTopCard.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun onPageScrollStateChanged(state: Int) = Unit | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     override fun isLoggedIn(): Boolean = sessionManager!!.isUserLoggedIn | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
|         presenter!!.onAttachView(this) | ||||
|         if (!isLoggedIn()) { | ||||
|             askUserToLogIn() | ||||
|         } | ||||
|         checkBlockStatus() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar | ||||
|      * is created to notify the user | ||||
|      */ | ||||
|     protected fun checkBlockStatus() { | ||||
|         compositeDisposable.add( | ||||
|             userClient!!.isUserBlockedFromCommons() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .filter { result: Boolean? -> result!! } | ||||
|                 .subscribe { result: Boolean? -> | ||||
|                     showAlertDialog( | ||||
|                         this, | ||||
|                         getString(R.string.block_notification_title), | ||||
|                         getString(R.string.block_notification), | ||||
|                         getString(R.string.ok) | ||||
|                     ) { finish() } | ||||
|                 }) | ||||
|     } | ||||
| 
 | ||||
|     fun checkStoragePermissions() { | ||||
|         // Check if all required permissions are granted | ||||
|         val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE) | ||||
|         val hasPartialAccess = hasPartialAccess(this) | ||||
|         if (hasAllPermissions || hasPartialAccess) { | ||||
|             // All required permissions are granted, so enable UI elements and perform actions | ||||
|             receiveSharedItems() | ||||
|             binding.cvContainerTopCard.visibility = View.VISIBLE | ||||
|         } else { | ||||
|             // Permissions are missing | ||||
|             binding.cvContainerTopCard.visibility = View.INVISIBLE | ||||
|             if (isShowPermissionsDialog) { | ||||
|                 checkPermissionsAndPerformAction( | ||||
|                     this, | ||||
|                     Runnable { | ||||
|                         binding.cvContainerTopCard.visibility = View.VISIBLE | ||||
|                         receiveSharedItems() | ||||
|                     }, Runnable { | ||||
|                         isShowPermissionsDialog = true | ||||
|                         checkStoragePermissions() | ||||
|                     }, | ||||
|                     R.string.storage_permission_title, | ||||
|                     R.string.write_storage_permission_rationale_for_image_share, | ||||
|                     *PERMISSIONS_STORAGE | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         /* If all permissions are not granted and a dialog is already showing on screen | ||||
|          showPermissionsDialog will set to false making it not show dialog again onResume, | ||||
|          but if user Denies any permission showPermissionsDialog will be to true | ||||
|          and permissions dialog will be shown again. | ||||
|          */ | ||||
|         isShowPermissionsDialog = hasAllPermissions | ||||
|     } | ||||
| 
 | ||||
|     override fun onStop() { | ||||
|         // Resetting setImageCancelled to false | ||||
|         setImageCancelled(false) | ||||
|         super.onStop() | ||||
|     } | ||||
| 
 | ||||
|     override fun returnToMainActivity() = finish() | ||||
| 
 | ||||
|     /** | ||||
|      * go to the uploadProgress activity to check the status of uploading | ||||
|      */ | ||||
|     override fun goToUploadProgressActivity() = | ||||
|         startActivity(Intent(this, UploadProgressActivity::class.java)) | ||||
| 
 | ||||
|     /** | ||||
|      * Show/Hide the progress dialog | ||||
|      */ | ||||
|     override fun showProgress(shouldShow: Boolean) { | ||||
|         if (shouldShow) { | ||||
|             if (!progressDialog!!.isShowing) { | ||||
|                 progressDialog!!.show() | ||||
|             } | ||||
|         } else { | ||||
|             if (progressDialog != null && !isFinishing) { | ||||
|                 progressDialog!!.dismiss() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int = | ||||
|         fragments!!.indexOf(fragment) | ||||
| 
 | ||||
|     override fun showMessage(messageResourceId: Int) { | ||||
|         showLongToast(this, messageResourceId) | ||||
|     } | ||||
| 
 | ||||
|     override fun getUploadableFiles(): List<UploadableFile>? { | ||||
|         return uploadableFiles | ||||
|     } | ||||
| 
 | ||||
|     override fun showHideTopCard(shouldShow: Boolean) { | ||||
|         binding.llContainerTopCard.visibility = | ||||
|             if (shouldShow) View.VISIBLE else View.GONE | ||||
|     } | ||||
| 
 | ||||
|     override fun onUploadMediaDeleted(index: Int) { | ||||
|         fragments!!.removeAt(index) //Remove the corresponding fragment | ||||
|         uploadableFiles.removeAt(index) //Remove the files from the list | ||||
|         thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter | ||||
|         uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager | ||||
|     } | ||||
| 
 | ||||
|     override fun updateTopCardTitle() { | ||||
|         binding.tvTopCardTitle.text = resources | ||||
|             .getQuantityString( | ||||
|                 R.plurals.upload_count_title, | ||||
|                 uploadableFiles!!.size, | ||||
|                 uploadableFiles!!.size | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
|     override fun makeUploadRequest() { | ||||
|         makeOneTimeWorkRequest( | ||||
|             applicationContext, | ||||
|             ExistingWorkPolicy.APPEND_OR_REPLACE | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun askUserToLogIn() { | ||||
|         Timber.d("current session is null, asking user to login") | ||||
|         showLongToast(this, getString(R.string.user_not_logged_in)) | ||||
|         val loginIntent = Intent(this@UploadActivity, LoginActivity::class.java) | ||||
|         startActivity(loginIntent) | ||||
|     } | ||||
| 
 | ||||
|     override fun onRequestPermissionsResult( | ||||
|         requestCode: Int, | ||||
|         permissions: Array<String>, | ||||
|         grantResults: IntArray | ||||
|     ) { | ||||
|         var areAllGranted = false | ||||
|         if (requestCode == RequestCodes.STORAGE) { | ||||
|             if (VERSION.SDK_INT >= VERSION_CODES.M) { | ||||
|                 for (i in grantResults.indices) { | ||||
|                     val permission = permissions[i] | ||||
|                     areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED | ||||
|                     if (grantResults[i] == PackageManager.PERMISSION_DENIED) { | ||||
|                         val showRationale = shouldShowRequestPermissionRationale(permission) | ||||
|                         if (!showRationale) { | ||||
|                             showAlertDialog( | ||||
|                                 this, | ||||
|                                 getString(R.string.storage_permissions_denied), | ||||
|                                 getString(R.string.unable_to_share_upload_item), | ||||
|                                 getString(android.R.string.ok) | ||||
|                             ) { finish() } | ||||
|                         } else { | ||||
|                             showAlertDialog( | ||||
|                                 this, | ||||
|                                 getString(R.string.storage_permission_title), | ||||
|                                 getString( | ||||
|                                     R.string.write_storage_permission_rationale_for_image_share | ||||
|                                 ), | ||||
|                                 getString(android.R.string.ok) | ||||
|                             ) { checkStoragePermissions() } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (areAllGranted) { | ||||
|                     receiveSharedItems() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults) | ||||
|     } | ||||
| 
 | ||||
|     private fun receiveSharedItems() { | ||||
|         val intent = intent | ||||
|         val action = intent.action | ||||
|         if (Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action) { | ||||
|             receiveExternalSharedItems() | ||||
|         } else if (ContributionController.ACTION_INTERNAL_UPLOADS == action) { | ||||
|             receiveInternalSharedItems() | ||||
|         } | ||||
| 
 | ||||
|         if (uploadableFiles == null || uploadableFiles!!.isEmpty()) { | ||||
|             handleNullMedia() | ||||
|         } else { | ||||
|             //Show thumbnails | ||||
|             if (uploadableFiles!!.size > 1) { | ||||
|                 if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails | ||||
|                     showAlertDialogForCategories() | ||||
|                 } | ||||
|                 if (uploadableFiles!!.size > 3 && | ||||
|                     !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload") | ||||
|                 ) { | ||||
|                     showAlertForBattery() | ||||
|                 } | ||||
|                 thumbnailsAdapter!!.uploadableFiles = uploadableFiles | ||||
|             } else { | ||||
|                 binding.llContainerTopCard.visibility = View.GONE | ||||
|             } | ||||
|             binding.tvTopCardTitle.text = resources | ||||
|                 .getQuantityString( | ||||
|                     R.plurals.upload_count_title, | ||||
|                     uploadableFiles!!.size, | ||||
|                     uploadableFiles!!.size | ||||
|                 ) | ||||
| 
 | ||||
| 
 | ||||
|             if (fragments == null) { | ||||
|                 fragments = mutableListOf() | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             for (uploadableFile in uploadableFiles!!) { | ||||
|                 val uploadMediaDetailFragment = UploadMediaDetailFragment() | ||||
| 
 | ||||
|                 if (!uploadIsOfAPlace) { | ||||
|                     handleLocation() | ||||
|                     uploadMediaDetailFragment.setImageToBeUploaded( | ||||
|                         uploadableFile, | ||||
|                         place, | ||||
|                         currLocation | ||||
|                     ) | ||||
|                     locationManager!!.unregisterLocationManager() | ||||
|                 } else { | ||||
|                     uploadMediaDetailFragment.setImageToBeUploaded( | ||||
|                         uploadableFile, | ||||
|                         place, | ||||
|                         currLocation | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 val uploadMediaDetailFragmentCallback: UploadMediaDetailFragmentCallback = | ||||
|                     object : UploadMediaDetailFragmentCallback { | ||||
|                         override fun deletePictureAtIndex(index: Int) { | ||||
|                             store!!.putInt( | ||||
|                                 keyForCurrentUploadImagesSize, | ||||
|                                 (store!!.getInt(keyForCurrentUploadImagesSize) - 1) | ||||
|                             ) | ||||
|                             presenter!!.deletePictureAtIndex(index) | ||||
|                         } | ||||
| 
 | ||||
|                         /** | ||||
|                          * Changes the thumbnail of an UploadableFile at the specified index. | ||||
|                          * This method updates the list of uploadableFiles by replacing the UploadableFile | ||||
|                          * at the given index with a new UploadableFile created from the provided file path. | ||||
|                          * After updating the list, it notifies the RecyclerView's adapter to refresh its data, | ||||
|                          * ensuring that the thumbnail change is reflected in the UI. | ||||
|                          * | ||||
|                          * @param index The index of the UploadableFile to be updated. | ||||
|                          * @param uri The file path of the new thumbnail image. | ||||
|                          */ | ||||
|                         override fun changeThumbnail(index: Int, uri: String) { | ||||
|                             uploadableFiles.removeAt(index) | ||||
|                             uploadableFiles.add(index, UploadableFile(File(uri))) | ||||
|                             binding.rvThumbnails.adapter!!.notifyDataSetChanged() | ||||
|                         } | ||||
| 
 | ||||
|                         override fun onNextButtonClicked(index: Int) { | ||||
|                             this@UploadActivity.onNextButtonClicked(index) | ||||
|                         } | ||||
| 
 | ||||
|                         override fun onPreviousButtonClicked(index: Int) { | ||||
|                             this@UploadActivity.onPreviousButtonClicked(index) | ||||
|                         } | ||||
| 
 | ||||
|                         override fun showProgress(shouldShow: Boolean) { | ||||
|                             this@UploadActivity.showProgress(shouldShow) | ||||
|                         } | ||||
| 
 | ||||
|                         override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int { | ||||
|                             return fragments!!.indexOf(fragment) | ||||
|                         } | ||||
| 
 | ||||
|                         override val totalNumberOfSteps: Int | ||||
|                             get() = fragments!!.size | ||||
| 
 | ||||
|                         override val isWLMUpload: Boolean | ||||
|                             get() = place != null && place!!.isMonument | ||||
|                     } | ||||
| 
 | ||||
|                 if (isFragmentsSaved) { | ||||
|                     val fragment = fragments!![0] as UploadMediaDetailFragment? | ||||
|                     fragment!!.fragmentCallback = uploadMediaDetailFragmentCallback | ||||
|                 } else { | ||||
|                     uploadMediaDetailFragment.fragmentCallback = uploadMediaDetailFragmentCallback | ||||
|                     fragments!!.add(uploadMediaDetailFragment) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             //If fragments are not created, create them and add them to the fragments ArrayList | ||||
|             if (!isFragmentsSaved) { | ||||
|                 uploadCategoriesFragment = UploadCategoriesFragment() | ||||
|                 if (place != null) { | ||||
|                     val categoryBundle = Bundle() | ||||
|                     categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place!!.category) | ||||
|                     uploadCategoriesFragment!!.arguments = categoryBundle | ||||
|                 } | ||||
| 
 | ||||
|                 uploadCategoriesFragment!!.callback = this | ||||
| 
 | ||||
|                 depictsFragment = DepictsFragment() | ||||
|                 val placeBundle = Bundle() | ||||
|                 placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place) | ||||
|                 depictsFragment!!.arguments = placeBundle | ||||
|                 depictsFragment!!.callback = this | ||||
| 
 | ||||
|                 mediaLicenseFragment = MediaLicenseFragment() | ||||
|                 mediaLicenseFragment!!.callback = this | ||||
| 
 | ||||
|                 fragments!!.add(depictsFragment!!) | ||||
|                 fragments!!.add(uploadCategoriesFragment!!) | ||||
|                 fragments!!.add(mediaLicenseFragment!!) | ||||
|             } else { | ||||
|                 for (i in 1 until fragments!!.size) { | ||||
|                     fragments!![i]!!.callback = object : UploadBaseFragment.Callback { | ||||
|                         override fun onNextButtonClicked(index: Int) { | ||||
|                             if (index < fragments!!.size - 1) { | ||||
|                                 binding.vpUpload.setCurrentItem(index + 1, false) | ||||
|                                 fragments!![index + 1]!!.onBecameVisible() | ||||
|                                 (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||
|                                     .scrollToPositionWithOffset( | ||||
|                                         if ((index > 0)) index - 1 else 0, | ||||
|                                         0 | ||||
|                                     ) | ||||
|                             } else { | ||||
|                                 presenter!!.handleSubmit() | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         override fun onPreviousButtonClicked(index: Int) { | ||||
|                             if (index != 0) { | ||||
|                                 binding.vpUpload.setCurrentItem(index - 1, true) | ||||
|                                 fragments!![index - 1]!!.onBecameVisible() | ||||
|                                 (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||
|                                     .scrollToPositionWithOffset( | ||||
|                                         if ((index > 3)) index - 2 else 0, | ||||
|                                         0 | ||||
|                                     ) | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         override fun showProgress(shouldShow: Boolean) { | ||||
|                             if (shouldShow) { | ||||
|                                 if (!progressDialog!!.isShowing) { | ||||
|                                     progressDialog!!.show() | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (progressDialog != null && !isFinishing) { | ||||
|                                     progressDialog!!.dismiss() | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         override fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int { | ||||
|                             return fragments!!.indexOf(fragment) | ||||
|                         } | ||||
| 
 | ||||
|                         override val totalNumberOfSteps: Int | ||||
|                             get() = fragments!!.size | ||||
| 
 | ||||
|                         override val isWLMUpload: Boolean | ||||
|                             get() = place != null && place!!.isMonument | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             uploadImagesAdapter!!.fragments = fragments!! | ||||
|             binding.vpUpload.offscreenPageLimit = fragments!!.size | ||||
|         } | ||||
|         // Saving size of uploadableFiles | ||||
|         store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. | ||||
|      * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) | ||||
|      * | ||||
|      * @param index Index of image to be removed | ||||
|      * @param maxSize Max size of the `uploadableFiles` | ||||
|      */ | ||||
|     override fun highlightNextImageOnCancelledImage(index: Int, maxSize: Int) { | ||||
|         if (index < maxSize) { | ||||
|             binding.vpUpload.setCurrentItem(index + 1, false) | ||||
|             binding.vpUpload.setCurrentItem(index, false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to check if user has cancelled upload of any image in current upload | ||||
|      * so that location compare doesn't show up again in same upload. | ||||
|      * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) | ||||
|      * | ||||
|      * @param isCancelled Is true when user has cancelled upload of any image in current upload | ||||
|      */ | ||||
|     override fun setImageCancelled(isCancelled: Boolean) { | ||||
|         val basicKvStore = BasicKvStore(this, "IsAnyImageCancelled") | ||||
|         basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculate the difference between current location and | ||||
|      * location recorded before capturing the image | ||||
|      * | ||||
|      */ | ||||
|     private fun getLocationDifference(currLocation: LatLng, prevLocation: LatLng?): Float { | ||||
|         if (prevLocation == null) { | ||||
|             return 0.0f | ||||
|         } | ||||
|         val distance = FloatArray(2) | ||||
|         Location.distanceBetween( | ||||
|             currLocation.latitude, currLocation.longitude, | ||||
|             prevLocation.latitude, prevLocation.longitude, distance | ||||
|         ) | ||||
|         return distance[0] | ||||
|     } | ||||
| 
 | ||||
|     private fun receiveExternalSharedItems() { | ||||
|         uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent) | ||||
|     } | ||||
| 
 | ||||
|     private fun receiveInternalSharedItems() { | ||||
|         val intent = intent | ||||
| 
 | ||||
|         Timber.d("Received intent %s with action %s", intent.toString(), intent.action) | ||||
| 
 | ||||
|         uploadableFiles = mutableListOf<UploadableFile>().apply { | ||||
|             addAll(intent.getParcelableArrayListExtra(EXTRA_FILES) ?: emptyList()) | ||||
|         } | ||||
|         isMultipleFilesSelected = uploadableFiles!!.size > 1 | ||||
|         Timber.i("Received multiple upload %s", uploadableFiles!!.size) | ||||
| 
 | ||||
|         place = intent.getParcelableExtra<Place>(PLACE_OBJECT) | ||||
|         prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE) | ||||
|         isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false) | ||||
|         resetDirectPrefs() | ||||
|     } | ||||
| 
 | ||||
|     fun resetDirectPrefs() = directKvStore!!.remove(PLACE_OBJECT) | ||||
| 
 | ||||
|     /** | ||||
|      * Handle null URI from the received intent. | ||||
|      * Current implementation will simply show a toast and finish the upload activity. | ||||
|      */ | ||||
|     private fun handleNullMedia() { | ||||
|         showLongToast(this, R.string.error_processing_image) | ||||
|         finish() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     override fun showAlertDialog(messageResourceId: Int, onPositiveClick: Runnable) { | ||||
|         showAlertDialog( | ||||
|             this, | ||||
|             "", | ||||
|             getString(messageResourceId), | ||||
|             getString(R.string.ok), | ||||
|             onPositiveClick | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun onNextButtonClicked(index: Int) { | ||||
|         if (index < fragments!!.size - 1) { | ||||
|             binding.vpUpload.setCurrentItem(index + 1, false) | ||||
|             fragments!![index + 1]!!.onBecameVisible() | ||||
|             (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||
|                 .scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0) | ||||
|             if (index < fragments!!.size - 4) { | ||||
|                 // check image quality if next image exists | ||||
|                 presenter!!.checkImageQuality(index + 1) | ||||
|             } | ||||
|         } else { | ||||
|             presenter!!.handleSubmit() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onPreviousButtonClicked(index: Int) { | ||||
|         if (index != 0) { | ||||
|             binding.vpUpload.setCurrentItem(index - 1, true) | ||||
|             fragments!![index - 1]!!.onBecameVisible() | ||||
|             (binding.rvThumbnails.layoutManager as LinearLayoutManager) | ||||
|                 .scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0) | ||||
|             if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) { | ||||
|                 // Shows the top card if it was hidden because of the last image being deleted and | ||||
|                 // now the user has hit previous button to go back to the media details | ||||
|                 showHideTopCard(true) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position) | ||||
| 
 | ||||
|     /** | ||||
|      * The adapter used to show image upload intermediate fragments | ||||
|      */ | ||||
|     private class UploadImageAdapter(fragmentManager: FragmentManager) : | ||||
|         FragmentStatePagerAdapter(fragmentManager) { | ||||
|         var fragments: List<UploadBaseFragment> = mutableListOf() | ||||
|             set(value) { | ||||
|                 field = value | ||||
|                 notifyDataSetChanged() | ||||
|             } | ||||
| 
 | ||||
|         override fun getItem(position: Int): Fragment { | ||||
|             return fragments[position] | ||||
|         } | ||||
| 
 | ||||
|         override fun getCount(): Int { | ||||
|             return fragments.size | ||||
|         } | ||||
| 
 | ||||
|         override fun getItemPosition(item: Any): Int { | ||||
|             return POSITION_NONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     fun onRlContainerTitleClicked() { | ||||
|         binding.rvThumbnails.visibility = | ||||
|             if (isTitleExpanded) View.GONE else View.VISIBLE | ||||
|         isTitleExpanded = !isTitleExpanded | ||||
|         binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180 | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         // Resetting all values in store by clearing them | ||||
|         store!!.clearAll() | ||||
|         presenter!!.onDetachView() | ||||
|         compositeDisposable.clear() | ||||
|         fragments = null | ||||
|         uploadImagesAdapter = null | ||||
|         if (mediaLicenseFragment != null) { | ||||
|             mediaLicenseFragment!!.callback = null | ||||
|         } | ||||
|         if (uploadCategoriesFragment != null) { | ||||
|             uploadCategoriesFragment!!.callback = null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Overrides the back button to make sure the user is prepared to lose their progress | ||||
|      */ | ||||
|     override fun onBackPressed() { | ||||
|         showAlertDialog( | ||||
|             this, | ||||
|             getString(R.string.back_button_warning), | ||||
|             getString(R.string.back_button_warning_desc), | ||||
|             getString(R.string.back_button_continue), | ||||
|             getString(R.string.back_button_warning), | ||||
|             null | ||||
|         ) { finish() } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If the user uploads more than 1 file informs that | ||||
|      * depictions/categories apply to all pictures of a multi upload. | ||||
|      * This method takes no arguments and does not return any value. | ||||
|      * It shows the AlertDialog and continues the flow of uploads. | ||||
|      */ | ||||
|     private fun showAlertDialogForCategories() { | ||||
|         UploadMediaPresenter.isCategoriesDialogShowing = true | ||||
|         // Inflate the custom layout | ||||
|         val inflater = layoutInflater | ||||
|         val view = inflater.inflate(R.layout.activity_upload_categories_dialog, null) | ||||
|         val checkBox = view.findViewById<CheckBox>(R.id.categories_checkbox) | ||||
|         // Create the alert dialog | ||||
|         val alertDialog = AlertDialog.Builder(this) | ||||
|             .setView(view) | ||||
|             .setTitle(getString(R.string.multiple_files_depiction_header)) | ||||
|             .setMessage(getString(R.string.multiple_files_depiction)) | ||||
|             .setPositiveButton("OK") { dialog: DialogInterface?, which: Int -> | ||||
|                 if (checkBox.isChecked) { | ||||
|                     // Save the user's choice to not show the dialog again | ||||
|                     defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true) | ||||
|                 } | ||||
|                 presenter!!.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) } | ||||
|                 presenter!!.checkImageQuality(0) | ||||
|                 UploadMediaPresenter.isCategoriesDialogShowing = false | ||||
|             } | ||||
|             .setNegativeButton("", null) | ||||
|             .create() | ||||
|         alertDialog.show() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** Suggest users to turn battery optimisation off when uploading | ||||
|      * more than a few files. That's because we have noticed that | ||||
|      * many-files uploads have a much higher probability of failing | ||||
|      * than uploads with less files. Show the dialog for Android 6 | ||||
|      * and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS | ||||
|      * intent was added in API level 23 | ||||
|      */ | ||||
|     private fun showAlertForBattery() { | ||||
|         if (VERSION.SDK_INT >= VERSION_CODES.M) { | ||||
|             // When battery-optimisation dialog is shown don't show the image quality dialog | ||||
|             UploadMediaPresenter.isBatteryDialogShowing = true | ||||
|             showAlertDialog( | ||||
|                 this, | ||||
|                 getString(R.string.unrestricted_battery_mode), | ||||
|                 getString(R.string.suggest_unrestricted_mode), | ||||
|                 getString(R.string.title_activity_settings), | ||||
|                 getString(R.string.cancel), | ||||
|                 { | ||||
|                     /* Since opening the right settings page might be device dependent, using | ||||
|                                               https://github.com/WaseemSabir/BatteryPermissionHelper | ||||
|                                               directly appeared like a promising idea. | ||||
|                                               However, this simply closed the popup and did not make | ||||
|                                               the settings page appear on a Pixel as well as a Xiaomi device. | ||||
|                                               Used the standard intent instead of using this library as | ||||
|                                               it shows a list of all the apps on the device and allows users to | ||||
|                                               turn battery optimisation off. | ||||
|                                             */ | ||||
|                     val batteryOptimisationSettingsIntent = Intent( | ||||
|                         Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS | ||||
|                     ) | ||||
|                     startActivity(batteryOptimisationSettingsIntent) | ||||
| 
 | ||||
|                     // calling checkImageQuality after battery dialog is interacted with | ||||
|                     // so that 2 dialogs do not pop up simultaneously | ||||
|                     UploadMediaPresenter.isBatteryDialogShowing = false | ||||
|                 }, | ||||
|                 { | ||||
|                     UploadMediaPresenter.isBatteryDialogShowing = false | ||||
|                 } | ||||
|             ) | ||||
|             defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If the permission for Location is turned on and certain | ||||
|      * conditions are met, returns current location of the user. | ||||
|      */ | ||||
|     private fun handleLocation() { | ||||
|         val locationPermissionsHelper = LocationPermissionsHelper( | ||||
|             this, locationManager!!, null | ||||
|         ) | ||||
|         if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||
|             currLocation = locationManager!!.getLastLocation() | ||||
|         } | ||||
| 
 | ||||
|         if (currLocation != null) { | ||||
|             val locationDifference = getLocationDifference(currLocation!!, prevLocation) | ||||
|             val isLocationTagUnchecked = isLocationTagUncheckedInTheSettings | ||||
|             /* Remove location if the user has unchecked the Location EXIF tag in the | ||||
|                        Manage EXIF Tags setting or turned "Record location for in-app shots" off. | ||||
|                        Also, location information is discarded if the difference between | ||||
|                        current location and location recorded just before capturing the image | ||||
|                        is greater than 100 meters */ | ||||
|             if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref") | ||||
|                 || !isInAppCameraUpload | ||||
|             ) { | ||||
|                 currLocation = null | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private var uploadIsOfAPlace = false | ||||
|         const val EXTRA_FILES: String = "commons_image_exta" | ||||
|         const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture" | ||||
|         const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload" | ||||
| 
 | ||||
|         /** | ||||
|          * Stores all nearby places found and related users response for | ||||
|          * each place while uploading media | ||||
|          */ | ||||
|         @JvmField | ||||
|         var nearbyPopupAnswers: MutableMap<Place, Boolean>? = null | ||||
| 
 | ||||
|         const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize" | ||||
|         const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities" | ||||
| 
 | ||||
|         /** | ||||
|          * Sets the flag indicating whether the upload is of a specific place. | ||||
|          * | ||||
|          * @param uploadOfAPlace a boolean value indicating whether the upload is of place. | ||||
|          */ | ||||
|         @JvmStatic | ||||
|         fun setUploadIsOfAPlace(uploadOfAPlace: Boolean) { | ||||
|             uploadIsOfAPlace = uploadOfAPlace | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -8,7 +8,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | |||
| abstract class UploadBaseFragment : CommonsDaggerSupportFragment() { | ||||
|     var callback: Callback? = null | ||||
| 
 | ||||
|     protected open fun onBecameVisible() = Unit | ||||
|     open fun onBecameVisible() = Unit | ||||
| 
 | ||||
|     interface Callback { | ||||
|         val totalNumberOfSteps: Int | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package fr.free.nrw.commons.upload | |||
| 
 | ||||
| import fr.free.nrw.commons.BasePresenter | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| 
 | ||||
| /** | ||||
|  * The contract using which the UplaodActivity would communicate with its presenter | ||||
|  | @ -73,5 +74,7 @@ interface UploadContract { | |||
|          * @param uploadItemIndex Index of next image, whose quality is to be checked | ||||
|          */ | ||||
|         fun checkImageQuality(uploadItemIndex: Int) | ||||
| 
 | ||||
|         fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import fr.free.nrw.commons.Utils | |||
| import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.utils.ImageUtils | ||||
| import io.reactivex.subjects.BehaviorSubject | ||||
| 
 | ||||
| class UploadItem( | ||||
|     var mediaUri: Uri?, | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import kotlinx.parcelize.Parcelize | |||
|  * Holds a description of an item being uploaded by [UploadActivity] | ||||
|  */ | ||||
| @Parcelize | ||||
| data class UploadMediaDetail constructor( | ||||
| data class UploadMediaDetail( | ||||
|     /** | ||||
|      * The language code ie. "en" or "fr". | ||||
|      * @param languageCode The language code ie. "en" or "fr". | ||||
|  | @ -18,19 +18,19 @@ data class UploadMediaDetail constructor( | |||
|      * The description text for the item being uploaded. | ||||
|      * @param descriptionText The description text. | ||||
|      */ | ||||
|     var descriptionText: String = "", | ||||
|     var descriptionText: String? = "", | ||||
|     /** | ||||
|      * The caption text for the item being uploaded. | ||||
|      * @param captionText The caption text. | ||||
|      */ | ||||
|     var captionText: String = "", | ||||
|     var captionText: String? = "", | ||||
| ) : Parcelable { | ||||
|     fun javaCopy() = copy() | ||||
| 
 | ||||
|     constructor(place: Place) : this( | ||||
|         place.language, | ||||
|         place.longDescription, | ||||
|         place.name, | ||||
|     constructor(place: Place?) : this( | ||||
|         place?.language, | ||||
|         place?.longDescription, | ||||
|         place?.name, | ||||
|     ) | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,633 +0,0 @@ | |||
| package fr.free.nrw.commons.upload; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.app.Dialog; | ||||
| import android.content.Intent; | ||||
| import android.speech.RecognizerIntent; | ||||
| import android.text.Editable; | ||||
| import android.text.InputFilter; | ||||
| import android.text.TextUtils; | ||||
| import android.text.TextWatcher; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.View.OnClickListener; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.AdapterView; | ||||
| import android.widget.AdapterView.OnItemClickListener; | ||||
| import android.widget.Button; | ||||
| import android.widget.EditText; | ||||
| import android.widget.FrameLayout; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.ListView; | ||||
| import android.widget.TextView; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.constraintlayout.widget.ConstraintLayout; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import com.google.android.material.textfield.TextInputLayout; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.databinding.RowItemDescriptionBinding; | ||||
| import fr.free.nrw.commons.recentlanguages.Language; | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; | ||||
| import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText; | ||||
| import fr.free.nrw.commons.utils.AbstractTextWatcher; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.regex.Pattern; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class UploadMediaDetailAdapter extends | ||||
|     RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> { | ||||
| 
 | ||||
|     RecentLanguagesDao recentLanguagesDao; | ||||
| 
 | ||||
|     private List<UploadMediaDetail> uploadMediaDetails; | ||||
|     private Callback callback; | ||||
|     private EventListener eventListener; | ||||
| 
 | ||||
|     private HashMap<Integer, String> selectedLanguages; | ||||
|     private final String savedLanguageValue; | ||||
|     private TextView recentLanguagesTextView; | ||||
|     private View separator; | ||||
|     private ListView languageHistoryListView; | ||||
|     private int currentPosition; | ||||
|     private Fragment fragment; | ||||
|     private Activity activity; | ||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher; | ||||
|     private SelectedVoiceIcon selectedVoiceIcon; | ||||
| 
 | ||||
|     private RowItemDescriptionBinding binding; | ||||
| 
 | ||||
|     public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue, | ||||
|         RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { | ||||
|         uploadMediaDetails = new ArrayList<>(); | ||||
|         selectedLanguages = new HashMap<>(); | ||||
|         this.savedLanguageValue = savedLanguageValue; | ||||
|         this.recentLanguagesDao = recentLanguagesDao; | ||||
|         this.fragment = fragment; | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; | ||||
|     } | ||||
| 
 | ||||
|     public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue, | ||||
|         List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { | ||||
|         this.uploadMediaDetails = uploadMediaDetails; | ||||
|         selectedLanguages = new HashMap<>(); | ||||
|         this.savedLanguageValue = savedLanguageValue; | ||||
|         this.recentLanguagesDao = recentLanguagesDao; | ||||
|         this.activity = activity; | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; | ||||
|     } | ||||
| 
 | ||||
|     public void setCallback(Callback callback) { | ||||
|         this.callback = callback; | ||||
|     } | ||||
| 
 | ||||
|     public void setEventListener(EventListener eventListener) { | ||||
|         this.eventListener = eventListener; | ||||
|     } | ||||
| 
 | ||||
|     public void setItems(List<UploadMediaDetail> uploadMediaDetails) { | ||||
|         this.uploadMediaDetails = uploadMediaDetails; | ||||
|         selectedLanguages = new HashMap<>(); | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
| 
 | ||||
|     public List<UploadMediaDetail> getItems() { | ||||
|         return uploadMediaDetails; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | ||||
|         LayoutInflater inflater = LayoutInflater.from(parent.getContext()); | ||||
|         binding = RowItemDescriptionBinding.inflate(inflater, parent, false); | ||||
|         return new ViewHolder(binding.getRoot()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This is a workaround for a known bug by android here | ||||
|      * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent | ||||
|      * fragments inside an adapter receptive to long click for copy/paste options | ||||
|      * | ||||
|      * @param holder the view holder | ||||
|      */ | ||||
|     @Override | ||||
|     public void onViewAttachedToWindow(@NonNull final ViewHolder holder) { | ||||
|         super.onViewAttachedToWindow(holder); | ||||
|         holder.captionItemEditText.setEnabled(false); | ||||
|         holder.captionItemEditText.setEnabled(true); | ||||
|         holder.descItemEditText.setEnabled(false); | ||||
|         holder.descItemEditText.setEnabled(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBindViewHolder(@NonNull ViewHolder holder, int position) { | ||||
|         holder.bind(position); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         return uploadMediaDetails.size(); | ||||
|     } | ||||
| 
 | ||||
|     public void addDescription(UploadMediaDetail uploadMediaDetail) { | ||||
|         selectedLanguages.put(uploadMediaDetails.size(), "en"); | ||||
|         this.uploadMediaDetails.add(uploadMediaDetail); | ||||
|         notifyItemInserted(uploadMediaDetails.size()); | ||||
|     } | ||||
| 
 | ||||
|     private void startSpeechInput(String locale) { | ||||
|         Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); | ||||
|         intent.putExtra( | ||||
|             RecognizerIntent.EXTRA_LANGUAGE_MODEL, | ||||
|             RecognizerIntent.LANGUAGE_MODEL_FREE_FORM | ||||
|         ); | ||||
|         intent.putExtra( | ||||
|             RecognizerIntent.EXTRA_LANGUAGE, | ||||
|             locale | ||||
|         ); | ||||
| 
 | ||||
|         try { | ||||
|             voiceInputResultLauncher.launch(intent); | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e.getMessage()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles the result of the speech input by processing the spoken text. | ||||
|      * If the spoken text is not empty, it capitalizes the first letter of the spoken text | ||||
|      * and updates the appropriate field (caption or description) of the current | ||||
|      * UploadMediaDetail based on the selected voice icon. | ||||
|      * Finally, it notifies the adapter that the data set has changed. | ||||
|      * | ||||
|      * @param spokenText the text input received from speech recognition. | ||||
|      */ | ||||
|     public void handleSpeechResult(String spokenText) { | ||||
|         if (!spokenText.isEmpty()) { | ||||
|             String spokenTextCapitalized = | ||||
|                 spokenText.substring(0, 1).toUpperCase() + spokenText.substring(1); | ||||
|             if (currentPosition < uploadMediaDetails.size()) { | ||||
|                 UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(currentPosition); | ||||
|                 switch (selectedVoiceIcon) { | ||||
|                     case CAPTION: | ||||
|                         uploadMediaDetail.setCaptionText(spokenTextCapitalized); | ||||
|                         break; | ||||
|                     case DESCRIPTION: | ||||
|                         uploadMediaDetail.setDescriptionText(spokenTextCapitalized); | ||||
|                         break; | ||||
|                 } | ||||
|                 notifyDataSetChanged(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove description based on position from the list and notifies the RecyclerView Adapter that | ||||
|      * data in adapter has been removed at that particular position. | ||||
|      * | ||||
|      * @param uploadMediaDetail | ||||
|      * @param position | ||||
|      */ | ||||
|     public void removeDescription(final UploadMediaDetail uploadMediaDetail, final int position) { | ||||
|         selectedLanguages.remove(position); | ||||
|         this.uploadMediaDetails.remove(uploadMediaDetail); | ||||
|         int i = position + 1; | ||||
|         while (selectedLanguages.containsKey(i)) { | ||||
|             selectedLanguages.remove(i); | ||||
|             i++; | ||||
|         } | ||||
|         notifyItemRemoved(position); | ||||
|         notifyItemRangeChanged(position, uploadMediaDetails.size() - position); | ||||
|         updateAddButtonVisibility(); | ||||
|     } | ||||
| 
 | ||||
|     public class ViewHolder extends RecyclerView.ViewHolder { | ||||
| 
 | ||||
|         TextView descriptionLanguages ; | ||||
| 
 | ||||
|         PasteSensitiveTextInputEditText descItemEditText; | ||||
| 
 | ||||
|         TextInputLayout descInputLayout; | ||||
| 
 | ||||
|         PasteSensitiveTextInputEditText captionItemEditText; | ||||
| 
 | ||||
|         TextInputLayout captionInputLayout; | ||||
| 
 | ||||
|         ImageView removeButton; | ||||
| 
 | ||||
|         ImageView addButton; | ||||
| 
 | ||||
|         ConstraintLayout clParent; | ||||
| 
 | ||||
|         LinearLayout betterCaptionLinearLayout; | ||||
| 
 | ||||
|         LinearLayout betterDescriptionLinearLayout; | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         AbstractTextWatcher captionListener; | ||||
| 
 | ||||
|         AbstractTextWatcher descriptionListener; | ||||
| 
 | ||||
|         public ViewHolder(View itemView) { | ||||
|             super(itemView); | ||||
|             Timber.i("descItemEditText:" + descItemEditText); | ||||
|         } | ||||
| 
 | ||||
|         public void bind(int position) { | ||||
|             UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position); | ||||
|             Timber.d("UploadMediaDetail is " + uploadMediaDetail); | ||||
| 
 | ||||
|             descriptionLanguages = binding.descriptionLanguages; | ||||
|             descItemEditText = binding.descriptionItemEditText; | ||||
|             descInputLayout = binding.descriptionItemEditTextInputLayout; | ||||
|             captionItemEditText = binding.captionItemEditText; | ||||
|             captionInputLayout = binding.captionItemEditTextInputLayout; | ||||
|             removeButton = binding.btnRemove; | ||||
|             addButton = binding.btnAdd; | ||||
|             clParent = binding.clParent; | ||||
|             betterCaptionLinearLayout = binding.llWriteBetterCaption; | ||||
|             betterDescriptionLinearLayout = binding.llWriteBetterDescription; | ||||
| 
 | ||||
| 
 | ||||
|             descriptionLanguages.setFocusable(false); | ||||
|             captionItemEditText.addTextChangedListener(new AbstractTextWatcher( | ||||
|                 value -> { | ||||
|                     if (position == 0) { | ||||
|                         eventListener.onPrimaryCaptionTextChange(value.length() != 0); | ||||
|                     } | ||||
|                 })); | ||||
|             captionItemEditText.removeTextChangedListener(captionListener); | ||||
|             descItemEditText.removeTextChangedListener(descriptionListener); | ||||
|             captionItemEditText.setText(uploadMediaDetail.getCaptionText()); | ||||
|             descItemEditText.setText(uploadMediaDetail.getDescriptionText()); | ||||
|             captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); | ||||
|             captionInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); | ||||
|             captionInputLayout.setEndIconOnClickListener(v -> { | ||||
|                 currentPosition = position; | ||||
|                 selectedVoiceIcon = SelectedVoiceIcon.CAPTION; | ||||
|                 startSpeechInput(descriptionLanguages.getText().toString()); | ||||
|             }); | ||||
|             descInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); | ||||
|             descInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); | ||||
|             descInputLayout.setEndIconOnClickListener(v -> { | ||||
|                 currentPosition = position; | ||||
|                 selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION; | ||||
|                 startSpeechInput(descriptionLanguages.getText().toString()); | ||||
|             }); | ||||
| 
 | ||||
|             if (position == 0) { | ||||
|                 removeButton.setVisibility(View.GONE); | ||||
|                 betterCaptionLinearLayout.setVisibility(View.VISIBLE); | ||||
|                 betterCaptionLinearLayout.setOnClickListener( | ||||
|                     v -> callback.showAlert(R.string.media_detail_caption, R.string.caption_info)); | ||||
|                 betterDescriptionLinearLayout.setVisibility(View.VISIBLE); | ||||
|                 betterDescriptionLinearLayout.setOnClickListener( | ||||
|                     v -> callback.showAlert(R.string.media_detail_description, | ||||
|                         R.string.description_info)); | ||||
|                 Objects.requireNonNull(captionInputLayout.getEditText()) | ||||
|                     .setFilters(new InputFilter[]{ | ||||
|                         new UploadMediaDetailInputFilter() | ||||
|                     }); | ||||
|             } else { | ||||
|                 removeButton.setVisibility(View.VISIBLE); | ||||
|                 betterCaptionLinearLayout.setVisibility(View.GONE); | ||||
|                 betterDescriptionLinearLayout.setVisibility(View.GONE); | ||||
|             } | ||||
| 
 | ||||
|             removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position)); | ||||
|             captionListener = new AbstractTextWatcher( | ||||
|                 captionText -> uploadMediaDetail.setCaptionText( | ||||
|                     convertIdeographicSpaceToLatinSpace(captionText.strip())) | ||||
|             ); | ||||
|             descriptionListener = new AbstractTextWatcher( | ||||
|                 descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText)); | ||||
|             captionItemEditText.addTextChangedListener(captionListener); | ||||
|             initLanguage(position, uploadMediaDetail); | ||||
| 
 | ||||
|             descItemEditText.addTextChangedListener(descriptionListener); | ||||
|             initLanguage(position, uploadMediaDetail); | ||||
| 
 | ||||
|             if (fragment != null) { | ||||
|                 FrameLayout.LayoutParams newLayoutParams = (FrameLayout.LayoutParams) clParent.getLayoutParams(); | ||||
|                 newLayoutParams.topMargin = 0; | ||||
|                 newLayoutParams.leftMargin = 0; | ||||
|                 newLayoutParams.rightMargin = 0; | ||||
|                 newLayoutParams.bottomMargin = 0; | ||||
|                 clParent.setLayoutParams(newLayoutParams); | ||||
|             } | ||||
|             updateAddButtonVisibility(); | ||||
|             addButton.setOnClickListener(v -> eventListener.addLanguage()); | ||||
| 
 | ||||
|             //If the description was manually added by the user, it deserves focus, if not, let the user decide | ||||
|             if (uploadMediaDetail.isManuallyAdded()) { | ||||
|                 captionItemEditText.requestFocus(); | ||||
|             } else { | ||||
|                 captionItemEditText.clearFocus(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         private void initLanguage(int position, UploadMediaDetail description) { | ||||
| 
 | ||||
|             final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages(); | ||||
| 
 | ||||
|             LanguagesAdapter languagesAdapter = new LanguagesAdapter( | ||||
|                 descriptionLanguages.getContext(), | ||||
|                 selectedLanguages | ||||
|             ); | ||||
| 
 | ||||
|             descriptionLanguages.setOnClickListener(new OnClickListener() { | ||||
|                 @Override | ||||
|                 public void onClick(View view) { | ||||
|                     Dialog dialog = new Dialog(view.getContext()); | ||||
|                     dialog.setContentView(R.layout.dialog_select_language); | ||||
|                     dialog.setCancelable(false); | ||||
|                     dialog.getWindow().setLayout( | ||||
|                         (int) (view.getContext().getResources().getDisplayMetrics().widthPixels | ||||
|                             * 0.90), | ||||
|                         (int) (view.getContext().getResources().getDisplayMetrics().heightPixels | ||||
|                             * 0.90)); | ||||
|                     dialog.show(); | ||||
| 
 | ||||
|                     EditText editText = dialog.findViewById(R.id.search_language); | ||||
|                     ListView listView = dialog.findViewById(R.id.language_list); | ||||
|                     final Button cancelButton = dialog.findViewById(R.id.cancel_button); | ||||
|                     languageHistoryListView = dialog.findViewById(R.id.language_history_list); | ||||
|                     recentLanguagesTextView = dialog.findViewById(R.id.recent_searches); | ||||
|                     separator = dialog.findViewById(R.id.separator); | ||||
|                     setUpRecentLanguagesSection(recentLanguages); | ||||
| 
 | ||||
|                     listView.setAdapter(languagesAdapter); | ||||
| 
 | ||||
|                     cancelButton.setOnClickListener(v -> dialog.dismiss()); | ||||
| 
 | ||||
|                     editText.addTextChangedListener(new TextWatcher() { | ||||
|                         @Override | ||||
|                         public void beforeTextChanged(CharSequence charSequence, int i, int i1, | ||||
|                             int i2) { | ||||
|                             hideRecentLanguagesSection(); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void onTextChanged(CharSequence charSequence, int i, int i1, | ||||
|                             int i2) { | ||||
|                             languagesAdapter.getFilter().filter(charSequence); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public void afterTextChanged(Editable editable) { | ||||
| 
 | ||||
|                         } | ||||
|                     }); | ||||
| 
 | ||||
|                     languageHistoryListView.setOnItemClickListener( | ||||
|                         (adapterView, view1, position, id) -> { | ||||
|                             onRecentLanguageClicked(dialog, adapterView, position, description); | ||||
|                         }); | ||||
| 
 | ||||
|                     listView.setOnItemClickListener(new OnItemClickListener() { | ||||
|                         @Override | ||||
|                         public void onItemClick(AdapterView<?> adapterView, View view, int i, | ||||
|                             long l) { | ||||
|                             description.setSelectedLanguageIndex(i); | ||||
|                             String languageCode = ((LanguagesAdapter) adapterView.getAdapter()) | ||||
|                                 .getLanguageCode(i); | ||||
|                             description.setLanguageCode(languageCode); | ||||
|                             final String languageName | ||||
|                                 = ((LanguagesAdapter) adapterView.getAdapter()).getLanguageName(i); | ||||
|                             final boolean isExists | ||||
|                                 = recentLanguagesDao.findRecentLanguage(languageCode); | ||||
|                             if (isExists) { | ||||
|                                 recentLanguagesDao.deleteRecentLanguage(languageCode); | ||||
|                             } | ||||
|                             recentLanguagesDao | ||||
|                                 .addRecentLanguage(new Language(languageName, languageCode)); | ||||
| 
 | ||||
|                             selectedLanguages.clear(); | ||||
|                             selectedLanguages.put(position, languageCode); | ||||
|                             ((LanguagesAdapter) adapterView | ||||
|                                 .getAdapter()).setSelectedLangCode(languageCode); | ||||
|                             Timber.d("Description language code is: " + languageCode); | ||||
|                             descriptionLanguages.setText(languageCode); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }); | ||||
| 
 | ||||
|                     dialog.setOnDismissListener( | ||||
|                         dialogInterface -> languagesAdapter.getFilter().filter("")); | ||||
| 
 | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             if (description.getSelectedLanguageIndex() == -1) { | ||||
|                 if (!TextUtils.isEmpty(savedLanguageValue)) { | ||||
|                     // If user has chosen a default language from settings activity | ||||
|                     // savedLanguageValue is not null | ||||
|                     if (!TextUtils.isEmpty(description.getLanguageCode())) { | ||||
|                         descriptionLanguages.setText(description.getLanguageCode()); | ||||
|                         selectedLanguages.remove(position); | ||||
|                         selectedLanguages.put(position, description.getLanguageCode()); | ||||
|                     } else { | ||||
|                         description.setLanguageCode(savedLanguageValue); | ||||
|                         descriptionLanguages.setText(savedLanguageValue); | ||||
|                         selectedLanguages.remove(position); | ||||
|                         selectedLanguages.put(position, savedLanguageValue); | ||||
|                     } | ||||
|                 } else if (!TextUtils.isEmpty(description.getLanguageCode())) { | ||||
|                     descriptionLanguages.setText(description.getLanguageCode()); | ||||
|                     selectedLanguages.remove(position); | ||||
|                     selectedLanguages.put(position, description.getLanguageCode()); | ||||
|                 } else { | ||||
|                     //Checking whether Language Code attribute is null or not. | ||||
|                     if (uploadMediaDetails.get(position).getLanguageCode() != null) { | ||||
|                         //If it is not null that means it is fetching details from the previous | ||||
|                         // upload (i.e. when user has pressed copy previous caption & description) | ||||
|                         //hence providing same language code for the current upload. | ||||
|                         descriptionLanguages.setText(uploadMediaDetails.get(position) | ||||
|                             .getLanguageCode()); | ||||
|                         selectedLanguages.remove(position); | ||||
|                         selectedLanguages.put(position, uploadMediaDetails.get(position) | ||||
|                             .getLanguageCode()); | ||||
|                     } else { | ||||
|                         if (position == 0) { | ||||
|                             final int defaultLocaleIndex = languagesAdapter | ||||
|                                 .getIndexOfUserDefaultLocale(descriptionLanguages | ||||
|                                     .getContext()); | ||||
|                             descriptionLanguages | ||||
|                                 .setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)); | ||||
|                             description.setLanguageCode( | ||||
|                                 languagesAdapter.getLanguageCode(defaultLocaleIndex)); | ||||
|                             selectedLanguages.remove(position); | ||||
|                             selectedLanguages.put(position, | ||||
|                                 languagesAdapter.getLanguageCode(defaultLocaleIndex)); | ||||
|                         } else { | ||||
|                             description.setLanguageCode(languagesAdapter.getLanguageCode(0)); | ||||
|                             descriptionLanguages.setText(languagesAdapter.getLanguageCode(0)); | ||||
|                             selectedLanguages.remove(position); | ||||
|                             selectedLanguages.put(position, languagesAdapter.getLanguageCode(0)); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|             } else { | ||||
|                 descriptionLanguages.setText(description.getLanguageCode()); | ||||
|                 selectedLanguages.remove(position); | ||||
|                 selectedLanguages.put(position, description.getLanguageCode()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Handles click event for recent language section | ||||
|          */ | ||||
|         private void onRecentLanguageClicked(final Dialog dialog, final AdapterView<?> adapterView, | ||||
|             final int position, final UploadMediaDetail description) { | ||||
|             description.setSelectedLanguageIndex(position); | ||||
|             final String languageCode = ((RecentLanguagesAdapter) adapterView.getAdapter()) | ||||
|                 .getLanguageCode(position); | ||||
|             description.setLanguageCode(languageCode); | ||||
|             final String languageName = ((RecentLanguagesAdapter) adapterView.getAdapter()) | ||||
|                 .getLanguageName(position); | ||||
|             final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode); | ||||
|             if (isExists) { | ||||
|                 recentLanguagesDao.deleteRecentLanguage(languageCode); | ||||
|             } | ||||
|             recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); | ||||
| 
 | ||||
|             selectedLanguages.clear(); | ||||
|             selectedLanguages.put(position, languageCode); | ||||
|             ((RecentLanguagesAdapter) adapterView | ||||
|                 .getAdapter()).setSelectedLangCode(languageCode); | ||||
|             Timber.d("Description language code is: %s", languageCode); | ||||
|             if (descriptionLanguages!=null) { | ||||
|                 descriptionLanguages.setText(languageCode); | ||||
|             } | ||||
|             dialog.dismiss(); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Hides recent languages section | ||||
|          */ | ||||
|         private void hideRecentLanguagesSection() { | ||||
|             languageHistoryListView.setVisibility(View.GONE); | ||||
|             recentLanguagesTextView.setVisibility(View.GONE); | ||||
|             separator.setVisibility(View.GONE); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Set up recent languages section | ||||
|          * | ||||
|          * @param recentLanguages recently used languages | ||||
|          */ | ||||
|         private void setUpRecentLanguagesSection(final List<Language> recentLanguages) { | ||||
|             if (recentLanguages.isEmpty()) { | ||||
|                 languageHistoryListView.setVisibility(View.GONE); | ||||
|                 recentLanguagesTextView.setVisibility(View.GONE); | ||||
|                 separator.setVisibility(View.GONE); | ||||
|             } else { | ||||
|                 if (recentLanguages.size() > 5) { | ||||
|                     for (int i = recentLanguages.size() - 1; i >= 5; i--) { | ||||
|                         recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i) | ||||
|                             .getLanguageCode()); | ||||
|                     } | ||||
|                 } | ||||
|                 languageHistoryListView.setVisibility(View.VISIBLE); | ||||
|                 recentLanguagesTextView.setVisibility(View.VISIBLE); | ||||
|                 separator.setVisibility(View.VISIBLE); | ||||
| 
 | ||||
|                 if (descriptionLanguages!=null) { | ||||
|                     final RecentLanguagesAdapter recentLanguagesAdapter | ||||
|                         = new RecentLanguagesAdapter( | ||||
|                         descriptionLanguages.getContext(), | ||||
|                         recentLanguagesDao.getRecentLanguages(), | ||||
|                         selectedLanguages); | ||||
|                     languageHistoryListView.setAdapter(recentLanguagesAdapter); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Convert Ideographic space to Latin space | ||||
|          * | ||||
|          * @param source the source text | ||||
|          * @return a string with Latin spaces instead of Ideographic spaces | ||||
|          */ | ||||
|         public String convertIdeographicSpaceToLatinSpace(String source) { | ||||
|             Pattern ideographicSpacePattern = Pattern.compile("\\x{3000}"); | ||||
|             return ideographicSpacePattern.matcher(source).replaceAll(" "); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hides the visibility of the "Add" button for all items in the RecyclerView except | ||||
|      * the last item in RecyclerView | ||||
|      */ | ||||
|     private void updateAddButtonVisibility() { | ||||
|         int lastItemPosition = getItemCount() - 1; | ||||
|         // Hide Add Button for all items | ||||
|         for (int i = 0; i < getItemCount(); i++) { | ||||
|             if (fragment != null) { | ||||
|                 if (fragment.getView() != null) { | ||||
|                     ViewHolder holder = (ViewHolder) ((RecyclerView) fragment.getView() | ||||
|                         .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition(i); | ||||
|                     if (holder != null) { | ||||
|                         holder.addButton.setVisibility(View.GONE); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 if (this.activity != null) { | ||||
|                     ViewHolder holder = (ViewHolder) ((RecyclerView) activity.findViewById( | ||||
|                         R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition(i); | ||||
|                     if (holder != null) { | ||||
|                         holder.addButton.setVisibility(View.GONE); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Show Add Button for the last item | ||||
|         if (fragment != null) { | ||||
|             if (fragment.getView() != null) { | ||||
|                 ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) fragment.getView() | ||||
|                     .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition( | ||||
|                     lastItemPosition); | ||||
|                 if (lastItemHolder != null) { | ||||
|                     lastItemHolder.addButton.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             if (this.activity != null) { | ||||
|                 ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) activity | ||||
|                     .findViewById(R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition( | ||||
|                     lastItemPosition); | ||||
|                 if (lastItemHolder != null) { | ||||
|                     lastItemHolder.addButton.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public interface Callback { | ||||
| 
 | ||||
|         void showAlert(int mediaDetailDescription, int descriptionInfo); | ||||
|     } | ||||
| 
 | ||||
|     public interface EventListener { | ||||
| 
 | ||||
|         void onPrimaryCaptionTextChange(boolean isNotEmpty); | ||||
| 
 | ||||
|         void addLanguage(); | ||||
|     } | ||||
| 
 | ||||
|     enum SelectedVoiceIcon { | ||||
|         CAPTION, | ||||
|         DESCRIPTION | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,563 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.app.Dialog | ||||
| import android.content.Intent | ||||
| import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH | ||||
| import android.speech.RecognizerIntent.EXTRA_LANGUAGE | ||||
| import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL | ||||
| import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM | ||||
| import android.text.Editable | ||||
| import android.text.InputFilter | ||||
| import android.text.TextUtils | ||||
| import android.text.TextWatcher | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.AdapterView | ||||
| import android.widget.AdapterView.OnItemClickListener | ||||
| import android.widget.EditText | ||||
| import android.widget.FrameLayout | ||||
| import android.widget.ImageView | ||||
| import android.widget.LinearLayout | ||||
| import android.widget.ListView | ||||
| import android.widget.TextView | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.google.android.material.textfield.TextInputLayout | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.databinding.RowItemDescriptionBinding | ||||
| import fr.free.nrw.commons.recentlanguages.Language | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||
| import fr.free.nrw.commons.utils.AbstractTextWatcher | ||||
| import timber.log.Timber | ||||
| import java.util.Locale | ||||
| import java.util.regex.Pattern | ||||
| 
 | ||||
| class UploadMediaDetailAdapter : RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> { | ||||
|     private var uploadMediaDetails: MutableList<UploadMediaDetail> | ||||
|     private var selectedLanguages: MutableMap<Int, String> | ||||
|     private val savedLanguageValue: String | ||||
|     private var recentLanguagesTextView: TextView? = null | ||||
|     private var separator: View? = null | ||||
|     private var languageHistoryListView: ListView? = null | ||||
|     private var currentPosition = 0 | ||||
|     private var fragment: Fragment? = null | ||||
|     private var activity: Activity? = null | ||||
|     private val voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||
|     private var selectedVoiceIcon: SelectedVoiceIcon? = null | ||||
|     var recentLanguagesDao: RecentLanguagesDao | ||||
|     var callback: Callback? = null | ||||
|     var eventListener: EventListener? = null | ||||
|     var items: List<UploadMediaDetail> | ||||
|         get() = uploadMediaDetails | ||||
|         set(value) { | ||||
|             uploadMediaDetails = value.toMutableList() | ||||
|             selectedLanguages = mutableMapOf() | ||||
|             notifyDataSetChanged() | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     constructor( | ||||
|         fragment: Fragment?, | ||||
|         savedLanguageValue: String, | ||||
|         recentLanguagesDao: RecentLanguagesDao, | ||||
|         voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         uploadMediaDetails = ArrayList() | ||||
|         selectedLanguages = mutableMapOf() | ||||
|         this.savedLanguageValue = savedLanguageValue | ||||
|         this.recentLanguagesDao = recentLanguagesDao | ||||
|         this.fragment = fragment | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher | ||||
|     } | ||||
| 
 | ||||
|     constructor( | ||||
|         activity: Activity?, | ||||
|         savedLanguageValue: String, | ||||
|         uploadMediaDetails: MutableList<UploadMediaDetail>, | ||||
|         recentLanguagesDao: RecentLanguagesDao, | ||||
|         voiceInputResultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         this.uploadMediaDetails = uploadMediaDetails | ||||
|         selectedLanguages = HashMap() | ||||
|         this.savedLanguageValue = savedLanguageValue | ||||
|         this.recentLanguagesDao = recentLanguagesDao | ||||
|         this.activity = activity | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||
|         val inflater = LayoutInflater.from(parent.context) | ||||
|         return ViewHolder(RowItemDescriptionBinding.inflate(inflater, parent, false)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This is a workaround for a known bug by android here | ||||
|      * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent | ||||
|      * fragments inside an adapter receptive to long click for copy/paste options | ||||
|      * | ||||
|      * @param holder the view holder | ||||
|      */ | ||||
|     override fun onViewAttachedToWindow(holder: ViewHolder) { | ||||
|         super.onViewAttachedToWindow(holder) | ||||
|         holder.binding.captionItemEditText.isEnabled = false | ||||
|         holder.binding.captionItemEditText.isEnabled = true | ||||
|         holder.binding.descriptionItemEditText.isEnabled = false | ||||
|         holder.binding.descriptionItemEditText.isEnabled = true | ||||
|     } | ||||
| 
 | ||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||
|         holder.bind(position) | ||||
|     } | ||||
| 
 | ||||
|     override fun getItemCount(): Int { | ||||
|         return uploadMediaDetails.size | ||||
|     } | ||||
| 
 | ||||
|     fun addDescription(uploadMediaDetail: UploadMediaDetail) { | ||||
|         selectedLanguages[uploadMediaDetails.size] = "en" | ||||
|         uploadMediaDetails.add(uploadMediaDetail) | ||||
|         notifyItemInserted(uploadMediaDetails.size) | ||||
|     } | ||||
| 
 | ||||
|     private fun startSpeechInput(locale: String) { | ||||
|         try { | ||||
|             voiceInputResultLauncher.launch(Intent(ACTION_RECOGNIZE_SPEECH).apply { | ||||
|                 putExtra(EXTRA_LANGUAGE_MODEL, LANGUAGE_MODEL_FREE_FORM) | ||||
|                 putExtra(EXTRA_LANGUAGE, locale) | ||||
|             }) | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles the result of the speech input by processing the spoken text. | ||||
|      * If the spoken text is not empty, it capitalizes the first letter of the spoken text | ||||
|      * and updates the appropriate field (caption or description) of the current | ||||
|      * UploadMediaDetail based on the selected voice icon. | ||||
|      * Finally, it notifies the adapter that the data set has changed. | ||||
|      * | ||||
|      * @param spokenText the text input received from speech recognition. | ||||
|      */ | ||||
|     fun handleSpeechResult(spokenText: String) { | ||||
|         if (spokenText.isNotEmpty()) { | ||||
|             val spokenTextCapitalized = | ||||
|                 spokenText.substring(0, 1).uppercase(Locale.getDefault()) + spokenText.substring(1) | ||||
|             if (currentPosition < uploadMediaDetails.size) { | ||||
|                 val uploadMediaDetail = uploadMediaDetails[currentPosition] | ||||
|                 when (selectedVoiceIcon) { | ||||
|                     SelectedVoiceIcon.CAPTION -> uploadMediaDetail.captionText = | ||||
|                         spokenTextCapitalized | ||||
| 
 | ||||
|                     SelectedVoiceIcon.DESCRIPTION -> uploadMediaDetail.descriptionText = | ||||
|                         spokenTextCapitalized | ||||
| 
 | ||||
|                     null -> {} | ||||
|                 } | ||||
|                 notifyDataSetChanged() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove description based on position from the list and notifies the RecyclerView Adapter that | ||||
|      * data in adapter has been removed at that particular position. | ||||
|      */ | ||||
|     fun removeDescription(uploadMediaDetail: UploadMediaDetail, position: Int) { | ||||
|         selectedLanguages.remove(position) | ||||
|         uploadMediaDetails.remove(uploadMediaDetail) | ||||
|         var i = position + 1 | ||||
|         while (selectedLanguages.containsKey(i)) { | ||||
|             selectedLanguages.remove(i) | ||||
|             i++ | ||||
|         } | ||||
|         notifyItemRemoved(position) | ||||
|         notifyItemRangeChanged(position, uploadMediaDetails.size - position) | ||||
|         updateAddButtonVisibility() | ||||
|     } | ||||
| 
 | ||||
|     inner class ViewHolder(val binding: RowItemDescriptionBinding) : | ||||
|         RecyclerView.ViewHolder(binding.root) { | ||||
| 
 | ||||
|         var addButton: ImageView? = null | ||||
| 
 | ||||
|         var clParent: ConstraintLayout? = null | ||||
| 
 | ||||
|         var betterCaptionLinearLayout: LinearLayout? = null | ||||
| 
 | ||||
|         var betterDescriptionLinearLayout: LinearLayout? = null | ||||
| 
 | ||||
|         private var captionListener: AbstractTextWatcher? = null | ||||
| 
 | ||||
|         var descriptionListener: AbstractTextWatcher? = null | ||||
| 
 | ||||
|         fun bind(position: Int) { | ||||
|             val uploadMediaDetail = uploadMediaDetails[position] | ||||
|             Timber.d("UploadMediaDetail is %s", uploadMediaDetail) | ||||
| 
 | ||||
|             addButton = binding.btnAdd | ||||
|             clParent = binding.clParent | ||||
|             betterCaptionLinearLayout = binding.llWriteBetterCaption | ||||
|             betterDescriptionLinearLayout = binding.llWriteBetterDescription | ||||
| 
 | ||||
| 
 | ||||
|             binding.descriptionLanguages.isFocusable = false | ||||
|             binding.captionItemEditText.addTextChangedListener(AbstractTextWatcher { value: String -> | ||||
|                 if (position == 0) { | ||||
|                     eventListener!!.onPrimaryCaptionTextChange(value.length != 0) | ||||
|                 } | ||||
|             }) | ||||
|             binding.captionItemEditText.removeTextChangedListener(captionListener) | ||||
|             binding.descriptionItemEditText.removeTextChangedListener(descriptionListener) | ||||
|             binding.captionItemEditText.setText(uploadMediaDetail.captionText) | ||||
|             binding.descriptionItemEditText.setText(uploadMediaDetail.descriptionText) | ||||
|             binding.captionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM | ||||
|             binding.captionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) | ||||
|             binding.captionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> | ||||
|                 currentPosition = position | ||||
|                 selectedVoiceIcon = SelectedVoiceIcon.CAPTION | ||||
|                 startSpeechInput(binding.descriptionLanguages.text.toString()) | ||||
|             } | ||||
|             binding.descriptionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM | ||||
|             binding.descriptionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) | ||||
|             binding.descriptionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> | ||||
|                 currentPosition = position | ||||
|                 selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION | ||||
|                 startSpeechInput(binding.descriptionLanguages.text.toString()) | ||||
|             } | ||||
| 
 | ||||
|             if (position == 0) { | ||||
|                 binding.btnRemove.visibility = View.GONE | ||||
|                 betterCaptionLinearLayout!!.visibility = View.VISIBLE | ||||
|                 betterCaptionLinearLayout!!.setOnClickListener { v: View? -> | ||||
|                     callback!!.showAlert( | ||||
|                         R.string.media_detail_caption, | ||||
|                         R.string.caption_info | ||||
|                     ) | ||||
|                 } | ||||
|                 betterDescriptionLinearLayout!!.visibility = View.VISIBLE | ||||
|                 betterDescriptionLinearLayout!!.setOnClickListener { v: View? -> | ||||
|                     callback!!.showAlert( | ||||
|                         R.string.media_detail_description, | ||||
|                         R.string.description_info | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 binding.captionItemEditTextInputLayout.editText?.let { | ||||
|                     it.filters = arrayOf<InputFilter>(UploadMediaDetailInputFilter()) | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.btnRemove.visibility = View.VISIBLE | ||||
|                 betterCaptionLinearLayout!!.visibility = View.GONE | ||||
|                 betterDescriptionLinearLayout!!.visibility = View.GONE | ||||
|             } | ||||
| 
 | ||||
|             binding.btnRemove.setOnClickListener { v: View? -> | ||||
|                 removeDescription( | ||||
|                     uploadMediaDetail, | ||||
|                     position | ||||
|                 ) | ||||
|             } | ||||
|             captionListener = AbstractTextWatcher { captionText: String -> | ||||
|                 uploadMediaDetail.captionText = | ||||
|                     convertIdeographicSpaceToLatinSpace(captionText.trim()) | ||||
|             } | ||||
|             descriptionListener = AbstractTextWatcher { value: String? -> | ||||
|                 uploadMediaDetail.descriptionText = value | ||||
|             } | ||||
|             binding.captionItemEditText.addTextChangedListener(captionListener) | ||||
|             initLanguage(position, uploadMediaDetail) | ||||
| 
 | ||||
|             binding.descriptionItemEditText.addTextChangedListener(descriptionListener) | ||||
|             initLanguage(position, uploadMediaDetail) | ||||
| 
 | ||||
|             if (fragment != null) { | ||||
|                 val newLayoutParams = clParent!!.layoutParams as FrameLayout.LayoutParams | ||||
|                 newLayoutParams.topMargin = 0 | ||||
|                 newLayoutParams.leftMargin = 0 | ||||
|                 newLayoutParams.rightMargin = 0 | ||||
|                 newLayoutParams.bottomMargin = 0 | ||||
|                 clParent!!.layoutParams = newLayoutParams | ||||
|             } | ||||
|             updateAddButtonVisibility() | ||||
|             addButton!!.setOnClickListener { v: View? -> eventListener!!.addLanguage() } | ||||
| 
 | ||||
|             //If the description was manually added by the user, it deserves focus, if not, let the user decide | ||||
|             if (uploadMediaDetail.isManuallyAdded) { | ||||
|                 binding.captionItemEditText.requestFocus() | ||||
|             } else { | ||||
|                 binding.captionItemEditText.clearFocus() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         private fun initLanguage(position: Int, description: UploadMediaDetail) { | ||||
|             val recentLanguages = recentLanguagesDao.getRecentLanguages() | ||||
| 
 | ||||
|             val languagesAdapter = LanguagesAdapter( | ||||
|                 binding.descriptionLanguages.context, | ||||
|                 selectedLanguages | ||||
|             ) | ||||
| 
 | ||||
|             binding.descriptionLanguages.setOnClickListener { view -> | ||||
|                 val dialog = Dialog(view.context) | ||||
|                 dialog.setContentView(R.layout.dialog_select_language) | ||||
|                 dialog.setCancelable(false) | ||||
|                 dialog.window!!.setLayout( | ||||
|                     (view.context.resources.displayMetrics.widthPixels | ||||
|                             * 0.90).toInt(), | ||||
|                     (view.context.resources.displayMetrics.heightPixels | ||||
|                             * 0.90).toInt() | ||||
|                 ) | ||||
|                 dialog.show() | ||||
| 
 | ||||
|                 val editText = | ||||
|                     dialog.findViewById<EditText>(R.id.search_language) | ||||
|                 val listView = | ||||
|                     dialog.findViewById<ListView>(R.id.language_list) | ||||
|                 languageHistoryListView = | ||||
|                     dialog.findViewById(R.id.language_history_list) | ||||
|                 recentLanguagesTextView = | ||||
|                     dialog.findViewById(R.id.recent_searches) | ||||
|                 separator = | ||||
|                     dialog.findViewById(R.id.separator) | ||||
|                 setUpRecentLanguagesSection(recentLanguages) | ||||
| 
 | ||||
|                 listView.adapter = languagesAdapter | ||||
| 
 | ||||
|                 editText.addTextChangedListener(object : TextWatcher { | ||||
|                     override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = | ||||
|                         hideRecentLanguagesSection() | ||||
| 
 | ||||
|                     override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { | ||||
|                         languagesAdapter.filter.filter(charSequence) | ||||
|                     } | ||||
| 
 | ||||
|                     override fun afterTextChanged(editable: Editable) = Unit | ||||
|                 }) | ||||
| 
 | ||||
|                 languageHistoryListView?.setOnItemClickListener { adapterView: AdapterView<*>, view1: View?, position: Int, id: Long -> | ||||
|                     onRecentLanguageClicked(dialog, adapterView, position, description) | ||||
|                 } | ||||
| 
 | ||||
|                 listView.onItemClickListener = OnItemClickListener { adapterView, _, i, l -> | ||||
|                         description.selectedLanguageIndex = i | ||||
|                         val languageCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(i) | ||||
|                         description.languageCode = languageCode | ||||
|                         val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(i) | ||||
|                         val isExists = recentLanguagesDao.findRecentLanguage(languageCode) | ||||
|                         if (isExists) { | ||||
|                             recentLanguagesDao.deleteRecentLanguage(languageCode) | ||||
|                         } | ||||
|                         recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) | ||||
| 
 | ||||
|                         selectedLanguages.clear() | ||||
|                         selectedLanguages[position] = languageCode | ||||
|                         (adapterView.adapter as LanguagesAdapter).selectedLangCode = languageCode | ||||
|                         Timber.d("Description language code is: %s", languageCode) | ||||
|                     binding.descriptionLanguages.text = languageCode | ||||
|                         dialog.dismiss() | ||||
|                     } | ||||
| 
 | ||||
|                 dialog.setOnDismissListener { | ||||
|                     languagesAdapter.filter.filter("") | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (description.selectedLanguageIndex == -1) { | ||||
|                 if (!TextUtils.isEmpty(savedLanguageValue)) { | ||||
|                     // If user has chosen a default language from settings activity | ||||
|                     // savedLanguageValue is not null | ||||
|                     if (!TextUtils.isEmpty(description.languageCode)) { | ||||
|                         binding.descriptionLanguages.text = description.languageCode | ||||
|                         selectedLanguages.remove(position) | ||||
|                         selectedLanguages[position] = description.languageCode!! | ||||
|                     } else { | ||||
|                         description.languageCode = savedLanguageValue | ||||
|                         binding.descriptionLanguages.text = savedLanguageValue | ||||
|                         selectedLanguages.remove(position) | ||||
|                         selectedLanguages[position] = savedLanguageValue | ||||
|                     } | ||||
|                 } else if (!TextUtils.isEmpty(description.languageCode)) { | ||||
|                     binding.descriptionLanguages.text = description.languageCode | ||||
|                     selectedLanguages.remove(position) | ||||
|                     selectedLanguages[position] = description.languageCode!! | ||||
|                 } else { | ||||
|                     //Checking whether Language Code attribute is null or not. | ||||
|                     if (uploadMediaDetails[position].languageCode != null) { | ||||
|                         //If it is not null that means it is fetching details from the previous | ||||
|                         // upload (i.e. when user has pressed copy previous caption & description) | ||||
|                         //hence providing same language code for the current upload. | ||||
|                         binding.descriptionLanguages.text = uploadMediaDetails[position] | ||||
|                             .languageCode | ||||
|                         selectedLanguages.remove(position) | ||||
|                         selectedLanguages[position] = uploadMediaDetails[position].languageCode!! | ||||
|                     } else { | ||||
|                         if (position == 0) { | ||||
|                             val defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale( | ||||
|                                 binding.descriptionLanguages.getContext()) | ||||
|                             binding.descriptionLanguages.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)) | ||||
|                             description.languageCode = languagesAdapter.getLanguageCode(defaultLocaleIndex) | ||||
|                             selectedLanguages.remove(position) | ||||
|                             selectedLanguages[position] = | ||||
|                                 languagesAdapter.getLanguageCode(defaultLocaleIndex) | ||||
|                         } else { | ||||
|                             description.languageCode = languagesAdapter.getLanguageCode(0) | ||||
|                             binding.descriptionLanguages.text = languagesAdapter.getLanguageCode(0) | ||||
|                             selectedLanguages.remove(position) | ||||
|                             selectedLanguages[position] = languagesAdapter.getLanguageCode(0) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.descriptionLanguages.text = description.languageCode | ||||
|                 selectedLanguages.remove(position) | ||||
|                 description.languageCode?.let { | ||||
|                     selectedLanguages[position] = it | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Handles click event for recent language section | ||||
|          */ | ||||
|         private fun onRecentLanguageClicked( | ||||
|             dialog: Dialog, adapterView: AdapterView<*>, | ||||
|             position: Int, description: UploadMediaDetail | ||||
|         ) { | ||||
|             description.selectedLanguageIndex = position | ||||
|             val languageCode = (adapterView.adapter as RecentLanguagesAdapter) | ||||
|                 .getLanguageCode(position) | ||||
|             description.languageCode = languageCode | ||||
|             val languageName = (adapterView.adapter as RecentLanguagesAdapter) | ||||
|                 .getLanguageName(position) | ||||
|             val isExists = recentLanguagesDao.findRecentLanguage(languageCode) | ||||
|             if (isExists) { | ||||
|                 recentLanguagesDao.deleteRecentLanguage(languageCode) | ||||
|             } | ||||
|             recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) | ||||
| 
 | ||||
|             selectedLanguages.clear() | ||||
|             selectedLanguages[position] = languageCode | ||||
|             (adapterView | ||||
|                 .adapter as RecentLanguagesAdapter).selectedLangCode = languageCode | ||||
|             Timber.d("Description language code is: %s", languageCode) | ||||
|             binding.descriptionLanguages.text = languageCode | ||||
|             dialog.dismiss() | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Hides recent languages section | ||||
|          */ | ||||
|         private fun hideRecentLanguagesSection() { | ||||
|             languageHistoryListView!!.visibility = View.GONE | ||||
|             recentLanguagesTextView!!.visibility = View.GONE | ||||
|             separator!!.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Set up recent languages section | ||||
|          * | ||||
|          * @param recentLanguages recently used languages | ||||
|          */ | ||||
|         private fun setUpRecentLanguagesSection(recentLanguages: List<Language>) { | ||||
|             if (recentLanguages.isEmpty()) { | ||||
|                 languageHistoryListView!!.visibility = View.GONE | ||||
|                 recentLanguagesTextView!!.visibility = View.GONE | ||||
|                 separator!!.visibility = View.GONE | ||||
|             } else { | ||||
|                 if (recentLanguages.size > 5) { | ||||
|                     for (i in recentLanguages.size - 1 downTo 5) { | ||||
|                         recentLanguagesDao.deleteRecentLanguage( | ||||
|                             recentLanguages[i] | ||||
|                                 .languageCode | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 languageHistoryListView!!.visibility = View.VISIBLE | ||||
|                 recentLanguagesTextView!!.visibility = View.VISIBLE | ||||
|                 separator!!.visibility = View.VISIBLE | ||||
| 
 | ||||
|                 val recentLanguagesAdapter = RecentLanguagesAdapter( | ||||
|                     binding.descriptionLanguages.context, | ||||
|                     recentLanguagesDao.getRecentLanguages(), | ||||
|                     selectedLanguages | ||||
|                 ) | ||||
|                 languageHistoryListView!!.adapter = recentLanguagesAdapter | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Convert Ideographic space to Latin space | ||||
|          * | ||||
|          * @param source the source text | ||||
|          * @return a string with Latin spaces instead of Ideographic spaces | ||||
|          */ | ||||
|         fun convertIdeographicSpaceToLatinSpace(source: String): String { | ||||
|             val ideographicSpacePattern = Pattern.compile("\\x{3000}") | ||||
|             return ideographicSpacePattern.matcher(source).replaceAll(" ") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hides the visibility of the "Add" button for all items in the RecyclerView except | ||||
|      * the last item in RecyclerView | ||||
|      */ | ||||
|     private fun updateAddButtonVisibility() { | ||||
|         val lastItemPosition = itemCount - 1 | ||||
|         // Hide Add Button for all items | ||||
|         for (i in 0 until itemCount) { | ||||
|             if (fragment != null) { | ||||
|                 if (fragment!!.view != null) { | ||||
|                     val holder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? | ||||
|                     if (holder != null) { | ||||
|                         holder.addButton!!.visibility = View.GONE | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 if (activity != null) { | ||||
|                     val holder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? | ||||
|                     if (holder != null) { | ||||
|                         holder.addButton!!.visibility = View.GONE | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Show Add Button for the last item | ||||
|         if (fragment != null) { | ||||
|             if (fragment!!.view != null) { | ||||
|                 val lastItemHolder = (fragment!!.requireView().findViewById<View>(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? | ||||
|                 if (lastItemHolder != null) { | ||||
|                     lastItemHolder.addButton!!.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             if (activity != null) { | ||||
|                 val lastItemHolder = (activity!!.findViewById<View>(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? | ||||
|                 if (lastItemHolder != null) { | ||||
|                     lastItemHolder.addButton!!.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun interface Callback { | ||||
|         fun showAlert(mediaDetailDescription: Int, descriptionInfo: Int) | ||||
|     } | ||||
| 
 | ||||
|     interface EventListener { | ||||
|         fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) | ||||
|         fun addLanguage() | ||||
|     } | ||||
| 
 | ||||
|     internal enum class SelectedVoiceIcon { | ||||
|         CAPTION, | ||||
|         DESCRIPTION | ||||
|     } | ||||
| } | ||||
|  | @ -1,297 +0,0 @@ | |||
| package fr.free.nrw.commons.upload; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.net.Uri; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.contributions.Contribution; | ||||
| 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.nearby.Place; | ||||
| import fr.free.nrw.commons.settings.Prefs; | ||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Date; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| @Singleton | ||||
| public class UploadModel { | ||||
| 
 | ||||
|     private final JsonKvStore store; | ||||
|     private final List<String> licenses; | ||||
|     private final Context context; | ||||
|     private String license; | ||||
|     private final Map<String, String> licensesByName; | ||||
|     private final List<UploadItem> items = new ArrayList<>(); | ||||
|     private final CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|     private final SessionManager sessionManager; | ||||
|     private final FileProcessor fileProcessor; | ||||
|     private final ImageProcessingService imageProcessingService; | ||||
|     private final List<String> selectedCategories = new ArrayList<>(); | ||||
|     private final List<DepictedItem> selectedDepictions = new ArrayList<>(); | ||||
|     /** | ||||
|      * Existing depicts which are selected | ||||
|      */ | ||||
|     private List<String> selectedExistingDepictions = new ArrayList<>(); | ||||
| 
 | ||||
|     @Inject | ||||
|     UploadModel(@Named("licenses") final List<String> licenses, | ||||
|             @Named("default_preferences") final JsonKvStore store, | ||||
|             @Named("licenses_by_name") final Map<String, String> licensesByName, | ||||
|             final Context context, | ||||
|             final SessionManager sessionManager, | ||||
|             final FileProcessor fileProcessor, | ||||
|             final ImageProcessingService imageProcessingService) { | ||||
|         this.licenses = licenses; | ||||
|         this.store = store; | ||||
|         this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); | ||||
|         this.licensesByName = licensesByName; | ||||
|         this.context = context; | ||||
|         this.sessionManager = sessionManager; | ||||
|         this.fileProcessor = fileProcessor; | ||||
|         this.imageProcessingService = imageProcessingService; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * cleanup the resources, I am Singleton, preparing for fresh upload | ||||
|      */ | ||||
|     public void cleanUp() { | ||||
|         compositeDisposable.clear(); | ||||
|         fileProcessor.cleanup(); | ||||
|         items.clear(); | ||||
|         selectedCategories.clear(); | ||||
|         selectedDepictions.clear(); | ||||
|         selectedExistingDepictions.clear(); | ||||
|     } | ||||
| 
 | ||||
|     public void setSelectedCategories(List<String> selectedCategories) { | ||||
|         this.selectedCategories.clear(); | ||||
|         this.selectedCategories.addAll(selectedCategories); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * pre process a one item at a time | ||||
|      */ | ||||
|     public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile, | ||||
|         final Place place, | ||||
|         final SimilarImageInterface similarImageInterface, | ||||
|         LatLng inAppPictureLocation) { | ||||
|         return Observable.just( | ||||
|             createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls validateImage() of ImageProcessingService to check quality of image | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose quality is to be checked | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @return Quality of UploadItem | ||||
|      */ | ||||
|     public Single<Integer> getImageQuality(final UploadItem uploadItem, LatLng inAppPictureLocation) { | ||||
|         return imageProcessingService.validateImage(uploadItem, inAppPictureLocation); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate | ||||
|      * | ||||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     public Single<Integer> checkDuplicateImage(String filePath){ | ||||
|         return imageProcessingService.checkDuplicateImage(filePath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls validateCaption() of ImageProcessingService to check caption of image | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose caption is to be checked | ||||
|      * @return Quality of caption of the UploadItem | ||||
|      */ | ||||
|     public Single<Integer> getCaptionQuality(final UploadItem uploadItem) { | ||||
|         return imageProcessingService.validateCaption(uploadItem); | ||||
|     } | ||||
| 
 | ||||
|     private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, | ||||
|         final Place place, | ||||
|         final SimilarImageInterface similarImageInterface, | ||||
|         LatLng inAppPictureLocation) { | ||||
|         final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile | ||||
|                 .getFileCreatedDate(context); | ||||
|         long fileCreatedDate = -1; | ||||
|         String createdTimestampSource = ""; | ||||
|         String fileCreatedDateString = ""; | ||||
|         if (dateTimeWithSource != null) { | ||||
|             fileCreatedDate = dateTimeWithSource.getEpochDate(); | ||||
|             fileCreatedDateString = dateTimeWithSource.getDateString(); | ||||
|             createdTimestampSource = dateTimeWithSource.getSource(); | ||||
|         } | ||||
|         Timber.d("File created date is %d", fileCreatedDate); | ||||
|         final ImageCoordinates imageCoordinates = fileProcessor | ||||
|                 .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(), | ||||
|                     inAppPictureLocation); | ||||
|         final UploadItem uploadItem = new UploadItem( | ||||
|             Uri.parse(uploadableFile.getFilePath()), | ||||
|                 uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, | ||||
|                 createdTimestampSource, | ||||
|                 uploadableFile.getContentUri(), | ||||
|                 fileCreatedDateString); | ||||
| 
 | ||||
|         // If an uploadItem of the same uploadableFile has been created before, we return that. | ||||
|         // This is to avoid multiple instances of uploadItem of same file passed around. | ||||
|         if (items.contains(uploadItem)) { | ||||
|             return items.get(items.indexOf(uploadItem)); | ||||
|         } | ||||
| 
 | ||||
|         if (place != null) { | ||||
|             uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); | ||||
|         } | ||||
|         if (!items.contains(uploadItem)) { | ||||
|             items.add(uploadItem); | ||||
|         } | ||||
|         return uploadItem; | ||||
|     } | ||||
| 
 | ||||
|     public int getCount() { | ||||
|         return items.size(); | ||||
|     } | ||||
| 
 | ||||
|     public List<UploadItem> getUploads() { | ||||
|         return items; | ||||
|     } | ||||
| 
 | ||||
|     public List<String> getLicenses() { | ||||
|         return licenses; | ||||
|     } | ||||
| 
 | ||||
|     public String getSelectedLicense() { | ||||
|         return license; | ||||
|     } | ||||
| 
 | ||||
|     public void setSelectedLicense(final String licenseName) { | ||||
|         this.license = licensesByName.get(licenseName); | ||||
|         store.putString(Prefs.DEFAULT_LICENSE, license); | ||||
|     } | ||||
| 
 | ||||
|     public Observable<Contribution> buildContributions() { | ||||
|         return Observable.fromIterable(items).map(item -> | ||||
|         { | ||||
|             String imageSHA1 = FileUtils.INSTANCE.getSHA1(context.getContentResolver().openInputStream(item.getContentUri())); | ||||
| 
 | ||||
|             final Contribution contribution = new Contribution( | ||||
|                 item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories), imageSHA1); | ||||
| 
 | ||||
|             contribution.setHasInvalidLocation(item.hasInvalidLocation()); | ||||
| 
 | ||||
|             Timber.d("Created timestamp while building contribution is %s, %s", | ||||
|                 item.getCreatedTimestamp(), | ||||
|                 new Date(item.getCreatedTimestamp())); | ||||
| 
 | ||||
|             if (item.getCreatedTimestamp() != -1L) { | ||||
|                 contribution.setDateCreated(new Date(item.getCreatedTimestamp())); | ||||
|                 contribution.setDateCreatedSource(item.getCreatedTimestampSource()); | ||||
|                 //Set the date only if you have it, else the upload service is gonna try it the other way | ||||
|             } | ||||
| 
 | ||||
|             if (contribution.getWikidataPlace() != null) { | ||||
|                 if (item.isWLMUpload()) { | ||||
|                     contribution.getWikidataPlace().setMonumentUpload(true); | ||||
|                 } else { | ||||
|                     contribution.getWikidataPlace().setMonumentUpload(false); | ||||
|                 } | ||||
|             } | ||||
|             contribution.setCountryCode(item.getCountryCode()); | ||||
|             return contribution; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void deletePicture(final String filePath) { | ||||
|         final Iterator<UploadItem> iterator = items.iterator(); | ||||
|         while (iterator.hasNext()) { | ||||
|             if (iterator.next().getMediaUri().toString().contains(filePath)) { | ||||
|                 iterator.remove(); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (items.isEmpty()) { | ||||
|             cleanUp(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public List<UploadItem> getItems() { | ||||
|         return items; | ||||
|     } | ||||
| 
 | ||||
|     public void onDepictItemClicked(DepictedItem depictedItem, Media media) { | ||||
|         if (media == null) { | ||||
|             if (depictedItem.isSelected()) { | ||||
|                 selectedDepictions.add(depictedItem); | ||||
|             } else { | ||||
|                 selectedDepictions.remove(depictedItem); | ||||
|             } | ||||
|         } else { | ||||
|             if (depictedItem.isSelected()) { | ||||
|                 if (media.getDepictionIds().contains(depictedItem.getId())) { | ||||
|                     selectedExistingDepictions.add(depictedItem.getId()); | ||||
|                 } else { | ||||
|                     selectedDepictions.add(depictedItem); | ||||
|                 } | ||||
|             } else { | ||||
|                 if (media.getDepictionIds().contains(depictedItem.getId())) { | ||||
|                     selectedExistingDepictions.remove(depictedItem.getId()); | ||||
|                     if (!media.getDepictionIds().contains(depictedItem.getId())) { | ||||
|                         final List<String> depictsList = new ArrayList<>(); | ||||
|                         depictsList.add(depictedItem.getId()); | ||||
|                         depictsList.addAll(media.getDepictionIds()); | ||||
|                         media.setDepictionIds(depictsList); | ||||
|                     } | ||||
|                 } else { | ||||
|                     selectedDepictions.remove(depictedItem); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NotNull | ||||
|     private <T> List<T> newListOf(final List<T> items) { | ||||
|         return items != null ? new ArrayList<>(items) : new ArrayList<>(); | ||||
|     } | ||||
| 
 | ||||
|     public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) { | ||||
|         fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates); | ||||
|         items.get(uploadItemIndex).setGpsCoords(imageCoordinates); | ||||
|     } | ||||
| 
 | ||||
|     public List<DepictedItem> getSelectedDepictions() { | ||||
|         return selectedDepictions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provides selected existing depicts | ||||
|      * | ||||
|      * @return selected existing depicts | ||||
|      */ | ||||
|     public List<String> getSelectedExistingDepictions() { | ||||
|         return selectedExistingDepictions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize existing depicts | ||||
|      * | ||||
|      * @param selectedExistingDepictions existing depicts | ||||
|      */ | ||||
|     public void setSelectedExistingDepictions(final List<String> selectedExistingDepictions) { | ||||
|         this.selectedExistingDepictions = selectedExistingDepictions; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										242
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,242 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| 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.nearby.Place | ||||
| import fr.free.nrw.commons.settings.Prefs | ||||
| import fr.free.nrw.commons.upload.FileUtils.getSHA1 | ||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.Single | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import timber.log.Timber | ||||
| import java.util.Date | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class UploadModel @Inject internal constructor( | ||||
|     @param:Named("licenses") val licenses: List<String>, | ||||
|     @param:Named("default_preferences") val store: JsonKvStore, | ||||
|     @param:Named("licenses_by_name") val licensesByName: Map<String, String>, | ||||
|     val context: Context, | ||||
|     val sessionManager: SessionManager, | ||||
|     val fileProcessor: FileProcessor, | ||||
|     val imageProcessingService: ImageProcessingService | ||||
| ) { | ||||
|     var license: String? = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3) | ||||
|     val items: MutableList<UploadItem> = mutableListOf() | ||||
|     val compositeDisposable: CompositeDisposable = CompositeDisposable() | ||||
|     val selectedCategories: MutableList<String> = mutableListOf() | ||||
|     val selectedDepictions: MutableList<DepictedItem> = mutableListOf() | ||||
| 
 | ||||
|     /** | ||||
|      * Existing depicts which are selected | ||||
|      */ | ||||
|     var selectedExistingDepictions: MutableList<String> = mutableListOf() | ||||
|     val count: Int | ||||
|         get() = items.size | ||||
| 
 | ||||
|     val uploads: List<UploadItem> | ||||
|         get() = items | ||||
| 
 | ||||
|     var selectedLicense: String? | ||||
|         get() = license | ||||
|         set(licenseName) { | ||||
|             license = licensesByName[licenseName] | ||||
|             if (license == null) { | ||||
|                 store.remove(Prefs.DEFAULT_LICENSE) | ||||
|             } else { | ||||
|                 store.putString(Prefs.DEFAULT_LICENSE, license!!) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     /** | ||||
|      * cleanup the resources, I am Singleton, preparing for fresh upload | ||||
|      */ | ||||
|     fun cleanUp() { | ||||
|         compositeDisposable.clear() | ||||
|         fileProcessor.cleanup() | ||||
|         items.clear() | ||||
|         selectedCategories.clear() | ||||
|         selectedDepictions.clear() | ||||
|         selectedExistingDepictions.clear() | ||||
|     } | ||||
| 
 | ||||
|     fun setSelectedCategories(categories: List<String>) { | ||||
|         selectedCategories.clear() | ||||
|         selectedCategories.addAll(categories) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * pre process a one item at a time | ||||
|      */ | ||||
|     fun preProcessImage( | ||||
|         uploadableFile: UploadableFile?, | ||||
|         place: Place?, | ||||
|         similarImageInterface: SimilarImageInterface?, | ||||
|         inAppPictureLocation: LatLng? | ||||
|     ): Observable<UploadItem> = Observable.just( | ||||
|         createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation) | ||||
|     ) | ||||
| 
 | ||||
|     /** | ||||
|      * Calls validateImage() of ImageProcessingService to check quality of image | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose quality is to be checked | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @return Quality of UploadItem | ||||
|      */ | ||||
|     fun getImageQuality(uploadItem: UploadItem, inAppPictureLocation: LatLng?): Single<Int> = | ||||
|         imageProcessingService.validateImage(uploadItem, inAppPictureLocation) | ||||
| 
 | ||||
|     /** | ||||
|      * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate | ||||
|      * | ||||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     fun checkDuplicateImage(filePath: String?): Single<Int> = | ||||
|         imageProcessingService.checkDuplicateImage(filePath) | ||||
| 
 | ||||
|     /** | ||||
|      * Calls validateCaption() of ImageProcessingService to check caption of image | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose caption is to be checked | ||||
|      * @return Quality of caption of the UploadItem | ||||
|      */ | ||||
|     fun getCaptionQuality(uploadItem: UploadItem): Single<Int> = | ||||
|         imageProcessingService.validateCaption(uploadItem) | ||||
| 
 | ||||
|     private fun createAndAddUploadItem( | ||||
|         uploadableFile: UploadableFile?, | ||||
|         place: Place?, | ||||
|         similarImageInterface: SimilarImageInterface?, | ||||
|         inAppPictureLocation: LatLng? | ||||
|     ): UploadItem { | ||||
|         val dateTimeWithSource = uploadableFile?.getFileCreatedDate(context) | ||||
|         var fileCreatedDate: Long = -1 | ||||
|         var createdTimestampSource = "" | ||||
|         var fileCreatedDateString: String? = "" | ||||
|         if (dateTimeWithSource != null) { | ||||
|             fileCreatedDate = dateTimeWithSource.epochDate | ||||
|             fileCreatedDateString = dateTimeWithSource.dateString | ||||
|             createdTimestampSource = dateTimeWithSource.source | ||||
|         } | ||||
|         Timber.d("File created date is %d", fileCreatedDate) | ||||
|         val imageCoordinates = fileProcessor | ||||
|             .processFileCoordinates( | ||||
|                 similarImageInterface, uploadableFile?.getFilePath(), | ||||
|                 inAppPictureLocation | ||||
|             ) | ||||
|         val uploadItem = UploadItem( | ||||
|             Uri.parse(uploadableFile?.getFilePath()), | ||||
|             uploadableFile?.getMimeType(context), imageCoordinates, place, fileCreatedDate, | ||||
|             createdTimestampSource, | ||||
|             uploadableFile?.contentUri, | ||||
|             fileCreatedDateString | ||||
|         ) | ||||
| 
 | ||||
|         // If an uploadItem of the same uploadableFile has been created before, we return that. | ||||
|         // This is to avoid multiple instances of uploadItem of same file passed around. | ||||
|         if (items.contains(uploadItem)) { | ||||
|             return items[items.indexOf(uploadItem)] | ||||
|         } | ||||
| 
 | ||||
|         uploadItem.uploadMediaDetails[0] = UploadMediaDetail(place) | ||||
|         if (!items.contains(uploadItem)) { | ||||
|             items.add(uploadItem) | ||||
|         } | ||||
|         return uploadItem | ||||
|     } | ||||
| 
 | ||||
|     fun buildContributions(): Observable<Contribution> { | ||||
|         return Observable.fromIterable(items).map { item: UploadItem -> | ||||
|             val imageSHA1 = getSHA1( | ||||
|                 context.contentResolver.openInputStream(item.contentUri!!)!! | ||||
|             ) | ||||
|             val contribution = Contribution( | ||||
|                 item, | ||||
|                 sessionManager, | ||||
|                 buildList { addAll(selectedDepictions) }, | ||||
|                 buildList { addAll(selectedCategories) }, | ||||
|                 imageSHA1 | ||||
|             ) | ||||
| 
 | ||||
|             contribution.setHasInvalidLocation(item.hasInvalidLocation()) | ||||
| 
 | ||||
|             Timber.d( | ||||
|                 "Created timestamp while building contribution is %s, %s", | ||||
|                 item.createdTimestamp, | ||||
|                 item.createdTimestamp?.let { Date(it) } | ||||
|             ) | ||||
| 
 | ||||
|             if (item.createdTimestamp != -1L) { | ||||
|                 contribution.dateCreated = item.createdTimestamp?.let { Date(it) } | ||||
|                 contribution.dateCreatedSource = item.createdTimestampSource | ||||
|                 //Set the date only if you have it, else the upload service is gonna try it the other way | ||||
|             } | ||||
| 
 | ||||
|             if (contribution.wikidataPlace != null) { | ||||
|                 contribution.wikidataPlace!!.isMonumentUpload = item.isWLMUpload | ||||
|             } | ||||
|             contribution.countryCode = item.countryCode | ||||
|             contribution | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun deletePicture(filePath: String) { | ||||
|         val iterator = items.iterator() | ||||
|         while (iterator.hasNext()) { | ||||
|             if (iterator.next().mediaUri.toString().contains(filePath)) { | ||||
|                 iterator.remove() | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|         if (items.isEmpty()) { | ||||
|             cleanUp() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) { | ||||
|         if (media == null) { | ||||
|             if (depictedItem.isSelected) { | ||||
|                 selectedDepictions.add(depictedItem) | ||||
|             } else { | ||||
|                 selectedDepictions.remove(depictedItem) | ||||
|             } | ||||
|         } else { | ||||
|             if (depictedItem.isSelected) { | ||||
|                 if (media.depictionIds.contains(depictedItem.id)) { | ||||
|                     selectedExistingDepictions.add(depictedItem.id) | ||||
|                 } else { | ||||
|                     selectedDepictions.add(depictedItem) | ||||
|                 } | ||||
|             } else { | ||||
|                 if (media.depictionIds.contains(depictedItem.id)) { | ||||
|                     selectedExistingDepictions.remove(depictedItem.id) | ||||
|                     if (!media.depictionIds.contains(depictedItem.id)) { | ||||
|                         media.depictionIds = mutableListOf<String>().apply { | ||||
|                             add(depictedItem.id) | ||||
|                             addAll(media.depictionIds) | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     selectedDepictions.remove(depictedItem) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) { | ||||
|         fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates) | ||||
|         items[uploadItemIndex].gpsCoords = imageCoordinates | ||||
|     } | ||||
| } | ||||
|  | @ -1,10 +1,10 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.CommonsApplication.Companion.IS_LIMITED_CONNECTION_MODE_ENABLED | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import fr.free.nrw.commons.repository.UploadRepository | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract | ||||
|  | @ -34,6 +34,8 @@ class UploadPresenter @Inject internal constructor( | |||
| 
 | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     lateinit var basicKvStoreFactory: (String) -> BasicKvStore | ||||
| 
 | ||||
|     /** | ||||
|      * Called by the submit button in [UploadActivity] | ||||
|      */ | ||||
|  | @ -69,8 +71,7 @@ class UploadPresenter @Inject internal constructor( | |||
|     private fun processContributionsForSubmission() { | ||||
|         if (view.isLoggedIn()) { | ||||
|             view.showProgress(true) | ||||
|             repository.buildContributions() | ||||
|                 ?.observeOn(Schedulers.io()) | ||||
|             repository.buildContributions().observeOn(Schedulers.io()) | ||||
|                 ?.subscribe(object : Observer<Contribution> { | ||||
|                     override fun onSubscribe(d: Disposable) { | ||||
|                         view.showProgress(false) | ||||
|  | @ -127,14 +128,20 @@ class UploadPresenter @Inject internal constructor( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) { | ||||
|         basicKvStoreFactory = factory | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image | ||||
|      * | ||||
|      * @param uploadItemIndex Index of next image, whose quality is to be checked | ||||
|      */ | ||||
|     override fun checkImageQuality(uploadItemIndex: Int) { | ||||
|         val uploadItem = repository.getUploadItem(uploadItemIndex) | ||||
|         presenter.checkImageQuality(uploadItem, uploadItemIndex) | ||||
|         repository.getUploadItem(uploadItemIndex)?.let { | ||||
|             presenter.setupBasicKvStoreFactory(basicKvStoreFactory) | ||||
|             presenter.checkImageQuality(it, uploadItemIndex) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun deletePictureAtIndex(index: Int) { | ||||
|  | @ -156,8 +163,9 @@ class UploadPresenter @Inject internal constructor( | |||
|         view.onUploadMediaDeleted(index) | ||||
|         if (index != uploadableFiles.size && index != 0) { | ||||
|             // if the deleted image was not the last item to be uploaded, check quality of next | ||||
|             val uploadItem = repository.getUploadItem(index) | ||||
|             presenter.checkImageQuality(uploadItem, index) | ||||
|             repository.getUploadItem(index)?.let { | ||||
|                 presenter.checkImageQuality(it, index) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (uploadableFiles.size < 2) { | ||||
|  |  | |||
|  | @ -140,7 +140,7 @@ class CategoriesPresenter | |||
|          */ | ||||
|         private fun getImageTitleList(): List<String> = | ||||
|             repository.getUploads() | ||||
|                 .map { it.uploadMediaDetails[0].captionText } | ||||
|                 .map { it.uploadMediaDetails[0].captionText!! } | ||||
|                 .filterNot { TextUtils.isEmpty(it) } | ||||
| 
 | ||||
|         /** | ||||
|  |  | |||
|  | @ -95,12 +95,10 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | |||
|         } | ||||
|         if (media == null) { | ||||
|             if (callback != null) { | ||||
|                 binding!!.tvTitle.text = getString( | ||||
|                     R.string.step_count, callback!!.getIndexInViewFlipper( | ||||
|                         this | ||||
|                     ) + 1, | ||||
|                     callback!!.totalNumberOfSteps, getString(R.string.categories_activity_title) | ||||
|                 ) | ||||
|                 binding!!.tvTitle.text = getString(R.string.step_count, | ||||
|                     callback!!.getIndexInViewFlipper(this) + 1, | ||||
|                     callback!!.totalNumberOfSteps, | ||||
|                     getString(R.string.categories_activity_title)) | ||||
|             } | ||||
|         } else { | ||||
|             binding!!.tvTitle.setText(R.string.edit_categories) | ||||
|  | @ -220,7 +218,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | |||
|     } | ||||
| 
 | ||||
|     override fun goToNextScreen() { | ||||
|         callback!!.onNextButtonClicked(callback!!.getIndexInViewFlipper(this)) | ||||
|         callback?.let { it.onNextButtonClicked(it.getIndexInViewFlipper(this)) } | ||||
|     } | ||||
| 
 | ||||
|     override fun showNoCategorySelected() { | ||||
|  | @ -322,7 +320,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | |||
|             mediaDetailFragment.onResume() | ||||
|             goBackToPreviousScreen() | ||||
|         } else { | ||||
|             callback!!.onPreviousButtonClicked(callback!!.getIndexInViewFlipper(this)) | ||||
|             callback?.let { it.onPreviousButtonClicked(it.getIndexInViewFlipper(this)) } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -96,11 +96,10 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View { | |||
| 
 | ||||
|         if (media == null) { | ||||
|             binding.depictsTitle.text = | ||||
|                 String.format( | ||||
|                     getString(R.string.step_count), callback!!.getIndexInViewFlipper( | ||||
|                         this | ||||
|                     ) + 1, | ||||
|                     callback!!.totalNumberOfSteps, getString(R.string.depicts_step_title) | ||||
|                 String.format(getString(R.string.step_count), | ||||
|                     callback!!.getIndexInViewFlipper(this) + 1, | ||||
|                     callback!!.totalNumberOfSteps, | ||||
|                     getString(R.string.depicts_step_title) | ||||
|                 ) | ||||
|         } else { | ||||
|             binding.depictsTitle.setText(R.string.edit_depictions) | ||||
|  |  | |||
|  | @ -45,8 +45,7 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View { | |||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         binding.tvTitle.text = getString( | ||||
|             R.string.step_count, | ||||
|         binding.tvTitle.text = getString(R.string.step_count, | ||||
|             callback!!.getIndexInViewFlipper(this) + 1, | ||||
|             callback!!.totalNumberOfSteps, | ||||
|             getString(R.string.license_step_title) | ||||
|  |  | |||
|  | @ -1,922 +0,0 @@ | |||
| package fr.free.nrw.commons.upload.mediaDetails; | ||||
| 
 | ||||
| import static android.app.Activity.RESULT_OK; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.content.Intent; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.speech.RecognizerIntent; | ||||
| import android.text.TextUtils; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.CheckBox; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResult; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.exifinterface.media.ExifInterface; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import fr.free.nrw.commons.CameraPosition; | ||||
| import fr.free.nrw.commons.locationpicker.LocationPicker; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.contributions.MainActivity; | ||||
| import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding; | ||||
| import fr.free.nrw.commons.edit.EditActivity; | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; | ||||
| import fr.free.nrw.commons.settings.Prefs; | ||||
| import fr.free.nrw.commons.upload.ImageCoordinates; | ||||
| import fr.free.nrw.commons.upload.SimilarImageDialogFragment; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.upload.UploadBaseFragment; | ||||
| import fr.free.nrw.commons.upload.UploadItem; | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetail; | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter; | ||||
| import fr.free.nrw.commons.utils.ActivityUtils; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.ImageUtils; | ||||
| import fr.free.nrw.commons.utils.NetworkUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class UploadMediaDetailFragment extends UploadBaseFragment implements | ||||
|     UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener { | ||||
| 
 | ||||
|     private UploadMediaDetailAdapter uploadMediaDetailAdapter; | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> startForResult = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|                 onCameraPosition(result); | ||||
|         }); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> startForEditActivityResult = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|             onEditActivityResult(result); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|             onVoiceInput(result); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     public static Activity activity ; | ||||
| 
 | ||||
|     private int indexOfFragment; | ||||
| 
 | ||||
|     /** | ||||
|      * A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex. | ||||
|      * 12.3433,54.78897 from applicationKvStore. | ||||
|      */ | ||||
|     public static final String LAST_LOCATION = "last_location_while_uploading"; | ||||
|     public static final String LAST_ZOOM = "last_zoom_level_while_uploading"; | ||||
| 
 | ||||
| 
 | ||||
|     public static final String UPLOADABLE_FILE = "uploadable_file"; | ||||
| 
 | ||||
|     public static final String UPLOAD_MEDIA_DETAILS = "upload_media_detail_adapter"; | ||||
| 
 | ||||
|     /** | ||||
|      * True when user removes location from the current image | ||||
|      */ | ||||
|     private boolean hasUserRemovedLocation; | ||||
| 
 | ||||
| 
 | ||||
|     @Inject | ||||
|     UploadMediaDetailsContract.UserActionListener presenter; | ||||
| 
 | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     JsonKvStore defaultKvStore; | ||||
| 
 | ||||
|     @Inject | ||||
|     RecentLanguagesDao recentLanguagesDao; | ||||
| 
 | ||||
|     private UploadableFile uploadableFile; | ||||
|     private Place place; | ||||
| 
 | ||||
|     private boolean isExpanded = true; | ||||
| 
 | ||||
|     /** | ||||
|      * True if location is added via the "missing location" popup dialog (which appears after | ||||
|      * tapping "Next" if the picture has no geographical coordinates). | ||||
|      */ | ||||
|     private boolean isMissingLocationDialog; | ||||
| 
 | ||||
|     /** | ||||
|      * showNearbyFound will be true, if any nearby location found that needs pictures and the nearby | ||||
|      * popup is yet to be shown Used to show and check if the nearby found popup is already shown | ||||
|      */ | ||||
|     private boolean showNearbyFound; | ||||
| 
 | ||||
|     /** | ||||
|      * nearbyPlace holds the detail of nearby place that need pictures, if any found | ||||
|      */ | ||||
|     private Place nearbyPlace; | ||||
|     private UploadItem uploadItem; | ||||
|     /** | ||||
|      * inAppPictureLocation: use location recorded while using the in-app camera if device camera | ||||
|      * does not record it in the EXIF | ||||
|      */ | ||||
|     private LatLng inAppPictureLocation; | ||||
|     /** | ||||
|      * editableUploadItem : Storing the upload item before going to update the coordinates | ||||
|      */ | ||||
|     private UploadItem editableUploadItem; | ||||
| 
 | ||||
|     private BasicKvStore basicKvStore; | ||||
|      | ||||
|     private final String keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing"; | ||||
| 
 | ||||
|     private UploadMediaDetailFragmentCallback callback; | ||||
| 
 | ||||
|     private FragmentUploadMediaDetailFragmentBinding binding; | ||||
| 
 | ||||
|     public void setCallback(UploadMediaDetailFragmentCallback callback) { | ||||
|         this.callback = callback; | ||||
|         UploadMediaPresenter.presenterCallback = callback; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
| 
 | ||||
| 
 | ||||
|         if(savedInstanceState!=null && uploadableFile==null) { | ||||
|             uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     public void setImageToBeUploaded(UploadableFile uploadableFile, Place place, | ||||
|         LatLng inAppPictureLocation) { | ||||
|         this.uploadableFile = uploadableFile; | ||||
|         this.place = place; | ||||
|         this.inAppPictureLocation = inAppPictureLocation; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
|         binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false); | ||||
|         return binding.getRoot(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
| 
 | ||||
|         activity = getActivity(); | ||||
|         basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities"); | ||||
| 
 | ||||
|         if (callback != null) { | ||||
|             indexOfFragment = callback.getIndexInViewFlipper(this); | ||||
|             init(); | ||||
|         } | ||||
| 
 | ||||
|         if(savedInstanceState!=null){ | ||||
|                 if(uploadMediaDetailAdapter.getItems().size()==0 && callback != null){ | ||||
|                     uploadMediaDetailAdapter.setItems(savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)); | ||||
|                     presenter.setUploadMediaDetails(uploadMediaDetailAdapter.getItems(), | ||||
|                         indexOfFragment); | ||||
|                 } | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) { | ||||
|                 ActivityUtils.startActivityWithFlags( | ||||
|                 getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, | ||||
|                 Intent.FLAG_ACTIVITY_SINGLE_TOP); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void init() { | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
|         binding.tvTitle.setText(getString(R.string.step_count, (indexOfFragment + 1), | ||||
|             callback.getTotalNumberOfSteps(), getString(R.string.media_detail_step_title))); | ||||
|         binding.tooltip.setOnClickListener( | ||||
|             v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip)); | ||||
|         initPresenter(); | ||||
|         presenter.receiveImage(uploadableFile, place, inAppPictureLocation); | ||||
|         initRecyclerView(); | ||||
| 
 | ||||
|         if (indexOfFragment == 0) { | ||||
|             binding.btnPrevious.setEnabled(false); | ||||
|             binding.btnPrevious.setAlpha(0.5f); | ||||
|         } else { | ||||
|             binding.btnPrevious.setEnabled(true); | ||||
|             binding.btnPrevious.setAlpha(1.0f); | ||||
|         } | ||||
| 
 | ||||
|         // If the image EXIF data contains the location, show the map icon with a green tick | ||||
|         if (inAppPictureLocation != null || | ||||
|                 (uploadableFile != null && uploadableFile.hasLocation())) { | ||||
|             Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); | ||||
|             binding.locationImageView.setImageDrawable(mapTick); | ||||
|             binding.locationTextView.setText(R.string.edit_location); | ||||
|         } else { | ||||
|             // Otherwise, show the map icon with a red question mark | ||||
|             Drawable mapQuestionMark = | ||||
|                 getResources().getDrawable(R.drawable.ic_map_not_available_20dp); | ||||
|             binding.locationImageView.setImageDrawable(mapQuestionMark); | ||||
|             binding.locationTextView.setText(R.string.add_location); | ||||
|         } | ||||
| 
 | ||||
|         //If this is the last media, we have nothing to copy, lets not show the button | ||||
|         if (indexOfFragment == callback.getTotalNumberOfSteps() - 4) { | ||||
|             binding.btnCopySubsequentMedia.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             binding.btnCopySubsequentMedia.setVisibility(View.VISIBLE); | ||||
|         } | ||||
| 
 | ||||
|         binding.btnNext.setOnClickListener(v -> onNextButtonClicked()); | ||||
|         binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked()); | ||||
|         binding.llEditImage.setOnClickListener(v -> onEditButtonClicked()); | ||||
|         binding.llContainerTitle.setOnClickListener(v -> onLlContainerTitleClicked()); | ||||
|         binding.llLocationStatus.setOnClickListener(v -> onIbMapClicked()); | ||||
|         binding.btnCopySubsequentMedia.setOnClickListener(v -> onButtonCopyTitleDescToSubsequentMedia()); | ||||
| 
 | ||||
| 
 | ||||
|         attachImageViewScaleChangeListener(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches the scale change listener to the image view | ||||
|      */ | ||||
|     private void attachImageViewScaleChangeListener() { | ||||
|         binding.backgroundImage.setOnScaleChangeListener( | ||||
|             (scaleFactor, focusX, focusY) -> { | ||||
|                 //Whenever the uses plays with the image, lets collapse the media detail container | ||||
|                 //only if it is not already collapsed, which resolves flickering of arrow | ||||
|                 if (isExpanded) { | ||||
|                     expandCollapseLlMediaDetail(false); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * attach the presenter with the view | ||||
|      */ | ||||
|     private void initPresenter() { | ||||
|         presenter.onAttachView(this); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * init the description recycler veiw and caption recyclerview | ||||
|      */ | ||||
|     private void initRecyclerView() { | ||||
|         uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this, | ||||
|             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher); | ||||
|         uploadMediaDetailAdapter.setCallback(this::showInfoAlert); | ||||
|         uploadMediaDetailAdapter.setEventListener(this); | ||||
|         binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         binding.rvDescriptions.setAdapter(uploadMediaDetailAdapter); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * show dialog with info | ||||
|      * @param titleStringID | ||||
|      * @param messageStringId | ||||
|      */ | ||||
|     private void showInfoAlert(int titleStringID, int messageStringId) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), | ||||
|             getString(messageStringId), getString(android.R.string.ok), null); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public void onNextButtonClicked() { | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation); | ||||
|     } | ||||
| 
 | ||||
|     public void onPreviousButtonClicked() { | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         callback.onPreviousButtonClicked(indexOfFragment); | ||||
|     } | ||||
| 
 | ||||
|     public void onEditButtonClicked() { | ||||
|         presenter.onEditButtonClicked(indexOfFragment); | ||||
|     } | ||||
|     @Override | ||||
|     public void showSimilarImageFragment(String originalFilePath, String possibleFilePath, | ||||
|         ImageCoordinates similarImageCoordinates) { | ||||
|         BasicKvStore basicKvStore = new BasicKvStore(getActivity(), "IsAnyImageCancelled"); | ||||
|         if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) { | ||||
|             SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); | ||||
|             newFragment.setCancelable(false); | ||||
|             newFragment.setCallback(new SimilarImageDialogFragment.Callback() { | ||||
|                 @Override | ||||
|                 public void onPositiveResponse() { | ||||
|                     Timber.d("positive response from similar image fragment"); | ||||
|                     presenter.useSimilarPictureCoordinates(similarImageCoordinates, | ||||
|                         indexOfFragment); | ||||
| 
 | ||||
|                     // set the description text when user selects to use coordinate from the other image | ||||
|                     // which was taken within 120s | ||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/4700 | ||||
|                     uploadMediaDetailAdapter.getItems().get(0).setDescriptionText( | ||||
|                         getString(R.string.similar_coordinate_description_auto_set)); | ||||
|                     updateMediaDetails(uploadMediaDetailAdapter.getItems()); | ||||
| 
 | ||||
|                     // Replace the 'Add location' button with 'Edit location' button when user clicks | ||||
|                     // yes in similar image dialog | ||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/5669 | ||||
|                     Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); | ||||
|                     binding.locationImageView.setImageDrawable(mapTick); | ||||
|                     binding.locationTextView.setText(R.string.edit_location); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onNegativeResponse() { | ||||
|                     Timber.d("negative response from similar image fragment"); | ||||
|                 } | ||||
|             }); | ||||
|             Bundle args = new Bundle(); | ||||
|             args.putString("originalImagePath", originalFilePath); | ||||
|             args.putString("possibleImagePath", possibleFilePath); | ||||
|             newFragment.setArguments(args); | ||||
|             newFragment.show(getChildFragmentManager(), "dialog"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onImageProcessed(UploadItem uploadItem, Place place) { | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
|         binding.backgroundImage.setImageURI(uploadItem.getMediaUri()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets variables to Show popup if any nearby location needing pictures matches uploadable picture's GPS location | ||||
|      * @param uploadItem | ||||
|      * @param place | ||||
|      */ | ||||
|     @Override | ||||
|     public void onNearbyPlaceFound(UploadItem uploadItem, Place place) { | ||||
|         nearbyPlace = place; | ||||
|         this.uploadItem = uploadItem; | ||||
|         showNearbyFound = true; | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         if (indexOfFragment == 0) { | ||||
|             if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { | ||||
|                 final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); | ||||
|                 if (response) { | ||||
|                     if (callback != null) { | ||||
|                         presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 showNearbyPlaceFound(nearbyPlace); | ||||
|             } | ||||
|             showNearbyFound = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows nearby place found popup | ||||
|      * @param place | ||||
|      */ | ||||
|     @SuppressLint("StringFormatInvalid") | ||||
|     // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format | ||||
|     private void showNearbyPlaceFound(Place place) { | ||||
|         final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null); | ||||
|         ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage); | ||||
|         nearbyFoundImage.setImageURI(uploadItem.getMediaUri()); | ||||
| 
 | ||||
|         final Activity activity = getActivity(); | ||||
| 
 | ||||
|         if (activity instanceof UploadActivity) { | ||||
|             final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); | ||||
| 
 | ||||
|             // Determine the message based on the selection status | ||||
|             String message; | ||||
|             if (isMultipleFilesSelected) { | ||||
|                 // Use plural message if multiple files are selected | ||||
|                 message = String.format(Locale.getDefault(), | ||||
|                     getString(R.string.upload_nearby_place_found_description_plural), | ||||
|                     place.getName()); | ||||
|             } else { | ||||
|                 // Use singular message if only one file is selected | ||||
|                 message = String.format(Locale.getDefault(), | ||||
|                     getString(R.string.upload_nearby_place_found_description_singular), | ||||
|                     place.getName()); | ||||
|             } | ||||
| 
 | ||||
|             // Show the AlertDialog with the determined message | ||||
|             DialogUtil.showAlertDialog(getActivity(), | ||||
|                 getString(R.string.upload_nearby_place_found_title), | ||||
|                 message, | ||||
|                 () -> { | ||||
|                     // Execute when user confirms the upload is of the specified place | ||||
|                     UploadActivity.nearbyPopupAnswers.put(place, true); | ||||
|                     presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment); | ||||
|                 }, | ||||
|                 () -> { | ||||
|                     // Execute when user cancels the upload of the specified place | ||||
|                     UploadActivity.nearbyPopupAnswers.put(place, false); | ||||
|                 }, | ||||
|                 customLayout | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showProgress(boolean shouldShow) { | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         callback.showProgress(shouldShow); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onImageValidationSuccess() { | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         callback.onNextButtonClicked(indexOfFragment); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method gets called whenever the next/previous button is pressed | ||||
|      */ | ||||
|     @Override | ||||
|     protected void onBecameVisible() { | ||||
|         super.onBecameVisible(); | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         presenter.fetchTitleAndDescription(indexOfFragment); | ||||
|         if (showNearbyFound) { | ||||
|             if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { | ||||
|                 final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); | ||||
|                 if (response) { | ||||
|                     presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment); | ||||
|                 } | ||||
|             } else { | ||||
|                 showNearbyPlaceFound(nearbyPlace); | ||||
|             } | ||||
|             showNearbyFound = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showMessage(int stringResourceId, int colorResourceId) { | ||||
|         ViewUtil.showLongToast(getContext(), stringResourceId); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showMessage(String message, int colorResourceId) { | ||||
|         ViewUtil.showLongToast(getContext(), message); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showDuplicatePicturePopup(UploadItem uploadItem) { | ||||
|         if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) { | ||||
|             String uploadTitleFormat = getString(R.string.upload_title_duplicate); | ||||
|             View checkBoxView = View | ||||
|                 .inflate(getActivity(), R.layout.nearby_permission_dialog, null); | ||||
|             CheckBox checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); | ||||
|             checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||
|                 if (isChecked) { | ||||
|                     defaultKvStore.putBoolean("showDuplicatePicturePopup", false); | ||||
|                 } | ||||
|             }); | ||||
|             DialogUtil.showAlertDialog(getActivity(), | ||||
|                 getString(R.string.duplicate_file_name), | ||||
|                 String.format(Locale.getDefault(), | ||||
|                     uploadTitleFormat, | ||||
|                     uploadItem.getFilename()), | ||||
|                 getString(R.string.upload), | ||||
|                 getString(R.string.cancel), | ||||
|                 () -> { | ||||
|                     uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); | ||||
|                     onImageValidationSuccess(); | ||||
|                 }, null, | ||||
|                 checkBoxView); | ||||
|         } else { | ||||
|             uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP); | ||||
|             onImageValidationSuccess(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user that internet connection is required for upload process | ||||
|      * Does nothing if there is network connectivity and then the user presses okay | ||||
|      */ | ||||
|     @Override | ||||
|     public void showConnectionErrorPopupForCaptionCheck() { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             getString(R.string.upload_connection_error_alert_title), | ||||
|             getString(R.string.upload_connection_error_alert_detail), | ||||
|             getString(R.string.ok), | ||||
|             getString(R.string.cancel_upload), | ||||
|             () -> { | ||||
|                 if (!NetworkUtils.isInternetConnectionEstablished(activity)) { | ||||
|                     showConnectionErrorPopupForCaptionCheck(); | ||||
|                 } | ||||
|             }, | ||||
|             () -> { | ||||
|                 activity.finish(); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user that internet connection is required for upload process | ||||
|      * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, | ||||
|      * if there is network connectivity and then the user presses okay | ||||
|      */ | ||||
|     @Override | ||||
|     public void showConnectionErrorPopup() { | ||||
|         try { | ||||
|             boolean FLAG_ALERT_DIALOG_SHOWING = basicKvStore.getBoolean( | ||||
|                 keyForShowingAlertDialog, false); | ||||
|             if (!FLAG_ALERT_DIALOG_SHOWING) { | ||||
|                 basicKvStore.putBoolean(keyForShowingAlertDialog, true); | ||||
|                 DialogUtil.showAlertDialog(getActivity(), | ||||
|                     getString(R.string.upload_connection_error_alert_title), | ||||
|                     getString(R.string.upload_connection_error_alert_detail), | ||||
|                     getString(R.string.ok), | ||||
|                     getString(R.string.cancel_upload), | ||||
|                     () -> { | ||||
|                         basicKvStore.putBoolean(keyForShowingAlertDialog, false); | ||||
|                         if (NetworkUtils.isInternetConnectionEstablished(activity)) { | ||||
|                             int sizeOfUploads = basicKvStore.getInt( | ||||
|                                 UploadActivity.keyForCurrentUploadImagesSize); | ||||
|                             for (int i = indexOfFragment; i < sizeOfUploads; i++) { | ||||
|                                 presenter.getImageQuality(i, inAppPictureLocation, activity); | ||||
|                             } | ||||
|                         } else { | ||||
|                             showConnectionErrorPopup(); | ||||
|                         } | ||||
|                     }, | ||||
|                     () -> { | ||||
|                         basicKvStore.putBoolean(keyForShowingAlertDialog, false); | ||||
|                         activity.finish(); | ||||
|                     }, | ||||
|                     null | ||||
|                 ); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showExternalMap(final UploadItem uploadItem) { | ||||
|         goToLocationPickerActivity(uploadItem); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launches the image editing activity to edit the specified UploadItem. | ||||
|      * | ||||
|      * @param uploadItem The UploadItem to be edited. | ||||
|      * | ||||
|      * This method is called to start the image editing activity for a specific UploadItem. | ||||
|      * It sets the UploadItem as the currently editable item, creates an intent to launch the | ||||
|      * EditActivity, and passes the image file path as an extra in the intent. The activity | ||||
|      * is started using resultLauncher that handles the result in respective callback. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showEditActivity(UploadItem uploadItem) { | ||||
|         editableUploadItem = uploadItem; | ||||
|         Intent intent = new Intent(getContext(), EditActivity.class); | ||||
|         intent.putExtra("image", uploadableFile.getFilePath().toString()); | ||||
|         startForEditActivityResult.launch(intent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start Location picker activity. Show the location first then user can modify it by clicking | ||||
|      * modify location button. | ||||
|      * @param uploadItem current upload item | ||||
|      */ | ||||
|     private void goToLocationPickerActivity(final UploadItem uploadItem) { | ||||
| 
 | ||||
|         editableUploadItem = uploadItem; | ||||
|         double defaultLatitude = 37.773972; | ||||
|         double defaultLongitude = -122.431297; | ||||
|         double defaultZoom = 16.0; | ||||
| 
 | ||||
|         final Intent locationPickerIntent; | ||||
| 
 | ||||
|         /* Retrieve image location from EXIF if present or | ||||
|            check if user has provided location while using the in-app camera. | ||||
|            Use location of last UploadItem if none of them is available */ | ||||
|         if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords() | ||||
|             .getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) { | ||||
|             defaultLatitude = uploadItem.getGpsCoords() | ||||
|                 .getDecLatitude(); | ||||
|             defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); | ||||
|             defaultZoom = uploadItem.getGpsCoords().getZoomLevel(); | ||||
| 
 | ||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) | ||||
|                 .activityKey("UploadActivity") | ||||
|                 .build(getActivity()); | ||||
|         } else { | ||||
|             if (defaultKvStore.getString(LAST_LOCATION) != null) { | ||||
|                 final String[] locationLatLng | ||||
|                     = defaultKvStore.getString(LAST_LOCATION).split(","); | ||||
|                 defaultLatitude = Double.parseDouble(locationLatLng[0]); | ||||
|                 defaultLongitude = Double.parseDouble(locationLatLng[1]); | ||||
|             } | ||||
|             if (defaultKvStore.getString(LAST_ZOOM) != null) { | ||||
|                 defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); | ||||
|             } | ||||
| 
 | ||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) | ||||
|                 .activityKey("NoLocationUploadActivity") | ||||
|                 .build(getActivity()); | ||||
|         } | ||||
|         startForResult.launch(locationPickerIntent); | ||||
|     } | ||||
| 
 | ||||
|     private void onCameraPosition(ActivityResult result){ | ||||
|         if (result.getResultCode() == RESULT_OK) { | ||||
| 
 | ||||
|             assert result.getData() != null; | ||||
|             final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData()); | ||||
| 
 | ||||
|             if (cameraPosition != null) { | ||||
| 
 | ||||
|                 final String latitude = String.valueOf(cameraPosition.getLatitude()); | ||||
|                 final String longitude = String.valueOf(cameraPosition.getLongitude()); | ||||
|                 final double zoom = cameraPosition.getZoom(); | ||||
| 
 | ||||
|                 editLocation(latitude, longitude, zoom); | ||||
|                 // If isMissingLocationDialog is true, it means that the user has already tapped the | ||||
|                 // "Next" button, so go directly to the next step. | ||||
|                 if (isMissingLocationDialog) { | ||||
|                     isMissingLocationDialog = false; | ||||
|                     onNextButtonClicked(); | ||||
|                 } | ||||
|             } else { | ||||
|                 // If camera position is null means location is removed by the user | ||||
|                 removeLocation(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void onVoiceInput(ActivityResult result) { | ||||
|         if (result.getResultCode() == RESULT_OK && result.getData() != null) { | ||||
|             ArrayList<String> resultData = result.getData().getStringArrayListExtra( | ||||
|                 RecognizerIntent.EXTRA_RESULTS); | ||||
|             uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0)); | ||||
|         }else { | ||||
|             Timber.e("Error %s", result.getResultCode()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void onEditActivityResult(ActivityResult result){ | ||||
|         if (result.getResultCode() == RESULT_OK) { | ||||
|             String path = result.getData().getStringExtra("editedImageFilePath"); | ||||
| 
 | ||||
|             if (Objects.equals(result, "Error")) { | ||||
|                 Timber.e("Error in rotating image"); | ||||
|                 return; | ||||
|             } | ||||
|             try { | ||||
|                 if (binding != null){ | ||||
|                     binding.backgroundImage.setImageURI(Uri.fromFile(new File(path))); | ||||
|                 } | ||||
|                 editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path))); | ||||
|                 callback.changeThumbnail(indexOfFragment, | ||||
|                     path); | ||||
|             } catch (Exception e) { | ||||
|                 Timber.e(e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes the location data from the image, by setting them to null | ||||
|      */ | ||||
|     public void removeLocation() { | ||||
|         editableUploadItem.getGpsCoords().setDecimalCoords(null); | ||||
|         try { | ||||
|             ExifInterface sourceExif = new ExifInterface(uploadableFile.getFilePath()); | ||||
|             String[] exifTags = { | ||||
|                 ExifInterface.TAG_GPS_LATITUDE, | ||||
|                 ExifInterface.TAG_GPS_LATITUDE_REF, | ||||
|                 ExifInterface.TAG_GPS_LONGITUDE, | ||||
|                 ExifInterface.TAG_GPS_LONGITUDE_REF, | ||||
|             }; | ||||
| 
 | ||||
|             for (String tag : exifTags) { | ||||
|                 sourceExif.setAttribute(tag, null); | ||||
|             } | ||||
|             sourceExif.saveAttributes(); | ||||
| 
 | ||||
|             Drawable mapQuestion = getResources().getDrawable(R.drawable.ic_map_not_available_20dp); | ||||
| 
 | ||||
|             if (binding != null) { | ||||
|                 binding.locationImageView.setImageDrawable(mapQuestion); | ||||
|                 binding.locationTextView.setText(R.string.add_location); | ||||
|             } | ||||
| 
 | ||||
|             editableUploadItem.getGpsCoords().setDecLatitude(0.0); | ||||
|             editableUploadItem.getGpsCoords().setDecLongitude(0.0); | ||||
|             editableUploadItem.getGpsCoords().setImageCoordsExists(false); | ||||
|             hasUserRemovedLocation = true; | ||||
| 
 | ||||
|             Toast.makeText(getContext(), getString(R.string.location_removed), Toast.LENGTH_LONG) | ||||
|                 .show(); | ||||
|         } catch (Exception e) { | ||||
|             Timber.d(e); | ||||
|             Toast.makeText(getContext(), "Location could not be removed due to internal error", | ||||
|                 Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the old coordinates with new one | ||||
|      * @param latitude new latitude | ||||
|      * @param longitude new longitude | ||||
|      */ | ||||
|     public void editLocation(final String latitude, final String longitude, final double zoom) { | ||||
| 
 | ||||
|         editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude)); | ||||
|         editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude)); | ||||
|         editableUploadItem.getGpsCoords().setDecimalCoords(latitude + "|" + longitude); | ||||
|         editableUploadItem.getGpsCoords().setImageCoordsExists(true); | ||||
|         editableUploadItem.getGpsCoords().setZoomLevel(zoom); | ||||
| 
 | ||||
|         // Replace the map icon using the one with a green tick | ||||
|         Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp); | ||||
| 
 | ||||
|         if (binding != null) { | ||||
|             binding.locationImageView.setImageDrawable(mapTick); | ||||
|             binding.locationTextView.setText(R.string.edit_location); | ||||
|         } | ||||
| 
 | ||||
|         Toast.makeText(getContext(), getString(R.string.location_updated), Toast.LENGTH_LONG).show(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails) { | ||||
|         uploadMediaDetailAdapter.setItems(uploadMediaDetails); | ||||
|         showNearbyFound = | ||||
|             showNearbyFound && ( | ||||
|                 uploadMediaDetails == null || uploadMediaDetails.isEmpty() | ||||
|                     || listContainsEmptyDetails( | ||||
|                     uploadMediaDetails)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * if the media details that come in here are empty | ||||
|      * (empty caption AND empty description, with caption being the decider here) | ||||
|      * this method allows usage of nearby place caption and description if any | ||||
|      * else it takes the media details saved in prior for this picture | ||||
|      * @param uploadMediaDetails saved media details, | ||||
|      *                           ex: in case when "copy to subsequent media" button is clicked | ||||
|      *                           for a previous image | ||||
|      * @return boolean whether the details are empty or not | ||||
|      */ | ||||
|     private boolean listContainsEmptyDetails(List<UploadMediaDetail> uploadMediaDetails) { | ||||
|         for (UploadMediaDetail uploadDetail: uploadMediaDetails) { | ||||
|             if (!TextUtils.isEmpty(uploadDetail.getCaptionText()) && !TextUtils.isEmpty(uploadDetail.getDescriptionText())) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Showing dialog for adding location | ||||
|      * | ||||
|      * @param onSkipClicked proceed for verifying image quality | ||||
|      */ | ||||
|     @Override | ||||
|     public void displayAddLocationDialog(final Runnable onSkipClicked) { | ||||
|         isMissingLocationDialog = true; | ||||
|         DialogUtil.showAlertDialog(requireActivity(), | ||||
|             getString(R.string.no_location_found_title), | ||||
|             getString(R.string.no_location_found_message), | ||||
|             getString(R.string.add_location), | ||||
|             getString(R.string.skip_login), | ||||
|             this::onIbMapClicked, | ||||
|             onSkipClicked); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         presenter.onDetachView(); | ||||
|     } | ||||
| 
 | ||||
|     public void onLlContainerTitleClicked() { | ||||
|         expandCollapseLlMediaDetail(!isExpanded); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * show hide media detail based on | ||||
|      * @param shouldExpand | ||||
|      */ | ||||
|     private void expandCollapseLlMediaDetail(boolean shouldExpand){ | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
|         binding.llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE); | ||||
|         isExpanded = !isExpanded; | ||||
|         binding.ibExpandCollapse.setRotation(binding.ibExpandCollapse.getRotation() + 180); | ||||
|     } | ||||
| 
 | ||||
|     public void onIbMapClicked() { | ||||
|         if (callback == null) { | ||||
|             return; | ||||
|         } | ||||
|         presenter.onMapIconClicked(indexOfFragment); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPrimaryCaptionTextChange(boolean isNotEmpty) { | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
|         binding.btnCopySubsequentMedia.setEnabled(isNotEmpty); | ||||
|         binding.btnCopySubsequentMedia.setClickable(isNotEmpty); | ||||
|         binding.btnCopySubsequentMedia.setAlpha(isNotEmpty ? 1.0f : 0.5f); | ||||
|         binding.btnNext.setEnabled(isNotEmpty); | ||||
|         binding.btnNext.setClickable(isNotEmpty); | ||||
|         binding.btnNext.setAlpha(isNotEmpty ? 1.0f : 0.5f); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds new language item to RecyclerView | ||||
|      */ | ||||
|     @Override | ||||
|     public void addLanguage() { | ||||
|         UploadMediaDetail uploadMediaDetail = new UploadMediaDetail(); | ||||
|         uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user | ||||
|         uploadMediaDetailAdapter.addDescription(uploadMediaDetail); | ||||
|         binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1); | ||||
|     } | ||||
| 
 | ||||
|     public interface UploadMediaDetailFragmentCallback extends Callback { | ||||
| 
 | ||||
|         void deletePictureAtIndex(int index); | ||||
| 
 | ||||
|         void changeThumbnail(int index, String uri); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public void onButtonCopyTitleDescToSubsequentMedia(){ | ||||
|         presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment); | ||||
|         Toast.makeText(getContext(), getResources().getString(R.string.copied_successfully), Toast.LENGTH_SHORT).show(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(final Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
| 
 | ||||
|         if(uploadableFile!=null){ | ||||
|             outState.putParcelable(UPLOADABLE_FILE,uploadableFile); | ||||
|         } | ||||
|         if(uploadMediaDetailAdapter!=null){ | ||||
|             outState.putParcelableArrayList(UPLOAD_MEDIA_DETAILS, | ||||
|                 (ArrayList<? extends Parcelable>) uploadMediaDetailAdapter.getItems()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         binding = null; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,903 @@ | |||
| package fr.free.nrw.commons.upload.mediaDetails | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.speech.RecognizerIntent | ||||
| import android.text.TextUtils | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.CheckBox | ||||
| import android.widget.CompoundButton | ||||
| import android.widget.ImageView | ||||
| import android.widget.Toast | ||||
| import androidx.activity.result.ActivityResult | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.os.bundleOf | ||||
| import androidx.exifinterface.media.ExifInterface | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import fr.free.nrw.commons.CameraPosition | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.contributions.MainActivity | ||||
| import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding | ||||
| import fr.free.nrw.commons.edit.EditActivity | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.locationpicker.LocationPicker | ||||
| import fr.free.nrw.commons.locationpicker.LocationPicker.getCameraPosition | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||
| import fr.free.nrw.commons.settings.Prefs | ||||
| import fr.free.nrw.commons.upload.ImageCoordinates | ||||
| import fr.free.nrw.commons.upload.SimilarImageDialogFragment | ||||
| import fr.free.nrw.commons.upload.UploadActivity | ||||
| import fr.free.nrw.commons.upload.UploadBaseFragment | ||||
| import fr.free.nrw.commons.upload.UploadItem | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetail | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter.Companion.presenterCallback | ||||
| import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.ImageUtils | ||||
| import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK | ||||
| import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult | ||||
| import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| import java.util.ArrayList | ||||
| import java.util.Locale | ||||
| import java.util.Objects | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View, | ||||
|     UploadMediaDetailAdapter.EventListener { | ||||
| 
 | ||||
|     private val startForResult = registerForActivityResult<Intent, ActivityResult>( | ||||
|         ActivityResultContracts.StartActivityForResult(), ::onCameraPosition) | ||||
| 
 | ||||
|     private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>( | ||||
|         ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult) | ||||
| 
 | ||||
|     private val voiceInputResultLauncher = registerForActivityResult<Intent, ActivityResult>( | ||||
|         ActivityResultContracts.StartActivityForResult(), ::onVoiceInput) | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var presenter: UploadMediaDetailsContract.UserActionListener | ||||
| 
 | ||||
|     @Inject | ||||
|     @field:Named("default_preferences") | ||||
|     lateinit var defaultKvStore: JsonKvStore | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var recentLanguagesDao: RecentLanguagesDao | ||||
| 
 | ||||
|     /** | ||||
|      * True when user removes location from the current image | ||||
|      */ | ||||
|     var hasUserRemovedLocation = false | ||||
| 
 | ||||
|     /** | ||||
|      * True if location is added via the "missing location" popup dialog (which appears after | ||||
|      * tapping "Next" if the picture has no geographical coordinates). | ||||
|      */ | ||||
|     private var isMissingLocationDialog = false | ||||
| 
 | ||||
|     /** | ||||
|      * showNearbyFound will be true, if any nearby location found that needs pictures and the nearby | ||||
|      * popup is yet to be shown Used to show and check if the nearby found popup is already shown | ||||
|      */ | ||||
|     private var showNearbyFound = false | ||||
| 
 | ||||
|     /** | ||||
|      * nearbyPlace holds the detail of nearby place that need pictures, if any found | ||||
|      */ | ||||
|     private var nearbyPlace: Place? = null | ||||
|     private var uploadItem: UploadItem? = null | ||||
| 
 | ||||
|     /** | ||||
|      * inAppPictureLocation: use location recorded while using the in-app camera if device camera | ||||
|      * does not record it in the EXIF | ||||
|      */ | ||||
|     var inAppPictureLocation: LatLng? = null | ||||
| 
 | ||||
|     /** | ||||
|      * editableUploadItem : Storing the upload item before going to update the coordinates | ||||
|      */ | ||||
|     private var editableUploadItem: UploadItem? = null | ||||
| 
 | ||||
|     private var _binding: FragmentUploadMediaDetailFragmentBinding? = null | ||||
|     private val binding: FragmentUploadMediaDetailFragmentBinding get() = _binding!! | ||||
| 
 | ||||
|     private var basicKvStore: BasicKvStore? = null | ||||
|     private val keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing" | ||||
|     private var uploadableFile: UploadableFile? = null | ||||
|     private var place: Place? = null | ||||
|     private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter | ||||
|     var indexOfFragment = 0 | ||||
|     var isExpanded = true | ||||
|     var fragmentCallback: UploadMediaDetailFragmentCallback? = null | ||||
|         set(value) { | ||||
|             field = value | ||||
|             UploadMediaPresenter.presenterCallback = value | ||||
|         } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
| 
 | ||||
|         if (savedInstanceState != null && uploadableFile == null) { | ||||
|             uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun setImageToBeUploaded( | ||||
|         uploadableFile: UploadableFile?, place: Place?, inAppPictureLocation: LatLng? | ||||
|     ) { | ||||
|         this.uploadableFile = uploadableFile | ||||
|         this.place = place | ||||
|         this.inAppPictureLocation = inAppPictureLocation | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         _binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false) | ||||
|         return binding.root | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| 
 | ||||
|         basicKvStore = BasicKvStore(requireActivity(), "CurrentUploadImageQualities") | ||||
| 
 | ||||
|         if (fragmentCallback != null) { | ||||
|             indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this) | ||||
|             initializeFragment() | ||||
|         } | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             if (uploadMediaDetailAdapter.items.isEmpty() && fragmentCallback != null) { | ||||
|                 uploadMediaDetailAdapter.items = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)!! | ||||
|                 presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             if (!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) { | ||||
|                 startActivityWithFlags( | ||||
|                     requireActivity(), | ||||
|                     MainActivity::class.java, | ||||
|                     Intent.FLAG_ACTIVITY_CLEAR_TOP, | ||||
|                     Intent.FLAG_ACTIVITY_SINGLE_TOP | ||||
|                 ) | ||||
|             } | ||||
|         } catch (_: Exception) { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun initializeFragment() { | ||||
|         if (_binding == null) { | ||||
|             return | ||||
|         } | ||||
|         binding.tvTitle.text = getString( | ||||
|             R.string.step_count, (indexOfFragment + 1), | ||||
|             fragmentCallback!!.totalNumberOfSteps, getString(R.string.media_detail_step_title) | ||||
|         ) | ||||
|         binding.tooltip.setOnClickListener { | ||||
|             showInfoAlert( | ||||
|                 R.string.media_detail_step_title, | ||||
|                 R.string.media_details_tooltip | ||||
|             ) | ||||
|         } | ||||
|         presenter.onAttachView(this) | ||||
|         presenter.setupBasicKvStoreFactory { BasicKvStore(requireActivity(), it) } | ||||
| 
 | ||||
|         presenter.receiveImage(uploadableFile, place, inAppPictureLocation) | ||||
|         initRecyclerView() | ||||
| 
 | ||||
|         with (binding){ | ||||
|             if (indexOfFragment == 0) { | ||||
|                 btnPrevious.isEnabled = false | ||||
|                 btnPrevious.alpha = 0.5f | ||||
|             } else { | ||||
|                 btnPrevious.isEnabled = true | ||||
|                 btnPrevious.alpha = 1.0f | ||||
|             } | ||||
| 
 | ||||
|             // If the image EXIF data contains the location, show the map icon with a green tick | ||||
|             if (inAppPictureLocation != null || (uploadableFile != null && uploadableFile!!.hasLocation())) { | ||||
|                 val mapTick = | ||||
|                     ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp) | ||||
|                 locationImageView.setImageDrawable(mapTick) | ||||
|                 locationTextView.setText(R.string.edit_location) | ||||
|             } else { | ||||
|                 // Otherwise, show the map icon with a red question mark | ||||
|                 val mapQuestionMark = ContextCompat.getDrawable( | ||||
|                     requireContext(), | ||||
|                     R.drawable.ic_map_not_available_20dp | ||||
|                 ) | ||||
|                 locationImageView.setImageDrawable(mapQuestionMark) | ||||
|                 locationTextView.setText(R.string.add_location) | ||||
|             } | ||||
| 
 | ||||
|             //If this is the last media, we have nothing to copy, lets not show the button | ||||
|             btnCopySubsequentMedia.visibility = | ||||
|                 if (indexOfFragment == fragmentCallback!!.totalNumberOfSteps - 4) { | ||||
|                     View.GONE | ||||
|                 } else { | ||||
|                     View.VISIBLE | ||||
|                 } | ||||
| 
 | ||||
|             btnNext.setOnClickListener { presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation) } | ||||
|             btnPrevious.setOnClickListener { fragmentCallback?.onPreviousButtonClicked(indexOfFragment) } | ||||
|             llEditImage.setOnClickListener { presenter.onEditButtonClicked(indexOfFragment) } | ||||
|             llContainerTitle.setOnClickListener { expandCollapseLlMediaDetail(!isExpanded) } | ||||
|             llLocationStatus.setOnClickListener { presenter.onMapIconClicked(indexOfFragment) } | ||||
|             btnCopySubsequentMedia.setOnClickListener { onButtonCopyTitleDescToSubsequentMedia() } | ||||
|         } | ||||
| 
 | ||||
|         attachImageViewScaleChangeListener() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches the scale change listener to the image view | ||||
|      */ | ||||
|     private fun attachImageViewScaleChangeListener() { | ||||
|         binding.backgroundImage.setOnScaleChangeListener { _: Float, _: Float, _: Float -> | ||||
|             //Whenever the uses plays with the image, lets collapse the media detail container | ||||
|             //only if it is not already collapsed, which resolves flickering of arrow | ||||
|             if (isExpanded) { | ||||
|                 expandCollapseLlMediaDetail(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * init the description recycler veiw and caption recyclerview | ||||
|      */ | ||||
|     private fun initRecyclerView() { | ||||
|         uploadMediaDetailAdapter = UploadMediaDetailAdapter( | ||||
|             this, | ||||
|             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!, | ||||
|             recentLanguagesDao, voiceInputResultLauncher | ||||
|         ) | ||||
|         uploadMediaDetailAdapter.callback = | ||||
|             UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int -> | ||||
|                 showInfoAlert(titleStringID, messageStringId) | ||||
|             } | ||||
|         uploadMediaDetailAdapter.eventListener = this | ||||
|         binding.rvDescriptions.layoutManager = LinearLayoutManager(context) | ||||
|         binding.rvDescriptions.adapter = uploadMediaDetailAdapter | ||||
|     } | ||||
| 
 | ||||
|     private fun showInfoAlert(titleStringID: Int, messageStringId: Int) { | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             getString(titleStringID), | ||||
|             getString(messageStringId), | ||||
|             getString(android.R.string.ok), | ||||
|             null | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun showSimilarImageFragment( | ||||
|         originalFilePath: String?, possibleFilePath: String?, | ||||
|         similarImageCoordinates: ImageCoordinates? | ||||
|     ) { | ||||
|         val basicKvStore = BasicKvStore(requireActivity(), "IsAnyImageCancelled") | ||||
|         if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) { | ||||
|             val newFragment = SimilarImageDialogFragment() | ||||
|             newFragment.isCancelable = false | ||||
|             newFragment.callback = object : SimilarImageDialogFragment.Callback { | ||||
|                 override fun onPositiveResponse() { | ||||
|                     Timber.d("positive response from similar image fragment") | ||||
|                     presenter.useSimilarPictureCoordinates( | ||||
|                         similarImageCoordinates!!, | ||||
|                         indexOfFragment | ||||
|                     ) | ||||
| 
 | ||||
|                     // set the description text when user selects to use coordinate from the other image | ||||
|                     // which was taken within 120s | ||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/4700 | ||||
|                     uploadMediaDetailAdapter.items[0].descriptionText = | ||||
|                         getString(R.string.similar_coordinate_description_auto_set) | ||||
|                     updateMediaDetails(uploadMediaDetailAdapter.items) | ||||
| 
 | ||||
|                     // Replace the 'Add location' button with 'Edit location' button when user clicks | ||||
|                     // yes in similar image dialog | ||||
|                     // fixing: https://github.com/commons-app/apps-android-commons/issues/5669 | ||||
|                     val mapTick = ContextCompat.getDrawable( | ||||
|                         requireContext(), | ||||
|                         R.drawable.ic_map_available_20dp | ||||
|                     ) | ||||
|                     binding.locationImageView.setImageDrawable(mapTick) | ||||
|                     binding.locationTextView.setText(R.string.edit_location) | ||||
|                 } | ||||
| 
 | ||||
|                 override fun onNegativeResponse() { | ||||
|                     Timber.d("negative response from similar image fragment") | ||||
|                 } | ||||
|             } | ||||
|             newFragment.arguments = bundleOf( | ||||
|                 "originalImagePath" to originalFilePath, | ||||
|                 "possibleImagePath" to possibleFilePath | ||||
|             ) | ||||
|             newFragment.show(childFragmentManager, "dialog") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onImageProcessed(uploadItem: UploadItem) { | ||||
|         if (_binding == null) { | ||||
|             return | ||||
|         } | ||||
|         binding.backgroundImage.setImageURI(uploadItem.mediaUri) | ||||
|     } | ||||
| 
 | ||||
|     override fun onNearbyPlaceFound( | ||||
|         uploadItem: UploadItem, place: Place? | ||||
|     ) { | ||||
|         nearbyPlace = place | ||||
|         this.uploadItem = uploadItem | ||||
|         showNearbyFound = true | ||||
|         if (fragmentCallback == null) { | ||||
|             return | ||||
|         } | ||||
|         if (indexOfFragment == 0) { | ||||
|             if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) { | ||||
|                 val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!! | ||||
|                 if (response) { | ||||
|                     if (fragmentCallback != null) { | ||||
|                         presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment) | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 showNearbyPlaceFound(nearbyPlace!!) | ||||
|             } | ||||
|             showNearbyFound = false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format | ||||
|     private fun showNearbyPlaceFound(place: Place) { | ||||
|         val customLayout = layoutInflater.inflate(R.layout.custom_nearby_found, null) | ||||
|         val nearbyFoundImage = customLayout.findViewById<ImageView>(R.id.nearbyItemImage) | ||||
|         nearbyFoundImage.setImageURI(uploadItem!!.mediaUri) | ||||
| 
 | ||||
|         val activity: Activity? = activity | ||||
| 
 | ||||
|         if (activity is UploadActivity) { | ||||
|             val isMultipleFilesSelected = activity.isMultipleFilesSelected | ||||
| 
 | ||||
|             // Determine the message based on the selection status | ||||
|             val message = if (isMultipleFilesSelected) { | ||||
|                 // Use plural message if multiple files are selected | ||||
|                 String.format( | ||||
|                     Locale.getDefault(), | ||||
|                     getString(R.string.upload_nearby_place_found_description_plural), | ||||
|                     place.getName() | ||||
|                 ) | ||||
|             } else { | ||||
|                 // Use singular message if only one file is selected | ||||
|                 String.format( | ||||
|                     Locale.getDefault(), | ||||
|                     getString(R.string.upload_nearby_place_found_description_singular), | ||||
|                     place.getName() | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             // Show the AlertDialog with the determined message | ||||
|             showAlertDialog( | ||||
|                 requireActivity(), | ||||
|                 getString(R.string.upload_nearby_place_found_title), | ||||
|                 message, | ||||
|                 { | ||||
|                     // Execute when user confirms the upload is of the specified place | ||||
|                     UploadActivity.nearbyPopupAnswers!![place] = true | ||||
|                     presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment) | ||||
|                 }, | ||||
|                 { | ||||
|                     // Execute when user cancels the upload of the specified place | ||||
|                     UploadActivity.nearbyPopupAnswers!![place] = false | ||||
|                 }, | ||||
|                 customLayout | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun showProgress(shouldShow: Boolean) { | ||||
|         if (fragmentCallback == null) { | ||||
|             return | ||||
|         } | ||||
|         fragmentCallback!!.showProgress(shouldShow) | ||||
|     } | ||||
| 
 | ||||
|     override fun onImageValidationSuccess() { | ||||
|         if (fragmentCallback == null) { | ||||
|             return | ||||
|         } | ||||
|         fragmentCallback!!.onNextButtonClicked(indexOfFragment) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method gets called whenever the next/previous button is pressed | ||||
|      */ | ||||
|     override fun onBecameVisible() { | ||||
|         super.onBecameVisible() | ||||
|         if (fragmentCallback == null) { | ||||
|             return | ||||
|         } | ||||
|         presenter.fetchTitleAndDescription(indexOfFragment) | ||||
|         if (showNearbyFound) { | ||||
|             if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) { | ||||
|                 val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!! | ||||
|                 if (response) { | ||||
|                     presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment) | ||||
|                 } | ||||
|             } else { | ||||
|                 showNearbyPlaceFound(nearbyPlace!!) | ||||
|             } | ||||
|             showNearbyFound = false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun showMessage(stringResourceId: Int, colorResourceId: Int) = | ||||
|         showLongToast(requireContext(), stringResourceId) | ||||
| 
 | ||||
|     override fun showMessage(message: String, colorResourceId: Int) = | ||||
|         showLongToast(requireContext(), message) | ||||
| 
 | ||||
|     override fun showDuplicatePicturePopup(uploadItem: UploadItem) { | ||||
|         if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) { | ||||
|             val uploadTitleFormat = getString(R.string.upload_title_duplicate) | ||||
|             val checkBoxView = View | ||||
|                 .inflate(activity, R.layout.nearby_permission_dialog, null) | ||||
|             val checkBox = checkBoxView.findViewById<View>(R.id.never_ask_again) as CheckBox | ||||
|             checkBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> | ||||
|                 if (isChecked) { | ||||
|                     defaultKvStore.putBoolean("showDuplicatePicturePopup", false) | ||||
|                 } | ||||
|             } | ||||
|             showAlertDialog( | ||||
|                 requireActivity(), | ||||
|                 getString(R.string.duplicate_file_name), | ||||
|                 String.format( | ||||
|                     Locale.getDefault(), | ||||
|                     uploadTitleFormat, | ||||
|                     uploadItem.filename | ||||
|                 ), | ||||
|                 getString(R.string.upload), | ||||
|                 getString(R.string.cancel), | ||||
|                 { | ||||
|                     uploadItem.imageQuality = ImageUtils.IMAGE_KEEP | ||||
|                     onImageValidationSuccess() | ||||
|                 }, null, | ||||
|                 checkBoxView | ||||
|             ) | ||||
|         } else { | ||||
|             uploadItem.imageQuality = ImageUtils.IMAGE_KEEP | ||||
|             onImageValidationSuccess() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user that internet connection is required for upload process | ||||
|      * Does nothing if there is network connectivity and then the user presses okay | ||||
|      */ | ||||
|     override fun showConnectionErrorPopupForCaptionCheck() { | ||||
|         showAlertDialog(requireActivity(), | ||||
|             getString(R.string.upload_connection_error_alert_title), | ||||
|             getString(R.string.upload_connection_error_alert_detail), | ||||
|             getString(R.string.ok), | ||||
|             getString(R.string.cancel_upload), | ||||
|             { | ||||
|                 if (!isInternetConnectionEstablished(requireActivity())) { | ||||
|                     showConnectionErrorPopupForCaptionCheck() | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 requireActivity().finish() | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user that internet connection is required for upload process | ||||
|      * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, | ||||
|      * if there is network connectivity and then the user presses okay | ||||
|      */ | ||||
|     override fun showConnectionErrorPopup() { | ||||
|         try { | ||||
|             val FLAG_ALERT_DIALOG_SHOWING = basicKvStore!!.getBoolean( | ||||
|                 keyForShowingAlertDialog, false | ||||
|             ) | ||||
|             if (!FLAG_ALERT_DIALOG_SHOWING) { | ||||
|                 basicKvStore!!.putBoolean(keyForShowingAlertDialog, true) | ||||
|                 showAlertDialog( | ||||
|                     requireActivity(), | ||||
|                     getString(R.string.upload_connection_error_alert_title), | ||||
|                     getString(R.string.upload_connection_error_alert_detail), | ||||
|                     getString(R.string.ok), | ||||
|                     getString(R.string.cancel_upload), | ||||
|                     { | ||||
|                         basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) | ||||
|                         if (isInternetConnectionEstablished(requireActivity())) { | ||||
|                             val sizeOfUploads = basicKvStore!!.getInt( | ||||
|                                 UploadActivity.keyForCurrentUploadImagesSize | ||||
|                             ) | ||||
|                             for (i in indexOfFragment until sizeOfUploads) { | ||||
|                                 presenter.getImageQuality( | ||||
|                                     i, | ||||
|                                     inAppPictureLocation, | ||||
|                                     requireActivity() | ||||
|                                 ) | ||||
|                             } | ||||
|                         } else { | ||||
|                             showConnectionErrorPopup() | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) | ||||
|                         requireActivity().finish() | ||||
|                     }, | ||||
|                     null | ||||
|                 ) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun showExternalMap(uploadItem: UploadItem) = | ||||
|         goToLocationPickerActivity(uploadItem) | ||||
| 
 | ||||
|     /** | ||||
|      * Launches the image editing activity to edit the specified UploadItem. | ||||
|      * | ||||
|      * @param uploadItem The UploadItem to be edited. | ||||
|      * | ||||
|      * This method is called to start the image editing activity for a specific UploadItem. | ||||
|      * It sets the UploadItem as the currently editable item, creates an intent to launch the | ||||
|      * EditActivity, and passes the image file path as an extra in the intent. The activity | ||||
|      * is started using resultLauncher that handles the result in respective callback. | ||||
|      */ | ||||
|     override fun showEditActivity(uploadItem: UploadItem) { | ||||
|         editableUploadItem = uploadItem | ||||
|         val intent = Intent(context, EditActivity::class.java) | ||||
|         intent.putExtra("image", uploadableFile!!.getFilePath().toString()) | ||||
|         startForEditActivityResult.launch(intent) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start Location picker activity. Show the location first then user can modify it by clicking | ||||
|      * modify location button. | ||||
|      * @param uploadItem current upload item | ||||
|      */ | ||||
|     private fun goToLocationPickerActivity(uploadItem: UploadItem) { | ||||
|         editableUploadItem = uploadItem | ||||
|         var defaultLatitude = 37.773972 | ||||
|         var defaultLongitude = -122.431297 | ||||
|         var defaultZoom = 16.0 | ||||
| 
 | ||||
|         val locationPickerIntent: Intent | ||||
| 
 | ||||
|         /* Retrieve image location from EXIF if present or | ||||
|            check if user has provided location while using the in-app camera. | ||||
|            Use location of last UploadItem if none of them is available */ | ||||
|         if (uploadItem.gpsCoords != null && uploadItem.gpsCoords!! | ||||
|                 .decLatitude != 0.0 && uploadItem.gpsCoords!!.decLongitude != 0.0 | ||||
|         ) { | ||||
|             defaultLatitude = uploadItem.gpsCoords!! | ||||
|                 .decLatitude | ||||
|             defaultLongitude = uploadItem.gpsCoords!!.decLongitude | ||||
|             defaultZoom = uploadItem.gpsCoords!!.zoomLevel | ||||
| 
 | ||||
|             locationPickerIntent = LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom)) | ||||
|                 .activityKey("UploadActivity") | ||||
|                 .build(requireActivity()) | ||||
|         } else { | ||||
|             if (defaultKvStore.getString(LAST_LOCATION) != null) { | ||||
|                 val locationLatLng = defaultKvStore.getString(LAST_LOCATION)!! | ||||
|                     .split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | ||||
|                 defaultLatitude = locationLatLng[0].toDouble() | ||||
|                 defaultLongitude = locationLatLng[1].toDouble() | ||||
|             } | ||||
|             if (defaultKvStore.getString(LAST_ZOOM) != null) { | ||||
|                 defaultZoom = defaultKvStore.getString(LAST_ZOOM)!! | ||||
|                     .toDouble() | ||||
|             } | ||||
| 
 | ||||
|             locationPickerIntent = LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom)) | ||||
|                 .activityKey("NoLocationUploadActivity") | ||||
|                 .build(requireActivity()) | ||||
|         } | ||||
|         startForResult.launch(locationPickerIntent) | ||||
|     } | ||||
| 
 | ||||
|     private fun onCameraPosition(result: ActivityResult) { | ||||
|         if (result.resultCode == Activity.RESULT_OK) { | ||||
|             checkNotNull(result.data) | ||||
|             val cameraPosition = getCameraPosition( | ||||
|                 result.data!! | ||||
|             ) | ||||
| 
 | ||||
|             if (cameraPosition != null) { | ||||
|                 val latitude = cameraPosition.latitude.toString() | ||||
|                 val longitude = cameraPosition.longitude.toString() | ||||
|                 val zoom = cameraPosition.zoom | ||||
| 
 | ||||
|                 editLocation(latitude, longitude, zoom) | ||||
|                 // If isMissingLocationDialog is true, it means that the user has already tapped the | ||||
|                 // "Next" button, so go directly to the next step. | ||||
|                 if (isMissingLocationDialog) { | ||||
|                     isMissingLocationDialog = false | ||||
|                     presenter.displayLocDialog( | ||||
|                         indexOfFragment, | ||||
|                         inAppPictureLocation, | ||||
|                         hasUserRemovedLocation | ||||
|                     ) | ||||
|                 } | ||||
|             } else { | ||||
|                 // If camera position is null means location is removed by the user | ||||
|                 removeLocation() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onVoiceInput(result: ActivityResult) { | ||||
|         if (result.resultCode == Activity.RESULT_OK && result.data != null) { | ||||
|             val resultData = result.data!!.getStringArrayListExtra( | ||||
|                 RecognizerIntent.EXTRA_RESULTS | ||||
|             ) | ||||
|             uploadMediaDetailAdapter.handleSpeechResult(resultData!![0]) | ||||
|         } else { | ||||
|             Timber.e("Error %s", result.resultCode) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onEditActivityResult(result: ActivityResult) { | ||||
|         if (result.resultCode == Activity.RESULT_OK) { | ||||
|             val path = result.data!!.getStringExtra("editedImageFilePath") | ||||
| 
 | ||||
|             if (Objects.equals(result, "Error")) { | ||||
|                 Timber.e("Error in rotating image") | ||||
|                 return | ||||
|             } | ||||
|             try { | ||||
|                 if (_binding != null) { | ||||
|                     binding.backgroundImage.setImageURI(Uri.fromFile(File(path!!))) | ||||
|                 } | ||||
|                 editableUploadItem!!.setContentAndMediaUri(Uri.fromFile(File(path!!))) | ||||
|                 fragmentCallback!!.changeThumbnail( | ||||
|                     indexOfFragment, | ||||
|                     path | ||||
|                 ) | ||||
|             } catch (e: Exception) { | ||||
|                 Timber.e(e) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes the location data from the image, by setting them to null | ||||
|      */ | ||||
|     private fun removeLocation() { | ||||
|         editableUploadItem!!.gpsCoords!!.decimalCoords = null | ||||
|         try { | ||||
|             val sourceExif = ExifInterface( | ||||
|                 uploadableFile!!.getFilePath() | ||||
|             ) | ||||
|             val exifTags = arrayOf( | ||||
|                 ExifInterface.TAG_GPS_LATITUDE, | ||||
|                 ExifInterface.TAG_GPS_LATITUDE_REF, | ||||
|                 ExifInterface.TAG_GPS_LONGITUDE, | ||||
|                 ExifInterface.TAG_GPS_LONGITUDE_REF, | ||||
|             ) | ||||
| 
 | ||||
|             for (tag in exifTags) { | ||||
|                 sourceExif.setAttribute(tag, null) | ||||
|             } | ||||
|             sourceExif.saveAttributes() | ||||
| 
 | ||||
|             val mapQuestion = | ||||
|                 ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_not_available_20dp) | ||||
| 
 | ||||
|             if (_binding != null) { | ||||
|                 binding.locationImageView.setImageDrawable(mapQuestion) | ||||
|                 binding.locationTextView.setText(R.string.add_location) | ||||
|             } | ||||
| 
 | ||||
|             editableUploadItem!!.gpsCoords!!.decLatitude = 0.0 | ||||
|             editableUploadItem!!.gpsCoords!!.decLongitude = 0.0 | ||||
|             editableUploadItem!!.gpsCoords!!.imageCoordsExists = false | ||||
|             hasUserRemovedLocation = true | ||||
| 
 | ||||
|             Toast.makeText(context, getString(R.string.location_removed), Toast.LENGTH_LONG) | ||||
|                 .show() | ||||
|         } catch (e: Exception) { | ||||
|             Timber.d(e) | ||||
|             Toast.makeText( | ||||
|                 context, "Location could not be removed due to internal error", | ||||
|                 Toast.LENGTH_LONG | ||||
|             ).show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the old coordinates with new one | ||||
|      * @param latitude new latitude | ||||
|      * @param longitude new longitude | ||||
|      */ | ||||
|     fun editLocation(latitude: String, longitude: String, zoom: Double) { | ||||
|         editableUploadItem!!.gpsCoords!!.decLatitude = latitude.toDouble() | ||||
|         editableUploadItem!!.gpsCoords!!.decLongitude = longitude.toDouble() | ||||
|         editableUploadItem!!.gpsCoords!!.decimalCoords = "$latitude|$longitude" | ||||
|         editableUploadItem!!.gpsCoords!!.imageCoordsExists = true | ||||
|         editableUploadItem!!.gpsCoords!!.zoomLevel = zoom | ||||
| 
 | ||||
|         // Replace the map icon using the one with a green tick | ||||
|         val mapTick = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp) | ||||
| 
 | ||||
|         if (_binding != null) { | ||||
|             binding.locationImageView.setImageDrawable(mapTick) | ||||
|             binding.locationTextView.setText(R.string.edit_location) | ||||
|         } | ||||
| 
 | ||||
|         Toast.makeText(context, getString(R.string.location_updated), Toast.LENGTH_LONG).show() | ||||
|     } | ||||
| 
 | ||||
|     override fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) { | ||||
|         uploadMediaDetailAdapter.items = uploadMediaDetails | ||||
|         showNearbyFound = | ||||
|             showNearbyFound && (uploadMediaDetails.isEmpty() || listContainsEmptyDetails( | ||||
|                 uploadMediaDetails | ||||
|             )) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * if the media details that come in here are empty | ||||
|      * (empty caption AND empty description, with caption being the decider here) | ||||
|      * this method allows usage of nearby place caption and description if any | ||||
|      * else it takes the media details saved in prior for this picture | ||||
|      * @param uploadMediaDetails saved media details, | ||||
|      * ex: in case when "copy to subsequent media" button is clicked | ||||
|      * for a previous image | ||||
|      * @return boolean whether the details are empty or not | ||||
|      */ | ||||
|     private fun listContainsEmptyDetails(uploadMediaDetails: List<UploadMediaDetail>): Boolean { | ||||
|         for ((_, descriptionText, captionText) in uploadMediaDetails) { | ||||
|             if (!TextUtils.isEmpty(captionText) && !TextUtils.isEmpty( | ||||
|                     descriptionText | ||||
|                 ) | ||||
|             ) { | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Showing dialog for adding location | ||||
|      * | ||||
|      * @param runnable proceed for verifying image quality | ||||
|      */ | ||||
|     override fun displayAddLocationDialog(runnable: Runnable) { | ||||
|         isMissingLocationDialog = true | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             getString(R.string.no_location_found_title), | ||||
|             getString(R.string.no_location_found_message), | ||||
|             getString(R.string.add_location), | ||||
|             getString(R.string.skip_login), | ||||
|             { | ||||
|                 presenter.onMapIconClicked(indexOfFragment) | ||||
|             }, | ||||
|             runnable | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) { | ||||
|         //If the error message is null, we will probably not show anything | ||||
|         val activity = requireActivity() | ||||
|         val errorMessageForResult = getErrorMessageForResult(activity, errorCode) | ||||
|         if (errorMessageForResult.isNotEmpty()) { | ||||
|             showAlertDialog( | ||||
|                 activity, | ||||
|                 activity.getString(R.string.upload_problem_image), | ||||
|                 errorMessageForResult, | ||||
|                 activity.getString(R.string.upload), | ||||
|                 activity.getString(R.string.cancel), | ||||
|                 { | ||||
|                     showProgress(false) | ||||
|                     uploadItem.imageQuality = IMAGE_OK | ||||
|                 }, | ||||
|                 { | ||||
|                     presenterCallback!!.deletePictureAtIndex(index) | ||||
|                 } | ||||
|             )?.setCancelable(false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         super.onDestroyView() | ||||
|         presenter.onDetachView() | ||||
|     } | ||||
| 
 | ||||
|     fun expandCollapseLlMediaDetail(shouldExpand: Boolean) { | ||||
|         if (_binding == null) { | ||||
|             return | ||||
|         } | ||||
|         binding.llContainerMediaDetail.visibility = | ||||
|             if (shouldExpand) View.VISIBLE else View.GONE | ||||
|         isExpanded = !isExpanded | ||||
|         binding.ibExpandCollapse.rotation = binding.ibExpandCollapse.rotation + 180 | ||||
|     } | ||||
| 
 | ||||
|     override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) { | ||||
|         if (_binding == null) { | ||||
|             return | ||||
|         } | ||||
|         binding.btnCopySubsequentMedia.isEnabled = isNotEmpty | ||||
|         binding.btnCopySubsequentMedia.isClickable = isNotEmpty | ||||
|         binding.btnCopySubsequentMedia.alpha = if (isNotEmpty) 1.0f else 0.5f | ||||
|         binding.btnNext.isEnabled = isNotEmpty | ||||
|         binding.btnNext.isClickable = isNotEmpty | ||||
|         binding.btnNext.alpha = if (isNotEmpty) 1.0f else 0.5f | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds new language item to RecyclerView | ||||
|      */ | ||||
|     override fun addLanguage() { | ||||
|         val uploadMediaDetail = UploadMediaDetail() | ||||
|         uploadMediaDetail.isManuallyAdded = true //This was manually added by the user | ||||
|         uploadMediaDetailAdapter.addDescription(uploadMediaDetail) | ||||
|         binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) | ||||
|     } | ||||
| 
 | ||||
|     fun onButtonCopyTitleDescToSubsequentMedia() { | ||||
|         presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment) | ||||
|         Toast.makeText(context, R.string.copied_successfully, Toast.LENGTH_SHORT).show() | ||||
|     } | ||||
| 
 | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
| 
 | ||||
|         if (uploadableFile != null) { | ||||
|             outState.putParcelable(UPLOADABLE_FILE, uploadableFile) | ||||
|         } | ||||
|         outState.putParcelableArrayList( | ||||
|             UPLOAD_MEDIA_DETAILS, | ||||
|             ArrayList(uploadMediaDetailAdapter.items) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         _binding = null | ||||
|     } | ||||
| 
 | ||||
|     interface UploadMediaDetailFragmentCallback : Callback { | ||||
|         fun deletePictureAtIndex(index: Int) | ||||
| 
 | ||||
|         fun changeThumbnail(index: Int, uri: String) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         /** | ||||
|          * A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex. | ||||
|          * 12.3433,54.78897 from applicationKvStore. | ||||
|          */ | ||||
|         const val LAST_LOCATION: String = "last_location_while_uploading" | ||||
|         const val LAST_ZOOM: String = "last_zoom_level_while_uploading" | ||||
|         const val UPLOADABLE_FILE: String = "uploadable_file" | ||||
|         const val UPLOAD_MEDIA_DETAILS: String = "upload_media_detail_adapter" | ||||
|     } | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.mediaDetails | |||
| import android.app.Activity | ||||
| import fr.free.nrw.commons.BasePresenter | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.upload.ImageCoordinates | ||||
|  | @ -15,9 +16,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail | |||
|  */ | ||||
| interface UploadMediaDetailsContract { | ||||
|     interface View : SimilarImageInterface { | ||||
|         fun onImageProcessed(uploadItem: UploadItem?, place: Place?) | ||||
|         fun onImageProcessed(uploadItem: UploadItem) | ||||
| 
 | ||||
|         fun onNearbyPlaceFound(uploadItem: UploadItem?, place: Place?) | ||||
|         fun onNearbyPlaceFound(uploadItem: UploadItem, place: Place?) | ||||
| 
 | ||||
|         fun showProgress(shouldShow: Boolean) | ||||
| 
 | ||||
|  | @ -25,9 +26,9 @@ interface UploadMediaDetailsContract { | |||
| 
 | ||||
|         fun showMessage(stringResourceId: Int, colorResourceId: Int) | ||||
| 
 | ||||
|         fun showMessage(message: String?, colorResourceId: Int) | ||||
|         fun showMessage(message: String, colorResourceId: Int) | ||||
| 
 | ||||
|         fun showDuplicatePicturePopup(uploadItem: UploadItem?) | ||||
|         fun showDuplicatePicturePopup(uploadItem: UploadItem) | ||||
| 
 | ||||
|         /** | ||||
|          * Shows a dialog alerting the user that internet connection is required for upload process | ||||
|  | @ -42,16 +43,20 @@ interface UploadMediaDetailsContract { | |||
|          */ | ||||
|         fun showConnectionErrorPopupForCaptionCheck() | ||||
| 
 | ||||
|         fun showExternalMap(uploadItem: UploadItem?) | ||||
|         fun showExternalMap(uploadItem: UploadItem) | ||||
| 
 | ||||
|         fun showEditActivity(uploadItem: UploadItem?) | ||||
|         fun showEditActivity(uploadItem: UploadItem) | ||||
| 
 | ||||
|         fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail?>?) | ||||
|         fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) | ||||
| 
 | ||||
|         fun displayAddLocationDialog(runnable: Runnable?) | ||||
|         fun displayAddLocationDialog(runnable: Runnable) | ||||
| 
 | ||||
|         fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) | ||||
|     } | ||||
| 
 | ||||
|     interface UserActionListener : BasePresenter<View?> { | ||||
|         fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) | ||||
| 
 | ||||
|         fun receiveImage( | ||||
|             uploadableFile: UploadableFile?, | ||||
|             place: Place?, | ||||
|  | @ -59,7 +64,7 @@ interface UploadMediaDetailsContract { | |||
|         ) | ||||
| 
 | ||||
|         fun setUploadMediaDetails( | ||||
|             uploadMediaDetails: List<UploadMediaDetail?>?, | ||||
|             uploadMediaDetails: List<UploadMediaDetail>, | ||||
|             uploadItemIndex: Int | ||||
|         ) | ||||
| 
 | ||||
|  | @ -74,7 +79,7 @@ interface UploadMediaDetailsContract { | |||
|         fun getImageQuality( | ||||
|             uploadItemIndex: Int, | ||||
|             inAppPictureLocation: LatLng?, | ||||
|             activity: Activity? | ||||
|             activity: Activity | ||||
|         ): Boolean | ||||
| 
 | ||||
|         /** | ||||
|  | @ -87,7 +92,8 @@ interface UploadMediaDetailsContract { | |||
|          * @param hasUserRemovedLocation True if user has removed location from the image | ||||
|          */ | ||||
|         fun displayLocDialog( | ||||
|             uploadItemIndex: Int, inAppPictureLocation: LatLng?, | ||||
|             uploadItemIndex: Int, | ||||
|             inAppPictureLocation: LatLng?, | ||||
|             hasUserRemovedLocation: Boolean | ||||
|         ) | ||||
| 
 | ||||
|  | @ -97,7 +103,7 @@ interface UploadMediaDetailsContract { | |||
|          * @param uploadItem UploadItem whose quality is to be checked | ||||
|          * @param index Index of the UploadItem whose quality is to be checked | ||||
|          */ | ||||
|         fun checkImageQuality(uploadItem: UploadItem?, index: Int) | ||||
|         fun checkImageQuality(uploadItem: UploadItem, index: Int) | ||||
| 
 | ||||
|         /** | ||||
|          * Updates the image qualities stored in JSON, whenever an image is deleted | ||||
|  | @ -111,7 +117,7 @@ interface UploadMediaDetailsContract { | |||
| 
 | ||||
|         fun fetchTitleAndDescription(indexInViewFlipper: Int) | ||||
| 
 | ||||
|         fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int) | ||||
|         fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) | ||||
| 
 | ||||
|         fun onMapIconClicked(indexInViewFlipper: Int) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,547 +0,0 @@ | |||
| package fr.free.nrw.commons.upload.mediaDetails; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; | ||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; | ||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.activity; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import androidx.annotation.Nullable; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.repository.UploadRepository; | ||||
| import fr.free.nrw.commons.upload.ImageCoordinates; | ||||
| import fr.free.nrw.commons.upload.SimilarImageInterface; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.upload.UploadItem; | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetail; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener; | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import io.github.coordinates2country.Coordinates2Country; | ||||
| import io.reactivex.Maybe; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import java.lang.reflect.Proxy; | ||||
| import java.net.UnknownHostException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| import org.json.JSONObject; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface { | ||||
| 
 | ||||
|     private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy | ||||
|             .newProxyInstance( | ||||
|                     UploadMediaDetailsContract.View.class.getClassLoader(), | ||||
|                     new Class[]{UploadMediaDetailsContract.View.class}, | ||||
|                     (proxy, method, methodArgs) -> null); | ||||
| 
 | ||||
|     private final UploadRepository repository; | ||||
|     private UploadMediaDetailsContract.View view = DUMMY; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable; | ||||
| 
 | ||||
|     private final JsonKvStore defaultKVStore; | ||||
|     private Scheduler ioScheduler; | ||||
|     private Scheduler mainThreadScheduler; | ||||
| 
 | ||||
|     public static UploadMediaDetailFragmentCallback presenterCallback ; | ||||
| 
 | ||||
|     private final List<String> WLM_SUPPORTED_COUNTRIES= Arrays.asList("am","at","az","br","hr","sv","fi","fr","de","gh","in","ie","il","mk","my","mt","pk","pe","pl","ru","rw","si","es","se","tw","ug","ua","us"); | ||||
|     private Map<String, String> countryNamesAndCodes = null; | ||||
| 
 | ||||
|     private final String keyForCurrentUploadImageQualities = "UploadedImagesQualities"; | ||||
| 
 | ||||
|     /** | ||||
|      * Variable used to determine if the battery-optimisation dialog is being shown or not | ||||
|      */ | ||||
|     public static boolean isBatteryDialogShowing; | ||||
| 
 | ||||
|     public static boolean isCategoriesDialogShowing; | ||||
| 
 | ||||
|     @Inject | ||||
|     public UploadMediaPresenter(final UploadRepository uploadRepository, | ||||
|         @Named("default_preferences") final JsonKvStore defaultKVStore, | ||||
|                                 @Named(IO_THREAD) final Scheduler ioScheduler, | ||||
|                                 @Named(MAIN_THREAD) final Scheduler mainThreadScheduler) { | ||||
|         this.repository = uploadRepository; | ||||
|         this.defaultKVStore = defaultKVStore; | ||||
|         this.ioScheduler = ioScheduler; | ||||
|         this.mainThreadScheduler = mainThreadScheduler; | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttachView(final View view) { | ||||
|         this.view = view; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetachView() { | ||||
|         this.view = DUMMY; | ||||
|         compositeDisposable.clear(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the Upload Media Details for the corresponding upload item | ||||
|      */ | ||||
|     @Override | ||||
|     public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) { | ||||
|         repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Receives the corresponding uploadable file, processes it and return the view with and uplaod item | ||||
|      */ | ||||
|     @Override | ||||
|     public void receiveImage(final UploadableFile uploadableFile, final Place place, | ||||
|                             final LatLng inAppPictureLocation) { | ||||
|         view.showProgress(true); | ||||
|         compositeDisposable.add( | ||||
|             repository | ||||
|                 .preProcessImage(uploadableFile, place, this, inAppPictureLocation) | ||||
|                 .map(uploadItem -> { | ||||
|                     if(place!=null && place.isMonument()){ | ||||
|                         if (place.location != null) { | ||||
|                             final String countryCode = reverseGeoCode(place.location); | ||||
|                             if (countryCode != null && WLM_SUPPORTED_COUNTRIES | ||||
|                                 .contains(countryCode.toLowerCase(Locale.ROOT))) { | ||||
|                                 uploadItem.setWLMUpload(true); | ||||
|                                 uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     return uploadItem; | ||||
|                 }) | ||||
|                 .subscribeOn(ioScheduler) | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe(uploadItem -> | ||||
|                     { | ||||
|                         view.onImageProcessed(uploadItem, place); | ||||
|                         view.updateMediaDetails(uploadItem.getUploadMediaDetails()); | ||||
|                         view.showProgress(false); | ||||
|                         final ImageCoordinates gpsCoords = uploadItem.getGpsCoords(); | ||||
|                         final boolean hasImageCoordinates = | ||||
|                             gpsCoords != null && gpsCoords.getImageCoordsExists(); | ||||
|                         if (hasImageCoordinates && place == null) { | ||||
|                             checkNearbyPlaces(uploadItem); | ||||
|                         } | ||||
|                     }, | ||||
|                     throwable -> Timber.e(throwable, "Error occurred in processing images"))); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     private String reverseGeoCode(final LatLng latLng){ | ||||
|         if(countryNamesAndCodes == null){ | ||||
|             countryNamesAndCodes = getCountryNamesAndCodes(); | ||||
|         } | ||||
|         return countryNamesAndCodes.get(Coordinates2Country.country(latLng.getLatitude(), latLng.getLongitude())); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates HashMap containing all ISO countries 2-letter codes provided by <code>Locale.getISOCountries()</code> | ||||
|      * and their english names | ||||
|      * | ||||
|      * @return HashMap where Key is country english name and Value is 2-letter country code | ||||
|      * e.g. ["Germany":"DE", ...] | ||||
|      */ | ||||
|     private Map<String, String> getCountryNamesAndCodes(){ | ||||
|         final Map<String, String> result = new HashMap<>(); | ||||
| 
 | ||||
|         final String[] isoCountries = Locale.getISOCountries(); | ||||
| 
 | ||||
|         for (final String isoCountry : isoCountries) { | ||||
|             result.put( | ||||
|                 new Locale("en", isoCountry).getDisplayCountry(Locale.ENGLISH), | ||||
|                 isoCountry | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method checks for the nearest location that needs images and suggests it to the user. | ||||
|      */ | ||||
|     private void checkNearbyPlaces(final UploadItem uploadItem) { | ||||
|         final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository | ||||
|                 .checkNearbyPlaces(uploadItem.getGpsCoords().getDecLatitude(), | ||||
|                         uploadItem.getGpsCoords().getDecLongitude())) | ||||
|                 .subscribeOn(ioScheduler) | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe(place -> { | ||||
|                         if (place != null) { | ||||
|                             view.onNearbyPlaceFound(uploadItem, place); | ||||
|                         } | ||||
|                     }, | ||||
|                     throwable -> Timber.e(throwable, "Error occurred in processing images")); | ||||
|             compositeDisposable.add(checkNearbyPlaces); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if the image has a location. Displays a dialog alerting user that no | ||||
|      * location has been to added to the image and asking them to add one, if location was not | ||||
|      * removed by the user | ||||
|      * | ||||
|      * @param uploadItemIndex Index of the uploadItem which has no location | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @param hasUserRemovedLocation True if user has removed location from the image | ||||
|      */ | ||||
|     @Override | ||||
|     public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation, | ||||
|         final boolean hasUserRemovedLocation) { | ||||
|         final List<UploadItem> uploadItems = repository.getUploads(); | ||||
|         final UploadItem uploadItem = uploadItems.get(uploadItemIndex); | ||||
|         if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null | ||||
|             && !hasUserRemovedLocation) { | ||||
|             final Runnable onSkipClicked = () -> { | ||||
|                 verifyCaptionQuality(uploadItem); | ||||
|             }; | ||||
|             view.displayAddLocationDialog(onSkipClicked); | ||||
|         } else { | ||||
|             verifyCaptionQuality(uploadItem); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Verifies the image's caption and calls function to handle the result | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose caption is checked | ||||
|      */ | ||||
|     private void verifyCaptionQuality(final UploadItem uploadItem) { | ||||
|         view.showProgress(true); | ||||
|         compositeDisposable.add( | ||||
|             repository | ||||
|                 .getCaptionQuality(uploadItem) | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe(capResult -> { | ||||
|                         view.showProgress(false); | ||||
|                         handleCaptionResult(capResult, uploadItem); | ||||
|                     }, | ||||
|                     throwable -> { | ||||
|                         view.showProgress(false); | ||||
|                         if (throwable instanceof UnknownHostException) { | ||||
|                             view.showConnectionErrorPopupForCaptionCheck(); | ||||
|                         } else { | ||||
|                             view.showMessage("" + throwable.getLocalizedMessage(), | ||||
|                                 R.color.color_error); | ||||
|                         } | ||||
|                         Timber.e(throwable, "Error occurred while handling image"); | ||||
|                     }) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles image's caption results and shows dialog if necessary | ||||
|      * | ||||
|      * @param errorCode Error code of the UploadItem | ||||
|      * @param uploadItem UploadItem whose caption is checked | ||||
|      */ | ||||
|     public void handleCaptionResult(final Integer errorCode, final UploadItem uploadItem) { | ||||
|         // If errorCode is empty caption show message | ||||
|         if (errorCode == EMPTY_CAPTION) { | ||||
|             Timber.d("Captions are empty. Showing toast"); | ||||
|             view.showMessage(R.string.add_caption_toast, R.color.color_error); | ||||
|         } | ||||
| 
 | ||||
|         // If image with same file name exists check the bit in errorCode is set or not | ||||
|         if ((errorCode & FILE_NAME_EXISTS) != 0) { | ||||
|             Timber.d("Trying to show duplicate picture popup"); | ||||
|             view.showDuplicatePicturePopup(uploadItem); | ||||
|         } | ||||
| 
 | ||||
|         // If caption is not duplicate or user still wants to upload it | ||||
|         if (errorCode == IMAGE_OK) { | ||||
|             Timber.d("Image captions are okay or user still wants to upload it"); | ||||
|             view.onImageValidationSuccess(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Copies the caption and description of the current item to the subsequent media | ||||
|      */ | ||||
|     @Override | ||||
|     public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) { | ||||
|       for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){ | ||||
|         final UploadItem subsequentUploadItem = repository.getUploads().get(i); | ||||
|         subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails())); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|   /** | ||||
|    * Fetches and set the caption and description of the item | ||||
|    */ | ||||
|   @Override | ||||
|   public void fetchTitleAndDescription(final int indexInViewFlipper) { | ||||
|     final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper); | ||||
|     view.updateMediaDetails(currentUploadItem.getUploadMediaDetails()); | ||||
|   } | ||||
| 
 | ||||
|   @NotNull | ||||
|   private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) { | ||||
|     final ArrayList<UploadMediaDetail> newList = new ArrayList<>(); | ||||
|     for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) { | ||||
|       newList.add(uploadMediaDetail.javaCopy()); | ||||
|     } | ||||
|     return newList; | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|   public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) { | ||||
|     repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|   public void onMapIconClicked(final int indexInViewFlipper) { | ||||
|     view.showExternalMap(repository.getUploads().get(indexInViewFlipper)); | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|   public void onEditButtonClicked(final int indexInViewFlipper){ | ||||
|       view.showEditActivity(repository.getUploads().get(indexInViewFlipper)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Updates the information regarding the specified place for the specified upload item | ||||
|    * when the user confirms the suggested nearby place. | ||||
|    * | ||||
|    * @param place The place to be associated with the uploads. | ||||
|    * @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed | ||||
|    */ | ||||
|   @Override | ||||
|   public void onUserConfirmedUploadIsOfPlace(final Place place, final int uploadItemIndex) { | ||||
|       final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex); | ||||
| 
 | ||||
|       uploadItem.setPlace(place); | ||||
|       final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails(); | ||||
|       // Update UploadMediaDetail object for this UploadItem | ||||
|       uploadMediaDetails.set(0, new UploadMediaDetail(place)); | ||||
| 
 | ||||
|       // Now that the UploadItem and its associated UploadMediaDetail objects have been updated, | ||||
|       // update the view with the modified media details of the first upload item | ||||
|       view.updateMediaDetails(uploadMediaDetails); | ||||
|       UploadActivity.setUploadIsOfAPlace(true); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Calculates the image quality | ||||
|      * | ||||
|      * @param uploadItemIndex      Index of the UploadItem whose quality is to be checked | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @param activity             Context reference | ||||
|      * @return true if no internal error occurs, else returns false | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation, | ||||
|         final Activity activity) { | ||||
|         final List<UploadItem> uploadItems = repository.getUploads(); | ||||
|         view.showProgress(true); | ||||
|         if (uploadItems.isEmpty()) { | ||||
|             view.showProgress(false); | ||||
|             // No internationalization required for this error message because it's an internal error. | ||||
|             view.showMessage( | ||||
|                 "Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.", | ||||
|                 R.color.color_error); | ||||
|             return false; | ||||
|         } | ||||
|         final UploadItem uploadItem = uploadItems.get(uploadItemIndex); | ||||
|         compositeDisposable.add( | ||||
|             repository | ||||
|                 .getImageQuality(uploadItem, inAppPictureLocation) | ||||
|                 .observeOn(mainThreadScheduler) | ||||
|                 .subscribe(imageResult -> { | ||||
|                         storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem); | ||||
|                     }, | ||||
|                     throwable -> { | ||||
|                         if (throwable instanceof UnknownHostException) { | ||||
|                             view.showProgress(false); | ||||
|                             view.showConnectionErrorPopup(); | ||||
|                         } else { | ||||
|                             view.showMessage("" + throwable.getLocalizedMessage(), | ||||
|                                 R.color.color_error); | ||||
|                         } | ||||
|                         Timber.e(throwable, "Error occurred while handling image"); | ||||
|                     }) | ||||
|         ); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stores the image quality in JSON format in SharedPrefs | ||||
|      * | ||||
|      * @param imageResult     Image quality | ||||
|      * @param uploadItemIndex Index of the UploadItem whose quality is calculated | ||||
|      * @param activity        Context reference | ||||
|      * @param uploadItem      UploadItem whose quality is to be checked | ||||
|      */ | ||||
|     private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity, | ||||
|         final UploadItem uploadItem) { | ||||
|         final BasicKvStore store = new BasicKvStore(activity, | ||||
|             UploadActivity.storeNameForCurrentUploadImagesSize); | ||||
|         final String value = store.getString(keyForCurrentUploadImageQualities, null); | ||||
|         final JSONObject jsonObject; | ||||
|         try { | ||||
|             if (value != null) { | ||||
|                 jsonObject = new JSONObject(value); | ||||
|             } else { | ||||
|                 jsonObject = new JSONObject(); | ||||
|             } | ||||
|             jsonObject.put("UploadItem" + uploadItemIndex, imageResult); | ||||
|             store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); | ||||
|         } catch (final Exception e) { | ||||
|             Timber.e(e); | ||||
|         } | ||||
| 
 | ||||
|         if (uploadItemIndex == 0) { | ||||
|             if (!isBatteryDialogShowing && !isCategoriesDialogShowing) { | ||||
|                 // if battery-optimisation dialog is not being shown, call checkImageQuality | ||||
|                 checkImageQuality(uploadItem, uploadItemIndex); | ||||
|             } else { | ||||
|                 view.showProgress(false); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to check image quality from stored qualities and display dialogs | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose quality is to be checked | ||||
|      * @param index      Index of the UploadItem whose quality is to be checked | ||||
|      */ | ||||
|     @Override | ||||
|     public void checkImageQuality(final UploadItem uploadItem, final int index) { | ||||
|         if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality() | ||||
|             != IMAGE_KEEP)) { | ||||
|             final BasicKvStore store = new BasicKvStore(activity, | ||||
|                 UploadActivity.storeNameForCurrentUploadImagesSize); | ||||
|             final String value = store.getString(keyForCurrentUploadImageQualities, null); | ||||
|             final JSONObject jsonObject; | ||||
|             try { | ||||
|                 if (value != null) { | ||||
|                     jsonObject = new JSONObject(value); | ||||
|                 } else { | ||||
|                     jsonObject = new JSONObject(); | ||||
|                 } | ||||
|                 final Integer imageQuality = (int) jsonObject.get("UploadItem" + index); | ||||
|                 view.showProgress(false); | ||||
|                 if (imageQuality == IMAGE_OK) { | ||||
|                     uploadItem.setHasInvalidLocation(false); | ||||
|                     uploadItem.setImageQuality(imageQuality); | ||||
|                 } else { | ||||
|                     handleBadImage(imageQuality, uploadItem, index); | ||||
|                 } | ||||
|             } catch (final Exception e) { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the image qualities stored in JSON, whenever an image is deleted | ||||
|      * | ||||
|      * @param size Size of uploadableFiles | ||||
|      * @param index Index of the UploadItem which was deleted | ||||
|      */ | ||||
|     @Override | ||||
|     public void updateImageQualitiesJSON(final int size, final int index) { | ||||
|         final BasicKvStore store = new BasicKvStore(activity, | ||||
|             UploadActivity.storeNameForCurrentUploadImagesSize); | ||||
|         final String value = store.getString(keyForCurrentUploadImageQualities, null); | ||||
|         final JSONObject jsonObject; | ||||
|         try { | ||||
|             if (value != null) { | ||||
|                 jsonObject = new JSONObject(value); | ||||
|             } else { | ||||
|                 jsonObject = new JSONObject(); | ||||
|             } | ||||
|             for (int i = index; i < (size - 1); i++) { | ||||
|                 jsonObject.put("UploadItem" + i, jsonObject.get("UploadItem" + (i + 1))); | ||||
|             } | ||||
|             jsonObject.remove("UploadItem" + (size - 1)); | ||||
|             store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); | ||||
|         } catch (final Exception e) { | ||||
|             Timber.e(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles bad pictures, like too dark, already on wikimedia, downloaded from internet | ||||
|      * | ||||
|      * @param errorCode Error code of the bad image quality | ||||
|      * @param uploadItem UploadItem whose quality is bad | ||||
|      * @param index Index of item whose quality is bad | ||||
|      */ | ||||
|     public void handleBadImage(final Integer errorCode, | ||||
|         final UploadItem uploadItem, final int index) { | ||||
|         Timber.d("Handle bad picture with error code %d", errorCode); | ||||
|         if (errorCode >= 8) { // If location of image and nearby does not match | ||||
|             uploadItem.setHasInvalidLocation(true); | ||||
|         } | ||||
| 
 | ||||
|         // If image has some other problems, show popup accordingly | ||||
|         if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) { | ||||
|             showBadImagePopup(errorCode, index, activity, uploadItem); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog describing the potential problems in the current image | ||||
|      * | ||||
|      * @param errorCode  Has the potential problems in the current image | ||||
|      * @param index      Index of the UploadItem which has problems | ||||
|      * @param activity   Context reference | ||||
|      * @param uploadItem UploadItem which has problems | ||||
|      */ | ||||
|     public void showBadImagePopup(final Integer errorCode, | ||||
|         final int index, final Activity activity, final UploadItem uploadItem) { | ||||
|         final String errorMessageForResult = getErrorMessageForResult(activity, errorCode); | ||||
|         if (!StringUtils.isBlank(errorMessageForResult)) { | ||||
|             DialogUtil.showAlertDialog(activity, | ||||
|                 activity.getString(R.string.upload_problem_image), | ||||
|                 errorMessageForResult, | ||||
|                 activity.getString(R.string.upload), | ||||
|                 activity.getString(R.string.cancel), | ||||
|                 () -> { | ||||
|                     view.showProgress(false); | ||||
|                     uploadItem.setImageQuality(IMAGE_OK); | ||||
|                 }, | ||||
|                 () -> { | ||||
|                     presenterCallback.deletePictureAtIndex(index); | ||||
|                 } | ||||
|             ).setCancelable(false); | ||||
|         } | ||||
|         //If the error message is null, we will probably not show anything | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * notifies the user that a similar image exists | ||||
|      */ | ||||
|     @Override | ||||
|     public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath, | ||||
|         final ImageCoordinates similarImageCoordinates) { | ||||
|         view.showSimilarImageFragment(originalFilePath, possibleFilePath, | ||||
|             similarImageCoordinates | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,441 @@ | |||
| package fr.free.nrw.commons.upload.mediaDetails | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.repository.UploadRepository | ||||
| import fr.free.nrw.commons.upload.ImageCoordinates | ||||
| import fr.free.nrw.commons.upload.SimilarImageInterface | ||||
| import fr.free.nrw.commons.upload.UploadActivity | ||||
| import fr.free.nrw.commons.upload.UploadActivity.Companion.setUploadIsOfAPlace | ||||
| import fr.free.nrw.commons.upload.UploadItem | ||||
| import fr.free.nrw.commons.upload.UploadMediaDetail | ||||
| import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback | ||||
| import fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION | ||||
| import fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS | ||||
| import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP | ||||
| import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK | ||||
| import io.github.coordinates2country.Coordinates2Country | ||||
| import io.reactivex.Maybe | ||||
| import io.reactivex.Scheduler | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import org.json.JSONObject | ||||
| import timber.log.Timber | ||||
| import java.lang.reflect.Method | ||||
| import java.lang.reflect.Proxy | ||||
| import java.net.UnknownHostException | ||||
| import java.util.Locale | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| class UploadMediaPresenter @Inject constructor( | ||||
|     private val repository: UploadRepository, | ||||
|     @param:Named(IO_THREAD) private val ioScheduler: Scheduler, | ||||
|     @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler | ||||
| ) : UploadMediaDetailsContract.UserActionListener, SimilarImageInterface { | ||||
|     private var view = DUMMY | ||||
| 
 | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     private val countryNamesAndCodes: Map<String, String> by lazy { | ||||
|         // Create a map containing all ISO countries 2-letter codes provided by | ||||
|         // `Locale.getISOCountries()` and their english names | ||||
|         buildMap { | ||||
|             Locale.getISOCountries().forEach { | ||||
|                 put(Locale("en", it).getDisplayCountry(Locale.ENGLISH), it) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     lateinit var basicKvStoreFactory: (String) -> BasicKvStore | ||||
| 
 | ||||
|     override fun onAttachView(view: UploadMediaDetailsContract.View) { | ||||
|         this.view = view | ||||
|     } | ||||
| 
 | ||||
|     override fun onDetachView() { | ||||
|         view = DUMMY | ||||
|         compositeDisposable.clear() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the Upload Media Details for the corresponding upload item | ||||
|      */ | ||||
|     override fun setUploadMediaDetails( | ||||
|         uploadMediaDetails: List<UploadMediaDetail>, | ||||
|         uploadItemIndex: Int | ||||
|     ) { | ||||
|         repository.getUploads()[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList() | ||||
|     } | ||||
| 
 | ||||
|     override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) { | ||||
|         basicKvStoreFactory = factory | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Receives the corresponding uploadable file, processes it and return the view with and uplaod item | ||||
|      */ | ||||
|     override fun receiveImage( | ||||
|         uploadableFile: UploadableFile?, | ||||
|         place: Place?, | ||||
|         inAppPictureLocation: LatLng? | ||||
|     ) { | ||||
|         view.showProgress(true) | ||||
|         compositeDisposable.add( | ||||
|             repository.preProcessImage( | ||||
|                 uploadableFile, place, this, inAppPictureLocation | ||||
|             ).map { uploadItem: UploadItem -> | ||||
|                 if (place != null && place.isMonument && place.location != null) { | ||||
|                     val countryCode = countryNamesAndCodes[Coordinates2Country.country( | ||||
|                         place.location.latitude, | ||||
|                         place.location.longitude | ||||
|                     )] | ||||
|                     if (countryCode != null && WLM_SUPPORTED_COUNTRIES.contains(countryCode.lowercase())) { | ||||
|                         uploadItem.isWLMUpload = true | ||||
|                         uploadItem.countryCode = countryCode.lowercase() | ||||
|                     } | ||||
|                 } | ||||
|                 uploadItem | ||||
|             }.subscribeOn(ioScheduler).observeOn(mainThreadScheduler) | ||||
|                 .subscribe({ uploadItem: UploadItem -> | ||||
|                     view.onImageProcessed(uploadItem) | ||||
|                     view.updateMediaDetails(uploadItem.uploadMediaDetails) | ||||
|                     view.showProgress(false) | ||||
|                     val gpsCoords = uploadItem.gpsCoords | ||||
|                     val hasImageCoordinates = gpsCoords != null && gpsCoords.imageCoordsExists | ||||
|                     if (hasImageCoordinates && place == null) { | ||||
|                         checkNearbyPlaces(uploadItem) | ||||
|                     } | ||||
|                 }, { throwable: Throwable? -> | ||||
|                     Timber.e(throwable, "Error occurred in processing images") | ||||
|                 }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method checks for the nearest location that needs images and suggests it to the user. | ||||
|      */ | ||||
|     private fun checkNearbyPlaces(uploadItem: UploadItem) { | ||||
|         compositeDisposable.add(Maybe.fromCallable { | ||||
|             repository.checkNearbyPlaces( | ||||
|                     uploadItem.gpsCoords!!.decLatitude, uploadItem.gpsCoords!!.decLongitude | ||||
|             ) | ||||
|         }.subscribeOn(ioScheduler).observeOn(mainThreadScheduler).subscribe({ | ||||
|                 view.onNearbyPlaceFound(uploadItem, it) | ||||
|             }, { throwable: Throwable? -> | ||||
|                 Timber.e(throwable, "Error occurred in processing images") | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if the image has a location. Displays a dialog alerting user that no | ||||
|      * location has been to added to the image and asking them to add one, if location was not | ||||
|      * removed by the user | ||||
|      * | ||||
|      * @param uploadItemIndex Index of the uploadItem which has no location | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @param hasUserRemovedLocation True if user has removed location from the image | ||||
|      */ | ||||
|     override fun displayLocDialog( | ||||
|         uploadItemIndex: Int, inAppPictureLocation: LatLng?, | ||||
|         hasUserRemovedLocation: Boolean | ||||
|     ) { | ||||
|         val uploadItem = repository.getUploads()[uploadItemIndex] | ||||
|         if (uploadItem.gpsCoords!!.decimalCoords == null && inAppPictureLocation == null && !hasUserRemovedLocation) { | ||||
|             view.displayAddLocationDialog { verifyCaptionQuality(uploadItem) } | ||||
|         } else { | ||||
|             verifyCaptionQuality(uploadItem) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Verifies the image's caption and calls function to handle the result | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose caption is checked | ||||
|      */ | ||||
|     private fun verifyCaptionQuality(uploadItem: UploadItem) { | ||||
|         view.showProgress(true) | ||||
|         compositeDisposable.add(repository.getCaptionQuality(uploadItem) | ||||
|             .observeOn(mainThreadScheduler) | ||||
|             .subscribe({ capResult: Int -> | ||||
|                 view.showProgress(false) | ||||
|                 handleCaptionResult(capResult, uploadItem) | ||||
|             }, { throwable: Throwable -> | ||||
|                 view.showProgress(false) | ||||
|                 if (throwable is UnknownHostException) { | ||||
|                     view.showConnectionErrorPopupForCaptionCheck() | ||||
|                 } else { | ||||
|                     view.showMessage(throwable.localizedMessage, R.color.color_error) | ||||
|                 } | ||||
|                 Timber.e(throwable, "Error occurred while handling image") | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles image's caption results and shows dialog if necessary | ||||
|      * | ||||
|      * @param errorCode Error code of the UploadItem | ||||
|      * @param uploadItem UploadItem whose caption is checked | ||||
|      */ | ||||
|     fun handleCaptionResult(errorCode: Int, uploadItem: UploadItem) { | ||||
|         // If errorCode is empty caption show message | ||||
|         if (errorCode == EMPTY_CAPTION) { | ||||
|             Timber.d("Captions are empty. Showing toast") | ||||
|             view.showMessage(R.string.add_caption_toast, R.color.color_error) | ||||
|         } | ||||
| 
 | ||||
|         // If image with same file name exists check the bit in errorCode is set or not | ||||
|         if ((errorCode and FILE_NAME_EXISTS) != 0) { | ||||
|             Timber.d("Trying to show duplicate picture popup") | ||||
|             view.showDuplicatePicturePopup(uploadItem) | ||||
|         } | ||||
| 
 | ||||
|         // If caption is not duplicate or user still wants to upload it | ||||
|         if (errorCode == IMAGE_OK) { | ||||
|             Timber.d("Image captions are okay or user still wants to upload it") | ||||
|             view.onImageValidationSuccess() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Copies the caption and description of the current item to the subsequent media | ||||
|      */ | ||||
|     override fun copyTitleAndDescriptionToSubsequentMedia(indexInViewFlipper: Int) { | ||||
|         for (i in indexInViewFlipper + 1 until repository.getCount()) { | ||||
|             val subsequentUploadItem = repository.getUploads()[i] | ||||
|             subsequentUploadItem.uploadMediaDetails = deepCopy( | ||||
|                 repository.getUploads()[indexInViewFlipper].uploadMediaDetails | ||||
|             ).toMutableList() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches and set the caption and description of the item | ||||
|      */ | ||||
|     override fun fetchTitleAndDescription(indexInViewFlipper: Int) = | ||||
|         view.updateMediaDetails(repository.getUploads()[indexInViewFlipper].uploadMediaDetails) | ||||
| 
 | ||||
|     private fun deepCopy(uploadMediaDetails: List<UploadMediaDetail>) = | ||||
|         uploadMediaDetails.map(UploadMediaDetail::javaCopy) | ||||
| 
 | ||||
|     override fun useSimilarPictureCoordinates( | ||||
|         imageCoordinates: ImageCoordinates, uploadItemIndex: Int | ||||
|     ) = repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex) | ||||
| 
 | ||||
|     override fun onMapIconClicked(indexInViewFlipper: Int) = | ||||
|         view.showExternalMap(repository.getUploads()[indexInViewFlipper]) | ||||
| 
 | ||||
|     override fun onEditButtonClicked(indexInViewFlipper: Int) = | ||||
|         view.showEditActivity(repository.getUploads()[indexInViewFlipper]) | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the information regarding the specified place for the specified upload item | ||||
|      * when the user confirms the suggested nearby place. | ||||
|      * | ||||
|      * @param place The place to be associated with the uploads. | ||||
|      * @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed | ||||
|      */ | ||||
|     override fun onUserConfirmedUploadIsOfPlace(place: Place?, uploadItemIndex: Int) { | ||||
|         val uploadItem = repository.getUploads()[uploadItemIndex] | ||||
| 
 | ||||
|         uploadItem.place = place | ||||
|         val uploadMediaDetails = uploadItem.uploadMediaDetails | ||||
|         // Update UploadMediaDetail object for this UploadItem | ||||
|         uploadMediaDetails[0] = UploadMediaDetail(place) | ||||
| 
 | ||||
|         // Now that the UploadItem and its associated UploadMediaDetail objects have been updated, | ||||
|         // update the view with the modified media details of the first upload item | ||||
|         view.updateMediaDetails(uploadMediaDetails) | ||||
|         setUploadIsOfAPlace(true) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Calculates the image quality | ||||
|      * | ||||
|      * @param uploadItemIndex      Index of the UploadItem whose quality is to be checked | ||||
|      * @param inAppPictureLocation In app picture location (if any) | ||||
|      * @param activity             Context reference | ||||
|      * @return true if no internal error occurs, else returns false | ||||
|      */ | ||||
|     override fun getImageQuality( | ||||
|         uploadItemIndex: Int, | ||||
|         inAppPictureLocation: LatLng?, | ||||
|         activity: Activity | ||||
|     ): Boolean { | ||||
|         val uploadItems = repository.getUploads() | ||||
|         view.showProgress(true) | ||||
|         if (uploadItems.isEmpty()) { | ||||
|             view.showProgress(false) | ||||
|             // No internationalization required for this error message because it's an internal error. | ||||
|             view.showMessage( | ||||
|                 "Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.", | ||||
|                 R.color.color_error | ||||
|             ) | ||||
|             return false | ||||
|         } | ||||
|         val uploadItem = uploadItems[uploadItemIndex] | ||||
|         compositeDisposable.add(repository.getImageQuality(uploadItem, inAppPictureLocation) | ||||
|             .observeOn(mainThreadScheduler) | ||||
|             .subscribe({ imageResult: Int -> | ||||
|                 storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem) | ||||
|             }, { throwable: Throwable -> | ||||
|                 if (throwable is UnknownHostException) { | ||||
|                     view.showProgress(false) | ||||
|                     view.showConnectionErrorPopup() | ||||
|                 } else { | ||||
|                     view.showMessage(throwable.localizedMessage, R.color.color_error) | ||||
|                 } | ||||
|                 Timber.e(throwable, "Error occurred while handling image") | ||||
|             }) | ||||
|         ) | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stores the image quality in JSON format in SharedPrefs | ||||
|      * | ||||
|      * @param imageResult     Image quality | ||||
|      * @param uploadItemIndex Index of the UploadItem whose quality is calculated | ||||
|      * @param activity        Context reference | ||||
|      * @param uploadItem      UploadItem whose quality is to be checked | ||||
|      */ | ||||
|     private fun storeImageQuality( | ||||
|         imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem | ||||
|     ) { | ||||
|         val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize) | ||||
|         val value = store.getString(UPLOAD_QUALITIES_KEY, null) | ||||
|         try { | ||||
|             val jsonObject = value.asJsonObject().apply { | ||||
|                 put("UploadItem$uploadItemIndex", imageResult) | ||||
|             } | ||||
|             store.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|         } | ||||
| 
 | ||||
|         if (uploadItemIndex == 0) { | ||||
|             if (!isBatteryDialogShowing && !isCategoriesDialogShowing) { | ||||
|                 // if battery-optimisation dialog is not being shown, call checkImageQuality | ||||
|                 checkImageQuality(uploadItem, uploadItemIndex) | ||||
|             } else { | ||||
|                 view.showProgress(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to check image quality from stored qualities and display dialogs | ||||
|      * | ||||
|      * @param uploadItem UploadItem whose quality is to be checked | ||||
|      * @param index      Index of the UploadItem whose quality is to be checked | ||||
|      */ | ||||
|     override fun checkImageQuality(uploadItem: UploadItem, index: Int) { | ||||
|         if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) { | ||||
|             val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||
|                 .getString(UPLOAD_QUALITIES_KEY, null) | ||||
|             try { | ||||
|                 val imageQuality = value.asJsonObject()["UploadItem$index"] as Int | ||||
|                 view.showProgress(false) | ||||
|                 if (imageQuality == IMAGE_OK) { | ||||
|                     uploadItem.hasInvalidLocation = false | ||||
|                     uploadItem.imageQuality = imageQuality | ||||
|                 } else { | ||||
|                     handleBadImage(imageQuality, uploadItem, index) | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 Timber.e(e) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the image qualities stored in JSON, whenever an image is deleted | ||||
|      * | ||||
|      * @param size Size of uploadableFiles | ||||
|      * @param index Index of the UploadItem which was deleted | ||||
|      */ | ||||
|     override fun updateImageQualitiesJSON(size: Int, index: Int) { | ||||
|         val value = basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||
|             .getString(UPLOAD_QUALITIES_KEY, null) | ||||
|         try { | ||||
|             val jsonObject = value.asJsonObject().apply { | ||||
|                 for (i in index until (size - 1)) { | ||||
|                     put("UploadItem$i", this["UploadItem" + (i + 1)]) | ||||
|                 } | ||||
|                 remove("UploadItem" + (size - 1)) | ||||
|             } | ||||
|             basicKvStoreFactory(UploadActivity.storeNameForCurrentUploadImagesSize) | ||||
|                 .putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles bad pictures, like too dark, already on wikimedia, downloaded from internet | ||||
|      * | ||||
|      * @param errorCode Error code of the bad image quality | ||||
|      * @param uploadItem UploadItem whose quality is bad | ||||
|      * @param index Index of item whose quality is bad | ||||
|      */ | ||||
|     private fun handleBadImage( | ||||
|         errorCode: Int, | ||||
|         uploadItem: UploadItem, index: Int | ||||
|     ) { | ||||
|         Timber.d("Handle bad picture with error code %d", errorCode) | ||||
|         if (errorCode >= 8) { // If location of image and nearby does not match | ||||
|             uploadItem.hasInvalidLocation = true | ||||
|         } | ||||
| 
 | ||||
|         // If image has some other problems, show popup accordingly | ||||
|         if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) { | ||||
|             view.showBadImagePopup(errorCode, index, uploadItem) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * notifies the user that a similar image exists | ||||
|      */ | ||||
|     override fun showSimilarImageFragment( | ||||
|         originalFilePath: String?, | ||||
|         possibleFilePath: String?, | ||||
|         similarImageCoordinates: ImageCoordinates? | ||||
|     ) = view.showSimilarImageFragment(originalFilePath, possibleFilePath, similarImageCoordinates) | ||||
| 
 | ||||
|     private fun String?.asJsonObject() = if (this != null) { | ||||
|         JSONObject(this) | ||||
|     } else { | ||||
|         JSONObject() | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val UPLOAD_QUALITIES_KEY = "UploadedImagesQualities" | ||||
|         private val WLM_SUPPORTED_COUNTRIES = listOf( | ||||
|             "am", "at", "az", "br", "hr", "sv", "fi", "fr", "de", "gh", | ||||
|             "in", "ie", "il", "mk", "my", "mt", "pk", "pe", "pl", "ru", | ||||
|             "rw", "si", "es", "se", "tw", "ug", "ua", "us" | ||||
|         ) | ||||
| 
 | ||||
|         private val DUMMY = Proxy.newProxyInstance( | ||||
|             UploadMediaDetailsContract.View::class.java.classLoader, | ||||
|             arrayOf<Class<*>>(UploadMediaDetailsContract.View::class.java) | ||||
|         ) { _: Any?, _: Method?, _: Array<Any?>? -> null } as UploadMediaDetailsContract.View | ||||
| 
 | ||||
|         var presenterCallback: UploadMediaDetailFragmentCallback? = null | ||||
| 
 | ||||
|         /** | ||||
|          * Variable used to determine if the battery-optimisation dialog is being shown or not | ||||
|          */ | ||||
|         var isBatteryDialogShowing: Boolean = false | ||||
| 
 | ||||
|         var isCategoriesDialogShowing: Boolean = false | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Paul Hawke
						Paul Hawke