mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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:
parent
c439143dd3
commit
0fdb0044b9
48 changed files with 1651 additions and 1647 deletions
|
|
@ -87,7 +87,7 @@ public class ContributionController {
|
||||||
},
|
},
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale,
|
R.string.write_storage_permission_rationale,
|
||||||
PermissionUtils.PERMISSIONS_STORAGE);
|
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -224,7 +224,7 @@ public class ContributionController {
|
||||||
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale,
|
R.string.write_storage_permission_rationale,
|
||||||
PermissionUtils.PERMISSIONS_STORAGE);
|
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.contributions.Contribution.STATE_PAUSED;
|
||||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
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.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.computeBearing;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||||
|
|
||||||
|
|
@ -23,12 +22,10 @@ import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.MenuItem.OnMenuItemClickListener;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
|
@ -39,7 +36,6 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
@ -16,10 +13,8 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import fr.free.nrw.commons.databinding.MainBinding;
|
import fr.free.nrw.commons.databinding.MainBinding;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
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.quiz.QuizChecker;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
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.UploadProgressActivity;
|
||||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package fr.free.nrw.commons.delete;
|
package fr.free.nrw.commons.delete;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
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.auth.csrf.InvalidLoginTokenException;
|
||||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
|
import fr.free.nrw.commons.utils.LangCodeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
|
||||||
|
|
@ -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.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
||||||
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
|
@ -21,22 +19,17 @@ import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.Settings;
|
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.MapController;
|
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.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
||||||
import fr.free.nrw.commons.explore.paging.LiveDataConverter;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
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.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.MapUtils;
|
import fr.free.nrw.commons.utils.MapUtils;
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
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.SystemThemeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
|
@ -310,7 +301,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startMapWithoutPermission() {
|
private void startMapWithoutPermission() {
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
|
|
@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||||
isPermissionDenied = true;
|
isPermissionDenied = true;
|
||||||
}
|
}
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void launchZoomActivity(final View view) {
|
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) {
|
if (hasPermission) {
|
||||||
launchZoomActivityAfterPermissionCheck(view);
|
launchZoomActivityAfterPermissionCheck(view);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -328,7 +328,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
},
|
},
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.read_storage_permission_rationale,
|
R.string.read_storage_permission_rationale,
|
||||||
PermissionUtils.PERMISSIONS_STORAGE
|
PermissionUtils.getPERMISSIONS_STORAGE()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ import android.view.ViewGroup;
|
||||||
import android.view.ViewGroup.LayoutParams;
|
import android.view.ViewGroup.LayoutParams;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
|
@ -701,7 +700,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
= new LatLng(Double.parseDouble(locationLatLng[0]),
|
= new LatLng(Double.parseDouble(locationLatLng[0]),
|
||||||
Double.parseDouble(locationLatLng[1]), 1f);
|
Double.parseDouble(locationLatLng[1]), 1f);
|
||||||
} else {
|
} else {
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
}
|
}
|
||||||
if (binding.map != null) {
|
if (binding.map != null) {
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
|
|
@ -793,7 +792,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
hideBottomSheet();
|
hideBottomSheet();
|
||||||
binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener(
|
binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener(
|
||||||
(v, hasFocus) -> {
|
(v, hasFocus) -> {
|
||||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25,
|
LayoutUtils.setLayoutHeightAlignedToWidth(1.25,
|
||||||
binding.nearbyFilterList.getRoot());
|
binding.nearbyFilterList.getRoot());
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE);
|
binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
|
@ -834,7 +833,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(),
|
.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(),
|
||||||
0.75);
|
0.75);
|
||||||
binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter);
|
binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter);
|
||||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView)
|
RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView)
|
||||||
.takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView))
|
.takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView))
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,10 @@ import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.view.View;
|
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
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.Label;
|
||||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
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.Place;
|
||||||
import fr.free.nrw.commons.nearby.PlaceDao;
|
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
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.utils.LocationUtils;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
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
|
* First checks for external storage permissions and then sends logs via email
|
||||||
*/
|
*/
|
||||||
private void checkPermissionsAndSendLogs() {
|
private void checkPermissionsAndSendLogs() {
|
||||||
if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE)) {
|
if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) {
|
||||||
commonsLogSender.send(getActivity(), null);
|
commonsLogSender.send(getActivity(), null);
|
||||||
} else {
|
} else {
|
||||||
requestExternalStoragePermissions();
|
requestExternalStoragePermissions();
|
||||||
|
|
@ -556,7 +555,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
*/
|
*/
|
||||||
private void requestExternalStoragePermissions() {
|
private void requestExternalStoragePermissions() {
|
||||||
Dexter.withActivity(getActivity())
|
Dexter.withActivity(getActivity())
|
||||||
.withPermissions(PermissionUtils.PERMISSIONS_STORAGE)
|
.withPermissions(PermissionUtils.getPERMISSIONS_STORAGE())
|
||||||
.withListener(new MultiplePermissionsListener() {
|
.withListener(new MultiplePermissionsListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPermissionsChecked(MultiplePermissionsReport report) {
|
public void onPermissionsChecked(MultiplePermissionsReport report) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
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.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.PLACE_OBJECT;
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
|
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.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
|
@ -277,7 +276,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
|
|
||||||
public void checkStoragePermissions() {
|
public void checkStoragePermissions() {
|
||||||
// Check if all required permissions are granted
|
// 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);
|
final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this);
|
||||||
if (hasAllPermissions || hasPartialAccess) {
|
if (hasAllPermissions || hasPartialAccess) {
|
||||||
// All required permissions are granted, so enable UI elements and perform actions
|
// 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.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale_for_image_share,
|
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
|
/* If all permissions are not granted and a dialog is already showing on screen
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
363
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt
Normal file
363
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt
Normal file
40
app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt
Normal file
47
app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
156
app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt
Normal file
156
app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt
Normal file
63
app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt
Normal file
39
app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt
Normal file
85
app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
231
app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt
Normal file
231
app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt
Normal file
50
app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package fr.free.nrw.commons.utils;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.SpannedString;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public final class StringUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param source String that may contain HTML tags.
|
|
||||||
* @return returned Spanned string that may contain spans parsed from the HTML source.
|
|
||||||
*/
|
|
||||||
@NonNull public static Spanned fromHtml(@Nullable String source) {
|
|
||||||
if (source == null) {
|
|
||||||
return new SpannedString("");
|
|
||||||
}
|
|
||||||
if (!source.contains("<") && !source.contains("&")) {
|
|
||||||
// If the string doesn't contain any hints of HTML entities, then skip the expensive
|
|
||||||
// processing that fromHtml() performs.
|
|
||||||
return new SpannedString(source);
|
|
||||||
}
|
|
||||||
source = source.replaceAll("‎", "\u200E")
|
|
||||||
.replaceAll("‏", "\u200F")
|
|
||||||
.replaceAll("&", "&");
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
|
|
||||||
} else {
|
|
||||||
//noinspection deprecation
|
|
||||||
return Html.fromHtml(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private StringUtil() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt
Normal file
37
app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt
Normal 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("‎", "\u200E")
|
||||||
|
.replace("‏", "\u200F")
|
||||||
|
.replace("&", "&")
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY)
|
||||||
|
} else {
|
||||||
|
//noinspection deprecation
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Html.fromHtml(processedSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt
Normal file
41
app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
151
app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt
Normal file
151
app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue