Merge branch 'main' into psh/convert-profile-to-kotlin

This commit is contained in:
Nicolas Raoul 2024-12-03 15:27:24 +09:00 committed by GitHub
commit 0d114db5cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 2859 additions and 3162 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import javax.inject.Inject
import javax.inject.Named

View file

@ -1,32 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import androidx.fragment.app.Fragment;
/**
* Data class for handling a bookmark fragment and it title
*/
public class BookmarkPages {
private Fragment page;
private String title;
BookmarkPages(Fragment fragment, String title) {
this.title = title;
this.page = fragment;
}
/**
* Return the fragment
* @return fragment object
*/
public Fragment getPage() {
return page;
}
/**
* Return the fragment title
* @return title
*/
public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons.bookmarks
import androidx.fragment.app.Fragment
data class BookmarkPages (
val page: Fragment? = null,
val title: String? = null
)

View file

@ -3,9 +3,8 @@ package fr.free.nrw.commons.campaigns
import android.annotation.SuppressLint
import fr.free.nrw.commons.BasePresenter
import fr.free.nrw.commons.campaigns.models.Campaign
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
import io.reactivex.Scheduler

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions
import androidx.paging.PagedList.BoundaryCallback
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
@ -20,7 +21,7 @@ class ContributionBoundaryCallback
private val repository: ContributionsRepository,
private val sessionManager: SessionManager,
private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
) : BoundaryCallback<Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
var userName: String? = null

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource;
@ -34,7 +36,7 @@ public class ContributionsListPresenter implements UserActionListener {
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRemoteDataSource contributionsRemoteDataSource,
final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
@Named(IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import androidx.work.ExistingWorkPolicy;
@ -31,7 +32,7 @@ public class ContributionsPresenter implements UserActionListener {
@Inject
ContributionsPresenter(ContributionsRepository repository,
UploadRepository uploadRepository,
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
@Named(IO_THREAD) Scheduler ioThreadScheduler) {
this.contributionsRepository = repository;
this.uploadRepository = uploadRepository;
this.ioThreadScheduler = ioThreadScheduler;

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.contributions
import androidx.paging.ItemKeyedDataSource
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
@ -16,7 +16,7 @@ class ContributionsRemoteDataSource
@Inject
constructor(
private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
) : ItemKeyedDataSource<Int, Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
var userName: String? = null

View file

@ -22,7 +22,10 @@ import java.util.Map;
public class Converters {
public static Gson getGson() {
return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson();
return ApplicationlessInjection
.getInstance(CommonsApplication.getInstance())
.getCommonsApplicationComponent()
.gson();
}
/**

View file

@ -1,90 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
import fr.free.nrw.commons.description.DescriptionEditActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.media.ZoomableActivity;
import fr.free.nrw.commons.nearby.WikidataFeedback;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
/**
* This Class handles the dependency injection (using dagger)
* so, if a developer needs to add a new activity to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract LoginActivity bindLoginActivity();
@ContributesAndroidInjector
abstract WelcomeActivity bindWelcomeActivity();
@ContributesAndroidInjector
abstract MainActivity bindContributionsActivity();
@ContributesAndroidInjector
abstract CustomSelectorActivity bindCustomSelectorActivity();
@ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity();
@ContributesAndroidInjector
abstract AboutActivity bindAboutActivity();
@ContributesAndroidInjector
abstract LocationPickerActivity bindLocationPickerActivity();
@ContributesAndroidInjector
abstract SignupActivity bindSignupActivity();
@ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector
abstract UploadActivity bindUploadActivity();
@ContributesAndroidInjector
abstract SearchActivity bindSearchActivity();
@ContributesAndroidInjector
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
@ContributesAndroidInjector
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
@ContributesAndroidInjector
abstract ProfileActivity bindAchievementsActivity();
@ContributesAndroidInjector
abstract ReviewActivity bindReviewActivity();
@ContributesAndroidInjector
abstract DescriptionEditActivity bindDescriptionEditActivity();
@ContributesAndroidInjector
abstract ZoomableActivity bindZoomableActivity();
@ContributesAndroidInjector
abstract UploadProgressActivity bindUploadProgressActivity();
@ContributesAndroidInjector
abstract WikidataFeedback bindWikiFeedback();
}

View file

@ -0,0 +1,89 @@
package fr.free.nrw.commons.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.AboutActivity
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity
import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SignupActivity
import fr.free.nrw.commons.category.CategoryDetailsActivity
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
import fr.free.nrw.commons.description.DescriptionEditActivity
import fr.free.nrw.commons.explore.SearchActivity
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.nearby.WikidataFeedback
import fr.free.nrw.commons.notification.NotificationActivity
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewActivity
import fr.free.nrw.commons.settings.SettingsActivity
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadProgressActivity
/**
* This Class handles the dependency injection (using dagger)
* so, if a developer needs to add a new activity to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@Suppress("unused")
abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract fun bindLoginActivity(): LoginActivity
@ContributesAndroidInjector
abstract fun bindWelcomeActivity(): WelcomeActivity
@ContributesAndroidInjector
abstract fun bindContributionsActivity(): MainActivity
@ContributesAndroidInjector
abstract fun bindCustomSelectorActivity(): CustomSelectorActivity
@ContributesAndroidInjector
abstract fun bindSettingsActivity(): SettingsActivity
@ContributesAndroidInjector
abstract fun bindAboutActivity(): AboutActivity
@ContributesAndroidInjector
abstract fun bindLocationPickerActivity(): LocationPickerActivity
@ContributesAndroidInjector
abstract fun bindSignupActivity(): SignupActivity
@ContributesAndroidInjector
abstract fun bindNotificationActivity(): NotificationActivity
@ContributesAndroidInjector
abstract fun bindUploadActivity(): UploadActivity
@ContributesAndroidInjector
abstract fun bindSearchActivity(): SearchActivity
@ContributesAndroidInjector
abstract fun bindCategoryDetailsActivity(): CategoryDetailsActivity
@ContributesAndroidInjector
abstract fun bindDepictionDetailsActivity(): WikidataItemDetailsActivity
@ContributesAndroidInjector
abstract fun bindAchievementsActivity(): ProfileActivity
@ContributesAndroidInjector
abstract fun bindReviewActivity(): ReviewActivity
@ContributesAndroidInjector
abstract fun bindDescriptionEditActivity(): DescriptionEditActivity
@ContributesAndroidInjector
abstract fun bindZoomableActivity(): ZoomableActivity
@ContributesAndroidInjector
abstract fun bindUploadProgressActivity(): UploadProgressActivity
@ContributesAndroidInjector
abstract fun bindWikiFeedback(): WikidataFeedback
}

View file

@ -1,105 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Activity;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.Context;
import androidx.fragment.app.Fragment;
import dagger.android.HasAndroidInjector;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector;
import dagger.android.HasBroadcastReceiverInjector;
import dagger.android.HasContentProviderInjector;
import dagger.android.HasFragmentInjector;
import dagger.android.HasServiceInjector;
import dagger.android.support.HasSupportFragmentInjector;
/**
* Provides injectors for all sorts of components
* Ex: Activities, Fragments, Services, ContentProviders
*/
public class ApplicationlessInjection
implements
HasAndroidInjector,
HasActivityInjector,
HasFragmentInjector,
HasSupportFragmentInjector,
HasServiceInjector,
HasBroadcastReceiverInjector,
HasContentProviderInjector {
private static ApplicationlessInjection instance = null;
@Inject DispatchingAndroidInjector<Object> androidInjector;
@Inject DispatchingAndroidInjector<Activity> activityInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
@Inject DispatchingAndroidInjector<Service> serviceInjector;
@Inject DispatchingAndroidInjector<ContentProvider> contentProviderInjector;
private CommonsApplicationComponent commonsApplicationComponent;
public ApplicationlessInjection(Context applicationContext) {
commonsApplicationComponent = DaggerCommonsApplicationComponent.builder()
.appModule(new CommonsApplicationModule(applicationContext)).build();
commonsApplicationComponent.inject(this);
}
@Override
public AndroidInjector<Object> androidInjector() {
return androidInjector;
}
@Override
public DispatchingAndroidInjector<Activity> activityInjector() {
return activityInjector;
}
@Override
public DispatchingAndroidInjector<android.app.Fragment> fragmentInjector() {
return fragmentInjector;
}
@Override
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
return supportFragmentInjector;
}
@Override
public DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
return broadcastReceiverInjector;
}
@Override
public DispatchingAndroidInjector<Service> serviceInjector() {
return serviceInjector;
}
@Override
public AndroidInjector<ContentProvider> contentProviderInjector() {
return contentProviderInjector;
}
public CommonsApplicationComponent getCommonsApplicationComponent() {
return commonsApplicationComponent;
}
public static ApplicationlessInjection getInstance(Context applicationContext) {
if (instance == null) {
synchronized (ApplicationlessInjection.class) {
if (instance == null) {
instance = new ApplicationlessInjection(applicationContext);
}
}
}
return instance;
}
}

View file

@ -0,0 +1,98 @@
package fr.free.nrw.commons.di
import android.app.Activity
import android.app.Fragment
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ContentProvider
import android.content.Context
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasAndroidInjector
import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasContentProviderInjector
import dagger.android.HasFragmentInjector
import dagger.android.HasServiceInjector
import dagger.android.support.HasSupportFragmentInjector
import javax.inject.Inject
import androidx.fragment.app.Fragment as AndroidXFragmen
/**
* Provides injectors for all sorts of components
* Ex: Activities, Fragments, Services, ContentProviders
*/
class ApplicationlessInjection(applicationContext: Context) : HasAndroidInjector,
HasActivityInjector, HasFragmentInjector, HasSupportFragmentInjector, HasServiceInjector,
HasBroadcastReceiverInjector, HasContentProviderInjector {
@Inject @JvmField
var androidInjector: DispatchingAndroidInjector<Any>? = null
@Inject @JvmField
var activityInjector: DispatchingAndroidInjector<Activity>? = null
@Inject @JvmField
var broadcastReceiverInjector: DispatchingAndroidInjector<BroadcastReceiver>? = null
@Inject @JvmField
var fragmentInjector: DispatchingAndroidInjector<Fragment>? = null
@Inject @JvmField
var supportFragmentInjector: DispatchingAndroidInjector<AndroidXFragmen>? = null
@Inject @JvmField
var serviceInjector: DispatchingAndroidInjector<Service>? = null
@Inject @JvmField
var contentProviderInjector: DispatchingAndroidInjector<ContentProvider>? = null
val instance: ApplicationlessInjection get() = _instance!!
val commonsApplicationComponent: CommonsApplicationComponent =
DaggerCommonsApplicationComponent
.builder()
.appModule(CommonsApplicationModule(applicationContext))
.build()
init {
commonsApplicationComponent.inject(this)
}
override fun androidInjector(): AndroidInjector<Any>? =
androidInjector
override fun activityInjector(): DispatchingAndroidInjector<Activity>? =
activityInjector
override fun fragmentInjector(): DispatchingAndroidInjector<Fragment>? =
fragmentInjector
override fun supportFragmentInjector(): DispatchingAndroidInjector<AndroidXFragmen>? =
supportFragmentInjector
override fun broadcastReceiverInjector(): DispatchingAndroidInjector<BroadcastReceiver>? =
broadcastReceiverInjector
override fun serviceInjector(): DispatchingAndroidInjector<Service>? =
serviceInjector
override fun contentProviderInjector(): AndroidInjector<ContentProvider>? =
contentProviderInjector
companion object {
private var _instance: ApplicationlessInjection? = null
@JvmStatic
fun getInstance(applicationContext: Context): ApplicationlessInjection {
if (_instance == null) {
synchronized(ApplicationlessInjection::class.java) {
if (_instance == null) {
_instance = ApplicationlessInjection(applicationContext)
}
}
}
return _instance!!
}
}
}

View file

@ -1,85 +0,0 @@
package fr.free.nrw.commons.di;
import com.google.gson.Gson;
import fr.free.nrw.commons.explore.categories.CategoriesModule;
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import javax.inject.Singleton;
import dagger.Component;
import dagger.android.AndroidInjectionModule;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsModule;
import fr.free.nrw.commons.explore.depictions.DepictionModule;
import fr.free.nrw.commons.explore.SearchModule;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FileProcessor;
import fr.free.nrw.commons.upload.UploadModule;
import fr.free.nrw.commons.widget.PicOfDayAppWidget;
/**
* Facilitates Injection from CommonsApplicationModule to all the
* classes seeking a dependency to be injected
*/
@Singleton
@Component(modules = {
CommonsApplicationModule.class,
NetworkingModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
ContentProviderBuilderModule.class,
UploadModule.class,
ContributionsModule.class,
SearchModule.class,
DepictionModule.class,
CategoriesModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application);
void inject(UploadWorker worker);
void inject(LoginActivity activity);
void inject(SettingsFragment fragment);
void inject(MoreBottomSheetFragment fragment);
void inject(MoreBottomSheetLoggedOutFragment fragment);
void inject(ReviewController reviewController);
//void inject(NavTabLayout view);
@Override
void inject(ApplicationlessInjection instance);
void inject(FileProcessor fileProcessor);
void inject(PicOfDayAppWidget picOfDayAppWidget);
@Singleton
void inject(NearbyController nearbyController);
Gson gson();
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {
Builder appModule(CommonsApplicationModule applicationModule);
CommonsApplicationComponent build();
}
}

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.di
import com.google.gson.Gson
import dagger.Component
import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.contributions.ContributionsModule
import fr.free.nrw.commons.explore.SearchModule
import fr.free.nrw.commons.explore.categories.CategoriesModule
import fr.free.nrw.commons.explore.depictions.DepictionModule
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment
import fr.free.nrw.commons.nearby.NearbyController
import fr.free.nrw.commons.review.ReviewController
import fr.free.nrw.commons.settings.SettingsFragment
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.UploadModule
import fr.free.nrw.commons.upload.worker.UploadWorker
import fr.free.nrw.commons.widget.PicOfDayAppWidget
import javax.inject.Singleton
/**
* Facilitates Injection from CommonsApplicationModule to all the
* classes seeking a dependency to be injected
*/
@Singleton
@Component(
modules = [
CommonsApplicationModule::class,
NetworkingModule::class,
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
ActivityBuilderModule::class,
FragmentBuilderModule::class,
ServiceBuilderModule::class,
ContentProviderBuilderModule::class,
UploadModule::class,
ContributionsModule::class,
SearchModule::class,
DepictionModule::class,
CategoriesModule::class
]
)
interface CommonsApplicationComponent : AndroidInjector<ApplicationlessInjection> {
fun inject(application: CommonsApplication)
fun inject(worker: UploadWorker)
fun inject(activity: LoginActivity)
fun inject(fragment: SettingsFragment)
fun inject(fragment: MoreBottomSheetFragment)
fun inject(fragment: MoreBottomSheetLoggedOutFragment)
fun inject(reviewController: ReviewController)
override fun inject(instance: ApplicationlessInjection)
fun inject(fileProcessor: FileProcessor)
fun inject(picOfDayAppWidget: PicOfDayAppWidget)
@Singleton
fun inject(nearbyController: NearbyController)
fun gson(): Gson
@Component.Builder
@Suppress("unused")
interface Builder {
fun appModule(applicationModule: CommonsApplicationModule): Builder
fun build(): CommonsApplicationComponent
}
}

View file

@ -1,314 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.view.inputmethod.InputMethodManager;
import androidx.collection.LruCache;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import com.google.gson.Gson;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.PlaceDao;
import fr.free.nrw.commons.review.ReviewDao;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.upload.depicts.DepictsDao;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Named;
import javax.inject.Singleton;
/**
* The Dependency Provider class for Commons Android.
*
* Provides all sorts of ContentProviderClients used by the app
* along with the Liscences, AccountUtility, UploadController, Logged User,
* Location manager etc
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
private Context applicationContext;
public static final String IO_THREAD="io_thread";
public static final String MAIN_THREAD="main_thread";
private AppDatabase appDatabase;
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE contribution "
+ " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0");
}
};
public CommonsApplicationModule(Context applicationContext) {
this.applicationContext = applicationContext;
}
/**
* Provides ImageFileLoader used to fetch device images.
* @param context
* @return
*/
@Provides
public ImageFileLoader providesImageFileLoader(Context context) {
return new ImageFileLoader(context);
}
@Provides
public Context providesApplicationContext() {
return this.applicationContext;
}
@Provides
public InputMethodManager provideInputMethodManager() {
return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
}
@Provides
@Named("licenses")
public List<String> provideLicenses(Context context) {
List<String> licenseItems = new ArrayList<>();
licenseItems.add(context.getString(R.string.license_name_cc0));
licenseItems.add(context.getString(R.string.license_name_cc_by));
licenseItems.add(context.getString(R.string.license_name_cc_by_sa));
licenseItems.add(context.getString(R.string.license_name_cc_by_four));
licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four));
return licenseItems;
}
@Provides
@Named("licenses_by_name")
public Map<String, String> provideLicensesByName(Context context) {
Map<String, String> byName = new HashMap<>();
byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0);
byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3);
byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3);
byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4);
byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4);
return byName;
}
/**
* Provides an instance of CategoryContentProviderClient i.e. the categories
* that are there in local storage
*/
@Provides
@Named("category")
public ContentProviderClient provideCategoryContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY);
}
/**
* This method is used to provide instance of RecentSearchContentProviderClient
* which provides content of Recent Searches from database
* @param context
* @return returns RecentSearchContentProviderClient
*/
@Provides
@Named("recentsearch")
public ContentProviderClient provideRecentSearchContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY);
}
@Provides
@Named("contribution")
public ContentProviderClient provideContributionContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY);
}
@Provides
@Named("modification")
public ContentProviderClient provideModificationContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY);
}
@Provides
@Named("bookmarks")
public ContentProviderClient provideBookmarkContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY);
}
@Provides
@Named("bookmarksLocation")
public ContentProviderClient provideBookmarkLocationContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY);
}
@Provides
@Named("bookmarksItem")
public ContentProviderClient provideBookmarkItemContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY);
}
/**
* This method is used to provide instance of RecentLanguagesContentProvider
* which provides content of recent used languages from database
* @param context Context
* @return returns RecentLanguagesContentProvider
*/
@Provides
@Named("recent_languages")
public ContentProviderClient provideRecentLanguagesContentProviderClient(final Context context) {
return context.getContentResolver()
.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY);
}
/**
* Provides a Json store instance(JsonKvStore) which keeps
* the provided Gson in it's instance
* @param gson stored inside the store instance
*/
@Provides
@Named("default_preferences")
public JsonKvStore providesDefaultKvStore(Context context, Gson gson) {
String storeName = context.getPackageName() + "_preferences";
return new JsonKvStore(context, storeName, gson);
}
@Provides
public UploadController providesUploadController(SessionManager sessionManager,
@Named("default_preferences") JsonKvStore kvStore,
Context context, ContributionDao contributionDao) {
return new UploadController(sessionManager, context, kvStore);
}
@Provides
@Singleton
public LocationServiceManager provideLocationServiceManager(Context context) {
return new LocationServiceManager(context);
}
@Provides
@Singleton
public DBOpenHelper provideDBOpenHelper(Context context) {
return new DBOpenHelper(context);
}
@Provides
@Singleton
@Named("thumbnail-cache")
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
@Provides
@Singleton
public WikidataEditListener provideWikidataEditListener() {
return new WikidataEditListenerImpl();
}
/**
* Provides app flavour. Can be used to alter flows in the app
* @return
*/
@Named("isBeta")
@Provides
@Singleton
public boolean provideIsBetaVariant() {
return ConfigUtils.isBetaFlavour();
}
/**
* Provide JavaRx IO scheduler which manages IO operations
* across various Threads
*/
@Named(IO_THREAD)
@Provides
public Scheduler providesIoThread(){
return Schedulers.io();
}
@Named(MAIN_THREAD)
@Provides
public Scheduler providesMainThread() {
return AndroidSchedulers.mainThread();
}
@Named("username")
@Provides
public String provideLoggedInUsername(SessionManager sessionManager) {
return Objects.toString(sessionManager.getUserName(), "");
}
@Provides
@Singleton
public AppDatabase provideAppDataBase() {
appDatabase = Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db")
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigration()
.build();
return appDatabase;
}
@Provides
public ContributionDao providesContributionsDao(AppDatabase appDatabase) {
return appDatabase.contributionDao();
}
@Provides
public PlaceDao providesPlaceDao(AppDatabase appDatabase) {
return appDatabase.PlaceDao();
}
/**
* Get the reference of DepictsDao class.
*/
@Provides
public DepictsDao providesDepictDao(AppDatabase appDatabase) {
return appDatabase.DepictsDao();
}
/**
* Get the reference of UploadedStatus class.
*/
@Provides
public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) {
return appDatabase.UploadedStatusDao();
}
/**
* Get the reference of NotForUploadStatus class.
*/
@Provides
public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) {
return appDatabase.NotForUploadStatusDao();
}
/**
* Get the reference of ReviewDao class
*/
@Provides
public ReviewDao providesReviewDao(AppDatabase appDatabase){
return appDatabase.ReviewDao();
}
@Provides
public ContentResolver providesContentResolver(Context context){
return context.getContentResolver();
}
}

View file

@ -0,0 +1,239 @@
package fr.free.nrw.commons.di
import android.app.Activity
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.collection.LruCache
import androidx.room.Room.databaseBuilder
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.db.AppDatabase
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.nearby.PlaceDao
import fr.free.nrw.commons.review.ReviewDao
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.UploadController
import fr.free.nrw.commons.upload.depicts.DepictsDao
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.wikidata.WikidataEditListener
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import java.util.Objects
import javax.inject.Named
import javax.inject.Singleton
/**
* The Dependency Provider class for Commons Android.
* Provides all sorts of ContentProviderClients used by the app
* along with the Liscences, AccountUtility, UploadController, Logged User,
* Location manager etc
*/
@Module
@Suppress("unused")
open class CommonsApplicationModule(private val applicationContext: Context) {
@Provides
fun providesImageFileLoader(context: Context): ImageFileLoader =
ImageFileLoader(context)
@Provides
fun providesApplicationContext(): Context =
applicationContext
@Provides
fun provideInputMethodManager(): InputMethodManager =
applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
@Provides
@Named("licenses")
fun provideLicenses(context: Context): List<String> = listOf(
context.getString(R.string.license_name_cc0),
context.getString(R.string.license_name_cc_by),
context.getString(R.string.license_name_cc_by_sa),
context.getString(R.string.license_name_cc_by_four),
context.getString(R.string.license_name_cc_by_sa_four)
)
@Provides
@Named("licenses_by_name")
fun provideLicensesByName(context: Context): Map<String, String> = mapOf(
context.getString(R.string.license_name_cc0) to Prefs.Licenses.CC0,
context.getString(R.string.license_name_cc_by) to Prefs.Licenses.CC_BY_3,
context.getString(R.string.license_name_cc_by_sa) to Prefs.Licenses.CC_BY_SA_3,
context.getString(R.string.license_name_cc_by_four) to Prefs.Licenses.CC_BY_4,
context.getString(R.string.license_name_cc_by_sa_four) to Prefs.Licenses.CC_BY_SA_4
)
/**
* Provides an instance of CategoryContentProviderClient i.e. the categories
* that are there in local storage
*/
@Provides
@Named("category")
open fun provideCategoryContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY)
@Provides
@Named("recentsearch")
fun provideRecentSearchContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY)
@Provides
@Named("contribution")
open fun provideContributionContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY)
@Provides
@Named("modification")
open fun provideModificationContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY)
@Provides
@Named("bookmarks")
fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY)
@Provides
@Named("bookmarksLocation")
fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY)
@Provides
@Named("bookmarksItem")
fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY)
/**
* This method is used to provide instance of RecentLanguagesContentProvider
* which provides content of recent used languages from database
* @param context Context
* @return returns RecentLanguagesContentProvider
*/
@Provides
@Named("recent_languages")
fun provideRecentLanguagesContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY)
/**
* Provides a Json store instance(JsonKvStore) which keeps
* the provided Gson in it's instance
* @param gson stored inside the store instance
*/
@Provides
@Named("default_preferences")
open fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore =
JsonKvStore(context, "${context.packageName}_preferences", gson)
@Provides
fun providesUploadController(
sessionManager: SessionManager,
@Named("default_preferences") kvStore: JsonKvStore,
context: Context
): UploadController = UploadController(sessionManager, context, kvStore)
@Provides
@Singleton
open fun provideLocationServiceManager(context: Context): LocationServiceManager =
LocationServiceManager(context)
@Provides
@Singleton
open fun provideDBOpenHelper(context: Context): DBOpenHelper =
DBOpenHelper(context)
@Provides
@Singleton
@Named("thumbnail-cache")
open fun provideLruCache(): LruCache<String, String> =
LruCache(1024)
@Provides
@Singleton
fun provideWikidataEditListener(): WikidataEditListener =
WikidataEditListenerImpl()
@Named("isBeta")
@Provides
@Singleton
fun provideIsBetaVariant(): Boolean =
isBetaFlavour
@Named(IO_THREAD)
@Provides
fun providesIoThread(): Scheduler =
Schedulers.io()
@Named(MAIN_THREAD)
@Provides
fun providesMainThread(): Scheduler =
AndroidSchedulers.mainThread()
@Named("username")
@Provides
fun provideLoggedInUsername(sessionManager: SessionManager): String =
Objects.toString(sessionManager.userName, "")
@Provides
@Singleton
fun provideAppDataBase(): AppDatabase = databaseBuilder(
applicationContext,
AppDatabase::class.java,
"commons_room.db"
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
@Provides
fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao =
appDatabase.contributionDao()
@Provides
fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao =
appDatabase.PlaceDao()
@Provides
fun providesDepictDao(appDatabase: AppDatabase): DepictsDao =
appDatabase.DepictsDao()
@Provides
fun providesUploadedStatusDao(appDatabase: AppDatabase): UploadedStatusDao =
appDatabase.UploadedStatusDao()
@Provides
fun providesNotForUploadStatusDao(appDatabase: AppDatabase): NotForUploadStatusDao =
appDatabase.NotForUploadStatusDao()
@Provides
fun providesReviewDao(appDatabase: AppDatabase): ReviewDao =
appDatabase.ReviewDao()
@Provides
fun providesContentResolver(context: Context): ContentResolver =
context.contentResolver
companion object {
const val IO_THREAD: String = "io_thread"
const val MAIN_THREAD: String = "main_thread"
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE contribution " + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0"
)
}
}
}
}

