From e4b4ceb39dd7e92aa8974bab51fbfbb26ee7b896 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Mon, 23 Dec 2024 21:54:21 -0600 Subject: [PATCH] Convert UploadActivity to kotlin --- .../nrw/commons/upload/UploadActivity.java | 986 ------------------ .../free/nrw/commons/upload/UploadActivity.kt | 946 +++++++++++++++++ .../nrw/commons/upload/UploadBaseFragment.kt | 2 +- .../fr/free/nrw/commons/upload/UploadModel.kt | 2 +- .../categories/UploadCategoriesFragment.kt | 14 +- .../commons/upload/depicts/DepictsFragment.kt | 9 +- .../upload/license/MediaLicenseFragment.kt | 3 +- .../UploadMediaDetailFragment.java | 4 +- .../free/nrw/commons/utils/PermissionUtils.kt | 2 +- 9 files changed, 962 insertions(+), 1006 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java deleted file mode 100644 index 01a084ed2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ /dev/null @@ -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 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 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 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 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 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 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 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: Issue - * - * @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: Issue - * - * @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 fragments; - - public UploadImageAdapter(final FragmentManager fragmentManager) { - super(fragmentManager); - this.fragments = new ArrayList<>(); - } - - public void setFragments(final List 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; - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt new file mode 100644 index 000000000..bc4704147 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt @@ -0,0 +1,946 @@ +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? = 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 = 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 = + 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().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? { + 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, + 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 filepath The file path of the new thumbnail image. + */ + override fun changeThumbnail(index: Int, filepath: String) { + uploadableFiles.removeAt(index) + uploadableFiles.add(index, UploadableFile(File(filepath))) + 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!!.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 = 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().apply { + addAll(intent.getParcelableArrayListExtra(EXTRA_FILES) ?: emptyList()) + } + 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() + } + + 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 = 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(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!!.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? = 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 + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt index cdde44c1d..57e62d572 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt index 68ff30c4f..dab89e427 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt @@ -175,7 +175,7 @@ class UploadModel @Inject internal constructor( Timber.d( "Created timestamp while building contribution is %s, %s", item.createdTimestamp, - Date(item.createdTimestamp) + Date(item.createdTimestamp!!) ) if (item.createdTimestamp != -1L) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt index e46fdf45f..80037a028 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt @@ -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.let { it.onNextButtonClicked(it.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.let { it.onPreviousButtonClicked(it.getIndexInViewFlipper(this)) } + callback?.let { it.onPreviousButtonClicked(it.getIndexInViewFlipper(this)) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt index 99fd52571..39bcabb46 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt index 5ba61ab75..0415d3270 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 884ad9831..9da781ce6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -422,7 +422,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements final Activity activity = getActivity(); if (activity instanceof UploadActivity) { - final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); + final boolean isMultipleFilesSelected = ((UploadActivity) activity).isMultipleFilesSelected(); // Determine the message based on the selection status String message; @@ -476,7 +476,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements * This method gets called whenever the next/previous button is pressed */ @Override - protected void onBecameVisible() { + public void onBecameVisible() { super.onBecameVisible(); if (callback == null) { return; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt index f5a98dc5d..daf158fc1 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt @@ -208,7 +208,7 @@ object PermissionUtils { activity.getString(android.R.string.cancel), { if (activity is UploadActivity) { - activity.setShowPermissionsDialog(true) + activity.isShowPermissionsDialog = true } token.continuePermissionRequest() },