Migrated util module from Java to Kotlin (#5938)

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- AbstractTextWatcher
- ActivityUtils
- CommonsDateUtil
- DateUtil

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- DeviceInfoUtil
- ExecutorUtils
- FragmentUtils

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- ImageUtils
- ImageUtilsWrapper
- LangCodeUtils
- LayoutUtils
- LengthUtils
- LocationUtils
- MapUtils

* Rename .java to .kt

* Migrated all remaining files in util module
This commit is contained in:
Saifuddin Adenwala 2024-11-18 19:10:35 +05:30 committed by GitHub
parent c439143dd3
commit 0fdb0044b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1651 additions and 1647 deletions

View file

@ -87,7 +87,7 @@ public class ContributionController {
},
R.string.storage_permission_title,
R.string.write_storage_permission_rationale,
PermissionUtils.PERMISSIONS_STORAGE);
PermissionUtils.getPERMISSIONS_STORAGE());
}
/**
@ -224,7 +224,7 @@ public class ContributionController {
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
R.string.storage_permission_title,
R.string.write_storage_permission_rationale,
PermissionUtils.PERMISSIONS_STORAGE);
PermissionUtils.getPERMISSIONS_STORAGE());
}

View file

@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
import static fr.free.nrw.commons.utils.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.LayoutInflater;
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.annotation.Nullable;
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;

View file

@ -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.NonNull;
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.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import fr.free.nrw.commons.utils.PermissionUtils;

View file

@ -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.actions.PageEditClient;
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;

View file

@ -4,14 +4,12 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChange
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.Location;
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.databinding.FragmentExploreMapBinding;
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.nearby.Place;
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 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
}
private void startMapWithoutPermission() {
lastKnownLocation = MapUtils.defaultLatLng;
lastKnownLocation = MapUtils.getDefaultLatLng();
moveCameraToPosition(
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
presenter.onMapReady(exploreMapController);
@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
isPermissionDenied = true;
}
lastKnownLocation = MapUtils.defaultLatLng;
lastKnownLocation = MapUtils.getDefaultLatLng();
moveCameraToPosition(
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
presenter.onMapReady(exploreMapController);

View file

@ -318,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
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 class MediaDetailFragment extends CommonsDaggerSupportFragment implements
},
R.string.storage_permission_title,
R.string.read_storage_permission_rationale,
PermissionUtils.PERMISSIONS_STORAGE
PermissionUtils.getPERMISSIONS_STORAGE()
);
}
}

View file

@ -43,7 +43,6 @@ import android.view.ViewGroup;
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 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
= 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 class NearbyParentFragment extends CommonsDaggerSupportFragment
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 class NearbyParentFragment extends CommonsDaggerSupportFragment
.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))

View file

@ -11,13 +11,10 @@ import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
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.CheckBoxTriStates;
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;

View file

@ -12,7 +12,6 @@ import android.net.Uri;
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 @@ public class SettingsFragment extends PreferenceFragmentCompat {
* 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 @@ public class SettingsFragment extends PreferenceFragmentCompat {
*/
private void requestExternalStoragePermissions() {
Dexter.withActivity(getActivity())
.withPermissions(PermissionUtils.PERMISSIONS_STORAGE)
.withPermissions(PermissionUtils.getPERMISSIONS_STORAGE())
.withListener(new MultiplePermissionsListener() {
@Override
public void onPermissionsChecked(MultiplePermissionsReport report) {

View file

@ -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.recyclerview.widget.LinearLayoutManager;
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 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
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 class UploadActivity extends BaseActivity implements UploadContract.View,
},
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

View file

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

View file

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

View file

@ -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<Integer> checkIfImageIsTooDark(String bitmapPath) {
return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation());
}
public Single<Integer> 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);
}
}

View file

@ -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<Int> {
return Single.fromCallable { ImageUtils.checkIfImageIsTooDark(bitmapPath) }
.subscribeOn(Schedulers.computation())
}
fun checkImageGeolocationIsDifferent(
geolocationOfFileString: String,
latLng: LatLng
): Single<Int> {
return Single.fromCallable {
ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng)
}
.subscribeOn(Schedulers.computation())
.map { isDifferent ->
if (isDifferent) ImageUtils.IMAGE_GEOLOCATION_DIFFERENT else ImageUtils.IMAGE_OK
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <a href="https://www.movable-type.co.uk/scripts/latlong.html">Bearing</a>
* @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;
}
}

View file

@ -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 <a href="https://www.movable-type.co.uk/scripts/latlong.html">Bearing</a>
* @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
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String> extractCategoriesFromList(String source) {
if (StringUtils.isBlank(source)) {
return new ArrayList<>();
}
String[] cats = source.split("\\|");
List<String> categories = new ArrayList<>();
for (String category : cats) {
if (!StringUtils.isBlank(category.trim())) {
categories.add(category);
}
}
return categories;
}
}

View file

@ -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<String> {
if (source.isBlank()) {
return emptyList()
}
val cats = source.split("|")
val categories = mutableListOf<String>()
for (category in cats) {
if (category.trim().isNotBlank()) {
categories.add(category)
}
}
return categories
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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
* <p>
* rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no
* permission rationale will be displayed and permission would be requested
* <p>
* Sample usage:
* <p>
* PermissionUtils.checkPermissionsAndPerformAction(activity,
* Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity),
* R.string.storage_permission_title, R.string.write_storage_permission_rationale);
* <p>
* If you don't want the permission rationale to be shown then use:
* <p>
* 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
* <p>
* Sample usage:
* <p>
* 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<PermissionRequest> 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();
}
}

View file

@ -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<String> = getPermissionsStorage()
@JvmStatic
private fun getPermissionsStorage(): Array<String> {
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<String>): 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
* <p>
* rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no
* permission rationale will be displayed and permission would be requested
* <p>
* Sample usage:
* <p>
* PermissionUtils.checkPermissionsAndPerformAction(activity,
* Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity),
* R.string.storage_permission_title, R.string.write_storage_permission_rationale);
* <p>
* If you don't want the permission rationale to be shown then use:
* <p>
* 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
* <p>
* Sample usage:
* <p>
* 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<PermissionRequest>, 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()
}
}

View file

@ -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<Place> mediaToExplorePlace( List<Media> mediaList) {
List<Place> 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;
}
}

View file

@ -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<Media>): List<Place> {
val explorePlaceList = mutableListOf<Place>()
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
}
}

View file

@ -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<CategoryItem> 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()];
}
}

View file

@ -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<CategoryItem> {
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]
}
}

View file

@ -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("&#8206;", "\u200E")
.replaceAll("&#8207;", "\u200F")
.replaceAll("&amp;", "&");
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() {
}
}

View file

@ -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("&#8206;", "\u200E")
.replace("&#8207;", "\u200F")
.replace("&amp;", "&")
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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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