Merge branch 'main' into Migrate-Feedback-Module-from-java-to-kt

This commit is contained in:
Neel Doshi 2024-12-04 18:29:13 +05:30 committed by GitHub
commit 6e7c1cc3ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2619 additions and 3359 deletions

View file

@ -1,77 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import android.app.Activity;
import android.content.Intent;
import fr.free.nrw.commons.CameraPosition;
import fr.free.nrw.commons.Media;
/**
* Helper class for starting the activity
*/
public final class LocationPicker {
/**
* Getting camera position from the intent using constants
*
* @param data intent
* @return CameraPosition
*/
public static CameraPosition getCameraPosition(final Intent data) {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
public static class IntentBuilder {
private final Intent intent;
/**
* Creates a new builder that creates an intent to launch the place picker activity.
*/
public IntentBuilder() {
intent = new Intent();
}
/**
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
/**
* Gets and puts activity name in intent
* @param activity activity key
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder activityKey(
final String activity) {
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity);
return this;
}
/**
* Gets and puts media in intent
* @param media Media
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder media(
final Media media) {
intent.putExtra(LocationPickerConstants.MEDIA, media);
return this;
}
/**
* Gets and sets the activity
* @param activity Activity
* @return Intent
*/
public Intent build(final Activity activity) {
intent.setClass(activity, LocationPickerActivity.class);
return intent;
}
}
}

View file

@ -0,0 +1,72 @@
package fr.free.nrw.commons.LocationPicker
import android.app.Activity
import android.content.Intent
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.Media
/**
* Helper class for starting the activity
*/
object LocationPicker {
/**
* Getting camera position from the intent using constants
*
* @param data intent
* @return CameraPosition
*/
@JvmStatic
fun getCameraPosition(data: Intent): CameraPosition? {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION)
}
class IntentBuilder
/**
* Creates a new builder that creates an intent to launch the place picker activity.
*/() {
private val intent: Intent = Intent()
/**
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
fun defaultLocation(position: CameraPosition): IntentBuilder {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position)
return this
}
/**
* Gets and puts activity name in intent
* @param activity activity key
* @return LocationPicker.IntentBuilder
*/
fun activityKey(activity: String): IntentBuilder {
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity)
return this
}
/**
* Gets and puts media in intent
* @param media Media
* @return LocationPicker.IntentBuilder
*/
fun media(media: Media): IntentBuilder {
intent.putExtra(LocationPickerConstants.MEDIA, media)
return this
}
/**
* Gets and sets the activity
* @param activity Activity
* @return Intent
*/
fun build(activity: Activity): Intent {
intent.setClass(activity, LocationPickerActivity::class.java)
return intent
}
}
}

View file

@ -1,681 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.animation.OvershootInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.CameraPosition;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
import fr.free.nrw.commons.filepicker.Constants;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.util.constants.GeoConstants;
import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.ScaleDiskOverlay;
import org.osmdroid.views.overlay.TilesOverlay;
import timber.log.Timber;
/**
* Helps to pick location and return the result with an intent
*/
public class LocationPickerActivity extends BaseActivity implements
LocationPermissionCallback {
/**
* coordinateEditHelper: helps to edit coordinates
*/
@Inject
CoordinateEditHelper coordinateEditHelper;
/**
* media : Media object
*/
private Media media;
/**
* cameraPosition : position of picker
*/
private CameraPosition cameraPosition;
/**
* markerImage : picker image
*/
private ImageView markerImage;
/**
* mapView : OSM Map
*/
private org.osmdroid.views.MapView mapView;
/**
* tvAttribution : credit
*/
private AppCompatTextView tvAttribution;
/**
* activity : activity key
*/
private String activity;
/**
* modifyLocationButton : button for start editing location
*/
Button modifyLocationButton;
/**
* removeLocationButton : button to remove location metadata
*/
Button removeLocationButton;
/**
* showInMapButton : button for showing in map
*/
TextView showInMapButton;
/**
* placeSelectedButton : fab for selecting location
*/
FloatingActionButton placeSelectedButton;
/**
* fabCenterOnLocation: button for center on location;
*/
FloatingActionButton fabCenterOnLocation;
/**
* shadow : imageview of shadow
*/
private ImageView shadow;
/**
* largeToolbarText : textView of shadow
*/
private TextView largeToolbarText;
/**
* smallToolbarText : textView of shadow
*/
private TextView smallToolbarText;
/**
* applicationKvStore : for storing values
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
BasicKvStore store;
/**
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
*/
@Inject
SystemThemeUtils systemThemeUtils;
private boolean isDarkTheme;
private boolean moveToCurrentLocation;
@Inject
LocationServiceManager locationManager;
LocationPermissionsHelper locationPermissionsHelper;
@Inject
SessionManager sessionManager;
/**
* Constants
*/
private static final String CAMERA_POS = "cameraPosition";
private static final String ACTIVITY = "activity";
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
super.onCreate(savedInstanceState);
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
moveToCurrentLocation = false;
store = new BasicKvStore(this, "LocationPermissions");
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
setContentView(R.layout.activity_location_picker);
if (savedInstanceState == null) {
cameraPosition = getIntent()
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA);
}else{
cameraPosition = savedInstanceState.getParcelable(CAMERA_POS);
activity = savedInstanceState.getString(ACTIVITY);
media = savedInstanceState.getParcelable("sMedia");
}
bindViews();
addBackButtonListener();
addPlaceSelectedButton();
addCredits();
getToolbarUI();
addCenterOnGPSButton();
org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(),
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()));
mapView.setTileSource(TileSourceFactory.WIKIMEDIA);
mapView.setTilesScaledToDpi(true);
mapView.setMultiTouchControls(true);
org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put(
"Referer", "http://maps.wikimedia.org/"
);
mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
mapView.getController().setZoom(ZOOM_LEVEL);
mapView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
markerImage.animate().translationY(0)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
return false;
});
if ("UploadActivity".equals(activity)) {
placeSelectedButton.setVisibility(View.GONE);
modifyLocationButton.setVisibility(View.VISIBLE);
removeLocationButton.setVisibility(View.VISIBLE);
showInMapButton.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.image_location));
smallToolbarText.setText(getResources().
getString(R.string.check_whether_location_is_correct));
fabCenterOnLocation.setVisibility(View.GONE);
markerImage.setVisibility(View.GONE);
shadow.setVisibility(View.GONE);
assert cameraPosition != null;
showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(),
cameraPosition.getLongitude()));
}
setupMapView();
}
/**
* Moves the center of the map to the specified coordinates
*
*/
private void moveMapTo(double latitude, double longitude){
if(mapView != null && mapView.getController() != null){
GeoPoint point = new GeoPoint(latitude, longitude);
mapView.getController().setCenter(point);
mapView.getController().animateTo(point);
}
}
/**
* Moves the center of the map to the specified coordinates
* @param point The GeoPoint object which contains the coordinates to move to
*/
private void moveMapTo(GeoPoint point){
if(point != null){
moveMapTo(point.getLatitude(), point.getLongitude());
}
}
/**
* For showing credits
*/
private void addCredits() {
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* For setting up Dark Theme
*/
private void darkThemeSetup() {
if (isDarkTheme) {
shadow.setColorFilter(Color.argb(255, 255, 255, 255));
mapView.getOverlayManager().getTilesOverlay()
.setColorFilter(TilesOverlay.INVERT_COLORS);
}
}
/**
* Clicking back button destroy locationPickerActivity
*/
private void addBackButtonListener() {
final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button);
backButton.setOnClickListener(v -> {
finish();
});
}
/**
* Binds mapView and location picker icon
*/
private void bindViews() {
mapView = findViewById(R.id.map_view);
markerImage = findViewById(R.id.location_picker_image_view_marker);
tvAttribution = findViewById(R.id.tv_attribution);
modifyLocationButton = findViewById(R.id.modify_location);
removeLocationButton = findViewById(R.id.remove_location);
showInMapButton = findViewById(R.id.show_in_map);
showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase(
Locale.ROOT));
shadow = findViewById(R.id.location_picker_image_view_shadow);
}
/**
* Gets toolbar color
*/
private void getToolbarUI() {
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view);
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view);
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
}
private void setupMapView() {
requestLocationPermissions();
//If location metadata is available, move map to that location.
if(activity.equals("UploadActivity") || activity.equals("MediaActivity")){
moveMapToMediaLocation();
} else {
//If location metadata is not available, move map to device GPS location.
moveMapToGPSLocation();
}
modifyLocationButton.setOnClickListener(v -> onClickModifyLocation());
removeLocationButton.setOnClickListener(v -> onClickRemoveLocation());
showInMapButton.setOnClickListener(v -> showInMapApp());
darkThemeSetup();
}
/**
* Handles onclick event of modifyLocationButton
*/
private void onClickModifyLocation() {
placeSelectedButton.setVisibility(View.VISIBLE);
modifyLocationButton.setVisibility(View.GONE);
removeLocationButton.setVisibility(View.GONE);
showInMapButton.setVisibility(View.GONE);
markerImage.setVisibility(View.VISIBLE);
shadow.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.choose_a_location));
smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust));
fabCenterOnLocation.setVisibility(View.VISIBLE);
removeSelectedLocationMarker();
moveMapToMediaLocation();
}
/**
* Handles onclick event of removeLocationButton
*/
private void onClickRemoveLocation() {
DialogUtil.showAlertDialog(this,
getString(R.string.remove_location_warning_title),
getString(R.string.remove_location_warning_desc),
getString(R.string.continue_message),
getString(R.string.cancel), () -> removeLocationFromImage(), null);
}
/**
* Method to remove the location from the picture
*/
private void removeLocationFromImage() {
if (media != null) {
getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
, media, "0.0", "0.0", "0.0f")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Coordinates are removed from the image");
}));
}
final Intent returningIntent = new Intent();
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
/**
* Show the location in map app. Map will center on the location metadata, if available.
* If there is no location metadata, the map will center on the commons app map center.
*/
private void showInMapApp() {
fr.free.nrw.commons.location.LatLng position = null;
if(activity.equals("UploadActivity") && cameraPosition != null){
//location metadata is available
position = new fr.free.nrw.commons.location.LatLng(cameraPosition.getLatitude(),
cameraPosition.getLongitude(), 0.0f);
} else if(mapView != null){
//location metadata is not available
position = new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(),
mapView.getMapCenter().getLongitude(), 0.0f);
}
if(position != null){
Utils.handleGeoCoordinates(this, position);
}
}
/**
* Moves the center of the map to the media's location, if that data
* is available.
*/
private void moveMapToMediaLocation() {
if (cameraPosition != null) {
GeoPoint point = new GeoPoint(cameraPosition.getLatitude(),
cameraPosition.getLongitude());
moveMapTo(point);
}
}
/**
* Moves the center of the map to the device's GPS location, if that data is available.
*/
private void moveMapToGPSLocation(){
if(locationManager != null){
fr.free.nrw.commons.location.LatLng location = locationManager.getLastLocation();
if(location != null){
GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude());
moveMapTo(point);
}
}
}
/**
* Select the preferable location
*/
private void addPlaceSelectedButton() {
placeSelectedButton = findViewById(R.id.location_chosen_button);
placeSelectedButton.setOnClickListener(view -> placeSelected());
}
/**
* Return the intent with required data
*/
void placeSelected() {
if (activity.equals("NoLocationUploadActivity")) {
applicationKvStore.putString(LAST_LOCATION,
mapView.getMapCenter().getLatitude()
+ ","
+ mapView.getMapCenter().getLongitude());
applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + "");
}
if (media == null) {
final Intent returningIntent = new Intent();
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
new CameraPosition(mapView.getMapCenter().getLatitude(),
mapView.getMapCenter().getLongitude(), 14.0));
setResult(AppCompatActivity.RESULT_OK, returningIntent);
} else {
updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()),
String.valueOf(mapView.getMapCenter().getLongitude()),
String.valueOf(0.0f));
}
finish();
}
/**
* Fetched coordinates are replaced with existing coordinates by a POST API call.
* @param Latitude to be added
* @param Longitude to be added
* @param Accuracy to be added
*/
public void updateCoordinates(final String Latitude, final String Longitude,
final String Accuracy) {
if (media == null) {
return;
}
try {
getCompositeDisposable().add(
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
Latitude, Longitude, Accuracy)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Coordinates are added.");
}));
} catch (Exception e) {
if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) {
final String username = sessionManager.getUserName();
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message),
username
);
CommonsApplication.getInstance().clearApplicationData(
this, logoutListener);
}
}
}
/**
* Center the camera on the last saved location
*/
private void addCenterOnGPSButton() {
fabCenterOnLocation = findViewById(R.id.center_on_gps);
fabCenterOnLocation.setOnClickListener(view -> {
moveToCurrentLocation = true;
requestLocationPermissions();
});
}
/**
* Adds selected location marker on the map
*/
private void showSelectedLocationMarker(GeoPoint point) {
Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker);
Marker marker = new Marker(mapView);
marker.setPosition(point);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
marker.setIcon(icon);
marker.setInfoWindow(null);
mapView.getOverlays().add(marker);
mapView.invalidate();
}
/**
* Removes selected location marker from the map
*/
private void removeSelectedLocationMarker() {
List<Overlay> overlays = mapView.getOverlays();
for (int i = 0; i < overlays.size(); i++) {
if (overlays.get(i) instanceof Marker) {
Marker item = (Marker) overlays.get(i);
if (cameraPosition.getLatitude() == item.getPosition().getLatitude()
&& cameraPosition.getLongitude() == item.getPosition().getLongitude()) {
mapView.getOverlays().remove(i);
mapView.invalidate();
break;
}
}
}
}
/**
* Center the map at user's current location
*/
private void requestLocationPermissions() {
locationPermissionsHelper = new LocationPermissionsHelper(
this, locationManager, this);
locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title,
R.string.upload_map_location_access);
}
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
if (requestCode == Constants.RequestCodes.LOCATION
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
onLocationPermissionGranted();
} else {
onLocationPermissionDenied(getString(R.string.upload_map_location_access));
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
public void onLocationPermissionDenied(String toastMessage) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
permission.ACCESS_FINE_LOCATION)) {
if (!locationPermissionsHelper.checkLocationPermission(this)) {
if (store.getBoolean("isPermissionDenied", false)) {
// means user has denied location permission twice or checked the "Don't show again"
locationPermissionsHelper.showAppSettingsDialog(this,
R.string.upload_map_location_access);
} else {
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
}
store.putBoolean("isPermissionDenied", true);
}
} else {
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
}
}
@Override
public void onLocationPermissionGranted() {
if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) {
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
locationManager.requestLocationUpdatesFromProvider(
LocationManager.NETWORK_PROVIDER);
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
addMarkerAtGPSLocation();
} else {
addMarkerAtGPSLocation();
locationPermissionsHelper.showLocationOffDialog(this,
R.string.ask_to_turn_location_on_text);
}
}
}
/**
* Adds a marker to the map at the most recent GPS location
* (which may be the current GPS location).
*/
private void addMarkerAtGPSLocation() {
fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
if (currLocation != null) {
GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(),
currLocation.getLongitude());
addLocationMarker(currLocationGeopoint);
markerImage.setTranslationY(0);
}
}
private void addLocationMarker(GeoPoint geoPoint) {
if (moveToCurrentLocation) {
mapView.getOverlays().clear();
}
ScaleDiskOverlay diskOverlay =
new ScaleDiskOverlay(this,
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
Paint circlePaint = new Paint();
circlePaint.setColor(Color.rgb(128, 128, 128));
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStrokeWidth(2f);
diskOverlay.setCirclePaint2(circlePaint);
Paint diskPaint = new Paint();
diskPaint.setColor(Color.argb(40, 128, 128, 128));
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
diskOverlay.setCirclePaint1(diskPaint);
diskOverlay.setDisplaySizeMin(900);
diskOverlay.setDisplaySizeMax(1700);
mapView.getOverlays().add(diskOverlay);
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
mapView);
startMarker.setPosition(geoPoint);
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
startMarker.setIcon(
ContextCompat.getDrawable(this, R.drawable.current_location_marker));
startMarker.setTitle("Your Location");
startMarker.setTextLabelFontSize(24);
mapView.getOverlays().add(startMarker);
}
/**
* Saves the state of the activity
* @param outState Bundle
*/
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
if(cameraPosition!=null){
outState.putParcelable(CAMERA_POS, cameraPosition);
}
if(activity!=null){
outState.putString(ACTIVITY, activity);
}
if(media!=null){
outState.putParcelable("sMedia", media);
}
}
}

View file

@ -0,0 +1,678 @@
package fr.free.nrw.commons.LocationPicker
import android.Manifest.permission
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.Paint
import android.location.LocationManager
import android.os.Bundle
import android.preference.PreferenceManager
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.MotionEvent
import android.view.View
import android.view.Window
import android.view.animation.OvershootInterpolator
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.AppCompatTextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.coordinates.CoordinateEditHelper
import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LocationPermissionsHelper
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback
import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.util.constants.GeoConstants
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.ScaleDiskOverlay
import org.osmdroid.views.overlay.TilesOverlay
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
/**
* Helps to pick location and return the result with an intent
*/
class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
/**
* coordinateEditHelper: helps to edit coordinates
*/
@Inject
lateinit var coordinateEditHelper: CoordinateEditHelper
/**
* media : Media object
*/
private var media: Media? = null
/**
* cameraPosition : position of picker
*/
private var cameraPosition: CameraPosition? = null
/**
* markerImage : picker image
*/
private lateinit var markerImage: ImageView
/**
* mapView : OSM Map
*/
private var mapView: org.osmdroid.views.MapView? = null
/**
* tvAttribution : credit
*/
private lateinit var tvAttribution: AppCompatTextView
/**
* activity : activity key
*/
private var activity: String? = null
/**
* modifyLocationButton : button for start editing location
*/
private lateinit var modifyLocationButton: Button
/**
* removeLocationButton : button to remove location metadata
*/
private lateinit var removeLocationButton: Button
/**
* showInMapButton : button for showing in map
*/
private lateinit var showInMapButton: TextView
/**
* placeSelectedButton : fab for selecting location
*/
private lateinit var placeSelectedButton: FloatingActionButton
/**
* fabCenterOnLocation: button for center on location;
*/
private lateinit var fabCenterOnLocation: FloatingActionButton
/**
* shadow : imageview of shadow
*/
private lateinit var shadow: ImageView
/**
* largeToolbarText : textView of shadow
*/
private lateinit var largeToolbarText: TextView
/**
* smallToolbarText : textView of shadow
*/
private lateinit var smallToolbarText: TextView
/**
* applicationKvStore : for storing values
*/
@Inject
@field: Named("default_preferences")
lateinit var applicationKvStore: JsonKvStore
private lateinit var store: BasicKvStore
/**
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
*/
private var isDarkTheme: Boolean = false
private var moveToCurrentLocation: Boolean = false
@Inject
lateinit var locationManager: LocationServiceManager
private lateinit var locationPermissionsHelper: LocationPermissionsHelper
@Inject
lateinit var sessionManager: SessionManager
/**
* Constants
*/
companion object {
private const val CAMERA_POS = "cameraPosition"
private const val ACTIVITY = "activity"
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_ACTION_BAR)
super.onCreate(savedInstanceState)
isDarkTheme = systemThemeUtils.isDeviceInNightMode()
moveToCurrentLocation = false
store = BasicKvStore(this, "LocationPermissions")
requestWindowFeature(Window.FEATURE_ACTION_BAR)
supportActionBar?.hide()
setContentView(R.layout.activity_location_picker)
if (savedInstanceState == null) {
cameraPosition = intent.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION)
activity = intent.getStringExtra(LocationPickerConstants.ACTIVITY_KEY)
media = intent.getParcelableExtra(LocationPickerConstants.MEDIA)
} else {
cameraPosition = savedInstanceState.getParcelable(CAMERA_POS)
activity = savedInstanceState.getString(ACTIVITY)
media = savedInstanceState.getParcelable("sMedia")
}
bindViews()
addBackButtonListener()
addPlaceSelectedButton()
addCredits()
getToolbarUI()
addCenterOnGPSButton()
org.osmdroid.config.Configuration.getInstance()
.load(
applicationContext, PreferenceManager.getDefaultSharedPreferences(
applicationContext
)
)
mapView?.setTileSource(TileSourceFactory.WIKIMEDIA)
mapView?.setTilesScaledToDpi(true)
mapView?.setMultiTouchControls(true)
org.osmdroid.config.Configuration.getInstance().additionalHttpRequestProperties["Referer"] =
"http://maps.wikimedia.org/"
mapView?.zoomController?.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
mapView?.controller?.setZoom(ZOOM_LEVEL.toDouble())
mapView?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_MOVE -> {
if (markerImage.translationY == 0f) {
markerImage.animate().translationY(-75f)
.setInterpolator(OvershootInterpolator()).duration = 250
}
}
MotionEvent.ACTION_UP -> {
markerImage.animate().translationY(0f)
.setInterpolator(OvershootInterpolator()).duration = 250
}
}
false
}
if (activity == "UploadActivity") {
placeSelectedButton.visibility = View.GONE
modifyLocationButton.visibility = View.VISIBLE
removeLocationButton.visibility = View.VISIBLE
showInMapButton.visibility = View.VISIBLE
largeToolbarText.text = getString(R.string.image_location)
smallToolbarText.text = getString(R.string.check_whether_location_is_correct)
fabCenterOnLocation.visibility = View.GONE
markerImage.visibility = View.GONE
shadow.visibility = View.GONE
cameraPosition?.let {
showSelectedLocationMarker(GeoPoint(it.latitude, it.longitude))
}
}
setupMapView()
}
/**
* Moves the center of the map to the specified coordinates
*/
private fun moveMapTo(latitude: Double, longitude: Double) {
mapView?.controller?.let {
val point = GeoPoint(latitude, longitude)
it.setCenter(point)
it.animateTo(point)
}
}
/**
* Moves the center of the map to the specified coordinates
* @param point The GeoPoint object which contains the coordinates to move to
*/
private fun moveMapTo(point: GeoPoint?) {
point?.let {
moveMapTo(it.latitude, it.longitude)
}
}
/**
* For showing credits
*/
private fun addCredits() {
tvAttribution.text = Html.fromHtml(getString(R.string.map_attribution))
tvAttribution.movementMethod = LinkMovementMethod.getInstance()
}
/**
* For setting up Dark Theme
*/
private fun darkThemeSetup() {
if (isDarkTheme) {
shadow.setColorFilter(Color.argb(255, 255, 255, 255))
mapView?.overlayManager?.tilesOverlay?.setColorFilter(TilesOverlay.INVERT_COLORS)
}
}
/**
* Clicking back button destroy locationPickerActivity
*/
private fun addBackButtonListener() {
val backButton = findViewById<ImageView>(R.id.maplibre_place_picker_toolbar_back_button)
backButton.setOnClickListener {
finish()
}
}
/**
* Binds mapView and location picker icon
*/
private fun bindViews() {
mapView = findViewById(R.id.map_view)
markerImage = findViewById(R.id.location_picker_image_view_marker)
tvAttribution = findViewById(R.id.tv_attribution)
modifyLocationButton = findViewById(R.id.modify_location)
removeLocationButton = findViewById(R.id.remove_location)
showInMapButton = findViewById(R.id.show_in_map)
showInMapButton.text = getString(R.string.show_in_map_app).uppercase(Locale.ROOT)
shadow = findViewById(R.id.location_picker_image_view_shadow)
}
/**
* Gets toolbar color
*/
private fun getToolbarUI() {
val toolbar: ConstraintLayout = findViewById(R.id.location_picker_toolbar)
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view)
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view)
toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.primaryColor))
}
private fun setupMapView() {
requestLocationPermissions()
//If location metadata is available, move map to that location.
if (activity == "UploadActivity" || activity == "MediaActivity") {
moveMapToMediaLocation()
} else {
//If location metadata is not available, move map to device GPS location.
moveMapToGPSLocation()
}
modifyLocationButton.setOnClickListener { onClickModifyLocation() }
removeLocationButton.setOnClickListener { onClickRemoveLocation() }
showInMapButton.setOnClickListener { showInMapApp() }
darkThemeSetup()
}
/**
* Handles onClick event of modifyLocationButton
*/
private fun onClickModifyLocation() {
placeSelectedButton.visibility = View.VISIBLE
modifyLocationButton.visibility = View.GONE
removeLocationButton.visibility = View.GONE
showInMapButton.visibility = View.GONE
markerImage.visibility = View.VISIBLE
shadow.visibility = View.VISIBLE
largeToolbarText.text = getString(R.string.choose_a_location)
smallToolbarText.text = getString(R.string.pan_and_zoom_to_adjust)
fabCenterOnLocation.visibility = View.VISIBLE
removeSelectedLocationMarker()
moveMapToMediaLocation()
}
/**
* Handles onClick event of removeLocationButton
*/
private fun onClickRemoveLocation() {
DialogUtil.showAlertDialog(
this,
getString(R.string.remove_location_warning_title),
getString(R.string.remove_location_warning_desc),
getString(R.string.continue_message),
getString(R.string.cancel),
{ removeLocationFromImage() },
null
)
}
/**
* Removes location metadata from the image
*/
private fun removeLocationFromImage() {
media?.let {
compositeDisposable.add(
coordinateEditHelper.makeCoordinatesEdit(
applicationContext, it, "0.0", "0.0", "0.0f"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ ->
Timber.d("Coordinates removed from the image")
}
)
}
setResult(RESULT_OK, Intent())
finish()
}
/**
* Show location in map app
*/
private fun showInMapApp() {
val position = when {
//location metadata is available
activity == "UploadActivity" && cameraPosition != null -> {
fr.free.nrw.commons.location.LatLng(cameraPosition!!.latitude, cameraPosition!!.longitude, 0.0f)
}
//location metadata is not available
mapView != null -> {
fr.free.nrw.commons.location.LatLng(
mapView?.mapCenter?.latitude!!,
mapView?.mapCenter?.longitude!!,
0.0f
)
}
else -> null
}
position?.let { Utils.handleGeoCoordinates(this, it) }
}
/**
* Moves map to media's location
*/
private fun moveMapToMediaLocation() {
cameraPosition?.let {
moveMapTo(GeoPoint(it.latitude, it.longitude))
}
}
/**
* Moves map to GPS location
*/
private fun moveMapToGPSLocation() {
locationManager.getLastLocation()?.let {
moveMapTo(GeoPoint(it.latitude, it.longitude))
}
}
/**
* Adds "Place Selected" button
*/
private fun addPlaceSelectedButton() {
placeSelectedButton = findViewById(R.id.location_chosen_button)
placeSelectedButton.setOnClickListener { placeSelected() }
}
/**
* Handles "Place Selected" action
*/
private fun placeSelected() {
if (activity == "NoLocationUploadActivity") {
applicationKvStore.putString(
LAST_LOCATION,
"${mapView?.mapCenter?.latitude},${mapView?.mapCenter?.longitude}"
)
applicationKvStore.putString(LAST_ZOOM, mapView?.zoomLevel?.toString()!!)
}
if (media == null) {
val intent = Intent().apply {
putExtra(
LocationPickerConstants.MAP_CAMERA_POSITION,
CameraPosition(mapView?.mapCenter?.latitude!!, mapView?.mapCenter?.longitude!!, 14.0)
)
}
setResult(RESULT_OK, intent)
} else {
updateCoordinates(
mapView?.mapCenter?.latitude.toString(),
mapView?.mapCenter?.longitude.toString(),
"0.0f"
)
}
finish()
}
/**
* Updates image with new coordinates
*/
fun updateCoordinates(latitude: String, longitude: String, accuracy: String) {
media?.let {
try {
compositeDisposable.add(
coordinateEditHelper.makeCoordinatesEdit(
applicationContext,
it,
latitude,
longitude,
accuracy
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ ->
Timber.d("Coordinates updated")
}
)
} catch (e: Exception) {
if (e.localizedMessage == CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE) {
val username = sessionManager.userName
CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message)
, username
).let {
CommonsApplication.instance.clearApplicationData(this, it)
}
} else { }
}
}
}
/**
* Adds a button to center the map at user's location
*/
private fun addCenterOnGPSButton() {
fabCenterOnLocation = findViewById(R.id.center_on_gps)
fabCenterOnLocation.setOnClickListener {
moveToCurrentLocation = true
requestLocationPermissions()
}
}
/**
* Shows a selected location marker
*/
private fun showSelectedLocationMarker(point: GeoPoint) {
val icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker)
Marker(mapView).apply {
position = point
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
setIcon(icon)
infoWindow = null
mapView?.overlays?.add(this)
}
mapView?.invalidate()
}
/**
* Removes selected location marker
*/
private fun removeSelectedLocationMarker() {
val overlays = mapView?.overlays
overlays?.filterIsInstance<Marker>()?.firstOrNull {
it.position.latitude ==
cameraPosition?.latitude && it.position.longitude == cameraPosition?.longitude
}?.let {
overlays.remove(it)
mapView?.invalidate()
}
}
/**
* Centers map at user's location
*/
private fun requestLocationPermissions() {
locationPermissionsHelper = LocationPermissionsHelper(this, locationManager, this)
locationPermissionsHelper.requestForLocationAccess(
R.string.location_permission_title,
R.string.upload_map_location_access
)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == Constants.RequestCodes.LOCATION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
onLocationPermissionGranted()
} else {
onLocationPermissionDenied(getString(R.string.upload_map_location_access))
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onResume() {
super.onResume()
mapView?.onResume()
}
override fun onPause() {
super.onPause()
mapView?.onPause()
}
override fun onLocationPermissionDenied(toastMessage: String) {
val isDeniedBefore = store.getBoolean("isPermissionDenied", false)
val showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, permission.ACCESS_FINE_LOCATION)
if (!showRationale) {
if (!locationPermissionsHelper.checkLocationPermission(this)) {
if (isDeniedBefore) {
locationPermissionsHelper.showAppSettingsDialog(this, R.string.upload_map_location_access)
} else {
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show()
}
store.putBoolean("isPermissionDenied", true)
}
} else {
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show()
}
}
override fun onLocationPermissionGranted() {
if (moveToCurrentLocation || activity != "MediaActivity") {
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
addMarkerAtGPSLocation()
} else {
addMarkerAtGPSLocation()
locationPermissionsHelper.showLocationOffDialog(this, R.string.ask_to_turn_location_on_text)
}
}
}
/**
* Adds a marker at the user's GPS location
*/
private fun addMarkerAtGPSLocation() {
locationManager.getLastLocation()?.let {
addLocationMarker(GeoPoint(it.latitude, it.longitude))
markerImage.translationY = 0f
}
}
private fun addLocationMarker(geoPoint: GeoPoint) {
if (moveToCurrentLocation) {
mapView?.overlays?.clear()
}
val diskOverlay = ScaleDiskOverlay(
this,
geoPoint,
2000,
GeoConstants.UnitOfMeasure.foot
)
val circlePaint = Paint().apply {
color = Color.rgb(128, 128, 128)
style = Paint.Style.STROKE
strokeWidth = 2f
}
diskOverlay.setCirclePaint2(circlePaint)
val diskPaint = Paint().apply {
color = Color.argb(40, 128, 128, 128)
style = Paint.Style.FILL_AND_STROKE
}
diskOverlay.setCirclePaint1(diskPaint)
diskOverlay.setDisplaySizeMin(900)
diskOverlay.setDisplaySizeMax(1700)
mapView?.overlays?.add(diskOverlay)
val startMarker = Marker(mapView).apply {
position = geoPoint
setAnchor(
Marker.ANCHOR_CENTER,
Marker.ANCHOR_BOTTOM
)
icon = ContextCompat.getDrawable(this@LocationPickerActivity, R.drawable.current_location_marker)
title = "Your Location"
textLabelFontSize = 24
}
mapView?.overlays?.add(startMarker)
}
/**
* Saves the state of the activity
* @param outState Bundle
*/
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
cameraPosition?.let {
outState.putParcelable(CAMERA_POS, it)
}
activity?.let {
outState.putString(ACTIVITY, it)
}
media?.let {
outState.putParcelable("sMedia", it)
}
}
}