View file

@ -1,48 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> supportFragmentInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
inject();
super.onCreate(savedInstanceState);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return supportFragmentInjector;
}
/**
* when this Activity is created it injects an instance of this class inside
* activityInjector method of ApplicationlessInjection
*/
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
AndroidInjector<Activity> activityInjector = injection.activityInjector();
if (activityInjector == null) {
throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null");
}
activityInjector.inject(this);
}
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.di
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
import javax.inject.Inject
abstract class CommonsDaggerAppCompatActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject @JvmField
var supportFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
override fun onCreate(savedInstanceState: Bundle?) {
inject()
super.onCreate(savedInstanceState)
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return supportFragmentInjector!!
}
/**
* when this Activity is created it injects an instance of this class inside
* activityInjector method of ApplicationlessInjection
*/
private fun inject() {
val injection = getInstance(applicationContext)
val activityInjector = injection.activityInjector()
?: throw NullPointerException("ApplicationlessInjection.activityInjector() returned null")
activityInjector.inject(this)
}
}

View file

@ -1,35 +0,0 @@
package fr.free.nrw.commons.di;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import dagger.android.AndroidInjector;
/**
* Receives broadcast then injects it's instance to the broadcastReceiverInjector method of
* ApplicationlessInjection class
*/
public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver {
public CommonsDaggerBroadcastReceiver() {
super();
}
@Override
public void onReceive(Context context, Intent intent) {
inject(context);
}
private void inject(Context context) {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext());
AndroidInjector<BroadcastReceiver> serviceInjector = injection.broadcastReceiverInjector();
if (serviceInjector == null) {
throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null");
}
serviceInjector.inject(this);
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.di
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
/**
* Receives broadcast then injects it's instance to the broadcastReceiverInjector method of
* ApplicationlessInjection class
*/
abstract class CommonsDaggerBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
inject(context)
}
private fun inject(context: Context) {
val injection = getInstance(context.applicationContext)
val serviceInjector = injection.broadcastReceiverInjector()
?: throw NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null")
serviceInjector.inject(this)
}
}

View file

@ -1,32 +0,0 @@
package fr.free.nrw.commons.di;
import android.content.ContentProvider;
import dagger.android.AndroidInjector;
public abstract class CommonsDaggerContentProvider extends ContentProvider {
public CommonsDaggerContentProvider() {
super();
}
@Override
public boolean onCreate() {
inject();
return true;
}
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext());
AndroidInjector<ContentProvider> serviceInjector = injection.contentProviderInjector();
if (serviceInjector == null) {
throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null");
}
serviceInjector.inject(this);
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.di
import android.content.ContentProvider
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
abstract class CommonsDaggerContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
inject()
return true
}
private fun inject() {
val injection = getInstance(context!!)
val serviceInjector = injection.contentProviderInjector()
?: throw NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null")
serviceInjector.inject(this)
}
}

View file

@ -1,32 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.IntentService;
import android.app.Service;
import dagger.android.AndroidInjector;
public abstract class CommonsDaggerIntentService extends IntentService {
public CommonsDaggerIntentService(String name) {
super(name);
}
@Override
public void onCreate() {
inject();
super.onCreate();
}
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
if (serviceInjector == null) {
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
}
serviceInjector.inject(this);
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.di
import android.app.IntentService
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
abstract class CommonsDaggerIntentService(name: String?) : IntentService(name) {
override fun onCreate() {
inject()
super.onCreate()
}
private fun inject() {
val injection = getInstance(applicationContext)
val serviceInjector = injection.serviceInjector()
?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null")
serviceInjector.inject(this)
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Service;
import dagger.android.AndroidInjector;
public abstract class CommonsDaggerService extends Service {
public CommonsDaggerService() {
super();
}
@Override
public void onCreate() {
inject();
super.onCreate();
}
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
if (serviceInjector == null) {
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
}
serviceInjector.inject(this);
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.di
import android.app.Service
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
abstract class CommonsDaggerService : Service() {
override fun onCreate() {
inject()
super.onCreate()
}
private fun inject() {
val injection = getInstance(applicationContext)
val serviceInjector = injection.serviceInjector()
?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null")
serviceInjector.inject(this)
}
}

View file

@ -1,75 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Activity;
import android.content.Context;
import androidx.fragment.app.Fragment;
import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
import io.reactivex.disposables.CompositeDisposable;
public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> childFragmentInjector;
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
@Override
public void onAttach(Context context) {
inject();
super.onAttach(context);
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return childFragmentInjector;
}
public void inject() {
HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector();
AndroidInjector<Fragment> fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector();
if (fragmentInjector == null) {
throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName()));
}
fragmentInjector.inject(this);
}
private HasSupportFragmentInjector findHasFragmentInjector() {
Fragment parentFragment = this;
while ((parentFragment = parentFragment.getParentFragment()) != null) {
if (parentFragment instanceof HasSupportFragmentInjector) {
return (HasSupportFragmentInjector) parentFragment;
}
}
Activity activity = getActivity();
if (activity instanceof HasSupportFragmentInjector) {
return (HasSupportFragmentInjector) activity;
}
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext());
if (injection != null) {
return injection;
}
throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName()));
}
}

View file

@ -0,0 +1,66 @@
package fr.free.nrw.commons.di
import android.app.Activity
import android.content.Context
import androidx.fragment.app.Fragment
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
import io.reactivex.disposables.CompositeDisposable
import javax.inject.Inject
abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInjector {
@Inject @JvmField
var childFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
@JvmField
protected var compositeDisposable: CompositeDisposable = CompositeDisposable()
override fun onAttach(context: Context) {
inject()
super.onAttach(context)
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> =
childFragmentInjector!!
fun inject() {
val hasSupportFragmentInjector = findHasFragmentInjector()
val fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector()
?: throw NullPointerException(
String.format(
"%s.supportFragmentInjector() returned null",
hasSupportFragmentInjector.javaClass.canonicalName
)
)
fragmentInjector.inject(this)
}
private fun findHasFragmentInjector(): HasSupportFragmentInjector {
var parentFragment: Fragment? = this
while ((parentFragment!!.parentFragment.also { parentFragment = it }) != null) {
if (parentFragment is HasSupportFragmentInjector) {
return parentFragment as HasSupportFragmentInjector
}
}
val activity: Activity = requireActivity()
if (activity is HasSupportFragmentInjector) {
return activity
}
return getInstance(activity.applicationContext)
}
}

View file

@ -1,38 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider;
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new ContentProvider to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({ "WeakerAccess", "unused" })
public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();
@ContributesAndroidInjector
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();
@ContributesAndroidInjector
abstract BookmarkPicturesContentProvider bindBookmarkContentProvider();
@ContributesAndroidInjector
abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider();
@ContributesAndroidInjector
abstract BookmarkItemsContentProvider bindBookmarkItemContentProvider();
@ContributesAndroidInjector
abstract RecentLanguagesContentProvider bindRecentLanguagesContentProvider();
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
import fr.free.nrw.commons.category.CategoryContentProvider
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider
import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new ContentProvider to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@Suppress("unused")
abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract fun bindCategoryContentProvider(): CategoryContentProvider
@ContributesAndroidInjector
abstract fun bindRecentSearchesContentProvider(): RecentSearchesContentProvider
@ContributesAndroidInjector
abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider
@ContributesAndroidInjector
abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider
@ContributesAndroidInjector
abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider
@ContributesAndroidInjector
abstract fun bindRecentLanguagesContentProvider(): RecentLanguagesContentProvider
}

View file

@ -1,166 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.customselector.ui.selector.FolderFragment;
import fr.free.nrw.commons.customselector.ui.selector.ImageFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.explore.ExploreListRootFragment;
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FailedUploadsFragment;
import fr.free.nrw.commons.upload.PendingUploadsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new Fragment to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment();
@ContributesAndroidInjector
abstract MediaDetailFragment bindMediaDetailFragment();
@ContributesAndroidInjector
abstract FolderFragment bindFolderFragment();
@ContributesAndroidInjector
abstract ImageFragment bindImageFragment();
@ContributesAndroidInjector
abstract MediaDetailPagerFragment bindMediaDetailPagerFragment();
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
@ContributesAndroidInjector
abstract DepictedImagesFragment bindDepictedImagesFragment();
@ContributesAndroidInjector
abstract SearchMediaFragment bindBrowseImagesListFragment();
@ContributesAndroidInjector
abstract SearchCategoryFragment bindSearchCategoryListFragment();
@ContributesAndroidInjector
abstract SearchDepictionsFragment bindSearchDepictionListFragment();
@ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment();
@ContributesAndroidInjector
abstract ContributionsFragment bindContributionsFragment();
@ContributesAndroidInjector(modules = NearbyParentFragmentModule.class)
abstract NearbyParentFragment bindNearbyParentFragment();
@ContributesAndroidInjector
abstract BookmarkPicturesFragment bindBookmarkPictureListFragment();
@ContributesAndroidInjector(modules = BookmarkLocationsFragmentModule.class)
abstract BookmarkLocationsFragment bindBookmarkLocationListFragment();
@ContributesAndroidInjector(modules = BookmarkItemsFragmentModule.class)
abstract BookmarkItemsFragment bindBookmarkItemListFragment();
@ContributesAndroidInjector
abstract ReviewImageFragment bindReviewOutOfContextFragment();
@ContributesAndroidInjector
abstract UploadMediaDetailFragment bindUploadMediaDetailFragment();
@ContributesAndroidInjector
abstract UploadCategoriesFragment bindUploadCategoriesFragment();
@ContributesAndroidInjector
abstract DepictsFragment bindDepictsFragment();
@ContributesAndroidInjector
abstract MediaLicenseFragment bindMediaLicenseFragment();
@ContributesAndroidInjector
abstract ParentDepictionsFragment bindParentDepictionsFragment();
@ContributesAndroidInjector
abstract ChildDepictionsFragment bindChildDepictionsFragment();
@ContributesAndroidInjector
abstract CategoriesMediaFragment bindCategoriesMediaFragment();
@ContributesAndroidInjector
abstract SubCategoriesFragment bindSubCategoriesFragment();
@ContributesAndroidInjector
abstract ParentCategoriesFragment bindParentCategoriesFragment();
@ContributesAndroidInjector
abstract ExploreFragment bindExploreFragmentFragment();
@ContributesAndroidInjector
abstract ExploreListRootFragment bindExploreFeaturedRootFragment();
@ContributesAndroidInjector(modules = ExploreMapFragmentModule.class)
abstract ExploreMapFragment bindExploreNearbyUploadsFragment();
@ContributesAndroidInjector
abstract ExploreMapRootFragment bindExploreNearbyUploadsRootFragment();
@ContributesAndroidInjector
abstract BookmarkListRootFragment bindBookmarkListRootFragment();
@ContributesAndroidInjector
abstract BookmarkFragment bindBookmarkFragmentFragment();
@ContributesAndroidInjector
abstract MoreBottomSheetFragment bindMoreBottomSheetFragment();
@ContributesAndroidInjector
abstract MoreBottomSheetLoggedOutFragment bindMoreBottomSheetLoggedOutFragment();
@ContributesAndroidInjector
abstract AchievementsFragment bindAchievementsFragment();
@ContributesAndroidInjector
abstract LeaderboardFragment bindLeaderboardFragment();
@ContributesAndroidInjector
abstract PendingUploadsFragment bindPendingUploadsFragment();
@ContributesAndroidInjector
abstract FailedUploadsFragment bindFailedUploadsFragment();
}

View file

@ -0,0 +1,165 @@
package fr.free.nrw.commons.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.bookmarks.BookmarkFragment
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
import fr.free.nrw.commons.contributions.ContributionsFragment
import fr.free.nrw.commons.contributions.ContributionsListFragment
import fr.free.nrw.commons.customselector.ui.selector.FolderFragment
import fr.free.nrw.commons.customselector.ui.selector.ImageFragment
import fr.free.nrw.commons.explore.ExploreFragment
import fr.free.nrw.commons.explore.ExploreListRootFragment
import fr.free.nrw.commons.explore.ExploreMapRootFragment
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment
import fr.free.nrw.commons.explore.map.ExploreMapFragment
import fr.free.nrw.commons.explore.media.SearchMediaFragment
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment
import fr.free.nrw.commons.media.MediaDetailFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
import fr.free.nrw.commons.profile.achievements.AchievementsFragment
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment
import fr.free.nrw.commons.review.ReviewImageFragment
import fr.free.nrw.commons.settings.SettingsFragment
import fr.free.nrw.commons.upload.FailedUploadsFragment
import fr.free.nrw.commons.upload.PendingUploadsFragment
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment
import fr.free.nrw.commons.upload.depicts.DepictsFragment
import fr.free.nrw.commons.upload.license.MediaLicenseFragment
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new Fragment to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@Suppress("unused")
abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract fun bindContributionsListFragment(): ContributionsListFragment
@ContributesAndroidInjector
abstract fun bindMediaDetailFragment(): MediaDetailFragment
@ContributesAndroidInjector
abstract fun bindFolderFragment(): FolderFragment
@ContributesAndroidInjector
abstract fun bindImageFragment(): ImageFragment
@ContributesAndroidInjector
abstract fun bindMediaDetailPagerFragment(): MediaDetailPagerFragment
@ContributesAndroidInjector
abstract fun bindSettingsFragment(): SettingsFragment
@ContributesAndroidInjector
abstract fun bindDepictedImagesFragment(): DepictedImagesFragment
@ContributesAndroidInjector
abstract fun bindBrowseImagesListFragment(): SearchMediaFragment
@ContributesAndroidInjector
abstract fun bindSearchCategoryListFragment(): SearchCategoryFragment
@ContributesAndroidInjector
abstract fun bindSearchDepictionListFragment(): SearchDepictionsFragment
@ContributesAndroidInjector
abstract fun bindRecentSearchesFragment(): RecentSearchesFragment
@ContributesAndroidInjector
abstract fun bindContributionsFragment(): ContributionsFragment
@ContributesAndroidInjector(modules = [NearbyParentFragmentModule::class])
abstract fun bindNearbyParentFragment(): NearbyParentFragment
@ContributesAndroidInjector
abstract fun bindBookmarkPictureListFragment(): BookmarkPicturesFragment
@ContributesAndroidInjector(modules = [BookmarkLocationsFragmentModule::class])
abstract fun bindBookmarkLocationListFragment(): BookmarkLocationsFragment
@ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class])
abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment
@ContributesAndroidInjector
abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment
@ContributesAndroidInjector
abstract fun bindUploadMediaDetailFragment(): UploadMediaDetailFragment
@ContributesAndroidInjector
abstract fun bindUploadCategoriesFragment(): UploadCategoriesFragment
@ContributesAndroidInjector
abstract fun bindDepictsFragment(): DepictsFragment
@ContributesAndroidInjector
abstract fun bindMediaLicenseFragment(): MediaLicenseFragment
@ContributesAndroidInjector
abstract fun bindParentDepictionsFragment(): ParentDepictionsFragment
@ContributesAndroidInjector
abstract fun bindChildDepictionsFragment(): ChildDepictionsFragment
@ContributesAndroidInjector
abstract fun bindCategoriesMediaFragment(): CategoriesMediaFragment
@ContributesAndroidInjector
abstract fun bindSubCategoriesFragment(): SubCategoriesFragment
@ContributesAndroidInjector
abstract fun bindParentCategoriesFragment(): ParentCategoriesFragment
@ContributesAndroidInjector
abstract fun bindExploreFragmentFragment(): ExploreFragment
@ContributesAndroidInjector
abstract fun bindExploreFeaturedRootFragment(): ExploreListRootFragment
@ContributesAndroidInjector(modules = [ExploreMapFragmentModule::class])
abstract fun bindExploreNearbyUploadsFragment(): ExploreMapFragment
@ContributesAndroidInjector
abstract fun bindExploreNearbyUploadsRootFragment(): ExploreMapRootFragment
@ContributesAndroidInjector
abstract fun bindBookmarkListRootFragment(): BookmarkListRootFragment
@ContributesAndroidInjector
abstract fun bindBookmarkFragmentFragment(): BookmarkFragment
@ContributesAndroidInjector
abstract fun bindMoreBottomSheetFragment(): MoreBottomSheetFragment
@ContributesAndroidInjector
abstract fun bindMoreBottomSheetLoggedOutFragment(): MoreBottomSheetLoggedOutFragment
@ContributesAndroidInjector
abstract fun bindAchievementsFragment(): AchievementsFragment
@ContributesAndroidInjector
abstract fun bindLeaderboardFragment(): LeaderboardFragment
@ContributesAndroidInjector
abstract fun bindPendingUploadsFragment(): PendingUploadsFragment
@ContributesAndroidInjector
abstract fun bindFailedUploadsFragment(): FailedUploadsFragment
}

View file

@ -1,350 +0,0 @@
package fr.free.nrw.commons.di;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BetaConstants;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.OkHttpConnectionFactory;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.PageEditInterface;
import fr.free.nrw.commons.actions.ThanksInterface;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface;
import fr.free.nrw.commons.auth.csrf.LogoutClient;
import fr.free.nrw.commons.auth.login.LoginClient;
import fr.free.nrw.commons.auth.login.LoginInterface;
import fr.free.nrw.commons.category.CategoryInterface;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaDetailInterface;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.media.PageMediaInterface;
import fr.free.nrw.commons.media.WikidataMediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.notification.NotificationInterface;
import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.wikidata.CommonsServiceFactory;
import fr.free.nrw.commons.wikidata.WikidataInterface;
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage;
import java.io.File;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import fr.free.nrw.commons.wikidata.model.WikiSite;
import fr.free.nrw.commons.wikidata.GsonUtil;
import timber.log.Timber;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class NetworkingModule {
private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql";
private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite";
public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite";
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
public static final String NAMED_WIKI_CSRF = "wiki-csrf";
@Provides
@Singleton
public OkHttpClient provideOkHttpClient(Context context,
HttpLoggingInterceptor httpLoggingInterceptor) {
File dir = new File(context.getCacheDir(), "okHttpCache");
return new OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.readTimeout(120, TimeUnit.SECONDS)
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE))
.build();
}
@Provides
@Singleton
public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) {
return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar));
}
@Provides
@Singleton
public HttpLoggingInterceptor provideHttpLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> {
Timber.tag("OkHttp").v(message);
});
httpLoggingInterceptor.setLevel(BuildConfig.DEBUG ? Level.BODY: Level.BASIC);
return httpLoggingInterceptor;
}
@Provides
@Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
depictsClient,
toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
gson);
}
@Provides
@Singleton
public CommonsCookieStorage provideCookieStorage(
@Named("default_preferences") JsonKvStore preferences) {
CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences);
cookieStorage.load();
return cookieStorage;
}
@Provides
@Singleton
public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) {
return new CommonsCookieJar(storage);
}
@Named(NAMED_COMMONS_CSRF)
@Provides
@Singleton
public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager,
@Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
}
/**
* Provides a singleton instance of CsrfTokenClient for Wikidata.
*
* @param sessionManager The session manager to manage user sessions.
* @param tokenInterface The interface for obtaining CSRF tokens.
* @param loginClient The client for handling login operations.
* @param logoutClient The client for handling logout operations.
* @return A singleton instance of CsrfTokenClient.
*/
@Named(NAMED_WIKI_CSRF)
@Provides
@Singleton
public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager,
@Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) {
return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient);
}
/**
* Provides a singleton instance of CsrfTokenInterface for Wikidata.
*
* @param serviceFactory The factory used to create service interfaces.
* @return A singleton instance of CsrfTokenInterface for Wikidata.
*/
@Named("wikidata-csrf-interface")
@Provides
@Singleton
public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class);
}
@Named("commons-csrf-interface")
@Provides
@Singleton
public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class);
}
@Provides
@Singleton
public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class);
}
@Provides
@Singleton
public LoginClient provideLoginClient(LoginInterface loginInterface) {
return new LoginClient(loginInterface);
}
@Provides
@Named("wikimedia_api_host")
@NonNull
@SuppressWarnings("ConstantConditions")
public String provideMwApiUrl() {
return BuildConfig.WIKIMEDIA_API_HOST;
}
@Provides
@Named("tools_forge")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideToolsForgeUrl() {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
@Provides
@Singleton
@Named(NAMED_WIKI_DATA_WIKI_SITE)
public WikiSite provideWikidataWikiSite() {
return new WikiSite(BuildConfig.WIKIDATA_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
*/
@Provides
@Singleton
public Gson provideGson() {
return GsonUtil.getDefaultGson();
}
@Provides
@Singleton
public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class);
}
@Provides
@Singleton
public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class);
}
@Provides
@Singleton
public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class);
}
@Provides
@Singleton
public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class);
}
@Named("commons-page-edit-service")
@Provides
@Singleton
public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class);
}
@Named("wikidata-page-edit-service")
@Provides
@Singleton
public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class);
}
@Named("commons-page-edit")
@Provides
@Singleton
public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
@Named("commons-page-edit-service") PageEditInterface pageEditInterface) {
return new PageEditClient(csrfTokenClient, pageEditInterface);
}
/**
* Provides a singleton instance of PageEditClient for Wikidata.
*
* @param csrfTokenClient The client used to manage CSRF tokens.
* @param pageEditInterface The interface for page edit operations.
* @return A singleton instance of PageEditClient for Wikidata.
*/
@Named("wikidata-page-edit")
@Provides
@Singleton
public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient,
@Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) {
return new PageEditClient(csrfTokenClient, pageEditInterface);
}
@Provides
@Singleton
public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class);
}
/**
* Add provider for WikidataMediaInterface
* It creates a retrofit service for the commons wiki site
* @param commonsWikiSite commonsWikiSite
* @return WikidataMediaInterface
*/
@Provides
@Singleton
public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class);
}
@Provides
@Singleton
public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class);
}
@Provides
@Singleton
public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class);
}
@Provides
@Singleton
public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class);
}
@Provides
@Singleton
public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class);
}
@Provides
@Singleton
public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class);
}
@Provides
@Singleton
public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) {
return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class);
}
/**
* Add provider for PageMediaInterface
* It creates a retrofit service for the wiki site using device's current language
*/
@Provides
@Singleton
public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) {
return serviceFactory.create(wikiSite.url(), PageMediaInterface.class);
}
@Provides
@Singleton
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
public WikiSite provideLanguageWikipediaSite() {
return WikiSite.forLanguageCode(Locale.getDefault().getLanguage());
}
}

View file

@ -0,0 +1,316 @@
package fr.free.nrw.commons.di
import android.content.Context
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import fr.free.nrw.commons.BetaConstants
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.OkHttpConnectionFactory
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.actions.PageEditInterface
import fr.free.nrw.commons.actions.ThanksInterface
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface
import fr.free.nrw.commons.auth.csrf.LogoutClient
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginInterface
import fr.free.nrw.commons.category.CategoryInterface
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.media.MediaDetailInterface
import fr.free.nrw.commons.media.MediaInterface
import fr.free.nrw.commons.media.PageMediaInterface
import fr.free.nrw.commons.media.WikidataMediaInterface
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.mwapi.UserInterface
import fr.free.nrw.commons.notification.NotificationInterface
import fr.free.nrw.commons.review.ReviewInterface
import fr.free.nrw.commons.upload.UploadInterface
import fr.free.nrw.commons.upload.WikiBaseInterface
import fr.free.nrw.commons.upload.depicts.DepictsInterface
import fr.free.nrw.commons.wikidata.CommonsServiceFactory
import fr.free.nrw.commons.wikidata.GsonUtil
import fr.free.nrw.commons.wikidata.WikidataInterface
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
import fr.free.nrw.commons.wikidata.model.WikiSite
import okhttp3.Cache
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import timber.log.Timber
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Named
import javax.inject.Singleton
@Module
@Suppress("unused")
class NetworkingModule {
@Provides
@Singleton
fun provideOkHttpClient(
context: Context,
httpLoggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.readTimeout(120, TimeUnit.SECONDS)
.cache(Cache(File(context.cacheDir, "okHttpCache"), OK_HTTP_CACHE_SIZE))
.build()
@Provides
@Singleton
fun serviceFactory(cookieJar: CommonsCookieJar): CommonsServiceFactory =
CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar))
@Provides
@Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor =
HttpLoggingInterceptor { message: String? ->
Timber.tag("OkHttp").v(message)
}.apply {
level = if (BuildConfig.DEBUG) Level.BODY else Level.BASIC
}
@Provides
@Singleton
fun provideOkHttpJsonApiClient(
okHttpClient: OkHttpClient,
depictsClient: DepictsClient,
@Named("tools_forge") toolsForgeUrl: HttpUrl,
gson: Gson
): OkHttpJsonApiClient = OkHttpJsonApiClient(
okHttpClient, depictsClient, toolsForgeUrl, WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, gson
)
@Provides
@Singleton
fun provideCookieStorage(
@Named("default_preferences") preferences: JsonKvStore
): CommonsCookieStorage = CommonsCookieStorage(preferences).also {
it.load()
}
@Provides
@Singleton
fun provideCookieJar(storage: CommonsCookieStorage): CommonsCookieJar =
CommonsCookieJar(storage)
@Named(NAMED_COMMONS_CSRF)
@Provides
@Singleton
fun provideCommonsCsrfTokenClient(
sessionManager: SessionManager,
@Named("commons-csrf-interface") tokenInterface: CsrfTokenInterface,
loginClient: LoginClient,
logoutClient: LogoutClient
): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient)
/**
* Provides a singleton instance of CsrfTokenClient for Wikidata.
*
* @param sessionManager The session manager to manage user sessions.
* @param tokenInterface The interface for obtaining CSRF tokens.
* @param loginClient The client for handling login operations.
* @param logoutClient The client for handling logout operations.
* @return A singleton instance of CsrfTokenClient.
*/
@Named(NAMED_WIKI_CSRF)
@Provides
@Singleton
fun provideWikiCsrfTokenClient(
sessionManager: SessionManager,
@Named("wikidata-csrf-interface") tokenInterface: CsrfTokenInterface,
loginClient: LoginClient,
logoutClient: LogoutClient
): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient)
/**
* Provides a singleton instance of CsrfTokenInterface for Wikidata.
*
* @param factory The factory used to create service interfaces.
* @return A singleton instance of CsrfTokenInterface for Wikidata.
*/
@Named("wikidata-csrf-interface")
@Provides
@Singleton
fun provideWikidataCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface =
factory.create(BuildConfig.WIKIDATA_URL)
@Named("commons-csrf-interface")
@Provides
@Singleton
fun provideCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideLoginInterface(factory: CommonsServiceFactory): LoginInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideLoginClient(loginInterface: LoginInterface): LoginClient =
LoginClient(loginInterface)
@Provides
@Named("tools_forge")
fun provideToolsForgeUrl(): HttpUrl = TOOLS_FORGE_URL.toHttpUrlOrNull()!!
@Provides
@Singleton
@Named(NAMED_WIKI_DATA_WIKI_SITE)
fun provideWikidataWikiSite(): WikiSite = WikiSite(BuildConfig.WIKIDATA_URL)
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
*/
@Provides
@Singleton
fun provideGson(): Gson = GsonUtil.getDefaultGson()
@Provides
@Singleton
fun provideReviewInterface(factory: CommonsServiceFactory): ReviewInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideDepictsInterface(factory: CommonsServiceFactory): DepictsInterface =
factory.create(BuildConfig.WIKIDATA_URL)
@Provides
@Singleton
fun provideWikiBaseInterface(factory: CommonsServiceFactory): WikiBaseInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideUploadInterface(factory: CommonsServiceFactory): UploadInterface =
factory.create(BuildConfig.COMMONS_URL)
@Named("commons-page-edit-service")
@Provides
@Singleton
fun providePageEditService(factory: CommonsServiceFactory): PageEditInterface =
factory.create(BuildConfig.COMMONS_URL)
@Named("wikidata-page-edit-service")
@Provides
@Singleton
fun provideWikiDataPageEditService(factory: CommonsServiceFactory): PageEditInterface =
factory.create(BuildConfig.WIKIDATA_URL)
@Named("commons-page-edit")
@Provides
@Singleton
fun provideCommonsPageEditClient(
@Named(NAMED_COMMONS_CSRF) csrfTokenClient: CsrfTokenClient,
@Named("commons-page-edit-service") pageEditInterface: PageEditInterface
): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface)
/**
* Provides a singleton instance of PageEditClient for Wikidata.
*
* @param csrfTokenClient The client used to manage CSRF tokens.
* @param pageEditInterface The interface for page edit operations.
* @return A singleton instance of PageEditClient for Wikidata.
*/
@Named("wikidata-page-edit")
@Provides
@Singleton
fun provideWikidataPageEditClient(
@Named(NAMED_WIKI_CSRF) csrfTokenClient: CsrfTokenClient,
@Named("wikidata-page-edit-service") pageEditInterface: PageEditInterface
): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface)
@Provides
@Singleton
fun provideMediaInterface(factory: CommonsServiceFactory): MediaInterface =
factory.create(BuildConfig.COMMONS_URL)
/**
* Add provider for WikidataMediaInterface
* It creates a retrofit service for the commons wiki site
* @param commonsWikiSite commonsWikiSite
* @return WikidataMediaInterface
*/
@Provides
@Singleton
fun provideWikidataMediaInterface(factory: CommonsServiceFactory): WikidataMediaInterface =
factory.create(BetaConstants.COMMONS_URL)
@Provides
@Singleton
fun providesMediaDetailInterface(factory: CommonsServiceFactory): MediaDetailInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideCategoryInterface(factory: CommonsServiceFactory): CategoryInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideThanksInterface(factory: CommonsServiceFactory): ThanksInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideNotificationInterface(factory: CommonsServiceFactory): NotificationInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideUserInterface(factory: CommonsServiceFactory): UserInterface =
factory.create(BuildConfig.COMMONS_URL)
@Provides
@Singleton
fun provideWikidataInterface(factory: CommonsServiceFactory): WikidataInterface =
factory.create(BuildConfig.WIKIDATA_URL)
/**
* Add provider for PageMediaInterface
* It creates a retrofit service for the wiki site using device's current language
*/
@Provides
@Singleton
fun providePageMediaInterface(
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) wikiSite: WikiSite,
factory: CommonsServiceFactory
): PageMediaInterface = factory.create(wikiSite.url())
@Provides
@Singleton
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
fun provideLanguageWikipediaSite(): WikiSite {
return WikiSite.forLanguageCode(Locale.getDefault().language)
}
companion object {
private const val WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql"
private const val TOOLS_FORGE_URL =
"https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"
const val OK_HTTP_CACHE_SIZE: Long = (10 * 1024 * 1024).toLong()
private const val NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"
private const val NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite"
const val NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE: String = "language-wikipedia-wikisite"
const val NAMED_COMMONS_CSRF: String = "commons-csrf"
const val NAMED_WIKI_CSRF: String = "wiki-csrf"
}
}

View file

@ -1,19 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService;
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new Service to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ServiceBuilderModule {
@ContributesAndroidInjector
abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService();
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new Service to the commons app
* then that must be mentioned here to inject the dependencies
*/
@Module
@Suppress("unused")
abstract class ServiceBuilderModule {
@ContributesAndroidInjector
abstract fun bindWikiAccountAuthenticatorService(): WikiAccountAuthenticatorService
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,10 @@ package fr.free.nrw.commons.mwapi;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
import com.google.gson.Gson;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.Collections;
@ -11,14 +14,11 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse;
import timber.log.Timber;
/**
@ -30,14 +30,11 @@ import timber.log.Timber;
public class CategoryApi {
private final OkHttpClient okHttpClient;
private final String commonsBaseUrl;
private final Gson gson;
@Inject
public CategoryApi(OkHttpClient okHttpClient, Gson gson,
@Named("wikimedia_api_host") String commonsBaseUrl) {
public CategoryApi(final OkHttpClient okHttpClient, final Gson gson) {
this.okHttpClient = okHttpClient;
this.commonsBaseUrl = commonsBaseUrl;
this.gson = gson;
}
@ -75,9 +72,9 @@ public class CategoryApi {
* @param coords Coordinates to build query with
* @return URL for API query
*/
private HttpUrl buildUrl(String coords) {
private HttpUrl buildUrl(final String coords) {
return HttpUrl
.parse(commonsBaseUrl)
.parse(BuildConfig.WIKIMEDIA_API_HOST)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("prop", "categories|coordinates|pageprops")

View file

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

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import android.content.Context;
@ -53,7 +54,7 @@ public class PendingUploadsPresenter implements UserActionListener {
final ContributionsRemoteDataSource contributionsRemoteDataSource,
final ContributionsRepository contributionsRepository,
final UploadRepository uploadRepository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
@Named(IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.contributionsRepository = contributionsRepository;
this.uploadRepository = uploadRepository;

View file

@ -9,6 +9,8 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.category.CategoryEditHelper
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.proxy
import io.reactivex.Observable
@ -30,8 +32,8 @@ class CategoriesPresenter
@Inject
constructor(
private val repository: UploadRepository,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler,
) : CategoriesContract.UserActionListener {
companion object {
private val DUMMY: CategoriesContract.View = proxy()

View file

@ -7,6 +7,8 @@ import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsController
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
@ -31,8 +33,8 @@ class DepictsPresenter
@Inject
constructor(
private val repository: UploadRepository,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler,
) : DepictsContract.UserActionListener {
companion object {
private val DUMMY = proxy<DepictsContract.View>()

View file

@ -8,7 +8,7 @@ import retrofit2.converter.gson.GsonConverterFactory
class CommonsServiceFactory(
private val okHttpClient: OkHttpClient,
) {
private val builder: Retrofit.Builder by lazy {
val builder: Retrofit.Builder by lazy {
// All instances of retrofit share this configuration, but create it lazily
Retrofit
.Builder()
@ -17,15 +17,11 @@ class CommonsServiceFactory(
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
}
private val retrofitCache: MutableMap<String, Retrofit> = mutableMapOf()
val retrofitCache: MutableMap<String, Retrofit> = mutableMapOf()
fun <T : Any> create(
baseUrl: String,
service: Class<T>,
): T =
retrofitCache
.getOrPut(baseUrl) {
// Cache instances of retrofit based on API backend
builder.baseUrl(baseUrl).build()
}.create(service)
inline fun <reified T: Any> create(baseUrl: String): T =
retrofitCache.getOrPut(baseUrl) {
// Cache instances of retrofit based on API backend
builder.baseUrl(baseUrl).build()
}.create(T::class.java)
}

View file

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

View file

@ -37,9 +37,8 @@ class TestCommonsApplication : Application() {
}
@Suppress("MemberVisibilityCanBePrivate")
class MockCommonsApplicationModule(
appContext: Context,
) : CommonsApplicationModule(appContext) {
class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) {
val defaultSharedPreferences: JsonKvStore = mock()
val locationServiceManager: LocationServiceManager = mock()
val mockDbOpenHelper: DBOpenHelper = mock()
@ -50,16 +49,13 @@ class MockCommonsApplicationModule(
val modificationClient: ContentProviderClient = mock()
val uploadPrefs: JsonKvStore = mock()
override fun provideCategoryContentProviderClient(context: Context?): ContentProviderClient = categoryClient
override fun provideCategoryContentProviderClient(context: Context): ContentProviderClient = categoryClient
override fun provideContributionContentProviderClient(context: Context?): ContentProviderClient = contributionClient
override fun provideContributionContentProviderClient(context: Context): ContentProviderClient = contributionClient
override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient
override fun provideModificationContentProviderClient(context: Context): ContentProviderClient = modificationClient
override fun providesDefaultKvStore(
context: Context,
gson: Gson,
): JsonKvStore = defaultSharedPreferences
override fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = defaultSharedPreferences
override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager

View file

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

View file

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