Convert UploadActivity to kotlin

This commit is contained in:
Paul Hawke 2024-12-23 21:54:21 -06:00
parent 87c8224793
commit e4b4ceb39d
9 changed files with 962 additions and 1006 deletions

View file

@ -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;
}
}
}
}

View file

@ -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<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 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<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!!.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
}
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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)) }
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -208,7 +208,7 @@ object PermissionUtils {
activity.getString(android.R.string.cancel),
{
if (activity is UploadActivity) {
activity.setShowPermissionsDialog(true)
activity.isShowPermissionsDialog = true
}
token.continuePermissionRequest()
},