View file

@ -1,20 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
/**
* Constants need for location picking
*/
public final class LocationPickerConstants {
public static final String ACTIVITY_KEY
= "location.picker.activity";
public static final String MAP_CAMERA_POSITION
= "location.picker.cameraPosition";
public static final String MEDIA
= "location.picker.media";
private LocationPickerConstants() {
}
}

View file

@ -0,0 +1,13 @@
package fr.free.nrw.commons.LocationPicker
/**
* Constants need for location picking
*/
object LocationPickerConstants {
const val ACTIVITY_KEY = "location.picker.activity"
const val MAP_CAMERA_POSITION = "location.picker.cameraPosition"
const val MEDIA = "location.picker.media"
}

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import fr.free.nrw.commons.CameraPosition;
import org.jetbrains.annotations.NotNull;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;
/**
* Observes live camera position data
*/
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
/**
* Wrapping CameraPosition with MutableLiveData
*/
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
/**
* Constructor for this class
*
* @param application Application
*/
public LocationPickerViewModel(@NonNull final Application application) {
super(application);
}
/**
* Responses on camera position changing
*
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if (response.body() == null) {
result.setValue(null);
return;
}
result.setValue(response.body());
}
@Override
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
Timber.e(t);
}
/**
* Gets live CameraPosition
*
* @return MutableLiveData<CameraPosition>
*/
public MutableLiveData<CameraPosition> getResult() {
return result;
}
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.LocationPicker
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import fr.free.nrw.commons.CameraPosition
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
/**
* Observes live camera position data
*/
class LocationPickerViewModel(
application: Application
): AndroidViewModel(application), Callback<CameraPosition> {
/**
* Wrapping CameraPosition with MutableLiveData
*/
val result = MutableLiveData<CameraPosition?>()
/**
* Responses on camera position changing
*
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
override fun onResponse(
call: Call<CameraPosition>,
response: Response<CameraPosition>
) {
if(response.body() == null) {
result.value = null
return
}
result.value = response.body()
}
override fun onFailure(call: Call<CameraPosition>, t: Throwable) {
Timber.e(t)
}
}

View file

@ -1,141 +0,0 @@
package fr.free.nrw.commons.language;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.R;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/** Immutable look up table for all app supported languages. All article languages may not be
* present in this table as it is statically bundled with the app. */
public class AppLanguageLookUpTable {
public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans";
public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant";
public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn";
public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk";
public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo";
public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg";
public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw";
public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue";
public static final String CHINESE_LANGUAGE_CODE = "zh";
public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no";
public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb";
public static final String TEST_LANGUAGE_CODE = "test";
public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys.
@NonNull private final Resources resources;
// Language codes for all app supported languages in fixed order. The special code representing
// the dynamic system language is null.
@NonNull private SoftReference<List<String>> codesRef = new SoftReference<>(null);
// English names for all app supported languages in fixed order.
@NonNull private SoftReference<List<String>> canonicalNamesRef = new SoftReference<>(null);
// Native names for all app supported languages in fixed order.
@NonNull private SoftReference<List<String>> localizedNamesRef = new SoftReference<>(null);
public AppLanguageLookUpTable(@NonNull Context context) {
resources = context.getResources();
}
/**
* @return Nonnull immutable list. The special code representing the dynamic system language is
* null.
*/
@NonNull
public List<String> getCodes() {
List<String> codes = codesRef.get();
if (codes == null) {
codes = getStringList(R.array.preference_language_keys);
codesRef = new SoftReference<>(codes);
}
return codes;
}
@Nullable
public String getCanonicalName(@Nullable String code) {
String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null);
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
if (code.equals(Locale.CHINESE.getLanguage())) {
name = Locale.CHINESE.getDisplayName(Locale.ENGLISH);
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
}
}
return name;
}
@Nullable
public String getLocalizedName(@Nullable String code) {
String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null);
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
if (code.equals(Locale.CHINESE.getLanguage())) {
name = Locale.CHINESE.getDisplayName(Locale.CHINESE);
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
}
}
return name;
}
public List<String> getCanonicalNames() {
List<String> names = canonicalNamesRef.get();
if (names == null) {
names = getStringList(R.array.preference_language_canonical_names);
canonicalNamesRef = new SoftReference<>(names);
}
return names;
}
public List<String> getLocalizedNames() {
List<String> names = localizedNamesRef.get();
if (names == null) {
names = getStringList(R.array.preference_language_local_names);
localizedNamesRef = new SoftReference<>(names);
}
return names;
}
public boolean isSupportedCode(@Nullable String code) {
return getCodes().contains(code);
}
private <T> T defaultIndex(List<T> list, int index, T defaultValue) {
return inBounds(list, index) ? list.get(index) : defaultValue;
}
/**
* Searches #codes for the specified language code and returns the index for use in
* #canonicalNames and #localizedNames.
*
* @param code The language code to search for. The special code representing the dynamic system
* language is null.
* @return The index of the language code or -1 if the code is not supported.
*/
private int indexOfCode(@Nullable String code) {
return getCodes().indexOf(code);
}
/** @return Nonnull immutable list. */
@NonNull
private List<String> getStringList(int id) {
return Arrays.asList(getStringArray(id));
}
private boolean inBounds(List<?> list, int index) {
return index >= 0 && index < list.size();
}
public String[] getStringArray(@ArrayRes int id) {
return resources.getStringArray(id);
}
}

View file

@ -0,0 +1,135 @@
package fr.free.nrw.commons.language
import android.content.Context
import android.content.res.Resources
import android.text.TextUtils
import androidx.annotation.ArrayRes
import fr.free.nrw.commons.R
import java.lang.ref.SoftReference
import java.util.Arrays
import java.util.Locale
/** Immutable look up table for all app supported languages. All article languages may not be
* present in this table as it is statically bundled with the app. */
class AppLanguageLookUpTable(context: Context) {
companion object {
const val SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans"
const val TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant"
const val CHINESE_CN_LANGUAGE_CODE = "zh-cn"
const val CHINESE_HK_LANGUAGE_CODE = "zh-hk"
const val CHINESE_MO_LANGUAGE_CODE = "zh-mo"
const val CHINESE_SG_LANGUAGE_CODE = "zh-sg"
const val CHINESE_TW_LANGUAGE_CODE = "zh-tw"
const val CHINESE_YUE_LANGUAGE_CODE = "zh-yue"
const val CHINESE_LANGUAGE_CODE = "zh"
const val NORWEGIAN_LEGACY_LANGUAGE_CODE = "no"
const val NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb"
const val TEST_LANGUAGE_CODE = "test"
const val FALLBACK_LANGUAGE_CODE = "en" // Must exist in preference_language_keys.
}
private val resources: Resources = context.resources
// Language codes for all app supported languages in fixed order. The special code representing
// the dynamic system language is null.
private var codesRef = SoftReference<List<String>>(null)
// English names for all app supported languages in fixed order.
private var canonicalNamesRef = SoftReference<List<String>>(null)
// Native names for all app supported languages in fixed order.
private var localizedNamesRef = SoftReference<List<String>>(null)
/**
* @return Nonnull immutable list. The special code representing the dynamic system language is
* null.
*/
fun getCodes(): List<String> {
var codes = codesRef.get()
if (codes == null) {
codes = getStringList(R.array.preference_language_keys)
codesRef = SoftReference(codes)
}
return codes
}
fun getCanonicalName(code: String?): String? {
var name = defaultIndex(getCanonicalNames(), indexOfCode(code), null)
if (name.isNullOrEmpty() && !code.isNullOrEmpty()) {
name = when (code) {
Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.ENGLISH)
NORWEGIAN_LEGACY_LANGUAGE_CODE ->
defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null)
else -> null
}
}
return name
}
fun getLocalizedName(code: String?): String? {
var name = defaultIndex(getLocalizedNames(), indexOfCode(code), null)
if (name.isNullOrEmpty() && !code.isNullOrEmpty()) {
name = when (code) {
Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.CHINESE)
NORWEGIAN_LEGACY_LANGUAGE_CODE ->
defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null)
else -> null
}
}
return name
}
fun getCanonicalNames(): List<String> {
var names = canonicalNamesRef.get()
if (names == null) {
names = getStringList(R.array.preference_language_canonical_names)
canonicalNamesRef = SoftReference(names)
}
return names
}
fun getLocalizedNames(): List<String> {
var names = localizedNamesRef.get()
if (names == null) {
names = getStringList(R.array.preference_language_local_names)
localizedNamesRef = SoftReference(names)
}
return names
}
fun isSupportedCode(code: String?): Boolean {
return getCodes().contains(code)
}
private fun <T> defaultIndex(list: List<T>, index: Int, defaultValue: T?): T? {
return if (inBounds(list, index)) list[index] else defaultValue
}
/**
* Searches #codes for the specified language code and returns the index for use in
* #canonicalNames and #localizedNames.
*
* @param code The language code to search for. The special code representing the dynamic system
* language is null.
* @return The index of the language code or -1 if the code is not supported.
*/
private fun indexOfCode(code: String?): Int {
return getCodes().indexOf(code)
}
/** @return Nonnull immutable list. */
private fun getStringList(id: Int): List<String> {
return getStringArray(id).toList()
}
private fun inBounds(list: List<*>, index: Int): Boolean {
return index in list.indices
}
fun getStringArray(@ArrayRes id: Int): Array<String> {
return resources.getStringArray(id)
}
}

View file

@ -1,198 +0,0 @@
package fr.free.nrw.commons.location;
import android.location.Location;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
/**
* a latitude and longitude point with accuracy information, often of a picture
*/
public class LatLng implements Parcelable {
private final double latitude;
private final double longitude;
private final float accuracy;
/**
* Accepts latitude and longitude.
* North and South values are cut off at 90°
*
* @param latitude the latitude
* @param longitude the longitude
* @param accuracy the accuracy
*
* Examples:
* the Statue of Liberty is located at 40.69° N, 74.04° W
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
* where positive signifies north, east and negative signifies south, west.
*/
public LatLng(double latitude, double longitude, float accuracy) {
if (-180.0D <= longitude && longitude < 180.0D) {
this.longitude = longitude;
} else {
this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
}
this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude));
this.accuracy = accuracy;
}
/**
* An alternate constructor for this class.
* @param in A parcelable which contains the latitude, longitude, and accuracy
*/
public LatLng(Parcel in) {
latitude = in.readDouble();
longitude = in.readDouble();
accuracy = in.readFloat();
}
/**
* gets the latitude and longitude of a given non-null location
* @param location the non-null location of the user
* @return LatLng the Latitude and Longitude of a given location
*/
public static LatLng from(@NonNull Location location) {
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
}
/**
* creates a hash code for the longitude and longitude
*/
public int hashCode() {
byte var1 = 1;
long var2 = Double.doubleToLongBits(this.latitude);
int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32);
var2 = Double.doubleToLongBits(this.longitude);
var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32);
return var3;
}
/**
* checks for equality of two LatLng objects
* @param o the second LatLng object
*/
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof LatLng)) {
return false;
} else {
LatLng var2 = (LatLng)o;
return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude);
}
}
/**
* returns a string representation of the latitude and longitude
*/
public String toString() {
return "lat/lng: (" + this.latitude + "," + this.longitude + ")";
}
/**
* Rounds the float to 4 digits and returns absolute value.
*
* @param coordinate A coordinate value as string.
* @return String of the rounded number.
*/
private String formatCoordinate(double coordinate) {
double roundedNumber = Math.round(coordinate * 10000d) / 10000d;
double absoluteNumber = Math.abs(roundedNumber);
return String.valueOf(absoluteNumber);
}
/**
* Returns "N" or "S" depending on the latitude.
*
* @return "N" or "S".
*/
private String getNorthSouth() {
if (this.latitude < 0) {
return "S";
}
return "N";
}
/**
* Returns "E" or "W" depending on the longitude.
*
* @return "E" or "W".
*/
private String getEastWest() {
if (this.longitude >= 0 && this.longitude < 180) {
return "E";
}
return "W";
}
/**
* Returns a nicely formatted coordinate string. Used e.g. in
* the detail view.
*
* @return The formatted string.
*/
public String getPrettyCoordinateString() {
return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", "
+ formatCoordinate(this.longitude) + " " + this.getEastWest();
}
/**
* Return the location accuracy in meter.
*
* @return float
*/
public float getAccuracy() {
return accuracy;
}
/**
* Return the longitude in degrees.
*
* @return double
*/
public double getLongitude() {
return longitude;
}
/**
* Return the latitude in degrees.
*
* @return double
*/
public double getLatitude() {
return latitude;
}
public Uri getGmmIntentUri() {
return Uri.parse("geo:" + latitude + "," + longitude + "?z=16");
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeDouble(latitude);
dest.writeDouble(longitude);
dest.writeFloat(accuracy);
}
public static final Creator<LatLng> CREATOR = new Creator<LatLng>() {
@Override
public LatLng createFromParcel(Parcel in) {
return new LatLng(in);
}
@Override
public LatLng[] newArray(int size) {
return new LatLng[size];
}
};
}

View file

@ -0,0 +1,150 @@
package fr.free.nrw.commons.location
import android.location.Location
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.round
/**
* A latitude and longitude point with accuracy information, often of a picture.
*/
data class LatLng(
var latitude: Double,
var longitude: Double,
val accuracy: Float
) : Parcelable {
/**
* Accepts latitude and longitude.
* North and South values are cut off at 90°
*
* Examples:
* the Statue of Liberty is located at 40.69° N, 74.04° W
* The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0)
* where positive signifies north, east and negative signifies south, west.
*/
init {
val adjustedLongitude = when {
longitude in -180.0..180.0 -> longitude
else -> ((longitude - 180.0) % 360.0 + 360.0) % 360.0 - 180.0
}
latitude = max(-90.0, min(90.0, latitude))
longitude = adjustedLongitude
}
/**
* Accepts a non-null [Location] and converts it to a [LatLng].
*/
companion object {
/**
* gets the latitude and longitude of a given non-null location
* @param location the non-null location of the user
* @return LatLng the Latitude and Longitude of a given location
*/
@JvmStatic
fun from(location: Location): LatLng {
return LatLng(location.latitude, location.longitude, location.accuracy)
}
@JvmField
val CREATOR: Parcelable.Creator<LatLng> = object : Parcelable.Creator<LatLng> {
override fun createFromParcel(parcel: Parcel): LatLng {
return LatLng(parcel)
}
override fun newArray(size: Int): Array<LatLng?> {
return arrayOfNulls(size)
}
}
}
/**
* An alternate constructor for this class.
* @param parcel A parcelable which contains the latitude, longitude, and accuracy
*/
private constructor(parcel: Parcel) : this(
latitude = parcel.readDouble(),
longitude = parcel.readDouble(),
accuracy = parcel.readFloat()
)
/**
* Creates a hash code for the latitude and longitude.
*/
override fun hashCode(): Int {
var result = 1
val latitudeBits = latitude.toBits()
result = 31 * result + (latitudeBits xor (latitudeBits ushr 32)).toInt()
val longitudeBits = longitude.toBits()
result = 31 * result + (longitudeBits xor (longitudeBits ushr 32)).toInt()
return result
}
/**
* Checks for equality of two LatLng objects.
* @param other the second LatLng object
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LatLng) return false
return latitude.toBits() == other.latitude.toBits() &&
longitude.toBits() == other.longitude.toBits()
}
/**
* Returns a string representation of the latitude and longitude.
*/
override fun toString(): String {
return "lat/lng: ($latitude,$longitude)"
}
/**
* Returns a nicely formatted coordinate string. Used e.g. in
* the detail view.
*
* @return The formatted string.
*/
fun getPrettyCoordinateString(): String {
return "${formatCoordinate(latitude)} ${getNorthSouth()}, " +
"${formatCoordinate(longitude)} ${getEastWest()}"
}
/**
* Gets a URI for a Google Maps intent at the location.
*/
fun getGmmIntentUri(): Uri {
return Uri.parse("geo:$latitude,$longitude?z=16")
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeDouble(latitude)
parcel.writeDouble(longitude)
parcel.writeFloat(accuracy)
}
override fun describeContents(): Int = 0
private fun formatCoordinate(coordinate: Double): String {
val roundedNumber = round(coordinate * 10000) / 10000
return abs(roundedNumber).toString()
}
/**
* Returns "N" or "S" depending on the latitude.
*
* @return "N" or "S".
*/
private fun getNorthSouth(): String = if (latitude < 0) "S" else "N"
/**
* Returns "E" or "W" depending on the longitude.
*
* @return "E" or "W".
*/
private fun getEastWest(): String = if (longitude in 0.0..179.999) "E" else "W"
}

View file

@ -1,186 +0,0 @@
package fr.free.nrw.commons.location;
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.provider.Settings;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.Constants.RequestCodes;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.PermissionUtils;
/**
* Helper class to handle location permissions.
*
* Location flow for fragments containing a map is as follows:
* Case 1: When location permission has never been asked for or denied before
* Check if permission is already granted or not.
* If not already granted, ask for it (if it isn't denied twice before).
* If now user grants permission, go to Case 3/4, else go to Case 2.
*
* Case 2: When location permission is just asked but has been denied
* Shows a toast to tell the user why location permission is needed.
* Also shows a rationale to the user, on agreeing to which, we go back to Case 1.
* Show current location / nearby pins / nearby images according to the default location.
*
* Case 3: When location permission are already granted, but location services are off
* Asks the user to turn on the location service, using a dialog.
* If the user rejects, checks for the last known location and shows stuff using that location.
* Also displays a toast telling the user why location should be turned on.
*
* Case 4: When location permission has been granted and location services are also on
* Do whatever is required by that particular activity / fragment using current location.
*
*/
public class LocationPermissionsHelper {
Activity activity;
LocationServiceManager locationManager;
LocationPermissionCallback callback;
public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager,
LocationPermissionCallback callback) {
this.activity = activity;
this.locationManager = locationManager;
this.callback = callback;
}
/**
* Ask for location permission if the user agrees on attaching location with pictures and the
* app does not have the access to location
*
* @param dialogTitleResource Resource id of the title of the dialog
* @param dialogTextResource Resource id of the text of the dialog
*/
public void requestForLocationAccess(
int dialogTitleResource,
int dialogTextResource
) {
if (checkLocationPermission(activity)) {
callback.onLocationPermissionGranted();
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
permission.ACCESS_FINE_LOCATION)) {
DialogUtil.showAlertDialog(activity, activity.getString(dialogTitleResource),
activity.getString(dialogTextResource),
activity.getString(android.R.string.ok),
activity.getString(android.R.string.cancel),
() -> {
ActivityCompat.requestPermissions(activity,
new String[]{permission.ACCESS_FINE_LOCATION}, 1);
},
() -> callback.onLocationPermissionDenied(
activity.getString(R.string.upload_map_location_access)),
null,
false);
} else {
ActivityCompat.requestPermissions(activity,
new String[]{permission.ACCESS_FINE_LOCATION},
RequestCodes.LOCATION);
}
}
}
/**
* Shows a dialog for user to open the settings page and turn on location services
*
* @param activity Activity object
* @param dialogTextResource int id of the required string resource
*/
public void showLocationOffDialog(Activity activity, int dialogTextResource) {
DialogUtil
.showAlertDialog(activity,
activity.getString(R.string.ask_to_turn_location_on),
activity.getString(dialogTextResource),
activity.getString(R.string.title_app_shortcut_setting),
activity.getString(R.string.cancel),
() -> openLocationSettings(activity),
() -> Toast.makeText(activity, activity.getString(dialogTextResource),
Toast.LENGTH_LONG).show()
);
}
/**
* Opens the location access page in settings, for user to turn on location services
*
* @param activity Activtiy object
*/
public void openLocationSettings(Activity activity) {
final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
final PackageManager packageManager = activity.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
activity.startActivity(intent);
} else {
Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG)
.show();
}
}
/**
* Shows a dialog for user to open the app's settings page and give location permission
*
* @param activity Activity object
* @param dialogTextResource int id of the required string resource
*/
public void showAppSettingsDialog(Activity activity, int dialogTextResource) {
DialogUtil
.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
activity.getString(dialogTextResource),
activity.getString(R.string.title_app_shortcut_setting),
activity.getString(R.string.cancel),
() -> openAppSettings(activity),
() -> Toast.makeText(activity, activity.getString(dialogTextResource),
Toast.LENGTH_LONG).show()
);
}
/**
* Opens detailed settings page of the app for the user to turn on location services
*
* @param activity Activity object
*/
public void openAppSettings(Activity activity) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
}
/**
* Check if apps have access to location even after having individual access
*
* @return Returns true if location services are on and false otherwise
*/
public boolean isLocationAccessToAppsTurnedOn() {
return (locationManager.isNetworkProviderEnabled()
|| locationManager.isGPSProviderEnabled());
}
/**
* Checks if location permission is already granted or not
*
* @param activity Activity object
* @return Returns true if location permission is granted and false otherwise
*/
public boolean checkLocationPermission(Activity activity) {
return PermissionUtils.hasPermission(activity,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
}
/**
* Handle onPermissionDenied within individual classes based on the requirements
*/
public interface LocationPermissionCallback {
void onLocationPermissionDenied(String toastMessage);
void onLocationPermissionGranted();
}
}

View file

@ -0,0 +1,200 @@
package fr.free.nrw.commons.location
import android.Manifest.permission
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.widget.Toast
import androidx.core.app.ActivityCompat
import fr.free.nrw.commons.R
import fr.free.nrw.commons.filepicker.Constants.RequestCodes
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.PermissionUtils
/**
* Helper class to handle location permissions.
*
* Location flow for fragments containing a map is as follows:
* Case 1: When location permission has never been asked for or denied before
* Check if permission is already granted or not.
* If not already granted, ask for it (if it isn't denied twice before).
* If now user grants permission, go to Case 3/4, else go to Case 2.
*
* Case 2: When location permission is just asked but has been denied
* Shows a toast to tell the user why location permission is needed.
* Also shows a rationale to the user, on agreeing to which, we go back to Case 1.
* Show current location / nearby pins / nearby images according to the default location.
*
* Case 3: When location permission are already granted, but location services are off
* Asks the user to turn on the location service, using a dialog.
* If the user rejects, checks for the last known location and shows stuff using that location.
* Also displays a toast telling the user why location should be turned on.
*
* Case 4: When location permission has been granted and location services are also on
* Do whatever is required by that particular activity / fragment using current location.
*
*/
class LocationPermissionsHelper(
private val activity: Activity,
private val locationManager: LocationServiceManager,
private val callback: LocationPermissionCallback?
) {
/**
* Ask for location permission if the user agrees on attaching location with pictures and the
* app does not have the access to location
*
* @param dialogTitleResource Resource id of the title of the dialog
* @param dialogTextResource Resource id of the text of the dialog
*/
fun requestForLocationAccess(
dialogTitleResource: Int,
dialogTextResource: Int
) {
if (checkLocationPermission(activity)) {
callback?.onLocationPermissionGranted()
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permission.ACCESS_FINE_LOCATION
)
) {
DialogUtil.showAlertDialog(
activity,
activity.getString(dialogTitleResource),
activity.getString(dialogTextResource),
activity.getString(android.R.string.ok),
activity.getString(android.R.string.cancel),
{
ActivityCompat.requestPermissions(
activity,
arrayOf(permission.ACCESS_FINE_LOCATION),
1
)
},
{
callback?.onLocationPermissionDenied(
activity.getString(R.string.upload_map_location_access)
)
},
null,
false
)
} else {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission.ACCESS_FINE_LOCATION),
RequestCodes.LOCATION
)
}
}
}
/**
* Shows a dialog for user to open the settings page and turn on location services
*
* @param activity Activity object
* @param dialogTextResource int id of the required string resource
*/
fun showLocationOffDialog(activity: Activity, dialogTextResource: Int) {
DialogUtil.showAlertDialog(
activity,
activity.getString(R.string.ask_to_turn_location_on),
activity.getString(dialogTextResource),
activity.getString(R.string.title_app_shortcut_setting),
activity.getString(R.string.cancel),
{ openLocationSettings(activity) },
{
Toast.makeText(
activity,
activity.getString(dialogTextResource),
Toast.LENGTH_LONG
).show()
}
)
}
/**
* Opens the location access page in settings, for user to turn on location services
*
* @param activity Activity object
*/
fun openLocationSettings(activity: Activity) {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
val packageManager = activity.packageManager
if (intent.resolveActivity(packageManager) != null) {
activity.startActivity(intent)
} else {
Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG)
.show()
}
}
/**
* Shows a dialog for user to open the app's settings page and give location permission
*
* @param activity Activity object
* @param dialogTextResource int id of the required string resource
*/
fun showAppSettingsDialog(activity: Activity, dialogTextResource: Int) {
DialogUtil.showAlertDialog(
activity,
activity.getString(R.string.location_permission_title),
activity.getString(dialogTextResource),
activity.getString(R.string.title_app_shortcut_setting),
activity.getString(R.string.cancel),
{ openAppSettings(activity) },
{
Toast.makeText(
activity,
activity.getString(dialogTextResource),
Toast.LENGTH_LONG
).show()
}
)
}
/**
* Opens detailed settings page of the app for the user to turn on location services
*
* @param activity Activity object
*/
private fun openAppSettings(activity: Activity) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
activity.startActivity(intent)
}
/**
* Check if apps have access to location even after having individual access
*
* @return Returns true if location services are on and false otherwise
*/
fun isLocationAccessToAppsTurnedOn(): Boolean {
return locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled()
}
/**
* Checks if location permission is already granted or not
*
* @param activity Activity object
* @return Returns true if location permission is granted and false otherwise
*/
fun checkLocationPermission(activity: Activity): Boolean {
return PermissionUtils.hasPermission(
activity,
arrayOf(permission.ACCESS_FINE_LOCATION)
)
}
/**
* Handle onPermissionDenied within individual classes based on the requirements
*/
interface LocationPermissionCallback {
fun onLocationPermissionDenied(toastMessage: String)
fun onLocationPermissionGranted()
}
}

View file

@ -1,274 +0,0 @@
package fr.free.nrw.commons.location;
import android.Manifest.permission;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import androidx.core.app.ActivityCompat;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import timber.log.Timber;
public class LocationServiceManager implements LocationListener {
// Maybe these values can be improved for efficiency
private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1;
private LocationManager locationManager;
private Location lastLocation;
//private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
private Context context;
/**
* Constructs a new instance of LocationServiceManager.
*
* @param context the context
*/
public LocationServiceManager(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
public LatLng getLastLocation() {
if (lastLocation == null) {
lastLocation = getLastKnownLocation();
if(lastLocation != null) {
return LatLng.from(lastLocation);
}
else {
return null;
}
}
return LatLng.from(lastLocation);
}
private Location getLastKnownLocation() {
List<String> providers = locationManager.getProviders(true);
Location bestLocation = null;
for (String provider : providers) {
Location l=null;
if (ActivityCompat.checkSelfPermission(context, permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(context, permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
l = locationManager.getLastKnownLocation(provider);
}
if (l == null) {
continue;
}
if (bestLocation == null
|| l.getAccuracy() < bestLocation.getAccuracy()) {
bestLocation = l;
}
}
if (bestLocation == null) {
return null;
}
return bestLocation;
}
/**
* Registers a LocationManager to listen for current location.
*/
public void registerLocationManager() {
if (!isLocationManagerRegistered) {
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
}
/**
* Requests location updates from the specified provider.
*
* @param locationProvider the location provider
* @return true if successful
*/
public boolean requestLocationUpdatesFromProvider(String locationProvider) {
try {
// If both providers are not available
if (locationManager == null || !(locationManager.getAllProviders().contains(locationProvider))) {
return false;
}
locationManager.requestLocationUpdates(locationProvider,
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
this);
return true;
} catch (IllegalArgumentException e) {
Timber.e(e, "Illegal argument exception");
return false;
} catch (SecurityException e) {
Timber.e(e, "Security exception");
return false;
}
}
/**
* Returns whether a given location is better than the current best location.
*
* @param location the location to be tested
* @param currentBestLocation the current best location
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
*/
private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
}
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isNewer = timeDelta > 0;
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
float[] results = new float[5];
Location.distanceBetween(
currentBestLocation.getLatitude(),
currentBestLocation.getLongitude(),
location.getLatitude(),
location.getLongitude(),
results);
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer
|| isMoreAccurate
|| (isNewer && !isLessAccurate)
|| (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) {
if (results[0] < 1000) { // Means change is smaller than 1000 meter
return LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
} else {
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
}
} else{
return LocationChangeType.LOCATION_NOT_CHANGED;
}
}
/**
* Checks whether two providers are the same
*/
private boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
/**
* Unregisters location manager.
*/
public void unregisterLocationManager() {
isLocationManagerRegistered = false;
locationExplanationDisplayed.clear();
try {
locationManager.removeUpdates(this);
} catch (SecurityException e) {
Timber.e(e, "Security exception");
}
}
/**
* Adds a new listener to the list of location listeners.
*
* @param listener the new listener
*/
public void addLocationListener(LocationUpdateListener listener) {
if (!locationListeners.contains(listener)) {
locationListeners.add(listener);
}
}
/**
* Removes a listener from the list of location listeners.
*
* @param listener the listener to be removed
*/
public void removeLocationListener(LocationUpdateListener listener) {
locationListeners.remove(listener);
}
@Override
public void onLocationChanged(Location location) {
Timber.d("on location changed");
if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSignificantly(LatLng.from(lastLocation));
}
} else if (location.distanceTo(lastLocation) >= 500) {
// Update nearby notification card at every 500 meters.
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedMedium(LatLng.from(lastLocation));
}
}
else if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSlightly(LatLng.from(lastLocation));
}
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Timber.d("%s's status changed to %d", provider, status);
}
@Override
public void onProviderEnabled(String provider) {
Timber.d("Provider %s enabled", provider);
}
@Override
public void onProviderDisabled(String provider) {
Timber.d("Provider %s disabled", provider);
}
public boolean isNetworkProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
public boolean isGPSProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates.
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED,
MAP_UPDATED,
SEARCH_CUSTOM_AREA,
CUSTOM_QUERY
}
}

View file

@ -0,0 +1,255 @@
package fr.free.nrw.commons.location
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.core.app.ActivityCompat
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
class LocationServiceManager(private val context: Context) : LocationListener {
companion object {
// Maybe these values can be improved for efficiency
private const val MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100L
private const val MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1f
}
private val locationManager: LocationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var lastLocationVar: Location? = null
private val locationListeners = CopyOnWriteArrayList<LocationUpdateListener>()
private var isLocationManagerRegistered = false
private val locationExplanationDisplayed = mutableSetOf<Activity>()
/**
* Constructs a new instance of LocationServiceManager.
*
*/
fun getLastLocation(): LatLng? {
if (lastLocationVar == null) {
lastLocationVar = getLastKnownLocation()
return lastLocationVar?.let { LatLng.from(it) }
}
return LatLng.from(lastLocationVar!!)
}
private fun getLastKnownLocation(): Location? {
val providers = locationManager.getProviders(true)
var bestLocation: Location? = null
for (provider in providers) {
val location: Location? = if (
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION)
==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION)
==
PackageManager.PERMISSION_GRANTED
) {
locationManager.getLastKnownLocation(provider)
} else {
null
}
if (
location != null
&&
(bestLocation == null || location.accuracy < bestLocation.accuracy)
) {
bestLocation = location
}
}
return bestLocation
}
/**
* Registers a LocationManager to listen for current location.
*/
fun registerLocationManager() {
if (!isLocationManagerRegistered) {
isLocationManagerRegistered =
requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) &&
requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
}
}
/**
* Requests location updates from the specified provider.
*
* @param locationProvider the location provider
* @return true if successful
*/
fun requestLocationUpdatesFromProvider(locationProvider: String): Boolean {
return try {
if (locationManager.allProviders.contains(locationProvider)) {
locationManager.requestLocationUpdates(
locationProvider,
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
this
)
true
} else {
false
}
} catch (e: IllegalArgumentException) {
Timber.e(e, "Illegal argument exception")
false
} catch (e: SecurityException) {
Timber.e(e, "Security exception")
false
}
}
/**
* Returns whether a given location is better than the current best location.
*
* @param location the location to be tested
* @param currentBestLocation the current best location
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
*/
private fun isBetterLocation(location: Location, currentBestLocation: Location?): LocationChangeType {
if (currentBestLocation == null) {
return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
}
val timeDelta = location.time - currentBestLocation.time
val isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS
val isNewer = timeDelta > 0
val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt()
val isMoreAccurate = accuracyDelta < 0
val isSignificantlyLessAccurate = accuracyDelta > 200
val isFromSameProvider = isSameProvider(location.provider, currentBestLocation.provider)
val results = FloatArray(5)
Location.distanceBetween(
currentBestLocation.latitude, currentBestLocation.longitude,
location.latitude, location.longitude,
results
)
return when {
isSignificantlyNewer
||
isMoreAccurate
||
(isNewer && !isSignificantlyLessAccurate && isFromSameProvider) -> {
if (results[0] < 1000) LocationChangeType.LOCATION_SLIGHTLY_CHANGED
else LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
}
else -> LocationChangeType.LOCATION_NOT_CHANGED
}
}
/**
* Checks whether two providers are the same
*/
private fun isSameProvider(provider1: String?, provider2: String?): Boolean {
return provider1 == provider2
}
/**
* Unregisters location manager.
*/
fun unregisterLocationManager() {
isLocationManagerRegistered = false
locationExplanationDisplayed.clear()
try {
locationManager.removeUpdates(this)
} catch (e: SecurityException) {
Timber.e(e, "Security exception")
}
}
/**
* Adds a new listener to the list of location listeners.
*
* @param listener the new listener
*/
fun addLocationListener(listener: LocationUpdateListener) {
if (!locationListeners.contains(listener)) {
locationListeners.add(listener)
}
}
/**
* Removes a listener from the list of location listeners.
*
* @param listener the listener to be removed
*/
fun removeLocationListener(listener: LocationUpdateListener) {
locationListeners.remove(listener)
}
override fun onLocationChanged(location: Location) {
Timber.d("on location changed")
val changeType = isBetterLocation(location, lastLocationVar)
if (changeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) {
lastLocationVar = location
locationListeners.forEach { it.onLocationChangedSignificantly(LatLng.from(location)) }
} else if (lastLocationVar?.let { location.distanceTo(it) }!! >= 500) {
locationListeners.forEach { it.onLocationChangedMedium(LatLng.from(location)) }
} else if (changeType == LocationChangeType.LOCATION_SLIGHTLY_CHANGED) {
lastLocationVar = location
locationListeners.forEach { it.onLocationChangedSlightly(LatLng.from(location)) }
}
}
@Deprecated("Deprecated in Java", ReplaceWith(
"Timber.d(\"%s's status changed to %d\", provider, status)",
"timber.log.Timber"
)
)
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
Timber.d("%s's status changed to %d", provider, status)
}
override fun onProviderEnabled(provider: String) {
Timber.d("Provider %s enabled", provider)
}
override fun onProviderDisabled(provider: String) {
Timber.d("Provider %s disabled", provider)
}
fun isNetworkProviderEnabled(): Boolean {
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
fun isGPSProviderEnabled(): Boolean {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
}
enum class LocationChangeType {
LOCATION_SIGNIFICANTLY_CHANGED,
LOCATION_SLIGHTLY_CHANGED,
LOCATION_MEDIUM_CHANGED,
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED,
MAP_UPDATED,
SEARCH_CUSTOM_AREA,
CUSTOM_QUERY
}
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons.location;
public interface LocationUpdateListener {
void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map
void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion
void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.location
interface LocationUpdateListener {
// Will be used to update all nearby markers on the map
fun onLocationChangedSignificantly(latLng: LatLng)
// Will be used to track users motion
fun onLocationChangedSlightly(latLng: LatLng)
// Will be used updating nearby card view notification
fun onLocationChangedMedium(latLng: LatLng)
}

View file

@ -1,125 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.PageKeyedDataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Objects;
import timber.log.Timber;
/**
* This class will call the leaderboard API to get new list when the pagination is performed
*/
public class DataSourceClass extends PageKeyedDataSource<Integer, LeaderboardList> {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
private MutableLiveData<String> progressLiveStatus;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private String duration;
private String category;
private int limit;
private int offset;
/**
* Initialise the Data Source Class with API params
* @param okHttpJsonApiClient
* @param sessionManager
* @param duration
* @param category
* @param limit
* @param offset
*/
public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager,
String duration, String category, int limit, int offset) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
this.duration = duration;
this.category = category;
this.limit = limit;
this.offset = offset;
progressLiveStatus = new MutableLiveData<>();
}
/**
* @return the status of the list
*/
public MutableLiveData<String> getProgressLiveStatus() {
return progressLiveStatus;
}
/**
* Loads the initial set of data from API
* @param params
* @param callback
*/
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
@NonNull LoadInitialCallback<Integer, LeaderboardList> callback) {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
duration, category, String.valueOf(limit), String.valueOf(offset))
.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), null, response.getLimit());
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));
}
/**
* Loads any data before the inital page is loaded
* @param params
* @param callback
*/
@Override
public void loadBefore(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
}
/**
* Loads the next set of data on scrolling with offset as the limit of the last set of data
* @param params
* @param callback
*/
@Override
public void loadAfter(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
duration, category, String.valueOf(limit), String.valueOf(params.key))
.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), params.key + limit);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));
}
}

View file

@ -0,0 +1,79 @@
package fr.free.nrw.commons.profile.leaderboard
import android.accounts.Account
import androidx.lifecycle.MutableLiveData
import androidx.paging.PageKeyedDataSource
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
import java.util.Objects
/**
* This class will call the leaderboard API to get new list when the pagination is performed
*/
class DataSourceClass(
private val okHttpJsonApiClient: OkHttpJsonApiClient,
private val sessionManager: SessionManager,
private val duration: String?,
private val category: String?,
private val limit: Int,
private val offset: Int
) : PageKeyedDataSource<Int, LeaderboardList>() {
val progressLiveStatus: MutableLiveData<LeaderboardConstants.LoadingStatus> = MutableLiveData()
private val compositeDisposable = CompositeDisposable()
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, LeaderboardList?>
) {
compositeDisposable.add(okHttpJsonApiClient.getLeaderboard(
sessionManager.currentAccount?.name,
duration,
category,
limit.toString(),
offset.toString()
).doOnSubscribe { disposable: Disposable? ->
compositeDisposable.add(disposable!!)
progressLiveStatus.postValue(LOADING)
}.subscribe({ response: LeaderboardResponse? ->
if (response != null && response.status == 200) {
progressLiveStatus.postValue(LOADED)
callback.onResult(response.leaderboardList!!, null, response.limit)
}
}, { t: Throwable? ->
Timber.e(t, "Fetching leaderboard statistics failed")
progressLiveStatus.postValue(LOADING)
}))
}
override fun loadBefore(
params: LoadParams<Int>, callback: LoadCallback<Int, LeaderboardList?>
) = Unit
override fun loadAfter(
params: LoadParams<Int>, callback: LoadCallback<Int, LeaderboardList?>
) {
compositeDisposable.add(okHttpJsonApiClient.getLeaderboard(
Objects.requireNonNull<Account?>(sessionManager.currentAccount).name,
duration,
category,
limit.toString(),
params.key.toString()
).doOnSubscribe { disposable: Disposable? ->
compositeDisposable.add(disposable!!)
progressLiveStatus.postValue(LOADING)
}.subscribe({ response: LeaderboardResponse? ->
if (response != null && response.status == 200) {
progressLiveStatus.postValue(LOADED)
callback.onResult(response.leaderboardList!!, params.key + limit)
}
}, { t: Throwable? ->
Timber.e(t, "Fetching leaderboard statistics failed")
progressLiveStatus.postValue(LOADING)
}))
}
}

View file

@ -1,110 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.DataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
/**
* This class will create a new instance of the data source class on pagination
*/
public class DataSourceFactory extends DataSource.Factory<Integer, LeaderboardList> {
private MutableLiveData<DataSourceClass> liveData;
private OkHttpJsonApiClient okHttpJsonApiClient;
private CompositeDisposable compositeDisposable;
private SessionManager sessionManager;
private String duration;
private String category;
private int limit;
private int offset;
/**
* Gets the current set leaderboard list duration
*/
public String getDuration() {
return duration;
}
/**
* Sets the current set leaderboard duration with the new duration
*/
public void setDuration(final String duration) {
this.duration = duration;
}
/**
* Gets the current set leaderboard list category
*/
public String getCategory() {
return category;
}
/**
* Sets the current set leaderboard category with the new category
*/
public void setCategory(final String category) {
this.category = category;
}
/**
* Gets the current set leaderboard list limit
*/
public int getLimit() {
return limit;
}
/**
* Sets the current set leaderboard limit with the new limit
*/
public void setLimit(final int limit) {
this.limit = limit;
}
/**
* Gets the current set leaderboard list offset
*/
public int getOffset() {
return offset;
}
/**
* Sets the current set leaderboard offset with the new offset
*/
public void setOffset(final int offset) {
this.offset = offset;
}
/**
* Constructor for DataSourceFactory class
* @param okHttpJsonApiClient client for OKhttp
* @param compositeDisposable composite disposable
* @param sessionManager sessionManager
*/
public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable,
SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.compositeDisposable = compositeDisposable;
this.sessionManager = sessionManager;
liveData = new MutableLiveData<>();
}
/**
* @return the live data
*/
public MutableLiveData<DataSourceClass> getMutableLiveData() {
return liveData;
}
/**
* Creates the new instance of data source class
* @return
*/
@Override
public DataSource<Integer, LeaderboardList> create() {
DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset);
liveData.postValue(dataSourceClass);
return dataSourceClass;
}
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.profile.leaderboard
import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
/**
* This class will create a new instance of the data source class on pagination
*/
class DataSourceFactory(
private val okHttpJsonApiClient: OkHttpJsonApiClient,
private val sessionManager: SessionManager
) : DataSource.Factory<Int, LeaderboardList>() {
val mutableLiveData: MutableLiveData<DataSourceClass> = MutableLiveData()
var duration: String? = null
var category: String? = null
var limit: Int = 0
var offset: Int = 0
/**
* Creates the new instance of data source class
*/
override fun create(): DataSource<Int, LeaderboardList> = DataSourceClass(
okHttpJsonApiClient, sessionManager, duration, category, limit, offset
).also { mutableLiveData.postValue(it) }
}

View file

@ -1,45 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
/**
* This class contains the constant variables for leaderboard
*/
public class LeaderboardConstants {
/**
* This is the size of the page i.e. number items to load in a batch when pagination is performed
*/
public static final int PAGE_SIZE = 100;
/**
* This is the starting offset, we set it to 0 to start loading from rank 1
*/
public static final int START_OFFSET = 0;
/**
* This is the prefix of the user's homepage url, appending the username will give us complete url
*/
public static final String USER_LINK_PREFIX = "https://commons.wikimedia.org/wiki/User:";
/**
* This is the a constant string for the state loading, when the pages are getting loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
public final static String LOADING = "Loading";
/**
* This is the a constant string for the state loaded, when the pages are loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
public final static String LOADED = "Loaded";
/**
* This API endpoint is to update the leaderboard avatar
*/
public final static String UPDATE_AVATAR_END_POINT = "/update_avatar.py";
/**
* This API endpoint is to get leaderboard data
*/
public final static String LEADERBOARD_END_POINT = "/leaderboard.py";
}

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.profile.leaderboard
/**
* This class contains the constant variables for leaderboard
*/
object LeaderboardConstants {
/**
* This is the size of the page i.e. number items to load in a batch when pagination is performed
*/
const val PAGE_SIZE: Int = 100
/**
* This is the starting offset, we set it to 0 to start loading from rank 1
*/
const val START_OFFSET: Int = 0
/**
* This is the prefix of the user's homepage url, appending the username will give us complete url
*/
const val USER_LINK_PREFIX: String = "https://commons.wikimedia.org/wiki/User:"
sealed class LoadingStatus {
/**
* This is the state loading, when the pages are getting loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
data object LOADING: LoadingStatus()
/**
* This is the state loaded, when the pages are loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
data object LOADED: LoadingStatus()
}
/**
* This API endpoint is to update the leaderboard avatar
*/
const val UPDATE_AVATAR_END_POINT: String = "/update_avatar.py"
/**
* This API endpoint is to get leaderboard data
*/
const val LEADERBOARD_END_POINT: String = "/leaderboard.py"
}

View file

@ -1,363 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET;
import android.accounts.Account;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.MergeAdapter;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Objects;
import javax.inject.Inject;
import timber.log.Timber;
/**
* This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment
*/
public class LeaderboardFragment extends CommonsDaggerSupportFragment {
@Inject
SessionManager sessionManager;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
ViewModelFactory viewModelFactory;
/**
* View model for the paged leaderboard list
*/
private LeaderboardListViewModel viewModel;
/**
* Composite disposable for API call
*/
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* Duration of the leaderboard API
*/
private String duration;
/**
* Category of the Leaderboard API
*/
private String category;
/**
* Page size of the leaderboard API
*/
private int limit = PAGE_SIZE;
/**
* offset for the leaderboard API
*/
private int offset = START_OFFSET;
/**
* Set initial User Rank to 0
*/
private int userRank;
/**
* This variable represents if user wants to scroll to his rank or not
*/
private boolean scrollToRank;
private String userName;
private FragmentLeaderboardBinding binding;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
userName = getArguments().getString(ProfileActivity.KEY_USERNAME);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentLeaderboardBinding.inflate(inflater, container, false);
hideLayouts();
// Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu
if(ConfigUtils.isBetaFlavour()) {
binding.progressBar.setVisibility(View.GONE);
binding.scroll.setVisibility(View.GONE);
return binding.getRoot();
}
binding.progressBar.setVisibility(View.VISIBLE);
setSpinners();
/**
* This array is for the duration filter, we have three filters weekly, yearly and all-time
* each filter have a key and value pair, the value represents the param of the API
*/
String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values);
/**
* This array is for the category filter, we have three filters upload, used and nearby
* each filter have a key and value pair, the value represents the param of the API
*/
String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values);
duration = durationValues[0];
category = categoryValues[0];
setLeaderboard(duration, category, limit, offset);
binding.durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
duration = durationValues[binding.durationSpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
binding.categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
category = categoryValues[binding.categorySpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
binding.scroll.setOnClickListener(view -> scrollToUserRank());
return binding.getRoot();
}
@Override
public void setMenuVisibility(boolean visible) {
super.setMenuVisibility(visible);
// Whenever this fragment is revealed in a menu,
// notify Beta users the page data is unavailable
if(ConfigUtils.isBetaFlavour() && visible) {
Context ctx = null;
if(getContext() != null) {
ctx = getContext();
} else if(getView() != null && getView().getContext() != null) {
ctx = getView().getContext();
}
if(ctx != null) {
Toast.makeText(ctx,
R.string.leaderboard_unavailable_beta,
Toast.LENGTH_LONG).show();
}
}
}
/**
* Refreshes the leaderboard list
*/
private void refreshLeaderboard() {
scrollToRank = false;
if (viewModel != null) {
viewModel.refresh(duration, category, limit, offset);
setLeaderboard(duration, category, limit, offset);
}
}
/**
* Performs Auto Scroll to the User's Rank
* We use userRank+1 to load one extra user and prevent overlapping of my rank button
* If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top
*/
private void scrollToUserRank() {
if(userRank==0){
Toast.makeText(getContext(),R.string.no_achievements_yet,Toast.LENGTH_SHORT).show();
}else {
if (binding == null) {
return;
}
if (Objects.requireNonNull(binding.leaderboardList.getAdapter()).getItemCount()
> userRank + 1) {
binding.leaderboardList.smoothScrollToPosition(userRank + 1);
} else {
if (viewModel != null) {
viewModel.refresh(duration, category, userRank + 1, 0);
setLeaderboard(duration, category, userRank + 1, 0);
scrollToRank = true;
}
}
}
}
/**
* Set the spinners for the leaderboard filters
*/
private void setSpinners() {
ArrayAdapter<CharSequence> categoryAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_categories, android.R.layout.simple_spinner_item);
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.categorySpinner.setAdapter(categoryAdapter);
ArrayAdapter<CharSequence> durationAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_durations, android.R.layout.simple_spinner_item);
durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.durationSpinner.setAdapter(durationAdapter);
}
/**
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private void setLeaderboard(String duration, String category, int limit, int offset) {
if (checkAccount()) {
try {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(userName),
duration, category, null, null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
userRank = response.getRank();
setViews(response, duration, category, limit, offset);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
onError();
}
));
}
catch (Exception e){
Timber.d(e+"success");
}
}
}
/**
* Set the views
* @param response Leaderboard Response Object
*/
private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) {
viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class);
viewModel.setParams(duration, category, limit, offset);
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter();
UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response);
MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
binding.leaderboardList.setLayoutManager(linearLayoutManager);
binding.leaderboardList.setAdapter(mergeAdapter);
viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList);
viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> {
if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) {
showProgressBar();
} else if (status.equalsIgnoreCase(LOADED)) {
hideProgressBar();
if (scrollToRank) {
binding.leaderboardList.smoothScrollToPosition(userRank + 1);
}
}
});
}
/**
* to hide progressbar
*/
private void hideProgressBar() {
if (binding != null) {
binding.progressBar.setVisibility(View.GONE);
binding.categorySpinner.setVisibility(View.VISIBLE);
binding.durationSpinner.setVisibility(View.VISIBLE);
binding.scroll.setVisibility(View.VISIBLE);
binding.leaderboardList.setVisibility(View.VISIBLE);
}
}
/**
* to show progressbar
*/
private void showProgressBar() {
if (binding != null) {
binding.progressBar.setVisibility(View.VISIBLE);
binding.scroll.setVisibility(View.INVISIBLE);
}
}
/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
binding.categorySpinner.setVisibility(View.INVISIBLE);
binding.durationSpinner.setVisibility(View.INVISIBLE);
binding.leaderboardList.setVisibility(View.INVISIBLE);
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(getActivity());
return false;
}
return true;
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private void onError() {
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
if (binding!=null) {
binding.progressBar.setVisibility(View.GONE);
}
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
binding = null;
}
}

View file

@ -0,0 +1,319 @@
package fr.free.nrw.commons.profile.leaderboard
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.MergeAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.Objects
import javax.inject.Inject
/**
* This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment
*/
class LeaderboardFragment : CommonsDaggerSupportFragment() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
@Inject
lateinit var viewModelFactory: ViewModelFactory
private var viewModel: LeaderboardListViewModel? = null
private var duration: String? = null
private var category: String? = null
private val limit: Int = PAGE_SIZE
private val offset: Int = START_OFFSET
private var userRank = 0
private var scrollToRank = false
private var userName: String? = null
private var binding: FragmentLeaderboardBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { userName = it.getString(ProfileActivity.KEY_USERNAME) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLeaderboardBinding.inflate(inflater, container, false)
hideLayouts()
// Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu
if (isBetaFlavour) {
binding!!.progressBar.visibility = View.GONE
binding!!.scroll.visibility = View.GONE
return binding!!.root
}
binding!!.progressBar.visibility = View.VISIBLE
setSpinners()
/*
* This array is for the duration filter, we have three filters weekly, yearly and all-time
* each filter have a key and value pair, the value represents the param of the API
*/
val durationValues = requireContext().resources
.getStringArray(R.array.leaderboard_duration_values)
duration = durationValues[0]
/*
* This array is for the category filter, we have three filters upload, used and nearby
* each filter have a key and value pair, the value represents the param of the API
*/
val categoryValues = requireContext().resources
.getStringArray(R.array.leaderboard_category_values)
category = categoryValues[0]
setLeaderboard(duration, category, limit, offset)
with(binding!!) {
durationSpinner.onItemSelectedListener = SelectionListener {
duration = durationValues[durationSpinner.selectedItemPosition]
refreshLeaderboard()
}
categorySpinner.onItemSelectedListener = SelectionListener {
category = categoryValues[categorySpinner.selectedItemPosition]
refreshLeaderboard()
}
scroll.setOnClickListener { scrollToUserRank() }
return root
}
}
override fun setMenuVisibility(visible: Boolean) {
super.setMenuVisibility(visible)
// Whenever this fragment is revealed in a menu,
// notify Beta users the page data is unavailable
if (isBetaFlavour && visible) {
val ctx: Context? = if (context != null) {
context
} else if (view != null && requireView().context != null) {
requireView().context
} else {
null
}
ctx?.let {
Toast.makeText(it, R.string.leaderboard_unavailable_beta, Toast.LENGTH_LONG).show()
}
}
}
/**
* Refreshes the leaderboard list
*/
private fun refreshLeaderboard() {
scrollToRank = false
viewModel?.let {
it.refresh(duration, category, limit, offset)
setLeaderboard(duration, category, limit, offset)
}
}
/**
* Performs Auto Scroll to the User's Rank
* We use userRank+1 to load one extra user and prevent overlapping of my rank button
* If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top
*/
private fun scrollToUserRank() {
if (userRank == 0) {
Toast.makeText(context, R.string.no_achievements_yet, Toast.LENGTH_SHORT).show()
} else {
if (binding == null) {
return
}
val itemCount = binding?.leaderboardList?.adapter?.itemCount ?: 0
if (itemCount > userRank + 1) {
binding!!.leaderboardList.smoothScrollToPosition(userRank + 1)
} else {
viewModel?.let {
it.refresh(duration, category, userRank + 1, 0)
setLeaderboard(duration, category, userRank + 1, 0)
scrollToRank = true
}
}
}
}
/**
* Set the spinners for the leaderboard filters
*/
private fun setSpinners() {
val categoryAdapter = ArrayAdapter.createFromResource(
requireContext(),
R.array.leaderboard_categories, android.R.layout.simple_spinner_item
)
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding!!.categorySpinner.adapter = categoryAdapter
val durationAdapter = ArrayAdapter.createFromResource(
requireContext(),
R.array.leaderboard_durations, android.R.layout.simple_spinner_item
)
durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding!!.durationSpinner.adapter = durationAdapter
}
/**
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private fun setLeaderboard(duration: String?, category: String?, limit: Int, offset: Int) {
if (checkAccount()) {
try {
compositeDisposable.add(
okHttpJsonApiClient.getLeaderboard(
Objects.requireNonNull(userName),
duration, category, null, null
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response: LeaderboardResponse? ->
if (response != null && response.status == 200) {
userRank = response.rank!!
setViews(response, duration, category, limit, offset)
}
},
{ t: Throwable? ->
Timber.e(t, "Fetching leaderboard statistics failed")
onError()
}
))
} catch (e: Exception) {
Timber.d(e, "success")
}
}
}
/**
* Set the views
* @param response Leaderboard Response Object
*/
private fun setViews(
response: LeaderboardResponse,
duration: String?,
category: String?,
limit: Int,
offset: Int
) {
viewModel = ViewModelProvider(this, viewModelFactory).get(
LeaderboardListViewModel::class.java
)
viewModel!!.setParams(duration, category, limit, offset)
val leaderboardListAdapter = LeaderboardListAdapter()
val userDetailAdapter = UserDetailAdapter(response)
val mergeAdapter = MergeAdapter(userDetailAdapter, leaderboardListAdapter)
val linearLayoutManager = LinearLayoutManager(context)
binding!!.leaderboardList.layoutManager = linearLayoutManager
binding!!.leaderboardList.adapter = mergeAdapter
viewModel!!.listLiveData.observe(viewLifecycleOwner, leaderboardListAdapter::submitList)
viewModel!!.progressLoadStatus.observe(viewLifecycleOwner) { status ->
when (status) {
LOADING -> {
showProgressBar()
}
LOADED -> {
hideProgressBar()
if (scrollToRank) {
binding!!.leaderboardList.smoothScrollToPosition(userRank + 1)
}
}
}
}
}
/**
* to hide progressbar
*/
private fun hideProgressBar() = binding?.let {
it.progressBar.visibility = View.GONE
it.categorySpinner.visibility = View.VISIBLE
it.durationSpinner.visibility = View.VISIBLE
it.scroll.visibility = View.VISIBLE
it.leaderboardList.visibility = View.VISIBLE
}
/**
* to show progressbar
*/
private fun showProgressBar() = binding?.let {
it.progressBar.visibility = View.VISIBLE
it.scroll.visibility = View.INVISIBLE
}
/**
* used to hide the layouts while fetching results from api
*/
private fun hideLayouts() = binding?.let {
it.categorySpinner.visibility = View.INVISIBLE
it.durationSpinner.visibility = View.INVISIBLE
it.leaderboardList.visibility = View.INVISIBLE
}
/**
* check to ensure that user is logged in
*/
private fun checkAccount() = if (sessionManager.currentAccount == null) {
Timber.d("Current account is null")
showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in))
sessionManager.forceLogin(requireActivity())
false
} else {
true
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private fun onError() {
showLongToast(requireActivity(), resources.getString(R.string.error_occurred))
binding?.let { it.progressBar.visibility = View.GONE }
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
binding = null
}
private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) =
handler()
override fun onNothingSelected(p0: AdapterView<*>?) = Unit
}
}

View file

@ -1,137 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This class represents the leaderboard API response sub part of i.e. leaderboard list
* The leaderboard list will contain the ranking of the users from 1 to n,
* avatars, username and count in the selected category.
*/
public class LeaderboardList {
/**
* Username of the user
* Example value - Syced
*/
@SerializedName("username")
@Expose
private String username;
/**
* Count in the category
* Example value - 10
*/
@SerializedName("category_count")
@Expose
private Integer categoryCount;
/**
* URL of the avatar of user
* Example value = https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png
*/
@SerializedName("avatar")
@Expose
private String avatar;
/**
* Rank of the user
* Example value - 1
*/
@SerializedName("rank")
@Expose
private Integer rank;
/**
* @return the username of the user in the leaderboard list
*/
public String getUsername() {
return username;
}
/**
* Sets the username of the user in the leaderboard list
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the category count of the user in the leaderboard list
*/
public Integer getCategoryCount() {
return categoryCount;
}
/**
* Sets the category count of the user in the leaderboard list
*/
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
/**
* @return the avatar of the user in the leaderboard list
*/
public String getAvatar() {
return avatar;
}
/**
* Sets the avatar of the user in the leaderboard list
*/
public void setAvatar(String avatar) {
this.avatar = avatar;
}
/**
* @return the rank of the user in the leaderboard list
*/
public Integer getRank() {
return rank;
}
/**
* Sets the rank of the user in the leaderboard list
*/
public void setRank(Integer rank) {
this.rank = rank;
}
/**
* This method checks for the diff in the callbacks for paged lists
*/
public static DiffUtil.ItemCallback<LeaderboardList> DIFF_CALLBACK =
new ItemCallback<LeaderboardList>() {
@Override
public boolean areItemsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem == oldItem;
}
@Override
public boolean areContentsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem.getRank().equals(oldItem.getRank());
}
};
/**
* Returns true if two objects are equal, false otherwise
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
LeaderboardList leaderboardList = (LeaderboardList) obj;
return leaderboardList.getRank().equals(this.getRank());
}
}

View file

@ -0,0 +1,61 @@
package fr.free.nrw.commons.profile.leaderboard
import androidx.recyclerview.widget.DiffUtil
import com.google.gson.annotations.SerializedName
/**
* This class represents the leaderboard API response sub part of i.e. leaderboard list
* The leaderboard list will contain the ranking of the users from 1 to n,
* avatars, username and count in the selected category.
*/
data class LeaderboardList (
@SerializedName("username")
var username: String? = null,
@SerializedName("category_count")
var categoryCount: Int? = null,
@SerializedName("avatar")
var avatar: String? = null,
@SerializedName("rank")
var rank: Int? = null
) {
/**
* Returns true if two objects are equal, false otherwise
* @param other
* @return
*/
override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}
val leaderboardList = other as LeaderboardList
return leaderboardList.rank == rank
}
override fun hashCode(): Int {
var result = username?.hashCode() ?: 0
result = 31 * result + (categoryCount ?: 0)
result = 31 * result + (avatar?.hashCode() ?: 0)
result = 31 * result + (rank ?: 0)
return result
}
companion object {
/**
* This method checks for the diff in the callbacks for paged lists
*/
var DIFF_CALLBACK: DiffUtil.ItemCallback<LeaderboardList> =
object : DiffUtil.ItemCallback<LeaderboardList>() {
override fun areItemsTheSame(
oldItem: LeaderboardList,
newItem: LeaderboardList
): Boolean = newItem === oldItem
override fun areContentsTheSame(
oldItem: LeaderboardList,
newItem: LeaderboardList
): Boolean = newItem.rank == oldItem.rank
}
}
}

