mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +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
	
	 Neel Doshi
						Neel Doshi