Migrated helper modules to kotlin (#6007)

* Rename .java to .kt

* Migrated delete and description module to kotlin (WIP)

* Fix: Unit tests

* Fix: Unit tests

* Rename .java to .kt

* Migrated data, db, and converter module to kotlin

* Fix: Unit tests

* Fix: Unit tests

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Saifuddin Adenwala 2024-12-11 04:13:36 +05:30 committed by GitHub
parent 73311970c5
commit 3030a6fca7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1168 additions and 973 deletions

View file

@ -372,17 +372,19 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
*/
private fun removeLocationFromImage() {
media?.let {
compositeDisposable.add(
coordinateEditHelper.makeCoordinatesEdit(
applicationContext, it, "0.0", "0.0", "0.0f"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ ->
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe { _ ->
Timber.d("Coordinates removed from the image")
}
}?.let { it1 ->
compositeDisposable.add(
it1
)
}
}
setResult(RESULT_OK, Intent())
finish()
}
@ -473,19 +475,21 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback {
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 { _ ->
)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe { _ ->
Timber.d("Coordinates updated")
}
}?.let { it1 ->
compositeDisposable.add(
it1
)
}
} catch (e: Exception) {
if (e.localizedMessage == CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE) {
val username = sessionManager.userName

View file

@ -1,187 +0,0 @@
package fr.free.nrw.commons.coordinates;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_COORDINATES;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
/**
* Helper class for edit and update given coordinates and showing notification about new coordinates
* upgradation
*/
public class CoordinateEditHelper {
/**
* notificationHelper: helps creating notification
*/
private final NotificationHelper notificationHelper;
/**
* * pageEditClient: methods provided by this member posts the edited coordinates
* to the Media wiki api
*/
public final PageEditClient pageEditClient;
/**
* viewUtil: helps to show Toast
*/
private final ViewUtilWrapper viewUtil;
@Inject
public CoordinateEditHelper(final NotificationHelper notificationHelper,
@Named("commons-page-edit") final PageEditClient pageEditClient,
final ViewUtilWrapper viewUtil) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil;
}
/**
* Public interface to edit coordinates
* @param context to be added
* @param media to be added
* @param Accuracy to be added
* @return Single<Boolean>
*/
public Single<Boolean> makeCoordinatesEdit(final Context context, final Media media,
final String Latitude, final String Longitude, final String Accuracy) {
viewUtil.showShortToast(context,
context.getString(R.string.coordinates_edit_helper_make_edit_toast));
return addCoordinates(media, Latitude, Longitude, Accuracy)
.flatMapSingle(result -> Single.just(showCoordinatesEditNotification(context, media,
Latitude, Longitude, Accuracy, result)))
.firstOrError();
}
/**
* Replaces new coordinates
* @param media to be added
* @param Latitude to be added
* @param Longitude to be added
* @param Accuracy to be added
* @return Observable<Boolean>
*/
private Observable<Boolean> addCoordinates(final Media media, final String Latitude,
final String Longitude, final String Accuracy) {
Timber.d("thread is coordinates adding %s", Thread.currentThread().getName());
final String summary = "Adding Coordinates";
final StringBuilder buffer = new StringBuilder();
final String wikiText = pageEditClient.getCurrentWikiText(media.getFilename())
.subscribeOn(Schedulers.io())
.blockingGet();
if (Latitude != null) {
buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude)
.append("|").append(Accuracy).append("}}");
}
final String editedLocation = buffer.toString();
final String appendText = getFormattedWikiText(wikiText, editedLocation);
return pageEditClient.edit(Objects.requireNonNull(media.getFilename())
, appendText, summary);
}
/**
* Helps to get formatted wikitext with upgraded location
* @param wikiText current wikitext
* @param editedLocation new location
* @return String
*/
private String getFormattedWikiText(final String wikiText, final String editedLocation){
if (wikiText.contains("filedesc") && wikiText.contains("Location")) {
final String fromLocationToEnd = wikiText.substring(wikiText.indexOf("{{Location"));
final String firstHalf = wikiText.substring(0, wikiText.indexOf("{{Location"));
final String lastHalf = fromLocationToEnd.substring(
fromLocationToEnd.indexOf("}}") + 2);
final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText,
"==", 3);
final StringBuilder buffer = new StringBuilder();
if (wikiText.charAt(wikiText.indexOf("{{Location")-1) == '\n') {
buffer.append(editedLocation.substring(1));
} else {
buffer.append(editedLocation);
}
if (startOfSecondSection != -1 && wikiText.charAt(startOfSecondSection-1)!= '\n') {
buffer.append("\n");
}
return firstHalf + buffer + lastHalf;
}
if (wikiText.contains("filedesc") && !wikiText.contains("Location")) {
final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText,
"==", 3);
if (startOfSecondSection != -1) {
final String firstHalf = wikiText.substring(0, startOfSecondSection);
final String lastHalf = wikiText.substring(startOfSecondSection);
final String buffer = editedLocation.substring(1)
+ "\n";
return firstHalf + buffer + lastHalf;
}
return wikiText + editedLocation;
}
return "== {{int:filedesc}} ==" + editedLocation + wikiText;
}
/**
* Update coordinates and shows notification about coordinates update
* @param context to be added
* @param media to be added
* @param latitude to be added
* @param longitude to be added
* @param Accuracy to be added
* @param result to be added
* @return boolean
*/
private boolean showCoordinatesEditNotification(final Context context, final Media media,
final String latitude, final String longitude, final String Accuracy,
final boolean result) {
final String message;
String title = context.getString(R.string.coordinates_edit_helper_show_edit_title);
if (result) {
media.setCoordinates(
new fr.free.nrw.commons.location.LatLng(Double.parseDouble(latitude),
Double.parseDouble(longitude),
Float.parseFloat(Accuracy)));
title += ": " + context
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
final StringBuilder coordinatesInMessage = new StringBuilder();
final String mediaCoordinate = String.valueOf(media.getCoordinates());
coordinatesInMessage.append(mediaCoordinate);
message = context.getString(R.string.coordinates_edit_helper_show_edit_message,
coordinatesInMessage.toString());
} else {
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title);
message = context.getString(R.string.coordinates_edit_helper_edit_message_else) ;
}
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_COORDINATES,
browserIntent);
return result;
}
}

View file

@ -0,0 +1,189 @@
package fr.free.nrw.commons.coordinates
import android.content.Context
import android.content.Intent
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_EDIT_COORDINATES
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import java.util.Objects
import javax.inject.Inject
import javax.inject.Named
import org.apache.commons.lang3.StringUtils
import timber.log.Timber
/**
* Helper class for edit and update given coordinates and showing notification about new coordinates
* upgradation
*/
class CoordinateEditHelper @Inject constructor(
private val notificationHelper: NotificationHelper,
@Named("commons-page-edit") private val pageEditClient: PageEditClient,
private val viewUtil: ViewUtilWrapper
) {
/**
* Public interface to edit coordinates
* @param context to be added
* @param media to be added
* @param latitude to be added
* @param longitude to be added
* @param accuracy to be added
* @return Single<Boolean>
*/
fun makeCoordinatesEdit(
context: Context,
media: Media,
latitude: String,
longitude: String,
accuracy: String
): Single<Boolean>? {
viewUtil.showShortToast(
context,
context.getString(R.string.coordinates_edit_helper_make_edit_toast)
)
return addCoordinates(media, latitude, longitude, accuracy)
?.flatMapSingle { result ->
Single.just(showCoordinatesEditNotification(context, media, latitude, longitude, accuracy, result))
}
?.firstOrError()
}
/**
* Replaces new coordinates
* @param media to be added
* @param Latitude to be added
* @param Longitude to be added
* @param Accuracy to be added
* @return Observable<Boolean>
*/
private fun addCoordinates(
media: Media,
Latitude: String,
Longitude: String,
Accuracy: String
): Observable<Boolean>? {
Timber.d("thread is coordinates adding %s", Thread.currentThread().getName())
val summary = "Adding Coordinates"
val buffer = StringBuilder()
val wikiText = media.filename?.let {
pageEditClient.getCurrentWikiText(it)
.subscribeOn(Schedulers.io())
.blockingGet()
}
if (Latitude != null) {
buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude)
.append("|").append(Accuracy).append("}}")
}
val editedLocation = buffer.toString()
val appendText = wikiText?.let { getFormattedWikiText(it, editedLocation) }
return Objects.requireNonNull(media.filename)
?.let { pageEditClient.edit(it, appendText!!, summary) }
}
/**
* Helps to get formatted wikitext with upgraded location
* @param wikiText current wikitext
* @param editedLocation new location
* @return String
*/
private fun getFormattedWikiText(wikiText: String, editedLocation: String): String {
if (wikiText.contains("filedesc") && wikiText.contains("Location")) {
val fromLocationToEnd = wikiText.substring(wikiText.indexOf("{{Location"))
val firstHalf = wikiText.substring(0, wikiText.indexOf("{{Location"))
val lastHalf = fromLocationToEnd.substring(fromLocationToEnd.indexOf("}}") + 2)
val startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, "==", 3)
val buffer = StringBuilder()
if (wikiText[wikiText.indexOf("{{Location") - 1] == '\n') {
buffer.append(editedLocation.substring(1))
} else {
buffer.append(editedLocation)
}
if (startOfSecondSection != -1 && wikiText[startOfSecondSection - 1] != '\n') {
buffer.append("\n")
}
return firstHalf + buffer + lastHalf
}
if (wikiText.contains("filedesc") && !wikiText.contains("Location")) {
val startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, "==", 3)
if (startOfSecondSection != -1) {
val firstHalf = wikiText.substring(0, startOfSecondSection)
val lastHalf = wikiText.substring(startOfSecondSection)
val buffer = editedLocation.substring(1) + "\n"
return firstHalf + buffer + lastHalf
}
return wikiText + editedLocation
}
return "== {{int:filedesc}} ==$editedLocation$wikiText"
}
/**
* Update coordinates and shows notification about coordinates update
* @param context to be added
* @param media to be added
* @param latitude to be added
* @param longitude to be added
* @param Accuracy to be added
* @param result to be added
* @return boolean
*/
private fun showCoordinatesEditNotification(
context: Context,
media: Media,
latitude: String,
longitude: String,
Accuracy: String,
result: Boolean
): Boolean {
val message: String
var title = context.getString(R.string.coordinates_edit_helper_show_edit_title)
if (result) {
media.coordinates = fr.free.nrw.commons.location.LatLng(
latitude.toDouble(),
longitude.toDouble(),
Accuracy.toFloat()
)
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title_success)
val coordinatesInMessage = StringBuilder()
val mediaCoordinate = media.coordinates.toString()
coordinatesInMessage.append(mediaCoordinate)
message = context.getString(
R.string.coordinates_edit_helper_show_edit_message,
coordinatesInMessage.toString()
)
} else {
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title)
message = context.getString(R.string.coordinates_edit_helper_edit_message_else)
}
val urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.filename
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
notificationHelper.showNotification(
context,
title,
message,
NOTIFICATION_EDIT_COORDINATES,
browserIntent
)
return result
}
}

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.data;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "commons.db";
private static final int DATABASE_VERSION = 20;
public static final String CONTRIBUTIONS_TABLE = "contributions";
private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s";
/**
* Do not use directly - @Inject an instance where it's needed and let
* dependency injection take care of managing this as a singleton.
*/
public DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
CategoryDao.Table.onCreate(sqLiteDatabase);
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
BookmarkItemsDao.Table.onCreate(sqLiteDatabase);
RecentSearchesDao.Table.onCreate(sqLiteDatabase);
RecentLanguagesDao.Table.onCreate(sqLiteDatabase);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkItemsDao.Table.onUpdate(sqLiteDatabase, from, to);
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
RecentLanguagesDao.Table.onUpdate(sqLiteDatabase, from, to);
deleteTable(sqLiteDatabase,CONTRIBUTIONS_TABLE);
}
/**
* Delete table in the given db
* @param db
* @param tableName
*/
public void deleteTable(SQLiteDatabase db, String tableName) {
try {
db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName));
onCreate(db);
} catch (SQLiteException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,62 @@
package fr.free.nrw.commons.data
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
class DBOpenHelper(
context: Context
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "commons.db"
private const val DATABASE_VERSION = 20
const val CONTRIBUTIONS_TABLE = "contributions"
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
}
/**
* Do not use directly - @Inject an instance where it's needed and let
* dependency injection take care of managing this as a singleton.
*/
override fun onCreate(db: SQLiteDatabase) {
CategoryDao.Table.onCreate(db)
BookmarkPicturesDao.Table.onCreate(db)
BookmarkLocationsDao.Table.onCreate(db)
BookmarkItemsDao.Table.onCreate(db)
RecentSearchesDao.Table.onCreate(db)
RecentLanguagesDao.Table.onCreate(db)
}
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
CategoryDao.Table.onUpdate(db, from, to)
BookmarkPicturesDao.Table.onUpdate(db, from, to)
BookmarkLocationsDao.Table.onUpdate(db, from, to)
BookmarkItemsDao.Table.onUpdate(db, from, to)
RecentSearchesDao.Table.onUpdate(db, from, to)
RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE)
}
/**
* Delete table in the given db
* @param db
* @param tableName
*/
fun deleteTable(db: SQLiteDatabase, tableName: String) {
try {
db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName))
onCreate(db)
} catch (e: SQLiteException) {
e.printStackTrace()
}
}
}

View file

@ -1,165 +0,0 @@
package fr.free.nrw.commons.db;
import android.net.Uri;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Sitelinks;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* This class supplies converters to write/read types to/from the database.
*/
public class Converters {
public static Gson getGson() {
return ApplicationlessInjection
.getInstance(CommonsApplication.getInstance())
.getCommonsApplicationComponent()
.gson();
}
/**
* convert DepictedItem object to string
* input Example -> DepictedItem depictedItem=new DepictedItem ()
* output Example -> string
*/
@TypeConverter
public static String depictsItemToString(DepictedItem objects) {
return writeObjectToString(objects);
}
/**
* convert string to DepictedItem object
* output Example -> DepictedItem depictedItem=new DepictedItem ()
* input Example -> string
*/
@TypeConverter
public static DepictedItem stringToDepicts(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<DepictedItem>() {
});
}
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
@TypeConverter
public static Uri fromString(String value) {
return value == null ? null : Uri.parse(value);
}
@TypeConverter
public static String uriToString(Uri uri) {
return uri == null ? null : uri.toString();
}
@TypeConverter
public static String listObjectToString(List<String> objectList) {
return writeObjectToString(objectList);
}
@TypeConverter
public static List<String> stringToListObject(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<List<String>>() {});
}
@TypeConverter
public static String mapObjectToString(Map<String,String> objectList) {
return writeObjectToString(objectList);
}
@TypeConverter
public static String mapObjectToString2(Map<String,Boolean> objectList) {
return writeObjectToString(objectList);
}
@TypeConverter
public static Map<String,String> stringToMap(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
}
@TypeConverter
public static Map<String,Boolean> stringToMap2(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<Map<String,Boolean>>(){});
}
@TypeConverter
public static String latlngObjectToString(LatLng latlng) {
return writeObjectToString(latlng);
}
@TypeConverter
public static LatLng stringToLatLng(String objectList) {
return readObjectFromString(objectList,LatLng.class);
}
@TypeConverter
public static String wikidataPlaceToString(WikidataPlace wikidataPlace) {
return writeObjectToString(wikidataPlace);
}
@TypeConverter
public static WikidataPlace stringToWikidataPlace(String wikidataPlace) {
return readObjectFromString(wikidataPlace, WikidataPlace.class);
}
@TypeConverter
public static String chunkInfoToString(ChunkInfo chunkInfo) {
return writeObjectToString(chunkInfo);
}
@TypeConverter
public static ChunkInfo stringToChunkInfo(String chunkInfo) {
return readObjectFromString(chunkInfo, ChunkInfo.class);
}
@TypeConverter
public static String depictionListToString(List<DepictedItem> depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static List<DepictedItem> stringToList(String depictedItems) {
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
}
@TypeConverter
public static Sitelinks sitelinksFromString(String value) {
Type type = new TypeToken<Sitelinks>() {}.getType();
return new Gson().fromJson(value, type);
}
@TypeConverter
public static String fromSitelinks(Sitelinks sitelinks) {
Gson gson = new Gson();
return gson.toJson(sitelinks);
}
private static String writeObjectToString(Object object) {
return object == null ? null : getGson().toJson(object);
}
private static<T> T readObjectFromString(String objectAsString, Class<T> clazz) {
return objectAsString == null ? null : getGson().fromJson(objectAsString, clazz);
}
private static <T> T readObjectWithTypeToken(String objectList, TypeToken<T> typeToken) {
return objectList == null ? null : getGson().fromJson(objectList, typeToken.getType());
}
}

View file

@ -0,0 +1,182 @@
package fr.free.nrw.commons.db
import android.net.Uri
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Sitelinks
import fr.free.nrw.commons.upload.WikidataPlace
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import java.util.Date
/**
* This object supplies converters to write/read types to/from the database.
*/
object Converters {
fun getGson(): Gson {
return ApplicationlessInjection
.getInstance(CommonsApplication.instance)
.commonsApplicationComponent
.gson()
}
/**
* convert DepictedItem object to string
* input Example -> DepictedItem depictedItem=new DepictedItem ()
* output Example -> string
*/
@TypeConverter
@JvmStatic
fun depictsItemToString(objects: DepictedItem?): String? {
return writeObjectToString(objects)
}
/**
* convert string to DepictedItem object
* output Example -> DepictedItem depictedItem=new DepictedItem ()
* input Example -> string
*/
@TypeConverter
@JvmStatic
fun stringToDepicts(objectList: String?): DepictedItem? {
return readObjectWithTypeToken(objectList, object : TypeToken<DepictedItem>() {})
}
@TypeConverter
@JvmStatic
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
@JvmStatic
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
@TypeConverter
@JvmStatic
fun fromString(value: String?): Uri? {
return value?.let { Uri.parse(it) }
}
@TypeConverter
@JvmStatic
fun uriToString(uri: Uri?): String? {
return uri?.toString()
}
@TypeConverter
@JvmStatic
fun listObjectToString(objectList: List<String>?): String? {
return writeObjectToString(objectList)
}
@TypeConverter
@JvmStatic
fun stringToListObject(objectList: String?): List<String>? {
return readObjectWithTypeToken(objectList, object : TypeToken<List<String>>() {})
}
@TypeConverter
@JvmStatic
fun mapObjectToString(objectList: Map<String, String>?): String? {
return writeObjectToString(objectList)
}
@TypeConverter
@JvmStatic
fun mapObjectToString2(objectList: Map<String, Boolean>?): String? {
return writeObjectToString(objectList)
}
@TypeConverter
@JvmStatic
fun stringToMap(objectList: String?): Map<String, String>? {
return readObjectWithTypeToken(objectList, object : TypeToken<Map<String, String>>() {})
}
@TypeConverter
@JvmStatic
fun stringToMap2(objectList: String?): Map<String, Boolean>? {
return readObjectWithTypeToken(objectList, object : TypeToken<Map<String, Boolean>>() {})
}
@TypeConverter
@JvmStatic
fun latlngObjectToString(latlng: LatLng?): String? {
return writeObjectToString(latlng)
}
@TypeConverter
@JvmStatic
fun stringToLatLng(objectList: String?): LatLng? {
return readObjectFromString(objectList, LatLng::class.java)
}
@TypeConverter
@JvmStatic
fun wikidataPlaceToString(wikidataPlace: WikidataPlace?): String? {
return writeObjectToString(wikidataPlace)
}
@TypeConverter
@JvmStatic
fun stringToWikidataPlace(wikidataPlace: String?): WikidataPlace? {
return readObjectFromString(wikidataPlace, WikidataPlace::class.java)
}
@TypeConverter
@JvmStatic
fun chunkInfoToString(chunkInfo: ChunkInfo?): String? {
return writeObjectToString(chunkInfo)
}
@TypeConverter
@JvmStatic
fun stringToChunkInfo(chunkInfo: String?): ChunkInfo? {
return readObjectFromString(chunkInfo, ChunkInfo::class.java)
}
@TypeConverter
@JvmStatic
fun depictionListToString(depictedItems: List<DepictedItem>?): String? {
return writeObjectToString(depictedItems)
}
@TypeConverter
@JvmStatic
fun stringToList(depictedItems: String?): List<DepictedItem>? {
return readObjectWithTypeToken(depictedItems, object : TypeToken<List<DepictedItem>>() {})
}
@TypeConverter
@JvmStatic
fun sitelinksFromString(value: String?): Sitelinks? {
val type = object : TypeToken<Sitelinks>() {}.type
return Gson().fromJson(value, type)
}
@TypeConverter
@JvmStatic
fun fromSitelinks(sitelinks: Sitelinks?): String? {
return Gson().toJson(sitelinks)
}
private fun writeObjectToString(`object`: Any?): String? {
return `object`?.let { getGson().toJson(it) }
}
private fun <T> readObjectFromString(objectAsString: String?, clazz: Class<T>): T? {
return objectAsString?.let { getGson().fromJson(it, clazz) }
}
private fun <T> readObjectWithTypeToken(objectList: String?, typeToken: TypeToken<T>): T? {
return objectList?.let { getGson().fromJson(it, typeToken.type) }
}
}

View file

@ -1,278 +0,0 @@
package fr.free.nrw.commons.delete;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import androidx.appcompat.app.AlertDialog;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.utils.LangCodeUtils;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber;
/**
* Refactored async task to Rx
*/
@Singleton
public class DeleteHelper {
private final NotificationHelper notificationHelper;
private final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
private final String username;
private AlertDialog d;
private DialogInterface.OnMultiChoiceClickListener listener;
@Inject
public DeleteHelper(NotificationHelper notificationHelper,
@Named("commons-page-edit") PageEditClient pageEditClient,
ViewUtilWrapper viewUtil,
@Named("username") String username) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil;
this.username = username;
}
/**
* Public interface to nominate a particular media file for deletion
* @param context
* @param media
* @param reason
* @return
*/
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
return delete(media, reason)
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
.firstOrError()
.onErrorResumeNext(throwable -> {
if (throwable instanceof InvalidLoginTokenException) {
return Single.error(throwable);
}
return Single.error(throwable);
});
}
/**
* Makes several API calls to nominate the file for deletion
* @param media
* @param reason
* @return
*/
private Observable<Boolean> delete(Media media, String reason) {
Timber.d("thread is delete %s", Thread.currentThread().getName());
String summary = "Nominating " + media.getFilename() + " for deletion.";
Calendar calendar = Calendar.getInstance();
String fileDeleteString = "{{delete|reason=" + reason +
"|subpage=" + media.getFilename() +
"|day=" + calendar.get(Calendar.DAY_OF_MONTH) +
"|month=" + calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH) +
"|year=" + calendar.get(Calendar.YEAR) +
"}}";
String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" +
reason +
" ~~~~";
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
"}}\n";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
String date = sdf.format(calendar.getTime());
String userPageString = "\n{{subst:idw|" + media.getFilename() +
"}} ~~~~";
String creator = media.getAuthor();
if (creator == null || creator.isEmpty()) {
throw new RuntimeException("Failed to nominate for deletion");
}
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
.onErrorResumeNext(throwable -> {
if (throwable instanceof InvalidLoginTokenException) {
return Observable.error(throwable);
}
return Observable.error(throwable);
})
.flatMap(result -> {
if (result) {
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
}
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
})
.flatMap(result -> {
if (result) {
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
}
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
})
.flatMap(result -> {
if (result) {
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
}
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
});
}
private boolean showDeletionNotification(Context context, Media media, boolean result) {
String message;
String title = context.getString(R.string.delete_helper_show_deletion_title);
if (result) {
title += ": " + context.getString(R.string.delete_helper_show_deletion_title_success);
message = context.getString((R.string.delete_helper_show_deletion_message_if),media.getDisplayTitle());
} else {
title += ": " + context.getString(R.string.delete_helper_show_deletion_title_failed);
message = context.getString(R.string.delete_helper_show_deletion_message_else) ;
}
String urlForDelete = BuildConfig.COMMONS_URL + "/wiki/Commons:Deletion_requests/" + media.getFilename();
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForDelete));
notificationHelper.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent);
return result;
}
/**
* Invoked when a reason needs to be asked before nominating for deletion
* @param media
* @param context
* @param question
* @param problem
*/
@SuppressLint("CheckResult")
public void askReasonAndExecute(Media media,
Context context,
String question,
ReviewController.DeleteReason problem,
ReviewController.ReviewCallback reviewCallback) {
AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(question);
boolean[] checkedItems = {false, false, false, false};
ArrayList<Integer> mUserReason = new ArrayList<>();
final String[] reasonList;
final String[] reasonListEnglish;
if (problem == ReviewController.DeleteReason.SPAM) {
reasonList = new String[] {
context.getString(R.string.delete_helper_ask_spam_selfie),
context.getString(R.string.delete_helper_ask_spam_blurry),
context.getString(R.string.delete_helper_ask_spam_nonsense)
};
reasonListEnglish = new String[] {
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_selfie),
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_blurry),
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_nonsense)
};
} else if (problem == ReviewController.DeleteReason.COPYRIGHT_VIOLATION) {
reasonList = new String[] {
context.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
context.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
context.getString(R.string.delete_helper_ask_reason_copyright_logo),
context.getString(R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama)
};
reasonListEnglish = new String[] {
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_press_photo),
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_logo),
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama)
};
} else {
reasonList = new String[] {};
reasonListEnglish = new String[] {};
}
alert.setMultiChoiceItems(reasonList, checkedItems, listener = (dialogInterface, position, isChecked) -> {
if (isChecked) {
mUserReason.add(position);
} else {
mUserReason.remove((Integer.valueOf(position)));
}
// disable the OK button if no reason selected
((AlertDialog) dialogInterface).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(
!mUserReason.isEmpty());
});
alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> {
reviewCallback.disableButtons();
String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " ";
for (int j = 0; j < mUserReason.size(); j++) {
reason = reason + reasonListEnglish[mUserReason.get(j)];
if (j != mUserReason.size() - 1) {
reason = reason + ", ";
}
}
Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().getName());
String finalReason = reason;
Single.defer((Callable<SingleSource<Boolean>>) () ->
makeDeletion(context, media, finalReason))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> {
reviewCallback.onSuccess();
}, throwable -> {
if (throwable instanceof InvalidLoginTokenException) {
reviewCallback.onTokenException((InvalidLoginTokenException) throwable);
} else {
reviewCallback.onFailure();
}
reviewCallback.enableButtons();
});
});
alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure());
d = alert.create();
d.show();
// disable the OK button by default
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
/**
* returns the instance of shown AlertDialog,
* used for taking reference during unit test
* */
public AlertDialog getDialog(){
return d;
}
/**
* returns the instance of shown DialogInterface.OnMultiChoiceClickListener,
* used for taking reference during unit test
* */
public DialogInterface.OnMultiChoiceClickListener getListener(){
return listener;
}
}

View file

@ -0,0 +1,333 @@
package fr.free.nrw.commons.delete
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AlertDialog
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_DELETE
import fr.free.nrw.commons.review.ReviewController
import fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
/**
* Refactored async task to Rx
*/
@Singleton
class DeleteHelper @Inject constructor(
private val notificationHelper: NotificationHelper,
@Named("commons-page-edit") private val pageEditClient: PageEditClient,
private val viewUtil: ViewUtilWrapper,
@Named("username") private val username: String
) {
private var d: AlertDialog? = null
private var listener: DialogInterface.OnMultiChoiceClickListener? = null
/**
* Public interface to nominate a particular media file for deletion
* @param context
* @param media
* @param reason
* @return
*/
fun makeDeletion(
context: Context?,
media: Media?,
reason: String?
): Single<Boolean>? {
if(context == null && media == null) {
return null
}
viewUtil.showShortToast(
context!!,
"Trying to nominate ${media?.displayTitle} for deletion"
)
return reason?.let {
delete(media!!, it)
.flatMapSingle { result ->
Single.just(showDeletionNotification(context, media, result))
}
.firstOrError()
.onErrorResumeNext { throwable ->
if (throwable is InvalidLoginTokenException) {
Single.error(throwable)
} else {
Single.error(throwable)
}
}
}
}
/**
* Makes several API calls to nominate the file for deletion
* @param media
* @param reason
* @return
*/
private fun delete(media: Media, reason: String): Observable<Boolean> {
Timber.d("thread is delete %s", Thread.currentThread().name)
val summary = "Nominating ${media.filename} for deletion."
val calendar = Calendar.getInstance()
val fileDeleteString = """
{{delete|reason=$reason|subpage=${media.filename}|day=
${calendar.get(Calendar.DAY_OF_MONTH)}|month=${
calendar.getDisplayName(
Calendar.MONTH,
Calendar.LONG,
Locale.ENGLISH
)
}|year=${calendar.get(Calendar.YEAR)}}}
""".trimIndent()
val subpageString = """
=== [[:${media.filename}]] ===
$reason ~~~~
""".trimIndent()
val logPageString = "\n{{Commons:Deletion requests/${media.filename}}}\n"
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
val date = sdf.format(calendar.time)
val userPageString = "\n{{subst:idw|${media.filename}}} ~~~~"
val creator = media.author
?: throw RuntimeException("Failed to nominate for deletion")
return pageEditClient.prependEdit(
media.filename!!,
"$fileDeleteString\n",
summary
)
.onErrorResumeNext { throwable: Throwable ->
if (throwable is InvalidLoginTokenException) {
Observable.error(throwable)
} else {
Observable.error(throwable)
}
}
.flatMap { result: Boolean ->
if (result) {
pageEditClient.edit(
"Commons:Deletion_requests/${media.filename}",
"$subpageString\n",
summary
)
} else {
Observable.error(RuntimeException("Failed to nominate for deletion"))
}
}
.flatMap { result: Boolean ->
if (result) {
pageEditClient.appendEdit(
"Commons:Deletion_requests/$date",
"$logPageString\n",
summary
)
} else {
Observable.error(RuntimeException("Failed to nominate for deletion"))
}
}
.flatMap { result: Boolean ->
if (result) {
pageEditClient.appendEdit("User_Talk:$creator", "$userPageString\n", summary)
} else {
Observable.error(RuntimeException("Failed to nominate for deletion"))
}
}
}
@SuppressLint("StringFormatInvalid")
private fun showDeletionNotification(
context: Context,
media: Media,
result: Boolean
): Boolean {
val title: String
val message: String
var baseTitle = context.getString(R.string.delete_helper_show_deletion_title)
if (result) {
baseTitle += ": ${
context.getString(R.string.delete_helper_show_deletion_title_success)
}"
title = baseTitle
message = context
.getString(R.string.delete_helper_show_deletion_message_if, media.displayTitle)
} else {
baseTitle += ": ${context.getString(R.string.delete_helper_show_deletion_title_failed)}"
title = baseTitle
message = context.getString(R.string.delete_helper_show_deletion_message_else)
}
val urlForDelete = "${BuildConfig.COMMONS_URL}/wiki/Commons:Deletion_requests/${
media.filename
}"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForDelete))
notificationHelper
.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent)
return result
}
/**
* Invoked when a reason needs to be asked before nominating for deletion
* @param media
* @param context
* @param question
* @param problem
*/
@SuppressLint("CheckResult")
fun askReasonAndExecute(
media: Media?,
context: Context,
question: String,
problem: ReviewController.DeleteReason,
reviewCallback: ReviewController.ReviewCallback
) {
val alert = AlertDialog.Builder(context)
alert.setTitle(question)
val checkedItems = booleanArrayOf(false, false, false, false)
val mUserReason = arrayListOf<Int>()
val reasonList: Array<String>
val reasonListEnglish: Array<String>
when (problem) {
ReviewController.DeleteReason.SPAM -> {
reasonList = arrayOf(
context.getString(R.string.delete_helper_ask_spam_selfie),
context.getString(R.string.delete_helper_ask_spam_blurry),
context.getString(R.string.delete_helper_ask_spam_nonsense)
)
reasonListEnglish = arrayOf(
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_spam_selfie),
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_spam_blurry),
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_spam_nonsense)
)
}
ReviewController.DeleteReason.COPYRIGHT_VIOLATION -> {
reasonList = arrayOf(
context.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
context.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
context.getString(R.string.delete_helper_ask_reason_copyright_logo),
context.getString(
R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama
)
)
reasonListEnglish = arrayOf(
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_reason_copyright_logo),
getLocalizedResources(context, Locale.ENGLISH)
.getString(
R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama
)
)
}
else -> {
reasonList = emptyArray()
reasonListEnglish = emptyArray()
}
}
alert.setMultiChoiceItems(
reasonList,
checkedItems
) { dialogInterface, position, isChecked ->
if (isChecked) {
mUserReason.add(position)
} else {
mUserReason.remove(position)
}
// Safely enable or disable the OK button based on selection
val dialog = dialogInterface as? AlertDialog
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = mUserReason.isNotEmpty()
}
alert.setPositiveButton(context.getString(R.string.ok)) { _, _ ->
reviewCallback.disableButtons()
val reason = buildString {
append(
getLocalizedResources(context, Locale.ENGLISH)
.getString(R.string.delete_helper_ask_alert_set_positive_button_reason)
)
append(" ")
mUserReason.forEachIndexed { index, position ->
append(reasonListEnglish[position])
if (index != mUserReason.lastIndex) {
append(", ")
}
}
}
Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().name)
if (media != null) {
Single.defer { makeDeletion(context, media, reason) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ reviewCallback.onSuccess() }, { throwable ->
when (throwable) {
is InvalidLoginTokenException ->
reviewCallback.onTokenException(throwable)
else -> reviewCallback.onFailure()
}
reviewCallback.enableButtons()
})
}
}
alert.setNegativeButton(
context.getString(R.string.cancel)
) { _, _ -> reviewCallback.onFailure() }
d = alert.create()
d?.setOnShowListener {
// Safely initialize the OK button state after the dialog is fully shown
d?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false
}
d?.show()
}
/**
* returns the instance of shown AlertDialog,
* used for taking reference during unit test
*/
fun getDialog(): AlertDialog? = d
/**
* returns the instance of shown DialogInterface.OnMultiChoiceClickListener,
* used for taking reference during unit test
*/
fun getListener(): DialogInterface.OnMultiChoiceClickListener? = listener
}

View file

@ -1,100 +0,0 @@
package fr.free.nrw.commons.delete;
import android.content.Context;
import fr.free.nrw.commons.utils.DateUtil;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import timber.log.Timber;
/**
* This class handles the reason for deleting a Media object
*/
@Singleton
public class ReasonBuilder {
private SessionManager sessionManager;
private OkHttpJsonApiClient okHttpJsonApiClient;
private Context context;
private ViewUtilWrapper viewUtilWrapper;
@Inject
public ReasonBuilder(Context context,
SessionManager sessionManager,
OkHttpJsonApiClient okHttpJsonApiClient,
ViewUtilWrapper viewUtilWrapper) {
this.context = context;
this.sessionManager = sessionManager;
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.viewUtilWrapper = viewUtilWrapper;
}
/**
* To process the reason and append the media's upload date and uploaded_by_me string
* @param media
* @param reason
* @return
*/
public Single<String> getReason(Media media, String reason) {
return fetchArticleNumber(media, reason);
}
/**
* get upload date for the passed Media
*/
private String prettyUploadedDate(Media media) {
Date date = media.getDateUploaded();
if (date == null || date.toString() == null || date.toString().isEmpty()) {
return "Uploaded date not available";
}
return DateUtil.getDateStringWithSkeletonPattern(date,"dd MMM yyyy");
}
private Single<String> fetchArticleNumber(Media media, String reason) {
if (checkAccount()) {
return okHttpJsonApiClient
.getAchievements(sessionManager.getUserName())
.map(feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason));
}
return Single.just("");
}
/**
* Takes the uploaded_by_me string, the upload date, name of articles using images
* and appends it to the received reason
* @param feedBack object
* @param media whose upload data is to be fetched
* @param reason
*/
private String appendArticlesUsed(FeedbackResponse feedBack, Media media, String reason) {
String reason1Template = context.getString(R.string.uploaded_by_myself);
reason += String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.getArticlesUsingImages());
Timber.i("New Reason %s", reason);
return reason;
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
if (!sessionManager.doesAccountExist()) {
Timber.d("Current account is null");
viewUtilWrapper.showLongToast(context, context.getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(context);
return false;
}
return true;
}
}

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.delete
import android.annotation.SuppressLint
import android.content.Context
import fr.free.nrw.commons.utils.DateUtil
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Single
import timber.log.Timber
/**
* This class handles the reason for deleting a Media object
*/
@Singleton
class ReasonBuilder @Inject constructor(
private val context: Context,
private val sessionManager: SessionManager,
private val okHttpJsonApiClient: OkHttpJsonApiClient,
private val viewUtilWrapper: ViewUtilWrapper
) {
/**
* To process the reason and append the media's upload date and uploaded_by_me string
* @param media
* @param reason
* @return
*/
fun getReason(media: Media?, reason: String?): Single<String> {
if (media == null || reason == null) {
return Single.just("Not known")
}
return fetchArticleNumber(media, reason)
}
/**
* get upload date for the passed Media
*/
private fun prettyUploadedDate(media: Media): String {
val date = media.dateUploaded
return if (date == null || date.toString().isEmpty()) {
"Uploaded date not available"
} else {
DateUtil.getDateStringWithSkeletonPattern(date, "dd MMM yyyy")
}
}
private fun fetchArticleNumber(media: Media, reason: String): Single<String> {
return if (checkAccount()) {
okHttpJsonApiClient
.getAchievements(sessionManager.userName)
.map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) }
} else {
Single.just("")
}
}
/**
* Takes the uploaded_by_me string, the upload date, name of articles using images
* and appends it to the received reason
* @param feedBack object
* @param media whose upload data is to be fetched
* @param reason
*/
@SuppressLint("StringFormatInvalid")
private fun appendArticlesUsed(feedBack: FeedbackResponse, media: Media, reason: String): String {
val reason1Template = context.getString(R.string.uploaded_by_myself)
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.articlesUsingImages)
.also { Timber.i("New Reason %s", it) }
}
/**
* check to ensure that user is logged in
* @return
*/
private fun checkAccount(): Boolean {
return if (!sessionManager.doesAccountExist()) {
Timber.d("Current account is null")
viewUtilWrapper.showLongToast(context, context.getString(R.string.user_not_logged_in))
sessionManager.forceLogin(context)
false
} else {
true
}
}
}

View file

@ -272,7 +272,7 @@ class DescriptionEditActivity :
.addCaption(
applicationContext,
media,
mediaDetail.languageCode,
mediaDetail.languageCode!!,
mediaDetail.captionText,
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View file

@ -1,137 +0,0 @@
package fr.free.nrw.commons.description;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_DESCRIPTION;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import io.reactivex.Single;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* Helper class for edit and update given descriptions and showing notification upgradation
*/
public class DescriptionEditHelper {
/**
* notificationHelper: helps creating notification
*/
private final NotificationHelper notificationHelper;
/**
* * pageEditClient: methods provided by this member posts the edited descriptions
* to the Media wiki api
*/
public final PageEditClient pageEditClient;
@Inject
public DescriptionEditHelper(final NotificationHelper notificationHelper,
@Named("commons-page-edit") final PageEditClient pageEditClient) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
}
/**
* Replaces new descriptions
*
* @param context context
* @param media to be added
* @param appendText to be added
* @return Observable<Boolean>
*/
public Single<Boolean> addDescription(final Context context, final Media media,
final String appendText) {
Timber.d("thread is description adding %s", Thread.currentThread().getName());
final String summary = "Updating Description";
return pageEditClient.edit(Objects.requireNonNull(media.getFilename()),
appendText, summary)
.flatMapSingle(result -> Single.just(showDescriptionEditNotification(context,
media, result)))
.firstOrError();
}
/**
* Adds new captions
*
* @param context context
* @param media to be added
* @param language to be added
* @param value to be added
* @return Observable<Boolean>
*/
public Single<Boolean> addCaption(final Context context, final Media media,
final String language, final String value) {
Timber.d("thread is caption adding %s", Thread.currentThread().getName());
final String summary = "Updating Caption";
return pageEditClient.setCaptions(summary, Objects.requireNonNull(media.getFilename()),
language, value)
.flatMapSingle(result -> Single.just(showCaptionEditNotification(context,
media, result)))
.firstOrError();
}
/**
* Update captions and shows notification about captions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private boolean showCaptionEditNotification(final Context context, final Media media,
final int result) {
final String message;
String title = context.getString(R.string.caption_edit_helper_show_edit_title);
if (result == 1) {
title += ": " + context
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
message = context.getString(R.string.caption_edit_helper_show_edit_message);
} else {
title += ": " + context.getString(R.string.caption_edit_helper_show_edit_title);
message = context.getString(R.string.caption_edit_helper_edit_message_else) ;
}
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
browserIntent);
return result == 1;
}
/**
* Update descriptions and shows notification about descriptions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private boolean showDescriptionEditNotification(final Context context, final Media media,
final boolean result) {
final String message;
String title = context.getString(R.string.description_edit_helper_show_edit_title);
if (result) {
title += ": " + context
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
message = context.getString(R.string.description_edit_helper_show_edit_message);
} else {
title += ": " + context.getString(R.string.description_edit_helper_show_edit_title);
message = context.getString(R.string.description_edit_helper_edit_message_else) ;
}
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
browserIntent);
return result;
}
}

View file

@ -0,0 +1,154 @@
package fr.free.nrw.commons.description
import android.content.Context
import android.content.Intent
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_EDIT_DESCRIPTION
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Named
import timber.log.Timber
/**
* Helper class for edit and update given descriptions and showing notification upgradation
*/
class DescriptionEditHelper @Inject constructor(
/**
* notificationHelper: helps creating notification
*/
private val notificationHelper: NotificationHelper,
/**
* pageEditClient: methods provided by this member posts the edited descriptions
* to the Media wiki api
*/
@Named("commons-page-edit") val pageEditClient: PageEditClient
) {
/**
* Replaces new descriptions
*
* @param context context
* @param media to be added
* @param appendText to be added
* @return Single<Boolean>
*/
fun addDescription(context: Context, media: Media, appendText: String): Single<Boolean> {
Timber.d("thread is description adding %s", Thread.currentThread().name)
val summary = "Updating Description"
return pageEditClient.edit(
requireNotNull(media.filename),
appendText,
summary
).flatMapSingle { result ->
Single.just(showDescriptionEditNotification(context, media, result))
}.firstOrError()
}
/**
* Adds new captions
*
* @param context context
* @param media to be added
* @param language to be added
* @param value to be added
* @return Single<Boolean>
*/
fun addCaption(
context: Context,
media: Media,
language: String,
value: String
): Single<Boolean> {
Timber.d("thread is caption adding %s", Thread.currentThread().name)
val summary = "Updating Caption"
return pageEditClient.setCaptions(
summary,
requireNotNull(media.filename),
language,
value
).flatMapSingle { result ->
Single.just(showCaptionEditNotification(context, media, result))
}.firstOrError()
}
/**
* Update captions and shows notification about captions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private fun showCaptionEditNotification(context: Context, media: Media, result: Int): Boolean {
val message: String
var title = context.getString(R.string.caption_edit_helper_show_edit_title)
if (result == 1) {
title += ": " + context.getString(
R.string.coordinates_edit_helper_show_edit_title_success
)
message = context.getString(R.string.caption_edit_helper_show_edit_message)
} else {
title += ": " + context.getString(R.string.caption_edit_helper_show_edit_title)
message = context.getString(R.string.caption_edit_helper_edit_message_else)
}
val urlForFile = "${BuildConfig.COMMONS_URL}/wiki/${media.filename}"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
notificationHelper.showNotification(
context,
title,
message,
NOTIFICATION_EDIT_DESCRIPTION,
browserIntent
)
return result == 1
}
/**
* Update descriptions and shows notification about descriptions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private fun showDescriptionEditNotification(
context: Context,
media: Media,
result: Boolean
): Boolean {
val message: String
var title= context.getString(
R.string.description_edit_helper_show_edit_title
)
if (result) {
title += ": " + context.getString(
R.string.coordinates_edit_helper_show_edit_title_success
)
message = context.getString(R.string.description_edit_helper_show_edit_message)
} else {
title += ": " + context.getString(R.string.description_edit_helper_show_edit_title)
message = context.getString(R.string.description_edit_helper_edit_message_else)
}
val urlForFile = "${BuildConfig.COMMONS_URL}/wiki/${media.filename}"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
notificationHelper.showNotification(
context,
title,
message,
NOTIFICATION_EDIT_DESCRIPTION,
browserIntent
)
return result
}
}

View file

@ -15,11 +15,14 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import fr.free.nrw.commons.R
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.powermock.api.mockito.PowerMockito.`when`
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@ -44,7 +47,7 @@ class DeleteHelperTest {
@Mock
internal lateinit var media: Media
lateinit var deleteHelper: DeleteHelper
private lateinit var deleteHelper: DeleteHelper
/**
* Init mocks for test
@ -60,19 +63,46 @@ class DeleteHelperTest {
*/
@Test
fun makeDeletion() {
whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true))
whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true))
whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true))
whenever(pageEditClient.prependEdit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(true))
whenever(pageEditClient.appendEdit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(true))
whenever(pageEditClient.edit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(true))
whenever(media.displayTitle).thenReturn("Test file")
`when`(context.getString(R.string.delete_helper_show_deletion_title))
.thenReturn("Deletion Notification")
`when`(context.getString(R.string.delete_helper_show_deletion_title_success))
.thenReturn("Success")
`when`(context.getString(R.string.delete_helper_show_deletion_title_failed))
.thenReturn("Failed")
`when`(context.getString(R.string.delete_helper_show_deletion_message_else))
.thenReturn("Media deletion failed")
`when`(context.getString(
R.string.delete_helper_show_deletion_message_if, media.displayTitle)
).thenReturn("Media successfully deleted: Test Media Title")
val creatorName = "Creator"
whenever(media.author).thenReturn("$creatorName")
whenever(media.filename).thenReturn("Test file.jpg")
val makeDeletion = deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
val makeDeletion = deleteHelper.makeDeletion(
context,
media,
"Test reason"
)?.blockingGet()
assertNotNull(makeDeletion)
assertTrue(makeDeletion!!)
verify(pageEditClient).appendEdit(eq("User_Talk:$creatorName"), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
@ -83,12 +113,24 @@ class DeleteHelperTest {
*/
@Test(expected = RuntimeException::class)
fun makeDeletionForPrependEditFailure() {
whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(false))
whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true))
whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true))
whenever(pageEditClient.prependEdit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(false))
whenever(pageEditClient.appendEdit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(true))
whenever(pageEditClient.edit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString())
).thenReturn(Observable.just(true))
whenever(media.displayTitle).thenReturn("Test file")
whenever(media.filename).thenReturn("Test file.jpg")
whenever(media.author).thenReturn("Creator (page does not exist)")
@ -141,16 +183,30 @@ class DeleteHelperTest {
@Test
fun alertDialogPositiveButtonDisableTest() {
val mContext = RuntimeEnvironment.getApplication().applicationContext
deleteHelper.askReasonAndExecute(media, mContext, "My Question", ReviewController.DeleteReason.COPYRIGHT_VIOLATION, callback)
assertEquals(false, deleteHelper.dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled)
deleteHelper.askReasonAndExecute(
media,
mContext,
"My Question",
ReviewController.DeleteReason.COPYRIGHT_VIOLATION, callback
)
deleteHelper.getListener()?.onClick(
deleteHelper.getDialog(),
1,
true
)
assertEquals(
true,
deleteHelper.getDialog()?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled
)
}
@Test
fun alertDialogPositiveButtonEnableTest() {
val mContext = RuntimeEnvironment.getApplication().applicationContext
deleteHelper.askReasonAndExecute(media, mContext, "My Question", ReviewController.DeleteReason.COPYRIGHT_VIOLATION, callback)
deleteHelper.listener.onClick(deleteHelper.dialog, 1, true)
assertEquals(true, deleteHelper.dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled)
deleteHelper.getListener()?.onClick(deleteHelper.getDialog(), 1, true)
assertEquals(true, deleteHelper.getDialog()?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled)
}
@Test(expected = RuntimeException::class)

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.delete
import android.content.Context
import android.content.res.Resources
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
@ -24,6 +25,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.powermock.api.mockito.PowerMockito
import java.util.Date
class ReasonBuilderTest {
@ -53,6 +55,9 @@ class ReasonBuilderTest {
@Test
fun forceLoginWhenAccountIsNull() {
PowerMockito.`when`(context?.getString(R.string.user_not_logged_in))
.thenReturn("Log-in expired. Please log in again.")
reasonBuilder!!.getReason(mock(Media::class.java), "test")
verify(sessionManager, times(1))!!.forceLogin(any(Context::class.java))
}

View file

@ -10,6 +10,7 @@ import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import fr.free.nrw.commons.R
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
@ -73,6 +74,15 @@ class DescriptionEditHelperUnitTest {
@Test
fun testShowCaptionEditNotificationCaseFalse() {
`when`(context.getString(R.string.caption_edit_helper_show_edit_title))
.thenReturn("Edit Caption")
`when`(context.getString(R.string.coordinates_edit_helper_show_edit_title_success))
.thenReturn("Success")
`when`(context.getString(R.string.caption_edit_helper_show_edit_message))
.thenReturn("Edit caption was successful")
`when`(context.getString(R.string.caption_edit_helper_edit_message_else))
.thenReturn("Edit caption failed")
val method: Method =
DescriptionEditHelper::class.java.getDeclaredMethod(
"showCaptionEditNotification",
@ -86,6 +96,15 @@ class DescriptionEditHelperUnitTest {
@Test
fun testShowCaptionEditNotificationCaseTrue() {
`when`(context.getString(R.string.caption_edit_helper_show_edit_title))
.thenReturn("Edit Caption")
`when`(context.getString(R.string.coordinates_edit_helper_show_edit_title_success))
.thenReturn("Success")
`when`(context.getString(R.string.caption_edit_helper_show_edit_message))
.thenReturn("Edit caption was successful")
`when`(context.getString(R.string.caption_edit_helper_edit_message_else))
.thenReturn("Edit caption failed")
val method: Method =
DescriptionEditHelper::class.java.getDeclaredMethod(
"showCaptionEditNotification",
@ -99,6 +118,15 @@ class DescriptionEditHelperUnitTest {
@Test
fun testShowDescriptionEditNotificationCaseFalse() {
`when`(context.getString(R.string.description_edit_helper_show_edit_title))
.thenReturn("Edit Description")
`when`(context.getString(R.string.coordinates_edit_helper_show_edit_title_success))
.thenReturn("Success")
`when`(context.getString(R.string.description_edit_helper_show_edit_message))
.thenReturn("Edit message")
`when`(context.getString(R.string.description_edit_helper_edit_message_else))
.thenReturn("Edit failed")
val method: Method =
DescriptionEditHelper::class.java.getDeclaredMethod(
"showDescriptionEditNotification",
@ -112,6 +140,15 @@ class DescriptionEditHelperUnitTest {
@Test
fun testShowDescriptionEditNotificationCaseTrue() {
`when`(context.getString(R.string.description_edit_helper_show_edit_title))
.thenReturn("Edit Description")
`when`(context.getString(R.string.coordinates_edit_helper_show_edit_title_success))
.thenReturn("Success")
`when`(context.getString(R.string.description_edit_helper_show_edit_message))
.thenReturn("Edit message")
`when`(context.getString(R.string.description_edit_helper_edit_message_else))
.thenReturn("Edit failed")
val method: Method =
DescriptionEditHelper::class.java.getDeclaredMethod(
"showDescriptionEditNotification",

View file

@ -27,6 +27,7 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.generic.GenericDraweeHierarchy
import com.facebook.drawee.view.SimpleDraweeView
import com.facebook.soloader.SoLoader
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity
@ -768,9 +769,16 @@ class MediaDetailFragmentUnitTests {
).thenReturn(true)
doReturn(
Single.just(true),
).`when`(deleteHelper).makeDeletion(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())
).`when`(deleteHelper).makeDeletion(
ArgumentMatchers.any(),
ArgumentMatchers.any(),
ArgumentMatchers.any()
)
doReturn(Single.just("")).`when`(reasonBuilder).getReason(ArgumentMatchers.any(), ArgumentMatchers.any())
doReturn(Single.just("")).`when`(reasonBuilder).getReason(
ArgumentMatchers.any(),
ArgumentMatchers.any()
)
val method: Method =
MediaDetailFragment::class.java.getDeclaredMethod(