mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-29 13:53:54 +01:00
Merge branch 'main' into Migrate-Feedback-Module-from-java-to-kt
This commit is contained in:
commit
6e7c1cc3ed
48 changed files with 2619 additions and 3359 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
150
app/src/main/java/fr/free/nrw/commons/location/LatLng.kt
Normal file
150
app/src/main/java/fr/free/nrw/commons/location/LatLng.kt
Normal 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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "Fæ",
|
||||
* "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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -62,5 +62,5 @@ class LatLngTests {
|
|||
private fun assertPrettyCoordinateString(
|
||||
expected: String,
|
||||
place: LatLng,
|
||||
) = assertEquals(expected, place.prettyCoordinateString)
|
||||
) = assertEquals(expected, place.getPrettyCoordinateString())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue