mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Convert ExploreMapController to kotlin
This commit is contained in:
parent
a0214d8ddc
commit
ed6576b385
3 changed files with 220 additions and 215 deletions
|
|
@ -1,213 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.map;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
|
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget;
|
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
|
||||||
import fr.free.nrw.commons.MapController;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
|
||||||
import fr.free.nrw.commons.utils.LocationUtils;
|
|
||||||
import fr.free.nrw.commons.utils.PlaceUtils;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class ExploreMapController extends MapController {
|
|
||||||
|
|
||||||
private final ExploreMapCalls exploreMapCalls;
|
|
||||||
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
|
|
||||||
public LatLng currentLocation; // current location of user
|
|
||||||
public double latestSearchRadius = 0; // Any last search radius
|
|
||||||
public double currentLocationSearchRadius = 0; // Search radius of only searches around current location
|
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ExploreMapController(ExploreMapCalls explorePlaces) {
|
|
||||||
this.exploreMapCalls = explorePlaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
|
|
||||||
* explorePlaceList and boundaryCoordinates
|
|
||||||
*
|
|
||||||
* @param currentLatLng is current geolocation
|
|
||||||
* @param searchLatLng is the location that we want to search around
|
|
||||||
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
|
|
||||||
* current location, false if another location
|
|
||||||
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
|
|
||||||
* boundaryCoordinates
|
|
||||||
*/
|
|
||||||
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng,
|
|
||||||
boolean checkingAroundCurrentLocation) {
|
|
||||||
|
|
||||||
if (searchLatLng == null) {
|
|
||||||
Timber.d("Loading attractions explore map, but search is null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
|
|
||||||
try {
|
|
||||||
explorePlacesInfo.currentLatLng = currentLatLng;
|
|
||||||
latestSearchLocation = searchLatLng;
|
|
||||||
|
|
||||||
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
|
|
||||||
LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south
|
|
||||||
mediaList.get(0).getCoordinates(), // north
|
|
||||||
mediaList.get(0).getCoordinates(), // west
|
|
||||||
mediaList.get(0).getCoordinates()};// east, init with a random location
|
|
||||||
|
|
||||||
if (searchLatLng != null) {
|
|
||||||
Timber.d("Sorting places by distance...");
|
|
||||||
final Map<Media, Double> distances = new HashMap<>();
|
|
||||||
for (Media media : mediaList) {
|
|
||||||
distances.put(media,
|
|
||||||
computeDistanceBetween(media.getCoordinates(), searchLatLng));
|
|
||||||
// Find boundaries with basic find max approach
|
|
||||||
if (media.getCoordinates().getLatitude()
|
|
||||||
< boundaryCoordinates[0].getLatitude()) {
|
|
||||||
boundaryCoordinates[0] = media.getCoordinates();
|
|
||||||
}
|
|
||||||
if (media.getCoordinates().getLatitude()
|
|
||||||
> boundaryCoordinates[1].getLatitude()) {
|
|
||||||
boundaryCoordinates[1] = media.getCoordinates();
|
|
||||||
}
|
|
||||||
if (media.getCoordinates().getLongitude()
|
|
||||||
< boundaryCoordinates[2].getLongitude()) {
|
|
||||||
boundaryCoordinates[2] = media.getCoordinates();
|
|
||||||
}
|
|
||||||
if (media.getCoordinates().getLongitude()
|
|
||||||
> boundaryCoordinates[3].getLongitude()) {
|
|
||||||
boundaryCoordinates[3] = media.getCoordinates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorePlacesInfo.mediaList = mediaList;
|
|
||||||
explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList);
|
|
||||||
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates;
|
|
||||||
|
|
||||||
// Sets latestSearchRadius to maximum distance among boundaries and search location
|
|
||||||
for (LatLng bound : boundaryCoordinates) {
|
|
||||||
double distance = LocationUtils.calculateDistance(bound.getLatitude(),
|
|
||||||
bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude());
|
|
||||||
if (distance > latestSearchRadius) {
|
|
||||||
latestSearchRadius = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
|
|
||||||
if (checkingAroundCurrentLocation) {
|
|
||||||
currentLocationSearchRadius = latestSearchRadius;
|
|
||||||
currentLocation = currentLatLng;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return explorePlacesInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads attractions from location for map view, we need to return places in Place data type
|
|
||||||
*
|
|
||||||
* @return baseMarkerOptions list that holds nearby places with their icons
|
|
||||||
*/
|
|
||||||
public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
|
|
||||||
LatLng currentLatLng,
|
|
||||||
final List<Place> placeList,
|
|
||||||
Context context,
|
|
||||||
NearbyBaseMarkerThumbCallback callback,
|
|
||||||
ExplorePlacesInfo explorePlacesInfo) {
|
|
||||||
List<BaseMarker> baseMarkerList = new ArrayList<>();
|
|
||||||
|
|
||||||
if (placeList == null) {
|
|
||||||
return baseMarkerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
VectorDrawableCompat vectorDrawable = null;
|
|
||||||
try {
|
|
||||||
vectorDrawable = VectorDrawableCompat.create(
|
|
||||||
context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme());
|
|
||||||
|
|
||||||
} catch (Resources.NotFoundException e) {
|
|
||||||
// ignore when running tests.
|
|
||||||
}
|
|
||||||
if (vectorDrawable != null) {
|
|
||||||
for (Place explorePlace : placeList) {
|
|
||||||
final BaseMarker baseMarker = new BaseMarker();
|
|
||||||
String distance = formatDistanceBetween(currentLatLng, explorePlace.location);
|
|
||||||
explorePlace.setDistance(distance);
|
|
||||||
|
|
||||||
baseMarker.setTitle(
|
|
||||||
explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
|
|
||||||
baseMarker.setPosition(
|
|
||||||
new fr.free.nrw.commons.location.LatLng(
|
|
||||||
explorePlace.location.getLatitude(),
|
|
||||||
explorePlace.location.getLongitude(), 0));
|
|
||||||
baseMarker.setPlace(explorePlace);
|
|
||||||
|
|
||||||
Glide.with(context)
|
|
||||||
.asBitmap()
|
|
||||||
.load(explorePlace.getThumb())
|
|
||||||
.placeholder(R.drawable.image_placeholder_96)
|
|
||||||
.apply(new RequestOptions().override(96, 96).centerCrop())
|
|
||||||
.into(new CustomTarget<Bitmap>() {
|
|
||||||
// We add icons to markers when bitmaps are ready
|
|
||||||
@Override
|
|
||||||
public void onResourceReady(@NonNull Bitmap resource,
|
|
||||||
@Nullable Transition<? super Bitmap> transition) {
|
|
||||||
baseMarker.setIcon(
|
|
||||||
ImageUtils.addRedBorder(resource, 6, context));
|
|
||||||
baseMarkerList.add(baseMarker);
|
|
||||||
if (baseMarkerList.size()
|
|
||||||
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
|
||||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
|
||||||
explorePlacesInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// We add thumbnail icon for images that couldn't be loaded
|
|
||||||
@Override
|
|
||||||
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
|
|
||||||
super.onLoadFailed(errorDrawable);
|
|
||||||
baseMarker.fromResource(context, R.drawable.image_placeholder_96);
|
|
||||||
baseMarkerList.add(baseMarker);
|
|
||||||
if (baseMarkerList.size()
|
|
||||||
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
|
|
||||||
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
|
|
||||||
explorePlacesInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return baseMarkerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NearbyBaseMarkerThumbCallback {
|
|
||||||
|
|
||||||
// Callback to notify thumbnails of explore markers are added as icons and ready
|
|
||||||
void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
|
|
||||||
ExplorePlacesInfo explorePlacesInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
package fr.free.nrw.commons.explore.map
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import fr.free.nrw.commons.BaseMarker
|
||||||
|
import fr.free.nrw.commons.MapController
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils.addRedBorder
|
||||||
|
import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween
|
||||||
|
import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween
|
||||||
|
import fr.free.nrw.commons.utils.LocationUtils.calculateDistance
|
||||||
|
import fr.free.nrw.commons.utils.PlaceUtils.mediaToExplorePlace
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ExploreMapController @Inject constructor(
|
||||||
|
private val exploreMapCalls: ExploreMapCalls
|
||||||
|
) : MapController() {
|
||||||
|
// Can be current and camera target on search this area button is used
|
||||||
|
private var latestSearchLocation: LatLng? = null
|
||||||
|
|
||||||
|
// Any last search radius
|
||||||
|
private var latestSearchRadius: Double = 0.0
|
||||||
|
|
||||||
|
// Search radius of only searches around current location
|
||||||
|
private var currentLocationSearchRadius: Double = 0.0
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
// current location of user
|
||||||
|
var currentLocation: LatLng? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
|
||||||
|
* explorePlaceList and boundaryCoordinates
|
||||||
|
*
|
||||||
|
* @param currentLatLng is current geolocation
|
||||||
|
* @param searchLatLng is the location that we want to search around
|
||||||
|
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
|
||||||
|
* current location, false if another location
|
||||||
|
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
|
||||||
|
* boundaryCoordinates
|
||||||
|
*/
|
||||||
|
fun loadAttractionsFromLocation(
|
||||||
|
currentLatLng: LatLng?, searchLatLng: LatLng?,
|
||||||
|
checkingAroundCurrentLocation: Boolean
|
||||||
|
): ExplorePlacesInfo? {
|
||||||
|
if (searchLatLng == null) {
|
||||||
|
Timber.d("Loading attractions explore map, but search is null")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val explorePlacesInfo = ExplorePlacesInfo()
|
||||||
|
try {
|
||||||
|
explorePlacesInfo.currentLatLng = currentLatLng
|
||||||
|
latestSearchLocation = searchLatLng
|
||||||
|
|
||||||
|
val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng)
|
||||||
|
val boundaryCoordinates = arrayOf(
|
||||||
|
mediaList[0].coordinates!!, // south
|
||||||
|
mediaList[0].coordinates!!, // north
|
||||||
|
mediaList[0].coordinates!!, // west
|
||||||
|
mediaList[0].coordinates!!
|
||||||
|
) // east, init with a random location
|
||||||
|
|
||||||
|
Timber.d("Sorting places by distance...")
|
||||||
|
val distances: MutableMap<Media, Double> = HashMap()
|
||||||
|
for (media in mediaList) {
|
||||||
|
distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng)
|
||||||
|
// Find boundaries with basic find max approach
|
||||||
|
if (media.coordinates!!.latitude
|
||||||
|
< boundaryCoordinates[0]!!.latitude
|
||||||
|
) {
|
||||||
|
boundaryCoordinates[0] = media.coordinates!!
|
||||||
|
}
|
||||||
|
if (media.coordinates!!.latitude
|
||||||
|
> boundaryCoordinates[1]!!.latitude
|
||||||
|
) {
|
||||||
|
boundaryCoordinates[1] = media.coordinates!!
|
||||||
|
}
|
||||||
|
if (media.coordinates!!.longitude
|
||||||
|
< boundaryCoordinates[2]!!.longitude
|
||||||
|
) {
|
||||||
|
boundaryCoordinates[2] = media.coordinates!!
|
||||||
|
}
|
||||||
|
if (media.coordinates!!.longitude
|
||||||
|
> boundaryCoordinates[3]!!.longitude
|
||||||
|
) {
|
||||||
|
boundaryCoordinates[3] = media.coordinates!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
explorePlacesInfo.mediaList = mediaList
|
||||||
|
explorePlacesInfo.explorePlaceList = mediaToExplorePlace(mediaList)
|
||||||
|
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates
|
||||||
|
|
||||||
|
// Sets latestSearchRadius to maximum distance among boundaries and search location
|
||||||
|
for ((latitude, longitude) in boundaryCoordinates) {
|
||||||
|
val distance = calculateDistance(
|
||||||
|
latitude,
|
||||||
|
longitude, searchLatLng.latitude, searchLatLng.longitude
|
||||||
|
)
|
||||||
|
if (distance > latestSearchRadius) {
|
||||||
|
latestSearchRadius = distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
|
||||||
|
if (checkingAroundCurrentLocation) {
|
||||||
|
currentLocationSearchRadius = latestSearchRadius
|
||||||
|
currentLocation = currentLatLng
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
return explorePlacesInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NearbyBaseMarkerThumbCallback {
|
||||||
|
// Callback to notify thumbnails of explore markers are added as icons and ready
|
||||||
|
fun onNearbyBaseMarkerThumbsReady(
|
||||||
|
baseMarkers: List<BaseMarker>?,
|
||||||
|
explorePlacesInfo: ExplorePlacesInfo?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Loads attractions from location for map view, we need to return places in Place data type
|
||||||
|
*
|
||||||
|
* @return baseMarkerOptions list that holds nearby places with their icons
|
||||||
|
*/
|
||||||
|
fun loadAttractionsFromLocationToBaseMarkerOptions(
|
||||||
|
currentLatLng: LatLng?,
|
||||||
|
placeList: List<Place>?,
|
||||||
|
context: Context,
|
||||||
|
callback: NearbyBaseMarkerThumbCallback,
|
||||||
|
explorePlacesInfo: ExplorePlacesInfo?
|
||||||
|
): List<BaseMarker> {
|
||||||
|
val baseMarkerList: MutableList<BaseMarker> = ArrayList()
|
||||||
|
|
||||||
|
if (placeList == null) {
|
||||||
|
return baseMarkerList
|
||||||
|
}
|
||||||
|
|
||||||
|
var vectorDrawable: VectorDrawableCompat? = null
|
||||||
|
try {
|
||||||
|
vectorDrawable = VectorDrawableCompat.create(
|
||||||
|
context.resources, R.drawable.ic_custom_map_marker_dark, context.theme
|
||||||
|
)
|
||||||
|
} catch (e: Resources.NotFoundException) {
|
||||||
|
// ignore when running tests.
|
||||||
|
}
|
||||||
|
if (vectorDrawable != null) {
|
||||||
|
for (explorePlace in placeList) {
|
||||||
|
val baseMarker = BaseMarker()
|
||||||
|
val distance = formatDistanceBetween(currentLatLng, explorePlace.location)
|
||||||
|
explorePlace.setDistance(distance)
|
||||||
|
|
||||||
|
baseMarker.title =
|
||||||
|
explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))
|
||||||
|
baseMarker.position = LatLng(
|
||||||
|
explorePlace.location.latitude,
|
||||||
|
explorePlace.location.longitude, 0f
|
||||||
|
)
|
||||||
|
baseMarker.place = explorePlace
|
||||||
|
|
||||||
|
Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.load(explorePlace.thumb)
|
||||||
|
.placeholder(R.drawable.image_placeholder_96)
|
||||||
|
.apply(RequestOptions().override(96, 96).centerCrop())
|
||||||
|
.into(object : CustomTarget<Bitmap>() {
|
||||||
|
// We add icons to markers when bitmaps are ready
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Bitmap,
|
||||||
|
transition: Transition<in Bitmap>?
|
||||||
|
) {
|
||||||
|
baseMarker.icon = addRedBorder(resource, 6, context)
|
||||||
|
baseMarkerList.add(baseMarker)
|
||||||
|
if (baseMarkerList.size == placeList.size) {
|
||||||
|
// if true, we added all markers to list and can trigger thumbs ready callback
|
||||||
|
callback.onNearbyBaseMarkerThumbsReady(
|
||||||
|
baseMarkerList,
|
||||||
|
explorePlacesInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) = Unit
|
||||||
|
|
||||||
|
// We add thumbnail icon for images that couldn't be loaded
|
||||||
|
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||||
|
super.onLoadFailed(errorDrawable)
|
||||||
|
baseMarker.fromResource(context, R.drawable.image_placeholder_96)
|
||||||
|
baseMarkerList.add(baseMarker)
|
||||||
|
if (baseMarkerList.size == placeList.size) {
|
||||||
|
// if true, we added all markers to list and can trigger thumbs ready callback
|
||||||
|
callback.onNearbyBaseMarkerThumbsReady(
|
||||||
|
baseMarkerList,
|
||||||
|
explorePlacesInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseMarkerList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -180,8 +180,7 @@ public class ExploreMapPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) {
|
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) {
|
||||||
exploreMapController
|
ExploreMapController.Companion.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
|
||||||
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
|
|
||||||
// Curlatlang will be used to calculate distances
|
// Curlatlang will be used to calculate distances
|
||||||
(List<Place>) explorePlacesInfo.explorePlaceList,
|
(List<Place>) explorePlacesInfo.explorePlaceList,
|
||||||
exploreMapFragmentView.getContext(),
|
exploreMapFragmentView.getContext(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue