diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index fcfd329740..e910799d07 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -87,7 +87,7 @@ public void initiateCameraPick(Activity activity, }, R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } /** @@ -224,7 +224,7 @@ public void initiateCustomGalleryPickWithPermission(final Activity activity, Act () -> FilePicker.openCustomSelector(activity, resultLauncher, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index a840aa8e11..1699f35f0e 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; @@ -23,12 +22,10 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; @@ -39,7 +36,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentTransaction; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.databinding.FragmentContributionsBinding; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index a9e9ee5c62..849ef3450b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -1,13 +1,10 @@ package fr.free.nrw.commons.contributions; -import android.Manifest.permission; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -16,10 +13,8 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.databinding.MainBinding; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -41,7 +36,6 @@ import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadProgressActivity; import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.PermissionUtils; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 02f7d418e1..134ee48d9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -1,10 +1,10 @@ package fr.free.nrw.commons.delete; import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE; +import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.annotation.SuppressLint; import android.content.Context; -import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; @@ -16,6 +16,7 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.review.ReviewController; +import fr.free.nrw.commons.utils.LangCodeUtils; import fr.free.nrw.commons.utils.ViewUtilWrapper; import io.reactivex.Observable; import io.reactivex.Single; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java index 52a5571e94..441f46e610 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java @@ -4,14 +4,12 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; -import android.Manifest; import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Paint; @@ -21,22 +19,17 @@ import android.location.LocationManager; import android.os.Bundle; import android.preference.PreferenceManager; -import android.provider.Settings; import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.Toast; -import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.MapController; @@ -48,7 +41,6 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.ExploreMapRootFragment; import fr.free.nrw.commons.explore.paging.LiveDataConverter; -import fr.free.nrw.commons.filepicker.Constants; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationPermissionsHelper; @@ -60,7 +52,6 @@ import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.MapUtils; import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.SystemThemeUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; @@ -310,7 +301,7 @@ private void unregisterNetworkReceiver() { } private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); @@ -331,7 +322,7 @@ private void performMapReadyActions() { !locationPermissionsHelper.checkLocationPermission(getActivity())) { isPermissionDenied = true; } - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index edfa874fcb..ed20809acd 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -318,7 +318,7 @@ public void run() { } public void launchZoomActivity(final View view) { - final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE); + final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE()); if (hasPermission) { launchZoomActivityAfterPermissionCheck(view); } else { @@ -328,7 +328,7 @@ public void launchZoomActivity(final View view) { }, R.string.storage_permission_title, R.string.read_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE + PermissionUtils.getPERMISSIONS_STORAGE() ); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 6a2e5c3a9e..fdbc727bc6 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -43,7 +43,6 @@ import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.Button; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; @@ -701,7 +700,7 @@ private void startMapWithoutPermission() { = new LatLng(Double.parseDouble(locationLatLng[0]), Double.parseDouble(locationLatLng[1]), 1f); } else { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); } if (binding.map != null) { moveCameraToPosition( @@ -793,7 +792,7 @@ public void initNearbyFilter() { hideBottomSheet(); binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener( (v, hasFocus) -> { - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); if (hasFocus) { binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE); @@ -834,7 +833,7 @@ public boolean isDarkTheme() { .getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(), 0.75); binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter); - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot()); + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); compositeDisposable.add( RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView) .takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView)) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java index 410aeb9f40..00a491e689 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java @@ -11,13 +11,10 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.location.Location; -import android.view.View; import androidx.annotation.MainThread; import androidx.annotation.Nullable; -import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; @@ -26,14 +23,10 @@ import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.nearby.MarkerPlaceGroup; import fr.free.nrw.commons.nearby.NearbyController; -import fr.free.nrw.commons.nearby.NearbyFilterState; import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.PlaceDao; import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; -import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.LocationUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; -import io.reactivex.disposables.CompositeDisposable; import java.lang.reflect.Proxy; import java.util.List; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 20fc831a84..d4ed379f09 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -12,7 +12,6 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -543,7 +542,7 @@ private String getCurrentLanguageCode(final String preferenceKey) { * First checks for external storage permissions and then sends logs via email */ private void checkPermissionsAndSendLogs() { - if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE)) { + if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) { commonsLogSender.send(getActivity(), null); } else { requestExternalStoragePermissions(); @@ -556,7 +555,7 @@ private void checkPermissionsAndSendLogs() { */ private void requestExternalStoragePermissions() { Dexter.withActivity(getActivity()) - .withPermissions(PermissionUtils.PERMISSIONS_STORAGE) + .withPermissions(PermissionUtils.getPERMISSIONS_STORAGE()) .withListener(new MultiplePermissionsListener() { @Override public void onPermissionsChecked(MultiplePermissionsReport report) { 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 index 35906c3fb5..ed65b05dff 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -1,8 +1,8 @@ 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.PERMISSIONS_STORAGE; 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; @@ -32,7 +32,6 @@ import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -277,7 +276,7 @@ protected void checkBlockStatus() { public void checkStoragePermissions() { // Check if all required permissions are granted - final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE); + 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 @@ -297,7 +296,7 @@ public void checkStoragePermissions() { }, R.string.storage_permission_title, R.string.write_storage_permission_rationale_for_image_share, - PERMISSIONS_STORAGE); + getPERMISSIONS_STORAGE()); } } /* If all permissions are not granted and a dialog is already showing on screen diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java deleted file mode 100644 index 99155a5e3b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ /dev/null @@ -1,351 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.ProgressDialog; -import android.app.WallpaperManager; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.exifinterface.media.ExifInterface; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.SetWallpaperWorker; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import timber.log.Timber; - -/** - * Created by bluesir9 on 3/10/17. - */ - -public class ImageUtils { - - /** - * Set 0th bit as 1 for dark image ie. 0001 - */ - public static final int IMAGE_DARK = 1 << 0; // 1 - /** - * Set 1st bit as 1 for blurry image ie. 0010 - */ - public static final int IMAGE_BLURRY = 1 << 1; // 2 - /** - * Set 2nd bit as 1 for duplicate image ie. 0100 - */ - public static final int IMAGE_DUPLICATE = 1 << 2; //4 - /** - * Set 3rd bit as 1 for image with different geo location ie. 1000 - */ - public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3; //8 - /** - * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains FBMD data else returns IMAGE_OK - * ie. 10000 - */ - public static final int FILE_FBMD = 1 << 4; - /** - * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does not contains EXIF data else returns IMAGE_OK - * ie. 100000 - */ - public static final int FILE_NO_EXIF = 1 << 5; - public static final int IMAGE_OK = 0; - public static final int IMAGE_KEEP = -1; - public static final int IMAGE_WAIT = -2; - public static final int EMPTY_CAPTION = -3; - public static final int FILE_NAME_EXISTS = 1 << 6; - static final int NO_CATEGORY_SELECTED = -5; - - private static ProgressDialog progressDialogWallpaper; - - private static ProgressDialog progressDialogAvatar; - - @IntDef( - flag = true, - value = { - IMAGE_DARK, - IMAGE_BLURRY, - IMAGE_DUPLICATE, - IMAGE_OK, - IMAGE_KEEP, - IMAGE_WAIT, - EMPTY_CAPTION, - FILE_NAME_EXISTS, - NO_CATEGORY_SELECTED, - IMAGE_GEOLOCATION_DIFFERENT - } - ) - @Retention(RetentionPolicy.SOURCE) - public @interface Result { - } - - /** - * @return IMAGE_OK if image is not too dark - * IMAGE_DARK if image is too dark - */ - static @Result int checkIfImageIsTooDark(String imagePath) { - long millis = System.currentTimeMillis(); - try { - Bitmap bmp = new ExifInterface(imagePath).getThumbnailBitmap(); - if (bmp == null) { - bmp = BitmapFactory.decodeFile(imagePath); - } - - if (checkIfImageIsDark(bmp)) { - return IMAGE_DARK; - } - - } catch (Exception e) { - Timber.d(e, "Error while checking image darkness."); - } finally { - Timber.d("Checking image darkness took " + (System.currentTimeMillis() - millis) + " ms."); - } - return IMAGE_OK; - } - - /** - * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string - * @param latLng Location of wikidata item will be edited after upload - * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null - * true if geolocation of the image and wikidata item are different - */ - static boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) { - Timber.d("Comparing geolocation of file with nearby place location"); - if (latLng == null) { // Means that geolocation for this image is not given - return false; // Since we don't know geolocation of file, we choose letting upload - } - - String[] geolocationOfFile = geolocationOfFileString.split("\\|"); - Double distance = LengthUtils.computeDistanceBetween( - new LatLng(Double.parseDouble(geolocationOfFile[0]),Double.parseDouble(geolocationOfFile[1]),0) - , latLng); - // Distance is more than 1 km, means that geolocation is wrong - return distance >= 1000; - } - - private static boolean checkIfImageIsDark(Bitmap bitmap) { - if (bitmap == null) { - Timber.e("Expected bitmap was null"); - return true; - } - - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - - int allPixelsCount = bitmapWidth * bitmapHeight; - int numberOfBrightPixels = 0; - int numberOfMediumBrightnessPixels = 0; - double brightPixelThreshold = 0.025 * allPixelsCount; - double mediumBrightPixelThreshold = 0.3 * allPixelsCount; - - for (int x = 0; x < bitmapWidth; x++) { - for (int y = 0; y < bitmapHeight; y++) { - int pixel = bitmap.getPixel(x, y); - int r = Color.red(pixel); - int g = Color.green(pixel); - int b = Color.blue(pixel); - - int secondMax = r > g ? r : g; - double max = (secondMax > b ? secondMax : b) / 255.0; - - int secondMin = r < g ? r : g; - double min = (secondMin < b ? secondMin : b) / 255.0; - - double luminance = ((max + min) / 2.0) * 100; - - int highBrightnessLuminance = 40; - int mediumBrightnessLuminance = 26; - - if (luminance < highBrightnessLuminance) { - if (luminance > mediumBrightnessLuminance) { - numberOfMediumBrightnessPixels++; - } - } else { - numberOfBrightPixels++; - } - - if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { - return false; - } - } - } - return true; - } - - /** - * Downloads the image from the URL and sets it as the phone's wallpaper - * Fails silently if download or setting wallpaper fails. - * - * @param context context - * @param imageUrl Url of the image - */ - public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { - - enqueueSetWallpaperWork(context, imageUrl); - - } - - private static void createNotificationChannel(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = "Wallpaper Setting"; - String description = "Notifications for wallpaper setting progress"; - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel("set_wallpaper_channel", name, importance); - channel.setDescription(description); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - } - - - /** - * Calls the set avatar api to set the image url as user's avatar - * @param context - * @param url - * @param username - * @param okHttpJsonApiClient - * @param compositeDisposable - */ - public static void setAvatarFromImageUrl(Context context, String url, String username, - OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable) { - showSettingAvatarProgressBar(context); - - try { - compositeDisposable.add(okHttpJsonApiClient - .setAvatar(username, url) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus().equals("200")) { - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)); - if (progressDialogAvatar != null && progressDialogAvatar.isShowing()) { - progressDialogAvatar.dismiss(); - } - } - }, - t -> { - Timber.e(t, "Setting Avatar Failed"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - - } - - public static void enqueueSetWallpaperWork(Context context, Uri imageUrl) { - createNotificationChannel(context); // Ensure the notification channel is created - - Data inputData = new Data.Builder() - .putString("imageUrl", imageUrl.toString()) - .build(); - - OneTimeWorkRequest setWallpaperWork = new OneTimeWorkRequest.Builder(SetWallpaperWorker.class) - .setInputData(inputData) - .build(); - - WorkManager.getInstance(context).enqueue(setWallpaperWork); - } - - - private static void showSettingWallpaperProgressBar(Context context) { - progressDialogWallpaper = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title), - context.getString(R.string.setting_wallpaper_dialog_message), true); - } - - private static void showSettingAvatarProgressBar(Context context) { - progressDialogAvatar = ProgressDialog.show(context, context.getString(R.string.setting_avatar_dialog_title), - context.getString(R.string.setting_avatar_dialog_message), true); - } - - /** - * Result variable is a result of an or operation of all possible problems. Ie. if result - * is 0001 means IMAGE_DARK - * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT - */ - public static String getErrorMessageForResult(Context context, @Result int result) { - StringBuilder errorMessage = new StringBuilder(); - if (result <= 0 ) { - Timber.d("No issues to warn user is found"); - } else { - Timber.d("Issues found to warn user"); - - errorMessage.append(context.getResources().getString(R.string.upload_problem_exist)); - - if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark)); - } - - if ((IMAGE_BLURRY & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry)); - } - - if ((IMAGE_DUPLICATE & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate)); - } - - if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation)); - } - - if ((FILE_FBMD & result) != 0) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_fbmd)); - } - - if ((FILE_NO_EXIF & result) != 0){ - errorMessage.append("\n - ").append(context.getResources().getString(R.string.internet_downloaded)); - } - - errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue)); - } - - return errorMessage.toString(); - } - - /** - * Adds red border to a bitmap - * @param bitmap - * @param borderSize - * @param context - * @return - */ - public static Bitmap addRedBorder(Bitmap bitmap, int borderSize, Context context) { - Bitmap bmpWithBorder = Bitmap.createBitmap(bitmap.getWidth() + borderSize * 2, bitmap.getHeight() + borderSize * 2, bitmap.getConfig()); - Canvas canvas = new Canvas(bmpWithBorder); - canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)); - canvas.drawBitmap(bitmap, borderSize, borderSize, null); - return bmpWithBorder; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt new file mode 100644 index 0000000000..78a877600b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt @@ -0,0 +1,363 @@ +package fr.free.nrw.commons.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.ProgressDialog +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.net.Uri +import android.os.Build +import androidx.annotation.IntDef +import androidx.core.content.ContextCompat +import androidx.exifinterface.media.ExifInterface +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.SetWallpaperWorker +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +/** + * Created by blueSir9 on 3/10/17. + */ + + +object ImageUtils { + + /** + * Set 0th bit as 1 for dark image ie. 0001 + */ + const val IMAGE_DARK = 1 shl 0 // 1 + + /** + * Set 1st bit as 1 for blurry image ie. 0010 + */ + const val IMAGE_BLURRY = 1 shl 1 // 2 + + /** + * Set 2nd bit as 1 for duplicate image ie. 0100 + */ + const val IMAGE_DUPLICATE = 1 shl 2 // 4 + + /** + * Set 3rd bit as 1 for image with different geo location ie. 1000 + */ + const val IMAGE_GEOLOCATION_DIFFERENT = 1 shl 3 // 8 + + /** + * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains + * FBMD data else returns IMAGE_OK + * ie. 10000 + */ + const val FILE_FBMD = 1 shl 4 // 16 + + /** + * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does + * not contains EXIF data else returns IMAGE_OK + * ie. 100000 + */ + const val FILE_NO_EXIF = 1 shl 5 // 32 + + const val IMAGE_OK = 0 + const val IMAGE_KEEP = -1 + const val IMAGE_WAIT = -2 + const val EMPTY_CAPTION = -3 + const val FILE_NAME_EXISTS = 1 shl 6 // 64 + const val NO_CATEGORY_SELECTED = -5 + + private var progressDialogWallpaper: ProgressDialog? = null + + private var progressDialogAvatar: ProgressDialog? = null + + @IntDef( + flag = true, + value = [ + IMAGE_DARK, + IMAGE_BLURRY, + IMAGE_DUPLICATE, + IMAGE_OK, + IMAGE_KEEP, + IMAGE_WAIT, + EMPTY_CAPTION, + FILE_NAME_EXISTS, + NO_CATEGORY_SELECTED, + IMAGE_GEOLOCATION_DIFFERENT + ] + ) + @Retention + annotation class Result + + /** + * @return IMAGE_OK if image is not too dark + * IMAGE_DARK if image is too dark + */ + @JvmStatic + fun checkIfImageIsTooDark(imagePath: String): Int { + val millis = System.currentTimeMillis() + return try { + var bmp = ExifInterface(imagePath).thumbnailBitmap + if (bmp == null) { + bmp = BitmapFactory.decodeFile(imagePath) + } + + if (checkIfImageIsDark(bmp)) { + IMAGE_DARK + } else { + IMAGE_OK + } + } catch (e: Exception) { + Timber.d(e, "Error while checking image darkness.") + IMAGE_OK + } finally { + Timber.d("Checking image darkness took ${System.currentTimeMillis() - millis} ms.") + } + } + + /** + * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will + * be an empty string + * @param latLng Location of wikidata item will be edited after upload + * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provide + * d is null true if geolocation of the image and wikidata item are different + */ + @JvmStatic + fun checkImageGeolocationIsDifferent(geolocationOfFileString: String, latLng: LatLng?): Boolean { + Timber.d("Comparing geolocation of file with nearby place location") + if (latLng == null) { // Means that geolocation for this image is not given + return false // Since we don't know geolocation of file, we choose letting upload + } + + val geolocationOfFile = geolocationOfFileString.split("|") + val distance = LengthUtils.computeDistanceBetween( + LatLng(geolocationOfFile[0].toDouble(), geolocationOfFile[1].toDouble(), 0.0F), + latLng + ) + // Distance is more than 1 km, means that geolocation is wrong + return distance >= 1000 + } + + @JvmStatic + private fun checkIfImageIsDark(bitmap: Bitmap?): Boolean { + if (bitmap == null) { + Timber.e("Expected bitmap was null") + return true + } + + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + + val allPixelsCount = bitmapWidth * bitmapHeight + var numberOfBrightPixels = 0 + var numberOfMediumBrightnessPixels = 0 + val brightPixelThreshold = 0.025 * allPixelsCount + val mediumBrightPixelThreshold = 0.3 * allPixelsCount + + for (x in 0 until bitmapWidth) { + for (y in 0 until bitmapHeight) { + val pixel = bitmap.getPixel(x, y) + val r = Color.red(pixel) + val g = Color.green(pixel) + val b = Color.blue(pixel) + + val max = maxOf(r, g, b) / 255.0 + val min = minOf(r, g, b) / 255.0 + + val luminance = ((max + min) / 2.0) * 100 + + val highBrightnessLuminance = 40 + val mediumBrightnessLuminance = 26 + + if (luminance < highBrightnessLuminance) { + if (luminance > mediumBrightnessLuminance) { + numberOfMediumBrightnessPixels++ + } + } else { + numberOfBrightPixels++ + } + + if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { + return false + } + } + } + return true + } + + /** + * Downloads the image from the URL and sets it as the phone's wallpaper + * Fails silently if download or setting wallpaper fails. + * + * @param context context + * @param imageUrl Url of the image + */ + @JvmStatic + fun setWallpaperFromImageUrl(context: Context, imageUrl: Uri) { + enqueueSetWallpaperWork(context, imageUrl) + } + + @JvmStatic + private fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Wallpaper Setting" + val description = "Notifications for wallpaper setting progress" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("set_wallpaper_channel", name, importance).apply { + this.description = description + } + val notificationManager = context.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + /** + * Calls the set avatar api to set the image url as user's avatar + * @param context + * @param url + * @param username + * @param okHttpJsonApiClient + * @param compositeDisposable + */ + @JvmStatic + fun setAvatarFromImageUrl( + context: Context, + url: String, + username: String, + okHttpJsonApiClient: OkHttpJsonApiClient, + compositeDisposable: CompositeDisposable + ) { + showSettingAvatarProgressBar(context) + + try { + compositeDisposable.add( + okHttpJsonApiClient + .setAvatar(username, url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response?.status == "200") { + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)) + progressDialogAvatar?.dismiss() + } + }, + { t -> + Timber.e(t, "Setting Avatar Failed") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + ) + ) + } catch (e: Exception) { + Timber.d("$e success") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + } + + @JvmStatic + fun enqueueSetWallpaperWork(context: Context, imageUrl: Uri) { + createNotificationChannel(context) // Ensure the notification channel is created + + val inputData = Data.Builder() + .putString("imageUrl", imageUrl.toString()) + .build() + + val setWallpaperWork = OneTimeWorkRequest.Builder(SetWallpaperWorker::class.java) + .setInputData(inputData) + .build() + + WorkManager.getInstance(context).enqueue(setWallpaperWork) + } + + @JvmStatic + private fun showSettingWallpaperProgressBar(context: Context) { + progressDialogWallpaper = ProgressDialog.show( + context, + context.getString(R.string.setting_wallpaper_dialog_title), + context.getString(R.string.setting_wallpaper_dialog_message), + true + ) + } + + @JvmStatic + private fun showSettingAvatarProgressBar(context: Context) { + progressDialogAvatar = ProgressDialog.show( + context, + context.getString(R.string.setting_avatar_dialog_title), + context.getString(R.string.setting_avatar_dialog_message), + true + ) + } + + /** + * Adds red border to bitmap with specified border size + * * @param bitmap + * * @param borderSize + * * @param context + * * @return + */ + @JvmStatic + fun addRedBorder(bitmap: Bitmap, borderSize: Int, context: Context): Bitmap { + val bmpWithBorder = Bitmap.createBitmap( + bitmap.width + borderSize * 2, + bitmap.height + borderSize * 2, + bitmap.config + ) + val canvas = Canvas(bmpWithBorder) + canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)) + canvas.drawBitmap(bitmap, borderSize.toFloat(), borderSize.toFloat(), null) + return bmpWithBorder + } + + /** + * Result variable is a result of an or operation of all possible problems. Ie. if result + * is 0001 means IMAGE_DARK + * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT + */ + @JvmStatic + fun getErrorMessageForResult(context: Context, @Result result: Int): String { + val errorMessage = StringBuilder() + if (result <= 0) { + Timber.d("No issues to warn user are found") + } else { + Timber.d("Issues found to warn user") + errorMessage.append(context.getString(R.string.upload_problem_exist)) + + if (result and IMAGE_DARK != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_dark)) + } + if (result and IMAGE_BLURRY != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_blurry)) + } + if (result and IMAGE_DUPLICATE != 0) { + errorMessage.append("\n - "). + append(context.getString(R.string.upload_problem_image_duplicate)) + } + if (result and IMAGE_GEOLOCATION_DIFFERENT != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_different_geolocation)) + } + if (result and FILE_FBMD != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_fbmd)) + } + if (result and FILE_NO_EXIF != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.internet_downloaded)) + } + errorMessage.append("\n\n") + .append(context.getString(R.string.upload_problem_do_you_continue)) + } + return errorMessage.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java deleted file mode 100644 index 634a73ad23..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ImageUtilsWrapper { - - @Inject - public ImageUtilsWrapper() { - - } - - public Single checkIfImageIsTooDark(String bitmapPath) { - return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath)) - .subscribeOn(Schedulers.computation()); - } - - public Single checkImageGeolocationIsDifferent(String geolocationOfFileString, - LatLng latLng) { - return Single.fromCallable( - () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng)) - .subscribeOn(Schedulers.computation()) - .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT - : ImageUtils.IMAGE_OK); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt new file mode 100644 index 0000000000..2e0efc6901 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ImageUtilsWrapper @Inject constructor() { + + fun checkIfImageIsTooDark(bitmapPath: String): Single { + return Single.fromCallable { ImageUtils.checkIfImageIsTooDark(bitmapPath) } + .subscribeOn(Schedulers.computation()) + } + + fun checkImageGeolocationIsDifferent( + geolocationOfFileString: String, + latLng: LatLng + ): Single { + return Single.fromCallable { + ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng) + } + .subscribeOn(Schedulers.computation()) + .map { isDifferent -> + if (isDifferent) ImageUtils.IMAGE_GEOLOCATION_DIFFERENT else ImageUtils.IMAGE_OK + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java deleted file mode 100644 index 73bd5c02b3..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import java.util.Locale; - -/** - * Utilities class for miscellaneous strings - */ -public class LangCodeUtils { - /** - * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. - * @param code Language code you want to update. - * @return Updated language code. If not in the "deprecated list" returns the same code. - */ - public static String fixLanguageCode(String code) { - if (code.equalsIgnoreCase("iw")) { - return "he"; - } else if (code.equalsIgnoreCase("in")) { - return "id"; - } else if (code.equalsIgnoreCase("ji")) { - return "yi"; - } else { - return code; - } - } - - /** - * Returns configuration for locale of - * our choice regardless of user's device settings - */ - public static Resources getLocalizedResources(Context context, Locale desiredLocale) { - Configuration conf = context.getResources().getConfiguration(); - conf = new Configuration(conf); - conf.setLocale(desiredLocale); - Context localizedContext = context.createConfigurationContext(conf); - return localizedContext.getResources(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt new file mode 100644 index 0000000000..5ef21a735b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import java.util.Locale + +/** + * Utilities class for miscellaneous strings + */ +object LangCodeUtils { + + /** + * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. + * @param code Language code you want to update. + * @return Updated language code. If not in the "deprecated list" returns the same code. + */ + @JvmStatic + fun fixLanguageCode(code: String): String { + return when (code.lowercase()) { + "iw" -> "he" + "in" -> "id" + "ji" -> "yi" + else -> code + } + } + + /** + * Returns configuration for locale of + * our choice regardless of user's device settings + */ + @JvmStatic + fun getLocalizedResources(context: Context, desiredLocale: Locale): Resources { + val conf = Configuration(context.resources.configuration).apply { + setLocale(desiredLocale) + } + val localizedContext = context.createConfigurationContext(conf) + return localizedContext.resources + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java deleted file mode 100644 index 76c52527b1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; - -public class LayoutUtils { - - /** - * Can be used for keeping aspect radios suggested by material guidelines. See: - * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios - * In some cases we don't know exact width, for such cases this method measures - * width and sets height by multiplying the width with height. - * @param rate Aspect ratios, ie 1 for 1:1. (width * rate = height) - * @param view view to change height - */ - public static void setLayoutHeightAllignedToWidth(double rate, View view) { - ViewTreeObserver vto = view.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - view.getViewTreeObserver().removeOnGlobalLayoutListener(this); - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - layoutParams.height = (int) (view.getWidth() * rate); - view.setLayoutParams(layoutParams); - } - }); - } - - public static double getScreenWidth(Context context, double rate) { - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - return displayMetrics.widthPixels * rate; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt new file mode 100644 index 0000000000..71e6697f77 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewTreeObserver + +/** + * Utility class for layout-related operations. + */ +object LayoutUtils { + + /** + * Can be used for keeping aspect ratios suggested by material guidelines. See: + * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios + * In some cases, we don't know the exact width, for such cases this method measures + * width and sets height by multiplying the width with height. + * @param rate Aspect ratios, i.e., 1 for 1:1 (width * rate = height) + * @param view View to change height + */ + @JvmStatic + fun setLayoutHeightAlignedToWidth(rate: Double, view: View) { + val vto = view.viewTreeObserver + vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + view.viewTreeObserver.removeOnGlobalLayoutListener(this) + val layoutParams = view.layoutParams + layoutParams.height = (view.width * rate).toInt() + view.layoutParams = layoutParams + } + }) + } + + /** + * Calculates and returns the screen width multiplied by the provided rate. + * @param context Context used to access display metrics. + * @param rate Multiplier for screen width. + * @return Calculated screen width multiplied by the rate. + */ + @JvmStatic + fun getScreenWidth(context: Context, rate: Double): Double { + val displayMetrics = DisplayMetrics() + (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics) + return displayMetrics.widthPixels * rate + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java deleted file mode 100644 index 0ca61a1d9c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.utils; - -import androidx.annotation.NonNull; - -import java.text.NumberFormat; - -import fr.free.nrw.commons.location.LatLng; - -public class LengthUtils { - /** - * Returns a formatted distance string between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return string distance - */ - public static String formatDistanceBetween(LatLng point1, LatLng point2) { - if (point1 == null || point2 == null) { - return null; - } - - int distance = (int) Math.round(computeDistanceBetween(point1, point2)); - return formatDistance(distance); - } - - /** - * Format a distance (in meters) as a string - * Example: 140 -> "140m" - * 3841 -> "3.8km" - * - * @param distance Distance, in meters - * @return A string representing the distance - * @throws IllegalArgumentException If distance is negative - */ - public static String formatDistance(int distance) { - if (distance < 0) { - throw new IllegalArgumentException("Distance must be non-negative"); - } - - NumberFormat numberFormat = NumberFormat.getNumberInstance(); - - // Adjust to km if distance is over 1000m (1km) - if (distance >= 1000) { - numberFormat.setMaximumFractionDigits(1); - return numberFormat.format(distance / 1000.0) + "km"; - } - - // Otherwise just return in meters - return numberFormat.format(distance) + "m"; - } - - /** - * Computes the distance between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return distance between the points in meters - * @throws NullPointerException if one or both the points are null - */ - public static double computeDistanceBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return computeAngleBetween(point1, point2) * 6371009.0D; // Earth's radius in meter - } - - /** - * Computes angle between two points - * - * @param point1 one of the two end points - * @param point2 one of the two end points - * @return Angle in radius - * @throws NullPointerException if one or both the points are null - */ - private static double computeAngleBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return distanceRadians( - Math.toRadians(point1.getLatitude()), - Math.toRadians(point1.getLongitude()), - Math.toRadians(point2.getLatitude()), - Math.toRadians(point2.getLongitude()) - ); - } - - /** - * Computes arc length between 2 points - * - * @param lat1 Latitude of point A - * @param lng1 Longitude of point A - * @param lat2 Latitude of point B - * @param lng2 Longitude of point B - * @return Arc length between the points - */ - private static double distanceRadians(double lat1, double lng1, double lat2, double lng2) { - return arcHav(havDistance(lat1, lat2, lng1 - lng2)); - } - - /** - * Computes inverse of haversine - * - * @param x Angle in radian - * @return Inverse of haversine - */ - private static double arcHav(double x) { - return 2.0D * Math.asin(Math.sqrt(x)); - } - - /** - * Computes distance between two points that are on same Longitude - * - * @param lat1 Latitude of point A - * @param lat2 Latitude of point B - * @param longitude Longitude on which they lie - * @return Arc length between points - */ - private static double havDistance(double lat1, double lat2, double longitude) { - return hav(lat1 - lat2) + hav(longitude) * Math.cos(lat1) * Math.cos(lat2); - } - - /** - * Computes haversine - * - * @param x Angle in radians - * @return Haversine of x - */ - private static double hav(double x) { - double sinHalf = Math.sin(x * 0.5D); - return sinHalf * sinHalf; - } - - /** - * Computes bearing between the two given points - * - * @see Bearing - * @param point1 Coordinates of first point - * @param point2 Coordinates of second point - * @return Bearing between the two end points in degrees - * @throws NullPointerException if one or both the points are null - */ - public static double computeBearing(@NonNull LatLng point1, @NonNull LatLng point2) { - double diffLongitute = Math.toRadians(point2.getLongitude() - point1.getLongitude()); - double lat1 = Math.toRadians(point1.getLatitude()); - double lat2 = Math.toRadians(point2.getLatitude()); - double y = Math.sin(diffLongitute) * Math.cos(lat2); - double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLongitute); - double bearing = Math.atan2(y, x); - return (Math.toDegrees(bearing) + 360) % 360; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt new file mode 100644 index 0000000000..48cf1a0209 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt @@ -0,0 +1,156 @@ +package fr.free.nrw.commons.utils + +import java.text.NumberFormat +import fr.free.nrw.commons.location.LatLng +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt + +object LengthUtils { + /** + * Returns a formatted distance string between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return string distance + */ + @JvmStatic + fun formatDistanceBetween(point1: LatLng?, point2: LatLng?): String? { + if (point1 == null || point2 == null) { + return null + } + + val distance = computeDistanceBetween(point1, point2).roundToInt() + return formatDistance(distance) + } + + /** + * Format a distance (in meters) as a string + * Example: 140 -> "140m" + * 3841 -> "3.8km" + * + * @param distance Distance, in meters + * @return A string representing the distance + * @throws IllegalArgumentException If distance is negative + */ + @JvmStatic + fun formatDistance(distance: Int): String { + if (distance < 0) { + throw IllegalArgumentException("Distance must be non-negative") + } + + val numberFormat = NumberFormat.getNumberInstance() + + // Adjust to km if distance is over 1000m (1km) + return if (distance >= 1000) { + numberFormat.maximumFractionDigits = 1 + "${numberFormat.format(distance / 1000.0)}km" + } else { + "${numberFormat.format(distance)}m" + } + } + + /** + * Computes the distance between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return distance between the points in meters + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeDistanceBetween(point1: LatLng, point2: LatLng): Double { + return computeAngleBetween(point1, point2) * 6371009.0 // Earth's radius in meters + } + + /** + * Computes angle between two points + * + * @param point1 one of the two end points + * @param point2 one of the two end points + * @return Angle in radians + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + private fun computeAngleBetween(point1: LatLng, point2: LatLng): Double { + return distanceRadians( + Math.toRadians(point1.latitude), + Math.toRadians(point1.longitude), + Math.toRadians(point2.latitude), + Math.toRadians(point2.longitude) + ) + } + + /** + * Computes arc length between 2 points + * + * @param lat1 Latitude of point A + * @param lng1 Longitude of point A + * @param lat2 Latitude of point B + * @param lng2 Longitude of point B + * @return Arc length between the points + */ + @JvmStatic + private fun distanceRadians(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double { + return arcHav(havDistance(lat1, lat2, lng1 - lng2)) + } + + /** + * Computes inverse of haversine + * + * @param x Angle in radian + * @return Inverse of haversine + */ + @JvmStatic + private fun arcHav(x: Double): Double { + return 2.0 * asin(sqrt(x)) + } + + /** + * Computes distance between two points that are on same Longitude + * + * @param lat1 Latitude of point A + * @param lat2 Latitude of point B + * @param longitude Longitude on which they lie + * @return Arc length between points + */ + @JvmStatic + private fun havDistance(lat1: Double, lat2: Double, longitude: Double): Double { + return hav(lat1 - lat2) + hav(longitude) * cos(lat1) * cos(lat2) + } + + /** + * Computes haversine + * + * @param x Angle in radians + * @return Haversine of x + */ + @JvmStatic + private fun hav(x: Double): Double { + val sinHalf = sin(x * 0.5) + return sinHalf * sinHalf + } + + /** + * Computes bearing between the two given points + * + * @see Bearing + * @param point1 Coordinates of first point + * @param point2 Coordinates of second point + * @return Bearing between the two end points in degrees + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeBearing(point1: LatLng, point2: LatLng): Double { + val diffLongitude = Math.toRadians(point2.longitude - point1.longitude) + val lat1 = Math.toRadians(point1.latitude) + val lat2 = Math.toRadians(point2.latitude) + val y = sin(diffLongitude) * cos(lat2) + val x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(diffLongitude) + val bearing = atan2(y, x) + return (Math.toDegrees(bearing) + 360) % 360 + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java deleted file mode 100644 index 01a8855387..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import timber.log.Timber; - -public class LocationUtils { - public static final double RADIUS_OF_EARTH_KM = 6371.0; // Earth's radius in kilometers - - public static LatLng deriveUpdatedLocationFromSearchQuery(String customQuery) { - LatLng latLng = null; - final int indexOfPrefix = customQuery.indexOf("Point("); - if (indexOfPrefix == -1) { - Timber.e("Invalid prefix index - Seems like user has entered an invalid query"); - return latLng; - } - final int indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix); - if (indexOfSuffix == -1) { - Timber.e("Invalid suffix index - Seems like user has entered an invalid query"); - return latLng; - } - String latLngString = customQuery.substring(indexOfPrefix+"Point(".length(), indexOfSuffix); - if (latLngString.isEmpty()) { - return null; - } - - String latLngArray[] = latLngString.split(" "); - if (latLngArray.length != 2) { - return null; - } - - try { - latLng = new LatLng(Double.parseDouble(latLngArray[1].trim()), - Double.parseDouble(latLngArray[0].trim()), 1f); - }catch (Exception e){ - Timber.e("Error while parsing user entered lat long: %s", e); - } - - return latLng; - } - - - public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - double lat1Rad = Math.toRadians(lat1); - double lon1Rad = Math.toRadians(lon1); - double lat2Rad = Math.toRadians(lat2); - double lon2Rad = Math.toRadians(lon2); - - // Haversine formula - double dlon = lon2Rad - lon1Rad; - double dlat = lat2Rad - lat1Rad; - double a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.pow(Math.sin(dlon / 2), 2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - double distance = RADIUS_OF_EARTH_KM * c; - - return distance; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt new file mode 100644 index 0000000000..2df42270eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import timber.log.Timber +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +object LocationUtils { + const val RADIUS_OF_EARTH_KM = 6371.0 // Earth's radius in kilometers + + @JvmStatic + fun deriveUpdatedLocationFromSearchQuery(customQuery: String): LatLng? { + var latLng: LatLng? = null + val indexOfPrefix = customQuery.indexOf("Point(") + if (indexOfPrefix == -1) { + Timber.e("Invalid prefix index - Seems like user has entered an invalid query") + return latLng + } + val indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix) + if (indexOfSuffix == -1) { + Timber.e("Invalid suffix index - Seems like user has entered an invalid query") + return latLng + } + val latLngString = customQuery.substring(indexOfPrefix + "Point(".length, indexOfSuffix) + if (latLngString.isEmpty()) { + return null + } + + val latLngArray = latLngString.split(" ") + if (latLngArray.size != 2) { + return null + } + + try { + latLng = LatLng(latLngArray[1].trim().toDouble(), + latLngArray[0].trim().toDouble(), 1f) + } catch (e: Exception) { + Timber.e("Error while parsing user entered lat long: %s", e) + } + + return latLng + } + + @JvmStatic + fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val lat1Rad = Math.toRadians(lat1) + val lon1Rad = Math.toRadians(lon1) + val lat2Rad = Math.toRadians(lat2) + val lon2Rad = Math.toRadians(lon2) + + // Haversine formula + val dlon = lon2Rad - lon1Rad + val dlat = lat2Rad - lat1Rad + val a = Math.pow( + sin(dlat / 2), 2.0) + cos(lat1Rad) * cos(lat2Rad) * Math.pow(sin(dlon / 2), 2.0 + ) + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return RADIUS_OF_EARTH_KM * c + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java deleted file mode 100644 index d3b5bd0e24..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationUpdateListener; -import timber.log.Timber; - -public class MapUtils { - public static final float ZOOM_LEVEL = 14f; - public static final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005; - public static final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004; - public static final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; - public static final float ZOOM_OUT = 0f; - - public static final LatLng defaultLatLng = new fr.free.nrw.commons.location.LatLng(51.50550,-0.07520,1f); - - public static void registerUnregisterLocationListener(final boolean removeLocationListener, LocationServiceManager locationManager, LocationUpdateListener locationUpdateListener) { - try { - if (removeLocationListener) { - locationManager.unregisterLocationManager(); - locationManager.removeLocationListener(locationUpdateListener); - Timber.d("Location service manager unregistered and removed"); - } else { - locationManager.addLocationListener(locationUpdateListener); - locationManager.registerLocationManager(); - Timber.d("Location service manager added and registered"); - } - }catch (final Exception e){ - Timber.e(e); - //Broadcasts are tricky, should be catchedonR - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt new file mode 100644 index 0000000000..adc3a5d908 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.location.LocationUpdateListener +import timber.log.Timber + +object MapUtils { + const val ZOOM_LEVEL = 14f + const val CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005 + const val CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004 + const val NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE" + const val ZOOM_OUT = 0f + + @JvmStatic + val defaultLatLng = LatLng(51.50550, -0.07520, 1f) + + @JvmStatic + fun registerUnregisterLocationListener( + removeLocationListener: Boolean, + locationManager: LocationServiceManager, + locationUpdateListener: LocationUpdateListener + ) { + try { + if (removeLocationListener) { + locationManager.unregisterLocationManager() + locationManager.removeLocationListener(locationUpdateListener) + Timber.d("Location service manager unregistered and removed") + } else { + locationManager.addLocationListener(locationUpdateListener) + locationManager.registerLocationManager() + Timber.d("Location service manager added and registered") + } + } catch (e: Exception) { + Timber.e(e) + // Broadcasts are tricky, should be caught on onR + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java deleted file mode 100644 index 8eb875bb56..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package fr.free.nrw.commons.utils; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.List; - -public class MediaDataExtractorUtil { - /** - * Extracts a list of categories from | separated category string - * - * @param source - * @return - */ - public static List extractCategoriesFromList(String source) { - if (StringUtils.isBlank(source)) { - return new ArrayList<>(); - } - String[] cats = source.split("\\|"); - List categories = new ArrayList<>(); - for (String category : cats) { - if (!StringUtils.isBlank(category.trim())) { - categories.add(category); - } - } - return categories; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt new file mode 100644 index 0000000000..9e46525da5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import org.apache.commons.lang3.StringUtils + +import java.util.ArrayList + +object MediaDataExtractorUtil { + + /** + * Extracts a list of categories from | separated category string + * + * @param source + * @return + */ + @JvmStatic + fun extractCategoriesFromList(source: String): List { + if (source.isBlank()) { + return emptyList() + } + val cats = source.split("|") + val categories = mutableListOf() + for (category in cats) { + if (category.trim().isNotBlank()) { + categories.add(category) + } + } + return categories + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java deleted file mode 100644 index bc6e6883ff..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -public class NearbyFABUtils { - /* - * Add anchors back before making them visible again. - * */ - public static void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END; - floatingActionButton.setLayoutParams(params); - } - - /* - * Add anchors back before making them visible again. Big and small fabs have different anchor - * gravities, therefore the are two methods. - * */ - public static void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.CENTER_HORIZONTAL; - floatingActionButton.setLayoutParams(params); - } - - /* - * We are not able to hide FABs without removing anchors, this method removes anchors - * */ - public static void removeAnchorFromFAB(FloatingActionButton floatingActionButton) { - //get rid of anchors - //Somehow this was the only way https://stackoverflow.com/questions/32732932 - // /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone - CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton - .getLayoutParams(); - param.setAnchorId(View.NO_ID); - // If we don't set them to zero, then they become visible for a moment on upper left side - param.width = 0; - param.height = 0; - floatingActionButton.setLayoutParams(param); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt new file mode 100644 index 0000000000..61b95a4139 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.utils + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.floatingactionbutton.FloatingActionButton + +object NearbyFABUtils { + + /* + * Add anchors back before making them visible again. + */ + @JvmStatic + fun addAnchorToBigFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.TOP or Gravity.RIGHT or Gravity.END + floatingActionButton.layoutParams = params + } + + /* + * Add anchors back before making them visible again. Big and small fabs have different anchor + * gravities, therefore there are two methods. + */ + @JvmStatic + fun addAnchorToSmallFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.CENTER_HORIZONTAL + floatingActionButton.layoutParams = params + } + + /* + * We are not able to hide FABs without removing anchors, this method removes anchors. + */ + @JvmStatic + fun removeAnchorFromFAB(floatingActionButton: FloatingActionButton) { + // get rid of anchors + // Somehow this was the only way https://stackoverflow.com/questions/32732932 + // floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone + val params = floatingActionButton.layoutParams as CoordinatorLayout.LayoutParams + params.anchorId = View.NO_ID + // If we don't set them to zero, then they become visible for a moment on upper left side + params.width = 0 + params.height = 0 + floatingActionButton.layoutParams = params + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java deleted file mode 100644 index ce64cb0317..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.utils; - - -import android.annotation.SuppressLint; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.telephony.TelephonyManager; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.utils.model.NetworkConnectionType; - -public class NetworkUtils { - - /** - * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java - * Check if internet connection is established. - * - * @param context context passed to this method could be null. - * @return Returns current internet connection status. Returns false if null context was passed. - */ - @SuppressLint("MissingPermission") - public static boolean isInternetConnectionEstablished(@Nullable Context context) { - if (context == null) { - return false; - } - - NetworkInfo activeNetwork = getNetworkInfo(context); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } - - /** - * Detect network connection type - */ - static NetworkConnectionType getNetworkType(Context context) { - TelephonyManager telephonyManager = (TelephonyManager) context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null) { - return NetworkConnectionType.UNKNOWN; - } - - NetworkInfo networkInfo = getNetworkInfo(context); - if (networkInfo == null) { - return NetworkConnectionType.UNKNOWN; - } - - int network = networkInfo.getType(); - if (network == ConnectivityManager.TYPE_WIFI) { - return NetworkConnectionType.WIFI; - } - - // TODO for Android 12+ request permission from user is mandatory - /* - int mobileNetwork = telephonyManager.getNetworkType(); - switch (mobileNetwork) { - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_1xRTT: - return NetworkConnectionType.TWO_G; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_UMTS: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EHRPD: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - return NetworkConnectionType.THREE_G; - case TelephonyManager.NETWORK_TYPE_LTE: - case TelephonyManager.NETWORK_TYPE_HSPAP: - return NetworkConnectionType.FOUR_G; - default: - return NetworkConnectionType.UNKNOWN; - } - */ - return NetworkConnectionType.UNKNOWN; - } - - /** - * Extracted private method to get nullable network info - */ - @Nullable - private static NetworkInfo getNetworkInfo(Context context) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - - if (connectivityManager == null) { - return null; - } - - return connectivityManager.getActiveNetworkInfo(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt new file mode 100644 index 0000000000..98fde9ef70 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.telephony.TelephonyManager + +import fr.free.nrw.commons.utils.model.NetworkConnectionType + +object NetworkUtils { + + /** + * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java + * Check if internet connection is established. + * + * @param context context passed to this method could be null. + * @return Returns current internet connection status. Returns false if null context was passed. + */ + @SuppressLint("MissingPermission") + @JvmStatic + fun isInternetConnectionEstablished(context: Context?): Boolean { + if (context == null) { + return false + } + + val activeNetwork = getNetworkInfo(context) + return activeNetwork != null && activeNetwork.isConnectedOrConnecting + } + + /** + * Detect network connection type + */ + @JvmStatic + fun getNetworkType(context: Context): NetworkConnectionType { + val telephonyManager = context.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + ?: return NetworkConnectionType.UNKNOWN + + val networkInfo = getNetworkInfo(context) + ?: return NetworkConnectionType.UNKNOWN + + val network = networkInfo.type + if (network == ConnectivityManager.TYPE_WIFI) { + return NetworkConnectionType.WIFI + } + + // TODO for Android 12+ request permission from user is mandatory + /* + val mobileNetwork = telephonyManager.networkType + return when (mobileNetwork) { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT -> NetworkConnectionType.TWO_G + + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_EVDO_B -> NetworkConnectionType.THREE_G + + TelephonyManager.NETWORK_TYPE_LTE, + TelephonyManager.NETWORK_TYPE_HSPAP -> NetworkConnectionType.FOUR_G + + else -> NetworkConnectionType.UNKNOWN + } + */ + return NetworkConnectionType.UNKNOWN + } + + /** + * Extracted private method to get nullable network info + */ + @JvmStatic + private fun getNetworkInfo(context: Context): NetworkInfo? { + val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return null + + return connectivityManager.activeNetworkInfo + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java deleted file mode 100644 index 692194234f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ /dev/null @@ -1,224 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.provider.Settings; -import android.widget.Toast; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.upload.UploadActivity; -import java.util.List; - -public class PermissionUtils { - public static String[] PERMISSIONS_STORAGE = getPermissionsStorage(); - - static String[] getPermissionsStorage() { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return new String[]{ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { - return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, - Manifest. permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - return new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE }; - } - - /** - * This method can be used by any activity which requires a permission which has been - * blocked(marked never ask again by the user) It open the app settings from where the user can - * manually give us the required permission. - * - * @param activity The Activity which requires a permission which has been blocked - */ - private static void askUserToManuallyEnablePermissionFromSettings(final Activity activity) { - final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - } - - /** - * Checks whether the app already has a particular permission - * - * @param activity The Activity context to check permissions against - * @param permissions An array of permission strings to check - * @return `true if the app has all the specified permissions, `false` otherwise - */ - public static boolean hasPermission(final Activity activity, final String[] permissions) { - boolean hasPermission = true; - for(final String permission : permissions) { - hasPermission = hasPermission && - ContextCompat.checkSelfPermission(activity, permission) - == PackageManager.PERMISSION_GRANTED; - } - return hasPermission; - } - - public static boolean hasPartialAccess(final Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return ContextCompat.checkSelfPermission(activity, - permission.READ_MEDIA_VISUAL_USER_SELECTED - ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( - activity, permission.READ_MEDIA_IMAGES - ) == PackageManager.PERMISSION_DENIED; - } - return false; - } - - /** - * Checks for a particular permission and runs the runnable to perform an action when the - * permission is granted Also, it shows a rationale if needed - *

- * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no - * permission rationale will be displayed and permission would be requested - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - * R.string.storage_permission_title, R.string.write_storage_permission_rationale); - *

- * If you don't want the permission rationale to be shown then use: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requests - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param rationaleTitle rationale title to be displayed when permission was denied. It can - * be an invalid @StringRes - * @param rationaleMessage rationale message to be displayed when permission was denied. It - * can be an invalid @StringRes - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - if (hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - checkPermissionsAndPerformAction(activity, onPermissionGranted, null, - rationaleTitle, rationaleMessage, permissions); - } - - /** - * Checks for a particular permission and runs the corresponding runnables to perform an action - * when the permission is granted/denied Also, it shows a rationale if needed - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> - * showMessage(), R.string.storage_permission_title, - * R.string.write_storage_permission_rationale); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requested - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param onPermissionDenied the runnable to be executed when the permission is denied(but not - * permanently) - * @param rationaleTitle rationale title to be displayed when permission was denied - * @param rationaleMessage rationale message to be displayed when permission was denied - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final Runnable onPermissionDenied, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - Dexter.withActivity(activity) - .withPermissions(permissions) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(final MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted() || hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - if (report.isAnyPermissionPermanentlyDenied()) { - // permission is denied permanently, we will show user a dialog message. - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(R.string.navigation_item_settings), - null, () -> { - askUserToManuallyEnablePermissionFromSettings(activity); - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - }, null, null, - !(activity instanceof UploadActivity)); - } else { - if (null != onPermissionDenied) { - onPermissionDenied.run(); - } - } - } - - @Override - public void onPermissionRationaleShouldBeShown( - final List permissions, - final PermissionToken token - ) { - if (rationaleTitle == -1 && rationaleMessage == -1) { - token.continuePermissionRequest(); - return; - } - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), - () -> { - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - token.continuePermissionRequest(); - }, - () -> { - Toast.makeText(activity.getApplicationContext(), - R.string.permissions_are_required_for_functionality, - Toast.LENGTH_LONG - ).show(); - token.cancelPermissionRequest(); - if (activity instanceof UploadActivity) { - activity.finish(); - } - }, null, false - ); - } - }).onSameThread().check(); - } -} 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 new file mode 100644 index 0000000000..305388fab7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.utils + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.upload.UploadActivity + + +object PermissionUtils { + + @JvmStatic + val PERMISSIONS_STORAGE: Array = getPermissionsStorage() + + @JvmStatic + private fun getPermissionsStorage(): Array { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> arrayOf( + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU -> arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + else -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + } + + /** + * This method can be used by any activity which requires a permission which has been + * blocked(marked never ask again by the user) It open the app settings from where the user can + * manually give us the required permission. + * + * @param activity The Activity which requires a permission which has been blocked + */ + @JvmStatic + private fun askUserToManuallyEnablePermissionFromSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", activity.packageName, null) + } + activity.startActivity(intent) + } + + /** + * Checks whether the app already has a particular permission + * + * @param activity The Activity context to check permissions against + * @param permissions An array of permission strings to check + * @return `true if the app has all the specified permissions, `false` otherwise + */ + @JvmStatic + fun hasPermission(activity: Activity, permissions: Array): Boolean { + return permissions.all { permission -> + ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED + } + } + + /** + * Check if the app has partial access permissions. + */ + @JvmStatic + fun hasPartialAccess(activity: Activity): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_IMAGES + ) == PackageManager.PERMISSION_DENIED + } else false + } + + /** + * Checks for a particular permission and runs the runnable to perform an action when the + * permission is granted Also, it shows a rationale if needed + *

+ * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no + * permission rationale will be displayed and permission would be requested + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), + * R.string.storage_permission_title, R.string.write_storage_permission_rationale); + *

+ * If you don't want the permission rationale to be shown then use: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requests + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param rationaleTitle rationale title to be displayed when permission was denied. It can + * be an invalid @StringRes + * @param rationaleMessage rationale message to be displayed when permission was denied. It + * can be an invalid @StringRes + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + if (hasPartialAccess(activity)) { + Thread(onPermissionGranted).start() + return + } + checkPermissionsAndPerformAction( + activity, onPermissionGranted, null, rationaleTitle, rationaleMessage, *permissions + ) + } + + /** + * Checks for a particular permission and runs the corresponding runnables to perform an action + * when the permission is granted/denied Also, it shows a rationale if needed + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> + * showMessage(), R.string.storage_permission_title, + * R.string.write_storage_permission_rationale); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requested + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param onPermissionDenied the runnable to be executed when the permission is denied(but not + * permanently) + * @param rationaleTitle rationale title to be displayed when permission was denied + * @param rationaleMessage rationale message to be displayed when permission was denied + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + onPermissionDenied: Runnable? = null, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + Dexter.withActivity(activity) + .withPermissions(*permissions) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + when { + report.areAllPermissionsGranted() || hasPartialAccess(activity) -> + Thread(onPermissionGranted).start() + report.isAnyPermissionPermanentlyDenied -> { + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(R.string.navigation_item_settings), + null, + { + askUserToManuallyEnablePermissionFromSettings(activity) + if (activity is UploadActivity) { + activity.isShowPermissionsDialog = true + } + }, + null, null, activity !is UploadActivity + ) + } + else -> Thread(onPermissionDenied).start() + } + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, token: PermissionToken + ) { + if (rationaleTitle == -1 && rationaleMessage == -1) { + token.continuePermissionRequest() + return + } + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(android.R.string.ok), + activity.getString(android.R.string.cancel), + { + if (activity is UploadActivity) { + activity.setShowPermissionsDialog(true) + } + token.continuePermissionRequest() + }, + { + Toast.makeText( + activity.applicationContext, + R.string.permissions_are_required_for_functionality, + Toast.LENGTH_LONG + ).show() + token.cancelPermissionRequest() + if (activity is UploadActivity) { + activity.finish() + } + }, + null, false + ) + } + }).onSameThread().check() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java deleted file mode 100644 index f1022a0418..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.Sitelinks; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import fr.free.nrw.commons.location.LatLng; - -public class PlaceUtils { - - public static LatLng latLngFromPointString(String pointString) { - double latitude; - double longitude; - Matcher matcher = Pattern.compile("Point\\(([^ ]+) ([^ ]+)\\)").matcher(pointString); - if (!matcher.find()) { - return null; - } - try { - longitude = Double.parseDouble(matcher.group(1)); - latitude = Double.parseDouble(matcher.group(2)); - } catch (NumberFormatException e) { - return null; - } - - return new LatLng(latitude, longitude, 0); - } - - /** - * Turns a Media list to a Place list by creating a new list in Place type - * @param mediaList - * @return - */ - public static List mediaToExplorePlace( List mediaList) { - List explorePlaceList = new ArrayList<>(); - for (Media media :mediaList) { - explorePlaceList.add(new Place(media.getFilename(), - media.getFallbackDescription(), - media.getCoordinates(), - media.getCategories().toString(), - new Sitelinks.Builder() - .setCommonsLink(media.getPageTitle().getCanonicalUri()) - .setWikipediaLink("") // we don't necessarily have them, can be fetched later - .setWikidataLink("") // we don't necessarily have them, can be fetched later - .build(), - media.getImageUrl(), - media.getThumbUrl(), - "")); - } - return explorePlaceList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt new file mode 100644 index 0000000000..907420f21f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt @@ -0,0 +1,50 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks + +object PlaceUtils { + + @JvmStatic + fun latLngFromPointString(pointString: String): LatLng? { + val matcher = Regex("Point\\(([^ ]+) ([^ ]+)\\)").find(pointString) ?: return null + return try { + val longitude = matcher.groupValues[1].toDouble() + val latitude = matcher.groupValues[2].toDouble() + LatLng(latitude, longitude, 0.0F) + } catch (e: NumberFormatException) { + null + } + } + + /** + * Turns a Media list to a Place list by creating a new list in Place type + * @param mediaList + * @return + */ + @JvmStatic + fun mediaToExplorePlace(mediaList: List): List { + val explorePlaceList = mutableListOf() + for (media in mediaList) { + explorePlaceList.add( + Place( + media.filename, + media.fallbackDescription, + media.coordinates, + media.categories.toString(), + Sitelinks.Builder() + .setCommonsLink(media.pageTitle.canonicalUri) + .setWikipediaLink("") // we don't necessarily have them, can be fetched later + .setWikidataLink("") // we don't necessarily have them, can be fetched later + .build(), + media.imageUrl, + media.thumbUrl, + "" + ) + ) + } + return explorePlaceList + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java deleted file mode 100644 index 3144679720..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.category.CategoryItem; -import java.util.Comparator; - -public class StringSortingUtils { - - private StringSortingUtils() { - //no-op - } - - /** - * Returns Comparator for sorting strings by their similarity to the filter. - * By using this Comparator we get results - * from the highest to the lowest similarity with the filter. - * - * @param filter String to compare similarity with - * @return Comparator with string similarity - */ - public static Comparator sortBySimilarity(final String filter) { - return (firstItem, secondItem) -> { - double firstItemSimilarity = calculateSimilarity(firstItem.getName(), filter); - double secondItemSimilarity = calculateSimilarity(secondItem.getName(), filter); - return (int) Math.signum(secondItemSimilarity - firstItemSimilarity); - }; - } - - - /** - * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 - * @param str1 String 1 - * @param str2 String 2 - * @return Double between 0.0 and 1.0 that reflects string similarity - */ - private static double calculateSimilarity(String str1, String str2) { - int longerLength = Math.max(str1.length(), str2.length()); - - if (longerLength == 0) return 1.0; - - int distanceBetweenStrings = levenshteinDistance(str1, str2); - return (longerLength - distanceBetweenStrings) / (double) longerLength; - } - - /** - * Levershtein distance algorithm - * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java - * - * @param str1 String 1 - * @param str2 String 2 - * @return Number of characters the strings differ by - */ - private static int levenshteinDistance(String str1, String str2) { - if (str1.equals(str2)) return 0; - if (str1.length() == 0) return str2.length(); - if (str2.length() == 0) return str1.length(); - - int[] cost = new int[str1.length() + 1]; - int[] newcost = new int[str1.length() + 1]; - - // initial cost of skipping prefix in str1 - for (int i = 0; i < cost.length; i++) cost[i] = i; - - // transformation cost for each letter in str2 - for (int j = 1; j <= str2.length(); j++) { - // initial cost of skipping prefix in String str2 - newcost[0] = j; - - // transformation cost for each letter in str1 - for(int i = 1; i < cost.length; i++) { - // matching current letters in both strings - int match = (str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1; - - // computing cost for each transformation - int cost_replace = cost[i - 1] + match; - int cost_insert = cost[i] + 1; - int cost_delete = newcost[i - 1] + 1; - - // keep minimum cost - newcost[i] = Math.min(Math.min(cost_insert, cost_delete), cost_replace); - } - - int[] tmp = cost; - cost = newcost; - newcost = tmp; - } - - // the distance is the cost for transforming all letters in both strings - return cost[str1.length()]; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt new file mode 100644 index 0000000000..d9f813ae04 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt @@ -0,0 +1,86 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.category.CategoryItem +import java.lang.Math.signum +import java.util.Comparator + + +object StringSortingUtils { + + /** + * Returns Comparator for sorting strings by their similarity to the filter. + * By using this Comparator we get results + * from the highest to the lowest similarity with the filter. + * + * @param filter String to compare similarity with + * @return Comparator with string similarity + */ + @JvmStatic + fun sortBySimilarity(filter: String): Comparator { + return Comparator { firstItem, secondItem -> + val firstItemSimilarity = calculateSimilarity(firstItem.name, filter) + val secondItemSimilarity = calculateSimilarity(secondItem.name, filter) + signum(secondItemSimilarity - firstItemSimilarity).toInt() + } + } + + /** + * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 + * @param str1 String 1 + * @param str2 String 2 + * @return Double between 0.0 and 1.0 that reflects string similarity + */ + private fun calculateSimilarity(str1: String, str2: String): Double { + val longerLength = maxOf(str1.length, str2.length) + + if (longerLength == 0) return 1.0 + + val distanceBetweenStrings = levenshteinDistance(str1, str2) + return (longerLength - distanceBetweenStrings) / longerLength.toDouble() + } + + /** + * Levenshtein distance algorithm + * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java + * + * @param str1 String 1 + * @param str2 String 2 + * @return Number of characters the strings differ by + */ + private fun levenshteinDistance(str1: String, str2: String): Int { + if (str1 == str2) return 0 + if (str1.isEmpty()) return str2.length + if (str2.isEmpty()) return str1.length + + var cost = IntArray(str1.length + 1) { it } + var newCost = IntArray(str1.length + 1) + + // transformation cost for each letter in str2 + for (j in 1..str2.length) { + // initial cost of skipping prefix in String str2 + newCost[0] = j + + // transformation cost for each letter in str1 + for (i in 1..str1.length) { + // matching current letters in both strings + val match = if (str1[i - 1] == str2[j - 1]) 0 else 1 + + // computing cost for each transformation + val costReplace = cost[i - 1] + match + val costInsert = cost[i] + 1 + val costDelete = newCost[i - 1] + 1 + + // keep minimum cost + newCost[i] = minOf(costInsert, costDelete, costReplace) + } + + // swap cost arrays + val tmp = cost + cost = newCost + newCost = tmp + } + + // the distance is the cost for transforming all letters in both strings + return cost[str1.length] + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java deleted file mode 100644 index a5bb6038e9..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Build; -import android.text.Html; -import android.text.Spanned; -import android.text.SpannedString; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class StringUtil { - - /** - * @param source String that may contain HTML tags. - * @return returned Spanned string that may contain spans parsed from the HTML source. - */ - @NonNull public static Spanned fromHtml(@Nullable String source) { - if (source == null) { - return new SpannedString(""); - } - if (!source.contains("<") && !source.contains("&")) { - // If the string doesn't contain any hints of HTML entities, then skip the expensive - // processing that fromHtml() performs. - return new SpannedString(source); - } - source = source.replaceAll("‎", "\u200E") - .replaceAll("‏", "\u200F") - .replaceAll("&", "&"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - return Html.fromHtml(source); - } - } - - private StringUtil() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt new file mode 100644 index 0000000000..b3c58d8b29 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.utils + +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.SpannedString + +object StringUtil { + + /** + * @param source String that may contain HTML tags. + * @return returned Spanned string that may contain spans parsed from the HTML source. + */ + @JvmStatic + fun fromHtml(source: String?): Spanned { + if (source == null) { + return SpannedString("") + } + if (!source.contains("<") && !source.contains("&")) { + // If the string doesn't contain any hints of HTML entities, then skip the expensive + // processing that fromHtml() performs. + return SpannedString(source) + } + val processedSource = source + .replace("‎", "\u200E") + .replace("‏", "\u200F") + .replace("&", "&") + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY) + } else { + //noinspection deprecation + @Suppress("DEPRECATION") + Html.fromHtml(processedSource) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java deleted file mode 100644 index 7ea7ef467e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java +++ /dev/null @@ -1,74 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Resources; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; - -import timber.log.Timber; - -/** - * A card view which informs onSwipe events to its child - */ -public abstract class SwipableCardView extends CardView { - float x1, x2; - private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; - - public SwipableCardView(@NonNull Context context) { - super(context); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { - super(context, attrs, defStyleAttr); - interceptOnTouchListener(); - } - - private void interceptOnTouchListener() { - this.setOnTouchListener((v, event) -> { - boolean isSwipe = false; - float deltaX = 0.0f; - Timber.e(event.getAction() + ""); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - x1 = event.getX(); - break; - case MotionEvent.ACTION_UP: - x2 = event.getX(); - deltaX = x2 - x1; - if (deltaX < 0) { - //Right to left swipe - isSwipe = true; - } else if (deltaX > 0) { - //Left to right swipe - isSwipe = true; - } - break; - } - if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { - return onSwipe(v); - } - return false; - }); - } - - /** - * abstract function which informs swipe events to those who have inherited from it - */ - public abstract boolean onSwipe(View view); - - private float pixelToDp(float pixels) { - return (pixels / Resources.getSystem().getDisplayMetrics().density); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt new file mode 100644 index 0000000000..5a8261c24f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View + +import androidx.cardview.widget.CardView + +import timber.log.Timber +import kotlin.math.abs + +/** + * A card view which informs onSwipe events to its child + */ +abstract class SwipableCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CardView(context, attrs, defStyleAttr) { + + private var x1 = 0f + private var x2 = 0f + private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f + + init { + interceptOnTouchListener() + } + + @SuppressLint("ClickableViewAccessibility") + private fun interceptOnTouchListener() { + this.setOnTouchListener { v, event -> + var isSwipe = false + var deltaX = 0f + Timber.e(event.action.toString()) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + x1 = event.x + } + MotionEvent.ACTION_UP -> { + x2 = event.x + deltaX = x2 - x1 + isSwipe = deltaX != 0f + } + } + if (isSwipe && pixelToDp(abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE) { + onSwipe(v) + return@setOnTouchListener true + } + false + } + } + + /** + * abstract function which informs swipe events to those who have inherited from it + */ + abstract fun onSwipe(view: View): Boolean + + private fun pixelToDp(pixels: Float): Float { + return pixels / Resources.getSystem().displayMetrics.density + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java deleted file mode 100644 index aa60a7aa85..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Configuration; - -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.settings.Prefs; - -public class SystemThemeUtils { - - private Context context; - private JsonKvStore applicationKvStore; - - public static final String THEME_MODE_DEFAULT = "0"; - public static final String THEME_MODE_DARK = "1"; - public static final String THEME_MODE_LIGHT = "2"; - - @Inject - public SystemThemeUtils(Context context, @Named("default_preferences") JsonKvStore applicationKvStore) { - this.context = context; - this.applicationKvStore = applicationKvStore; - } - - // Return true is system wide dark theme is enabled else false - public boolean getSystemDefaultThemeBool(String theme) { - if (theme.equals(THEME_MODE_DARK)) { - return true; - } else if (theme.equals(THEME_MODE_DEFAULT)) { - return getSystemDefaultThemeBool(getSystemDefaultTheme()); - } - return false; - } - - // Returns the default system wide theme - public String getSystemDefaultTheme() { - return (context.getResources().getConfiguration().uiMode & - Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES ? THEME_MODE_DARK : THEME_MODE_LIGHT; - } - - // Returns true if the device is in night mode or false otherwise - public boolean isDeviceInNightMode() { - return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt new file mode 100644 index 0000000000..f4b1f2625d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration + +import javax.inject.Inject +import javax.inject.Named + +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.settings.Prefs + + +class SystemThemeUtils @Inject constructor( + private val context: Context, + @Named("default_preferences") private val applicationKvStore: JsonKvStore +) { + + companion object { + const val THEME_MODE_DEFAULT = "0" + const val THEME_MODE_DARK = "1" + const val THEME_MODE_LIGHT = "2" + } + + // Return true if system wide dark theme is enabled else false + private fun getSystemDefaultThemeBool(theme: String): Boolean { + return when (theme) { + THEME_MODE_DARK -> true + THEME_MODE_DEFAULT -> getSystemDefaultThemeBool(getSystemDefaultTheme()) + else -> false + } + } + + // Returns the default system wide theme + private fun getSystemDefaultTheme(): String { + return if ( + ( + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES + ) { + THEME_MODE_DARK + } else { + THEME_MODE_LIGHT + } + } + + // Returns true if the device is in night mode or false otherwise + fun isDeviceInNightMode(): Boolean { + return getSystemDefaultThemeBool( + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + ) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java deleted file mode 100644 index acb6afbaa2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.util.DisplayMetrics; - -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import java.util.ArrayList; -import java.util.List; - -public class UiUtils { - - /** - * Draws a vectorial image onto a bitmap. - * @param vectorDrawable vectorial image - * @return bitmap representation of the vectorial image - */ - public static Bitmap getBitmap(VectorDrawableCompat vectorDrawable) { - Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), - vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - vectorDrawable.draw(canvas); - return bitmap; - } - - /** - * Converts dp unit to equivalent pixels. - * @param dp density independent pixels - * @param context Context to access display metrics - * @return px equivalent to dp value - */ - public static float convertDpToPixel(float dp, Context context) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt new file mode 100644 index 0000000000..9ff069ebcd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.DisplayMetrics +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat + + +object UiUtils { + + /** + * Draws a vectorial image onto a bitmap. + * @param vectorDrawable vectorial image + * @return bitmap representation of the vectorial image + */ + @JvmStatic + fun getBitmap(vectorDrawable: VectorDrawableCompat): Bitmap { + val bitmap = Bitmap.createBitmap( + vectorDrawable.intrinsicWidth, + vectorDrawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) + vectorDrawable.draw(canvas) + return bitmap + } + + /** + * Converts dp unit to equivalent pixels. + * @param dp density independent pixels + * @param context Context to access display metrics + * @return px equivalent to dp value + */ + @JvmStatic + fun convertDpToPixel(dp: Float, context: Context): Float { + val metrics = context.resources.displayMetrics + return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java deleted file mode 100644 index 1272dc4f18..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ /dev/null @@ -1,143 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Color; -import android.view.Display; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.StringRes; - -import androidx.core.content.ContextCompat; -import com.google.android.material.snackbar.Snackbar; - -import fr.free.nrw.commons.R; -import timber.log.Timber; - -public class ViewUtil { - /** - * Utility function to show short snack bar - * @param view - * @param messageResourceId - */ - public static void showShortSnackbar(View view, int messageResourceId) { - if (view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> { - try { - Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); - }catch (IllegalStateException e){ - Timber.e(e.getMessage()); - } - }); - } - public static void showLongSnackbar(View view, String text) { - if(view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(()-> { - try { - Snackbar snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT); - - View snack_view = snackbar.getView(); - TextView snack_text = snack_view.findViewById(R.id.snackbar_text); - - snack_view.setBackgroundColor(Color.LTGRAY); - snack_text.setTextColor(ContextCompat.getColor(view.getContext(), R.color.primaryColor)); - snackbar.setActionTextColor(Color.RED); - - snackbar.setAction("Dismiss", new View.OnClickListener() { - @Override - public void onClick(View v) { - // Handle the action click - snackbar.dismiss(); - } - }); - - snackbar.show(); - - }catch (IllegalStateException e) { - Timber.e(e.getMessage()); - } - }); - } - - public static void showLongToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); - } - - public static void showLongToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); - } - - public static void showShortToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show()); - } - - public static void showShortToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show()); - } - - public static boolean isPortrait(Context context) { - Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); - if (orientation.getWidth() < orientation.getHeight()){ - return true; - } else { - return false; - } - } - - public static void hideKeyboard(View view){ - if (view != null) { - InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - view.clearFocus(); - if (manager != null) { - manager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - } - } - - /** - * A snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - */ - public static void showDismissibleSnackBar(View view, - int messageResourceId, - int actionButtonResourceId, - View.OnClickListener onClickListener) { - if (view.getContext() == null) { - return; - } - ExecutorUtils.uiExecutor().execute(() -> { - Snackbar snackbar = Snackbar.make(view, view.getContext().getString(messageResourceId), - Snackbar.LENGTH_INDEFINITE); - snackbar.setAction(view.getContext().getString(actionButtonResourceId), v -> { - snackbar.dismiss(); - onClickListener.onClick(v); - }); - snackbar.show(); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt new file mode 100644 index 0000000000..64970ecf6f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Color +import android.view.Display +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import android.widget.Toast + +import androidx.annotation.StringRes + +import androidx.core.content.ContextCompat +import com.google.android.material.snackbar.Snackbar + +import fr.free.nrw.commons.R +import timber.log.Timber + + +object ViewUtil { + + /** + * Utility function to show short snack bar + * @param view + * @param messageResourceId + */ + @JvmStatic + fun showShortSnackbar(view: View, messageResourceId: Int) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show() + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongSnackbar(view: View, text: String) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + val snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT) + val snackView = snackbar.view + val snackText: TextView = snackView.findViewById(R.id.snackbar_text) + + snackView.setBackgroundColor(Color.LTGRAY) + snackText.setTextColor(ContextCompat.getColor(view.context, R.color.primaryColor)) + snackbar.setActionTextColor(Color.RED) + + snackbar.setAction("Dismiss") { snackbar.dismiss() } + snackbar.show() + + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showLongToast(context: Context, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showShortToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun showShortToast(context: Context?, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun isPortrait(context: Context): Boolean { + val orientation = (context as Activity).windowManager.defaultDisplay + return orientation.width < orientation.height + } + + @JvmStatic + fun hideKeyboard(view: View?) { + view?.let { + val manager = it.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + it.clearFocus() + manager?.hideSoftInputFromWindow(it.windowToken, 0) + } + } + + /** + * A snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + */ + @JvmStatic + fun showDismissibleSnackBar( + view: View, + messageResourceId: Int, + actionButtonResourceId: Int, + onClickListener: View.OnClickListener + ) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + val snackbar = Snackbar.make(view, view.context.getString(messageResourceId), Snackbar.LENGTH_INDEFINITE) + snackbar.setAction(view.context.getString(actionButtonResourceId)) { + snackbar.dismiss() + onClickListener.onClick(it) + } + snackbar.show() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java deleted file mode 100644 index 2721ef98dc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ViewUtilWrapper { - - @Inject - public ViewUtilWrapper() { - - } - - public void showShortToast(Context context, String text) { - ViewUtil.showShortToast(context, text); - } - - public void showLongToast(Context context, String text) { - ViewUtil.showLongToast(context, text); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt new file mode 100644 index 0000000000..b5ead3041c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ViewUtilWrapper @Inject constructor() { + + fun showShortToast(context: Context, text: String) { + ViewUtil.showShortToast(context, text) + } + + fun showLongToast(context: Context, text: String) { + ViewUtil.showLongToast(context, text) + } +}