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) |         AppLanguageLookUpTable(context) | ||||||
| 
 | 
 | ||||||
|     init { |     init { | ||||||
|         languageNamesList = language.localizedNames |         languageNamesList = language.getLocalizedNames() | ||||||
|         languageCodesList = language.codes |         languageCodesList = language.getCodes() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private val filter = LanguageFilter() |     private val filter = LanguageFilter() | ||||||
|  | @ -117,7 +117,7 @@ class LanguagesAdapter constructor( | ||||||
|      */ |      */ | ||||||
|     fun getIndexOfUserDefaultLocale(context: Context): Int { |     fun getIndexOfUserDefaultLocale(context: Context): Int { | ||||||
|         val userLanguageCode = context.locale?.language ?: return DEFAULT_INDEX |         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) |     fun getIndexOfLanguageCode(languageCode: String): Int = languageCodesList.indexOf(languageCode) | ||||||
|  | @ -128,17 +128,17 @@ class LanguagesAdapter constructor( | ||||||
|         override fun performFiltering(constraint: CharSequence?): FilterResults { |         override fun performFiltering(constraint: CharSequence?): FilterResults { | ||||||
|             val filterResults = FilterResults() |             val filterResults = FilterResults() | ||||||
|             val temp: LinkedHashMap<String, String> = LinkedHashMap() |             val temp: LinkedHashMap<String, String> = LinkedHashMap() | ||||||
|             if (constraint != null && language.localizedNames != null) { |             if (constraint != null) { | ||||||
|                 val length: Int = language.localizedNames.size |                 val length: Int = language.getLocalizedNames().size | ||||||
|                 var i = 0 |                 var i = 0 | ||||||
|                 while (i < length) { |                 while (i < length) { | ||||||
|                     val key: String = language.codes[i] |                     val key: String = language.getCodes()[i] | ||||||
|                     val value: String = language.localizedNames[i] |                     val value: String = language.getLocalizedNames()[i] | ||||||
|                     val defaultlanguagecode = getIndexOfUserDefaultLocale(context) |                     val defaultlanguagecode = getIndexOfUserDefaultLocale(context) | ||||||
|                     if (value.contains(constraint, true) || |                     if (value.contains(constraint, true) || | ||||||
|                         Locale(key) |                         Locale(key) | ||||||
|                             .getDisplayName( |                             .getDisplayName( | ||||||
|                                 Locale(language.codes[defaultlanguagecode]), |                                 Locale(language.getCodes()[defaultlanguagecode]), | ||||||
|                             ).contains(constraint, true) |                             ).contains(constraint, true) | ||||||
|                     ) { |                     ) { | ||||||
|                         temp[key] = value |                         temp[key] = value | ||||||
|  |  | ||||||
|  | @ -62,5 +62,5 @@ class LatLngTests { | ||||||
|     private fun assertPrettyCoordinateString( |     private fun assertPrettyCoordinateString( | ||||||
|         expected: String, |         expected: String, | ||||||
|         place: LatLng, |         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) |     @Throws(Exception::class) | ||||||
|     fun testOnUpdateCoordinatesClickedCurrentLocationNull() { |     fun testOnUpdateCoordinatesClickedCurrentLocationNull() { | ||||||
|         `when`(media.coordinates).thenReturn(null) |         `when`(media.coordinates).thenReturn(null) | ||||||
|         `when`(locationManager.lastLocation).thenReturn(null) |         `when`(locationManager.getLastLocation()).thenReturn(null) | ||||||
|         `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") |         `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") | ||||||
|         fragment.onUpdateCoordinatesClicked() |         fragment.onUpdateCoordinatesClicked() | ||||||
|         Mockito.verify(media, Mockito.times(1)).coordinates |         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 shadowActivity: ShadowActivity = shadowOf(activity) | ||||||
|         val startedIntent = shadowActivity.nextStartedActivity |         val startedIntent = shadowActivity.nextStartedActivity | ||||||
|         val shadowIntent: ShadowIntent = shadowOf(startedIntent) |         val shadowIntent: ShadowIntent = shadowOf(startedIntent) | ||||||
|  | @ -276,11 +276,11 @@ class MediaDetailFragmentUnitTests { | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() { |     fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() { | ||||||
|         `when`(media.coordinates).thenReturn(null) |         `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") |         `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") | ||||||
| 
 | 
 | ||||||
|         fragment.onUpdateCoordinatesClicked() |         fragment.onUpdateCoordinatesClicked() | ||||||
|         Mockito.verify(locationManager, Mockito.times(3)).lastLocation |         Mockito.verify(locationManager, Mockito.times(3)).getLastLocation() | ||||||
|         val shadowActivity: ShadowActivity = shadowOf(activity) |         val shadowActivity: ShadowActivity = shadowOf(activity) | ||||||
|         val startedIntent = shadowActivity.nextStartedActivity |         val startedIntent = shadowActivity.nextStartedActivity | ||||||
|         val shadowIntent: ShadowIntent = shadowOf(startedIntent) |         val shadowIntent: ShadowIntent = shadowOf(startedIntent) | ||||||
|  |  | ||||||
|  | @ -54,8 +54,8 @@ class LanguagesAdapterTest { | ||||||
|                 .from(context) |                 .from(context) | ||||||
|                 .inflate(R.layout.row_item_languages_spinner, null) as View |                 .inflate(R.layout.row_item_languages_spinner, null) as View | ||||||
| 
 | 
 | ||||||
|         languageNamesList = language.localizedNames |         languageNamesList = language.getLocalizedNames() | ||||||
|         languageCodesList = language.codes |         languageCodesList = language.getCodes() | ||||||
| 
 | 
 | ||||||
|         languagesAdapter = LanguagesAdapter(context, selectedLanguages) |         languagesAdapter = LanguagesAdapter(context, selectedLanguages) | ||||||
|     } |     } | ||||||
|  | @ -124,12 +124,12 @@ class LanguagesAdapterTest { | ||||||
|         var i = 0 |         var i = 0 | ||||||
|         var s = 0 |         var s = 0 | ||||||
|         while (i < length) { |         while (i < length) { | ||||||
|             val key: String = language.codes[i] |             val key: String = language.getCodes()[i] | ||||||
|             val value: String = language.localizedNames[i] |             val value: String = language.getLocalizedNames()[i] | ||||||
|             if (value.contains(constraint, true) || |             if (value.contains(constraint, true) || | ||||||
|                 Locale(key) |                 Locale(key) | ||||||
|                     .getDisplayName( |                     .getDisplayName( | ||||||
|                         Locale(language.codes[defaultlanguagecode!!]), |                         Locale(language.getCodes()[defaultlanguagecode!!]), | ||||||
|                     ).contains(constraint, true) |                     ).contains(constraint, true) | ||||||
|             ) { |             ) { | ||||||
|                 s++ |                 s++ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Neel Doshi
						Neel Doshi