View file

@ -1,93 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.profile.ProfileActivity;
/**
* This class extends RecyclerView.Adapter and creates the List section of the leaderboard
*/
public class LeaderboardListAdapter extends PagedListAdapter<LeaderboardList, LeaderboardListAdapter.ListViewHolder> {
public LeaderboardListAdapter() {
super(LeaderboardList.DIFF_CALLBACK);
}
public class ListViewHolder extends RecyclerView.ViewHolder {
TextView rank;
SimpleDraweeView avatar;
TextView username;
TextView count;
public ListViewHolder(View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.user_rank);
this.avatar = itemView.findViewById(R.id.user_avatar);
this.username = itemView.findViewById(R.id.user_name);
this.count = itemView.findViewById(R.id.user_count);
}
/**
* This method will return the Context
* @return Context
*/
public Context getContext() {
return itemView.getContext();
}
}
/**
* Overrides the onCreateViewHolder and inflates the recyclerview list item layout
* @param parent
* @param viewType
* @return
*/
@NonNull
@Override
public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_list_element, parent, false);
return new ListViewHolder(view);
}
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(getItem(position).getRank().toString());
avatar.setImageURI(Uri.parse(getItem(position).getAvatar()));
username.setText(getItem(position).getUsername());
count.setText(getItem(position).getCategoryCount().toString());
/*
Now that we have our in app profile-section, lets take the user there
*/
holder.itemView.setOnClickListener(view -> {
if (view.getContext() instanceof ProfileActivity) {
((Activity) (view.getContext())).finish();
}
ProfileActivity.startYourself(view.getContext(), getItem(position).getUsername(), true);
});
}
}

View file

@ -0,0 +1,64 @@
package fr.free.nrw.commons.profile.leaderboard
import android.app.Activity
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.view.SimpleDraweeView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.profile.leaderboard.LeaderboardList.Companion.DIFF_CALLBACK
import fr.free.nrw.commons.profile.leaderboard.LeaderboardListAdapter.ListViewHolder
/**
* This class extends RecyclerView.Adapter and creates the List section of the leaderboard
*/
class LeaderboardListAdapter : PagedListAdapter<LeaderboardList, ListViewHolder>(DIFF_CALLBACK) {
inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var rank: TextView? = itemView.findViewById(R.id.user_rank)
var avatar: SimpleDraweeView? = itemView.findViewById(R.id.user_avatar)
var username: TextView? = itemView.findViewById(R.id.user_name)
var count: TextView? = itemView.findViewById(R.id.user_count)
}
/**
* Overrides the onCreateViewHolder and inflates the recyclerview list item layout
* @param parent
* @param viewType
* @return
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder =
ListViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.leaderboard_list_element, parent, false)
)
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: ListViewHolder, position: Int) = with (holder) {
val item = getItem(position)!!
rank?.text = item.rank.toString()
avatar?.setImageURI(Uri.parse(item.avatar))
username?.text = item.username
count?.text = item.categoryCount.toString()
/*
Now that we have our in app profile-section, lets take the user there
*/
itemView.setOnClickListener { view: View ->
if (view.context is ProfileActivity) {
((view.context) as Activity).finish()
}
ProfileActivity.startYourself(view.context, item.username, true)
}
}
}

View file

@ -1,107 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
/**
* Extends the ViewModel class and creates the LeaderboardList View Model
*/
public class LeaderboardListViewModel extends ViewModel {
private DataSourceFactory dataSourceFactory;
private LiveData<PagedList<LeaderboardList>> listLiveData;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private LiveData<String> progressLoadStatus = new MutableLiveData<>();
/**
* Constructor for a new LeaderboardListViewModel
* @param okHttpJsonApiClient
* @param sessionManager
*/
public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager
sessionManager) {
dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient,
compositeDisposable, sessionManager);
initializePaging();
}
/**
* Initialises the paging
*/
private void initializePaging() {
PagedList.Config pagedListConfig =
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE).build();
listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig)
.build();
progressLoadStatus = Transformations
.switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus);
}
/**
* Refreshes the paged list with the new params and starts the loading of new data
* @param duration
* @param category
* @param limit
* @param offset
*/
public void refresh(String duration, String category, int limit, int offset) {
dataSourceFactory.setDuration(duration);
dataSourceFactory.setCategory(category);
dataSourceFactory.setLimit(limit);
dataSourceFactory.setOffset(offset);
dataSourceFactory.getMutableLiveData().getValue().invalidate();
}
/**
* Sets the new params for the paged list API calls
* @param duration
* @param category
* @param limit
* @param offset
*/
public void setParams(String duration, String category, int limit, int offset) {
dataSourceFactory.setDuration(duration);
dataSourceFactory.setCategory(category);
dataSourceFactory.setLimit(limit);
dataSourceFactory.setOffset(offset);
}
/**
* @return the loading status of paged list
*/
public LiveData<String> getProgressLoadStatus() {
return progressLoadStatus;
}
/**
* @return the paged list with live data
*/
public LiveData<PagedList<LeaderboardList>> getListLiveData() {
return listLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.profile.leaderboard
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE
/**
* Extends the ViewModel class and creates the LeaderboardList View Model
*/
class LeaderboardListViewModel(
okHttpJsonApiClient: OkHttpJsonApiClient,
sessionManager: SessionManager
) : ViewModel() {
private val dataSourceFactory = DataSourceFactory(okHttpJsonApiClient, sessionManager)
val listLiveData: LiveData<PagedList<LeaderboardList>> = LivePagedListBuilder(
dataSourceFactory,
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE).build()
).build()
val progressLoadStatus: LiveData<LoadingStatus> =
dataSourceFactory.mutableLiveData.switchMap { it.progressLiveStatus }
/**
* Refreshes the paged list with the new params and starts the loading of new data
*/
fun refresh(duration: String?, category: String?, limit: Int, offset: Int) {
dataSourceFactory.duration = duration
dataSourceFactory.category = category
dataSourceFactory.limit = limit
dataSourceFactory.offset = offset
dataSourceFactory.mutableLiveData.value!!.invalidate()
}
/**
* Sets the new params for the paged list API calls
*/
fun setParams(duration: String?, category: String?, limit: Int, offset: Int) {
dataSourceFactory.duration = duration
dataSourceFactory.category = category
dataSourceFactory.limit = limit
dataSourceFactory.offset = offset
}
}

View file

@ -1,237 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* GSON Response Class for Leaderboard API response
*/
public class LeaderboardResponse {
/**
* Status Code returned from the API
* Example value - 200
*/
@SerializedName("status")
@Expose
private Integer status;
/**
* Username returned from the API
* Example value - Syced
*/
@SerializedName("username")
@Expose
private String username;
/**
* Category count returned from the API
* Example value - 10
*/
@SerializedName("category_count")
@Expose
private Integer categoryCount;
/**
* Limit returned from the API
* Example value - 10
*/
@SerializedName("limit")
@Expose
private int limit;
/**
* Avatar returned from the API
* Example value - https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png
*/
@SerializedName("avatar")
@Expose
private String avatar;
/**
* Offset returned from the API
* Example value - 0
*/
@SerializedName("offset")
@Expose
private int offset;
/**
* Duration returned from the API
* Example value - yearly
*/
@SerializedName("duration")
@Expose
private String duration;
/**
* Leaderboard list returned from the API
* Example value - [{
* "username": "",
* "category_count": 107147,
* "avatar": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png",
* "rank": 1
* }]
*/
@SerializedName("leaderboard_list")
@Expose
private List<LeaderboardList> leaderboardList = null;
/**
* Category returned from the API
* Example value - upload
*/
@SerializedName("category")
@Expose
private String category;
/**
* Rank returned from the API
* Example value - 1
*/
@SerializedName("rank")
@Expose
private Integer rank;
/**
* @return the status code
*/
public Integer getStatus() {
return status;
}
/**
* Sets the status code
*/
public void setStatus(Integer status) {
this.status = status;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* Sets the username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the category count
*/
public Integer getCategoryCount() {
return categoryCount;
}
/**
* Sets the category count
*/
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
/**
* @return the limit
*/
public int getLimit() {
return limit;
}
/**
* Sets the limit
*/
public void setLimit(int limit) {
this.limit = limit;
}
/**
* @return the avatar
*/
public String getAvatar() {
return avatar;
}
/**
* Sets the avatar
*/
public void setAvatar(String avatar) {
this.avatar = avatar;
}
/**
* @return the offset
*/
public int getOffset() {
return offset;
}
/**
* Sets the offset
*/
public void setOffset(int offset) {
this.offset = offset;
}
/**
* @return the duration
*/
public String getDuration() {
return duration;
}
/**
* Sets the duration
*/
public void setDuration(String duration) {
this.duration = duration;
}
/**
* @return the leaderboard list
*/
public List<LeaderboardList> getLeaderboardList() {
return leaderboardList;
}
/**
* Sets the leaderboard list
*/
public void setLeaderboardList(List<LeaderboardList> leaderboardList) {
this.leaderboardList = leaderboardList;
}
/**
* @return the category
*/
public String getCategory() {
return category;
}
/**
* Sets the category
*/
public void setCategory(String category) {
this.category = category;
}
/**
* @return the rank
*/
public Integer getRank() {
return rank;
}
/**
* Sets the rank
*/
public void setRank(Integer rank) {
this.rank = rank;
}
}

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.profile.leaderboard
import com.google.gson.annotations.SerializedName
/**
* GSON Response Class for Leaderboard API response
*/
data class LeaderboardResponse(
@SerializedName("status") var status: Int? = null,
@SerializedName("username") var username: String? = null,
@SerializedName("category_count") var categoryCount: Int? = null,
@SerializedName("limit") var limit: Int = 0,
@SerializedName("avatar") var avatar: String? = null,
@SerializedName("offset") var offset: Int = 0,
@SerializedName("duration") var duration: String? = null,
@SerializedName("leaderboard_list") var leaderboardList: List<LeaderboardList>? = null,
@SerializedName("category") var category: String? = null,
@SerializedName("rank") var rank: Int? = null
)

View file

@ -1,77 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* GSON Response Class for Update Avatar API response
*/
public class UpdateAvatarResponse {
/**
* Status Code returned from the API
* Example value - 200
*/
@SerializedName("status")
@Expose
private String status;
/**
* Message returned from the API
* Example value - Avatar Updated
*/
@SerializedName("message")
@Expose
private String message;
/**
* Username returned from the API
* Example value - Syced
*/
@SerializedName("user")
@Expose
private String user;
/**
* @return the status code
*/
public String getStatus() {
return status;
}
/**
* Sets the status code
*/
public void setStatus(String status) {
this.status = status;
}
/**
* @return the message
*/
public String getMessage() {
return message;
}
/**
* Sets the message
*/
public void setMessage(String message) {
this.message = message;
}
/**
* @return the username
*/
public String getUser() {
return user;
}
/**
* Sets the username
*/
public void setUser(String user) {
this.user = user;
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.profile.leaderboard
/**
* GSON Response Class for Update Avatar API response
*/
data class UpdateAvatarResponse(
var status: String? = null,
var message: String? = null,
var user: String? = null
)

View file

@ -1,126 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
/**
* This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard
*/
public class UserDetailAdapter extends RecyclerView.Adapter<UserDetailAdapter.DataViewHolder> {
private LeaderboardResponse leaderboardResponse;
/**
* Stores the username of currently logged in user.
*/
private String currentlyLoggedInUserName = null;
public UserDetailAdapter(LeaderboardResponse leaderboardResponse) {
this.leaderboardResponse = leaderboardResponse;
}
public class DataViewHolder extends RecyclerView.ViewHolder {
private TextView rank;
private SimpleDraweeView avatar;
private TextView username;
private TextView count;
public DataViewHolder(@NonNull View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.rank);
this.avatar = itemView.findViewById(R.id.avatar);
this.username = itemView.findViewById(R.id.username);
this.count = itemView.findViewById(R.id.count);
}
/**
* This method will return the Context
* @return Context
*/
public Context getContext() {
return itemView.getContext();
}
}
/**
* Overrides the onCreateViewHolder and sets the view with leaderboard user element layout
* @param parent
* @param viewType
* @return
*/
@NonNull
@Override
public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_user_element, parent, false);
return new DataViewHolder(view);
}
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.rank_prefix),
leaderboardResponse.getRank()));
avatar.setImageURI(
Uri.parse(leaderboardResponse.getAvatar()));
username.setText(leaderboardResponse.getUsername());
count.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.count_prefix),
leaderboardResponse.getCategoryCount()));
// When user tap on avatar shows the toast on how to change avatar
// fixing: https://github.com/commons-app/apps-android-commons/issues/47747
if (currentlyLoggedInUserName == null) {
// If the current login username has not been fetched yet, then fetch it.
final AccountManager accountManager = AccountManager.get(username.getContext());
final Account[] allAccounts = accountManager.getAccountsByType(
BuildConfig.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
currentlyLoggedInUserName = allAccounts[0].name;
}
}
if (currentlyLoggedInUserName != null && currentlyLoggedInUserName.equals(
leaderboardResponse.getUsername())) {
avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(),
R.string.set_up_avatar_toast_string,
Toast.LENGTH_LONG).show();
}
});
}
}
@Override
public int getItemCount() {
return 1;
}
}

View file

@ -0,0 +1,91 @@
package fr.free.nrw.commons.profile.leaderboard
import android.accounts.AccountManager
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.view.SimpleDraweeView
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.profile.leaderboard.UserDetailAdapter.DataViewHolder
import java.util.Locale
/**
* This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard
*/
class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) :
RecyclerView.Adapter<DataViewHolder>() {
/**
* Stores the username of currently logged in user.
*/
private var currentlyLoggedInUserName: String? = null
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val rank: TextView = itemView.findViewById(R.id.rank)
val avatar: SimpleDraweeView = itemView.findViewById(R.id.avatar)
val username: TextView = itemView.findViewById(R.id.username)
val count: TextView = itemView.findViewById(R.id.count)
}
/**
* Overrides the onCreateViewHolder and sets the view with leaderboard user element layout
* @param parent
* @param viewType
* @return
*/
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DataViewHolder = DataViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.leaderboard_user_element, parent, false)
)
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: DataViewHolder, position: Int) = with(holder) {
val resources = itemView.context.resources
avatar.setImageURI(Uri.parse(leaderboardResponse.avatar))
username.text = leaderboardResponse.username
rank.text = String.format(
Locale.getDefault(),
"%s %d",
resources.getString(R.string.rank_prefix),
leaderboardResponse.rank
)
count.text = String.format(
Locale.getDefault(),
"%s %d",
resources.getString(R.string.count_prefix),
leaderboardResponse.categoryCount
)
// When user tap on avatar shows the toast on how to change avatar
// fixing: https://github.com/commons-app/apps-android-commons/issues/47747
if (currentlyLoggedInUserName == null) {
// If the current login username has not been fetched yet, then fetch it.
val accountManager = AccountManager.get(itemView.context)
val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE)
if (allAccounts.isNotEmpty()) {
currentlyLoggedInUserName = allAccounts[0].name
}
}
if (currentlyLoggedInUserName != null && currentlyLoggedInUserName == leaderboardResponse.username) {
avatar.setOnClickListener { v: View ->
Toast.makeText(
v.context, R.string.set_up_avatar_toast_string, Toast.LENGTH_LONG
).show()
}
}
}
override fun getItemCount(): Int = 1
}

View file

@ -1,41 +0,0 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import javax.inject.Inject;
/**
* This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class
* for leaderboardListViewModel
*/
public class ViewModelFactory implements ViewModelProvider.Factory {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
@Inject
public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
}
/**
* Creats a new LeaderboardListViewModel
* @param modelClass
* @param <T>
* @return
*/
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) {
return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager);
}
throw new IllegalArgumentException("Unknown class name");
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.profile.leaderboard
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import javax.inject.Inject
/**
* This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class
* for leaderboardListViewModel
*/
class ViewModelFactory @Inject constructor(
private val okHttpJsonApiClient: OkHttpJsonApiClient,
private val sessionManager: SessionManager
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
if (modelClass.isAssignableFrom(LeaderboardListViewModel::class.java)) {
LeaderboardListViewModel(okHttpJsonApiClient, sessionManager) as T
} else {
throw IllegalArgumentException("Unknown class name")
}
}

View file

@ -42,8 +42,8 @@ class LanguagesAdapter constructor(
AppLanguageLookUpTable(context)
init {
languageNamesList = language.localizedNames
languageCodesList = language.codes
languageNamesList = language.getLocalizedNames()
languageCodesList = language.getCodes()
}
private val filter = LanguageFilter()
@ -117,7 +117,7 @@ class LanguagesAdapter constructor(
*/
fun getIndexOfUserDefaultLocale(context: Context): Int {
val userLanguageCode = context.locale?.language ?: return DEFAULT_INDEX
return language.codes.indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX
return language.getCodes().indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX
}
fun getIndexOfLanguageCode(languageCode: String): Int = languageCodesList.indexOf(languageCode)
@ -128,17 +128,17 @@ class LanguagesAdapter constructor(
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterResults = FilterResults()
val temp: LinkedHashMap<String, String> = LinkedHashMap()
if (constraint != null && language.localizedNames != null) {
val length: Int = language.localizedNames.size
if (constraint != null) {
val length: Int = language.getLocalizedNames().size
var i = 0
while (i < length) {
val key: String = language.codes[i]
val value: String = language.localizedNames[i]
val key: String = language.getCodes()[i]
val value: String = language.getLocalizedNames()[i]
val defaultlanguagecode = getIndexOfUserDefaultLocale(context)
if (value.contains(constraint, true) ||
Locale(key)
.getDisplayName(
Locale(language.codes[defaultlanguagecode]),
Locale(language.getCodes()[defaultlanguagecode]),
).contains(constraint, true)
) {
temp[key] = value

View file

@ -62,5 +62,5 @@ class LatLngTests {
private fun assertPrettyCoordinateString(
expected: String,
place: LatLng,
) = assertEquals(expected, place.prettyCoordinateString)
) = assertEquals(expected, place.getPrettyCoordinateString())
}

View file

@ -1,116 +0,0 @@
package fr.free.nrw.commons.leaderboard;
import com.google.gson.Gson;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* This class tests the Leaderboard API calls
*/
public class LeaderboardApiTest {
MockWebServer server;
private static final String TEST_USERNAME = "user";
private static final String TEST_AVATAR = "avatar";
private static final int TEST_USER_RANK = 1;
private static final int TEST_USER_COUNT = 0;
private static final String FILE_NAME = "leaderboard_sample_response.json";
private static final String ENDPOINT = "/leaderboard.py";
/**
* This method initialises a Mock Server
*/
@Before
public void initTest() {
server = new MockWebServer();
}
/**
* This method will setup a Mock Server and load Test JSON Response File
* @throws Exception
*/
@Before
public void setUp() throws Exception {
String testResponseBody = convertStreamToString(getClass().getClassLoader().getResourceAsStream(FILE_NAME));
server.enqueue(new MockResponse().setBody(testResponseBody));
server.start();
}
/**
* This method converts a Input Stream to String
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
private static String convertStreamToString(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
/**
* This method will call the Mock Server and Test it with sample values.
* It will test the Leaderboard API call functionality and check if the object is
* being created with the correct values
* @throws IOException
*/
@Test
public void apiTest() throws IOException {
HttpUrl httpUrl = server.url(ENDPOINT);
LeaderboardResponse response = sendRequest(new OkHttpClient(), httpUrl);
Assert.assertEquals(TEST_AVATAR, response.getAvatar());
Assert.assertEquals(TEST_USERNAME, response.getUsername());
Assert.assertEquals(Integer.valueOf(TEST_USER_RANK), response.getRank());
Assert.assertEquals(Integer.valueOf(TEST_USER_COUNT), response.getCategoryCount());
}
/**
* This method will call the Mock API and returns the Leaderboard Response Object
* @param okHttpClient
* @param httpUrl
* @return Leaderboard Response Object
* @throws IOException
*/
private LeaderboardResponse sendRequest(OkHttpClient okHttpClient, HttpUrl httpUrl)
throws IOException {
Request request = new Builder().url(httpUrl).build();
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
Gson gson = new Gson();
return gson.fromJson(response.body().string(), LeaderboardResponse.class);
}
return null;
}
/**
* This method shuts down the Mock Server
* @throws IOException
*/
@After
public void shutdown() throws IOException {
server.shutdown();
}
}

View file

@ -0,0 +1,121 @@
package fr.free.nrw.commons.leaderboard
import com.google.gson.Gson
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
/**
* This class tests the Leaderboard API calls
*/
class LeaderboardApiTest {
lateinit var server: MockWebServer
/**
* This method initialises a Mock Server
*/
@Before
fun initTest() {
server = MockWebServer()
}
/**
* This method will setup a Mock Server and load Test JSON Response File
* @throws Exception
*/
@Before
@Throws(Exception::class)
fun setUp() {
val testResponseBody = convertStreamToString(
javaClass.classLoader!!.getResourceAsStream(FILE_NAME)
)
server.enqueue(MockResponse().setBody(testResponseBody))
server.start()
}
/**
* This method will call the Mock Server and Test it with sample values.
* It will test the Leaderboard API call functionality and check if the object is
* being created with the correct values
* @throws IOException
*/
@Test
@Throws(IOException::class)
fun apiTest() {
val httpUrl = server.url(ENDPOINT)
val response = sendRequest(OkHttpClient(), httpUrl)
Assert.assertEquals(TEST_AVATAR, response!!.avatar)
Assert.assertEquals(TEST_USERNAME, response.username)
Assert.assertEquals(TEST_USER_RANK, response.rank)
Assert.assertEquals(TEST_USER_COUNT, response.categoryCount)
}
/**
* This method will call the Mock API and returns the Leaderboard Response Object
* @param okHttpClient
* @param httpUrl
* @return Leaderboard Response Object
* @throws IOException
*/
@Throws(IOException::class)
private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): LeaderboardResponse? {
val request: Request = Request.Builder().url(httpUrl).build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
val gson = Gson()
return gson.fromJson(response.body!!.string(), LeaderboardResponse::class.java)
}
return null
}
/**
* This method shuts down the Mock Server
* @throws IOException
*/
@After
@Throws(IOException::class)
fun shutdown() {
server.shutdown()
}
companion object {
private const val TEST_USERNAME = "user"
private const val TEST_AVATAR = "avatar"
private const val TEST_USER_RANK = 1
private const val TEST_USER_COUNT = 0
private const val FILE_NAME = "leaderboard_sample_response.json"
private const val ENDPOINT = "/leaderboard.py"
/**
* This method converts a Input Stream to String
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
@Throws(Exception::class)
private fun convertStreamToString(`is`: InputStream): String {
val reader = BufferedReader(InputStreamReader(`is`))
val sb = StringBuilder()
var line: String?
while ((reader.readLine().also { line = it }) != null) {
sb.append(line).append("\n")
}
reader.close()
return sb.toString()
}
}
}

View file

@ -1,117 +0,0 @@
package fr.free.nrw.commons.leaderboard;
import com.google.gson.Gson;
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class UpdateAvatarApiTest {
private static final String TEST_USERNAME = "user";
private static final String TEST_STATUS = "200";
private static final String TEST_MESSAGE = "Avatar Updated";
private static final String FILE_NAME = "update_leaderboard_avatar_sample_response.json";
private static final String ENDPOINT = "/update_avatar.py";
MockWebServer server;
/**
* This method converts a Input Stream to String
*
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
private static String convertStreamToString(final InputStream is) throws Exception {
final BufferedReader reader = new BufferedReader(new InputStreamReader(is));
final StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
/**
* This method initialises a Mock Server
*/
@Before
public void initTest() {
server = new MockWebServer();
}
/**
* This method will setup a Mock Server and load Test JSON Response File
*
* @throws Exception
*/
@Before
public void setUp() throws Exception {
final String testResponseBody = convertStreamToString(
getClass().getClassLoader().getResourceAsStream(FILE_NAME));
server.enqueue(new MockResponse().setBody(testResponseBody));
server.start();
}
/**
* This method will call the Mock Server and Test it with sample values. It will test the Update
* Avatar API call functionality and check if the object is being created with the correct
* values
*
* @throws IOException
*/
@Test
public void apiTest() throws IOException {
final HttpUrl httpUrl = server.url(ENDPOINT);
final UpdateAvatarResponse response = sendRequest(new OkHttpClient(), httpUrl);
Assert.assertEquals(TEST_USERNAME, response.getUser());
Assert.assertEquals(TEST_STATUS, response.getStatus());
Assert.assertEquals(TEST_MESSAGE, response.getMessage());
}
/**
* This method will call the Mock API and returns the Update Avatar Response Object
*
* @param okHttpClient
* @param httpUrl
* @return Update Avatar Response Object
* @throws IOException
*/
private UpdateAvatarResponse sendRequest(final OkHttpClient okHttpClient, final HttpUrl httpUrl)
throws IOException {
final Request request = new Builder().url(httpUrl).build();
final Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
final Gson gson = new Gson();
return gson.fromJson(response.body().string(), UpdateAvatarResponse.class);
}
return null;
}
/**
* This method shuts down the Mock Server
*
* @throws IOException
*/
@After
public void shutdown() throws IOException {
server.shutdown();
}
}

View file

@ -0,0 +1,127 @@
package fr.free.nrw.commons.leaderboard
import com.google.gson.Gson
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
class UpdateAvatarApiTest {
lateinit var server: MockWebServer
/**
* This method initialises a Mock Server
*/
@Before
fun initTest() {
server = MockWebServer()
}
/**
* This method will setup a Mock Server and load Test JSON Response File
*
* @throws Exception
*/
@Before
@Throws(Exception::class)
fun setUp() {
val testResponseBody = convertStreamToString(
javaClass.classLoader!!.getResourceAsStream(FILE_NAME)
)
server.enqueue(MockResponse().setBody(testResponseBody))
server.start()
}
/**
* This method will call the Mock Server and Test it with sample values. It will test the Update
* Avatar API call functionality and check if the object is being created with the correct
* values
*
* @throws IOException
*/
@Test
@Throws(IOException::class)
fun apiTest() {
val httpUrl = server.url(ENDPOINT)
val response = sendRequest(OkHttpClient(), httpUrl)
Assert.assertNotNull(response)
with(response!!) {
Assert.assertEquals(TEST_USERNAME, user)
Assert.assertEquals(TEST_STATUS, status)
Assert.assertEquals(TEST_MESSAGE, message)
}
}
/**
* This method will call the Mock API and returns the Update Avatar Response Object
*
* @param okHttpClient
* @param httpUrl
* @return Update Avatar Response Object
* @throws IOException
*/
@Throws(IOException::class)
private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): UpdateAvatarResponse? {
val request: Request = Request.Builder().url(httpUrl).build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
val gson = Gson()
return gson.fromJson(
response.body!!.string(),
UpdateAvatarResponse::class.java
)
}
return null
}
/**
* This method shuts down the Mock Server
*
* @throws IOException
*/
@After
@Throws(IOException::class)
fun shutdown() {
server.shutdown()
}
companion object {
private const val TEST_USERNAME = "user"
private const val TEST_STATUS = "200"
private const val TEST_MESSAGE = "Avatar Updated"
private const val FILE_NAME = "update_leaderboard_avatar_sample_response.json"
private const val ENDPOINT = "/update_avatar.py"
/**
* This method converts a Input Stream to String
*
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
@Throws(Exception::class)
private fun convertStreamToString(`is`: InputStream): String {
val reader = BufferedReader(InputStreamReader(`is`))
val sb = StringBuilder()
var line: String?
while ((reader.readLine().also { line = it }) != null) {
sb.append(line).append("\n")
}
reader.close()
return sb.toString()
}
}
}

View file

@ -248,11 +248,11 @@ class MediaDetailFragmentUnitTests {
@Throws(Exception::class)
fun testOnUpdateCoordinatesClickedCurrentLocationNull() {
`when`(media.coordinates).thenReturn(null)
`when`(locationManager.lastLocation).thenReturn(null)
`when`(locationManager.getLastLocation()).thenReturn(null)
`when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297")
fragment.onUpdateCoordinatesClicked()
Mockito.verify(media, Mockito.times(1)).coordinates
Mockito.verify(locationManager, Mockito.times(1)).lastLocation
Mockito.verify(locationManager, Mockito.times(1)).getLastLocation()
val shadowActivity: ShadowActivity = shadowOf(activity)
val startedIntent = shadowActivity.nextStartedActivity
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
@ -276,11 +276,11 @@ class MediaDetailFragmentUnitTests {
@Throws(Exception::class)
fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() {
`when`(media.coordinates).thenReturn(null)
`when`(locationManager.lastLocation).thenReturn(LatLng(-0.000001, -0.999999, 0f))
`when`(locationManager.getLastLocation()).thenReturn(LatLng(-0.000001, -0.999999, 0f))
`when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297")
fragment.onUpdateCoordinatesClicked()
Mockito.verify(locationManager, Mockito.times(3)).lastLocation
Mockito.verify(locationManager, Mockito.times(3)).getLastLocation()
val shadowActivity: ShadowActivity = shadowOf(activity)
val startedIntent = shadowActivity.nextStartedActivity
val shadowIntent: ShadowIntent = shadowOf(startedIntent)

View file

@ -54,8 +54,8 @@ class LanguagesAdapterTest {
.from(context)
.inflate(R.layout.row_item_languages_spinner, null) as View
languageNamesList = language.localizedNames
languageCodesList = language.codes
languageNamesList = language.getLocalizedNames()
languageCodesList = language.getCodes()
languagesAdapter = LanguagesAdapter(context, selectedLanguages)
}
@ -124,12 +124,12 @@ class LanguagesAdapterTest {
var i = 0
var s = 0
while (i < length) {
val key: String = language.codes[i]
val value: String = language.localizedNames[i]
val key: String = language.getCodes()[i]
val value: String = language.getLocalizedNames()[i]
if (value.contains(constraint, true) ||
Locale(key)
.getDisplayName(
Locale(language.codes[defaultlanguagecode!!]),
Locale(language.getCodes()[defaultlanguagecode!!]),
).contains(constraint, true)
) {
s++