diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt index 1a5ec0a348..2b4ccf0ec5 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt @@ -372,16 +372,18 @@ 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 { _ -> - Timber.d("Coordinates removed from the image") - } + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, it, "0.0", "0.0", "0.0f" ) + ?.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 { _ -> - Timber.d("Coordinates updated") - } - ) + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, + it, + latitude, + longitude, + accuracy + )?.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 diff --git a/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java b/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java deleted file mode 100644 index 8b62093425..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.java +++ /dev/null @@ -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 - */ - public Single 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 - */ - private Observable 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.kt b/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.kt new file mode 100644 index 0000000000..3095497c36 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/coordinates/CoordinateEditHelper.kt @@ -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 + */ + fun makeCoordinatesEdit( + context: Context, + media: Media, + latitude: String, + longitude: String, + accuracy: String + ): Single? { + 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 + */ + private fun addCoordinates( + media: Media, + Latitude: String, + Longitude: String, + Accuracy: String + ): Observable? { + 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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java deleted file mode 100644 index 7ee417fbc1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ /dev/null @@ -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(); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt new file mode 100644 index 0000000000..83f7687d49 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -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() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java deleted file mode 100644 index c0f85420f5..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java +++ /dev/null @@ -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() { - }); - } - - @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 objectList) { - return writeObjectToString(objectList); - } - - @TypeConverter - public static List stringToListObject(String objectList) { - return readObjectWithTypeToken(objectList, new TypeToken>() {}); - } - - @TypeConverter - public static String mapObjectToString(Map objectList) { - return writeObjectToString(objectList); - } - - @TypeConverter - public static String mapObjectToString2(Map objectList) { - return writeObjectToString(objectList); - } - - @TypeConverter - public static Map stringToMap(String objectList) { - return readObjectWithTypeToken(objectList, new TypeToken>(){}); - } - - @TypeConverter - public static Map stringToMap2(String objectList) { - return readObjectWithTypeToken(objectList, new TypeToken>(){}); - } - - @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 depictedItems) { - return writeObjectToString(depictedItems); - } - - @TypeConverter - public static List stringToList(String depictedItems) { - return readObjectWithTypeToken(depictedItems, new TypeToken>() {}); - } - - @TypeConverter - public static Sitelinks sitelinksFromString(String value) { - Type type = new TypeToken() {}.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 readObjectFromString(String objectAsString, Class clazz) { - return objectAsString == null ? null : getGson().fromJson(objectAsString, clazz); - } - - private static T readObjectWithTypeToken(String objectList, TypeToken typeToken) { - return objectList == null ? null : getGson().fromJson(objectList, typeToken.getType()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.kt b/app/src/main/java/fr/free/nrw/commons/db/Converters.kt new file mode 100644 index 0000000000..43b3a61841 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.kt @@ -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() {}) + } + + @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? { + return writeObjectToString(objectList) + } + + @TypeConverter + @JvmStatic + fun stringToListObject(objectList: String?): List? { + return readObjectWithTypeToken(objectList, object : TypeToken>() {}) + } + + @TypeConverter + @JvmStatic + fun mapObjectToString(objectList: Map?): String? { + return writeObjectToString(objectList) + } + + @TypeConverter + @JvmStatic + fun mapObjectToString2(objectList: Map?): String? { + return writeObjectToString(objectList) + } + + @TypeConverter + @JvmStatic + fun stringToMap(objectList: String?): Map? { + return readObjectWithTypeToken(objectList, object : TypeToken>() {}) + } + + @TypeConverter + @JvmStatic + fun stringToMap2(objectList: String?): Map? { + return readObjectWithTypeToken(objectList, object : TypeToken>() {}) + } + + @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?): String? { + return writeObjectToString(depictedItems) + } + + @TypeConverter + @JvmStatic + fun stringToList(depictedItems: String?): List? { + return readObjectWithTypeToken(depictedItems, object : TypeToken>() {}) + } + + @TypeConverter + @JvmStatic + fun sitelinksFromString(value: String?): Sitelinks? { + val type = object : TypeToken() {}.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 readObjectFromString(objectAsString: String?, clazz: Class): T? { + return objectAsString?.let { getGson().fromJson(it, clazz) } + } + + private fun readObjectWithTypeToken(objectList: String?, typeToken: TypeToken): T? { + return objectList?.let { getGson().fromJson(it, typeToken.type) } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java deleted file mode 100644 index 134ee48d9d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ /dev/null @@ -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 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 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 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>) () -> - 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt new file mode 100644 index 0000000000..d594a25a9b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt @@ -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? { + + 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 { + 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() + + val reasonList: Array + val reasonListEnglish: Array + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java deleted file mode 100644 index 7912375a4d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java +++ /dev/null @@ -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 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 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.kt b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.kt new file mode 100644 index 0000000000..09018c249b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.kt @@ -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 { + 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 { + 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 + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index fa4349dbf4..6cf9ab7a7f 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -272,7 +272,7 @@ class DescriptionEditActivity : .addCaption( applicationContext, media, - mediaDetail.languageCode, + mediaDetail.languageCode!!, mediaDetail.captionText, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.java b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.java deleted file mode 100644 index 05f6e9f2de..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.java +++ /dev/null @@ -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 - */ - public Single 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 - */ - public Single 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.kt new file mode 100644 index 0000000000..5d948ddf9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditHelper.kt @@ -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 + */ + fun addDescription(context: Context, media: Media, appendText: String): Single { + 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 + */ + fun addCaption( + context: Context, + media: Media, + language: String, + value: String + ): Single { + 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 + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 9e60b74e6a..80cced5c99 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -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) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt index 75aadc17a4..e89a02dece 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/ReasonBuilderTest.kt @@ -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)) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditHelperUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditHelperUnitTest.kt index 5415a87a61..e7504f9ac4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditHelperUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditHelperUnitTest.kt @@ -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", diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index 37b5277f47..0695cbca74 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -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()) - - doReturn(Single.just("")).`when`(reasonBuilder).getReason(ArgumentMatchers.any(), ArgumentMatchers.any()) + ).`when`(deleteHelper).makeDeletion( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + + doReturn(Single.just("")).`when`(reasonBuilder).getReason( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(