Merge branch 'main' into recent-search

This commit is contained in:
Neel Doshi 2024-12-11 14:30:59 +05:30 committed by GitHub
commit 5415471045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 2160 additions and 2056 deletions

View file

@ -180,8 +180,8 @@ public class AboutActivity extends BaseActivity {
getString(R.string.about_translate_cancel),
positiveButtonRunnable,
() -> {},
spinner,
true);
spinner
);
}
}

View file

@ -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

View file

@ -50,6 +50,7 @@ public class WelcomeActivity extends BaseActivity {
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
final View contactPopupView = copyrightBinding.getRoot();
dialogBuilder.setView(contactPopupView);
dialogBuilder.setCancelable(false);
dialog = dialogBuilder.create();
dialog.show();

View file

@ -319,7 +319,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
isIndeterminate = true
setTitle(getString(R.string.logging_in_title))
setMessage(getString(R.string.logging_in_message))
setCanceledOnTouchOutside(false)
setCancelable(false)
}
progressDialog!!.show()
}

View file

@ -78,7 +78,13 @@ class CategoriesModel
// Newly used category...
if (category == null) {
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
category = Category(
null, item.name,
item.description,
item.thumbnail,
Date(),
0
)
}
category.incTimesUsed()
categoryDao.save(category)

View file

@ -1,115 +0,0 @@
package fr.free.nrw.commons.category;
import android.net.Uri;
import java.util.Date;
/**
* Represents a category
*/
public class Category {
private Uri contentUri;
private String name;
private String description;
private String thumbnail;
private Date lastUsed;
private int timesUsed;
public Category() {
}
public Category(Uri contentUri, String name, String description, String thumbnail, Date lastUsed, int timesUsed) {
this.contentUri = contentUri;
this.name = name;
this.description = description;
this.thumbnail = thumbnail;
this.lastUsed = lastUsed;
this.timesUsed = timesUsed;
}
/**
* Gets name
*
* @return name
*/
public String getName() {
return name;
}
/**
* Modifies name
*
* @param name Category name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets last used date
*
* @return Last used date
*/
public Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
/**
* Generates new last used date
*/
private void touch() {
lastUsed = new Date();
}
/**
* Gets no. of times the category is used
*
* @return no. of times used
*/
public int getTimesUsed() {
return timesUsed;
}
/**
* Increments timesUsed by 1 and sets last used date as now.
*/
public void incTimesUsed() {
timesUsed++;
touch();
}
/**
* Gets the content URI for this category
*
* @return content URI
*/
public Uri getContentUri() {
return contentUri;
}
/**
* Modifies the content URI - marking this category as already saved in the database
*
* @param contentUri the content URI
*/
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
public String getDescription() {
return description;
}
public String getThumbnail() {
return thumbnail;
}
public void setDescription(final String description) {
this.description = description;
}
public void setThumbnail(final String thumbnail) {
this.thumbnail = thumbnail;
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.category
import android.net.Uri
import java.util.Date
data class Category(
var contentUri: Uri? = null,
val name: String? = null,
val description: String? = null,
val thumbnail: String? = null,
val lastUsed: Date? = null,
var timesUsed: Int = 0
) {
fun incTimesUsed() {
timesUsed++
}
}

View file

@ -1,5 +0,0 @@
package fr.free.nrw.commons.category;
public interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.category
interface CategoryClickedListener {
fun categoryClicked(item: CategoryItem)
}

View file

@ -1,169 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
public class CategoryContentProvider extends CommonsDaggerContentProvider {
// For URI matcher
private static final int CATEGORIES = 1;
private static final int CATEGORIES_ID = 2;
private static final String BASE_PATH = "categories";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.CATEGORY_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
static {
uriMatcher.addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH, CATEGORIES);
uriMatcher.addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
case CATEGORIES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CATEGORIES_ID:
cursor = queryBuilder.query(db,
ALL_FIELDS,
"_id = ?",
new String[]{uri.getLastPathSegment()},
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case CATEGORIES:
id = sqlDB.insert(TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
return 0;
}
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (CategoryContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case CATEGORIES:
for (ContentValues value : values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the
outside world (exported="false"). Even then, we should make sure to sanitize
all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int,
and will error out otherwise.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case CATEGORIES_ID:
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,205 @@
package fr.free.nrw.commons.category
import android.content.ContentValues
import android.content.UriMatcher
import android.content.UriMatcher.NO_MATCH
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import androidx.annotation.NonNull
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import timber.log.Timber
import javax.inject.Inject
class CategoryContentProvider : CommonsDaggerContentProvider() {
private val uriMatcher = UriMatcher(NO_MATCH).apply {
addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH, CATEGORIES)
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
@SuppressWarnings("ConstantConditions")
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val uriType = uriMatcher.match(uri)
val db = dbOpenHelper.readableDatabase
val cursor: Cursor? = when (uriType) {
CATEGORIES -> queryBuilder.query(
db,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
CATEGORIES_ID -> queryBuilder.query(
db,
ALL_FIELDS,
"_id = ?",
arrayOf(uri.lastPathSegment),
null,
null,
sortOrder
)
else -> throw IllegalArgumentException("Unknown URI $uri")
}
cursor?.setNotificationUri(context?.contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? {
return null
}
@SuppressWarnings("ConstantConditions")
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val id: Long
when (uriType) {
CATEGORIES -> {
id = sqlDB.insert(TABLE_NAME, null, contentValues)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("${Companion.BASE_URI}/$id")
}
@SuppressWarnings("ConstantConditions")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
// Not implemented
return 0
}
@SuppressWarnings("ConstantConditions")
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
sqlDB.beginTransaction()
when (uriType) {
CATEGORIES -> {
for (value in values) {
Timber.d("Inserting! %s", value)
sqlDB.insert(TABLE_NAME, null, value)
}
sqlDB.setTransactionSuccessful()
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.endTransaction()
context?.contentResolver?.notifyChange(uri, null)
return values.size
}
@SuppressWarnings("ConstantConditions")
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
when (uriType) {
CATEGORIES_ID -> {
if (TextUtils.isEmpty(selection)) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid ID")
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString()))
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID")
}
}
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
}
context?.contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
companion object {
const val TABLE_NAME = "categories"
const val COLUMN_ID = "_id"
const val COLUMN_NAME = "name"
const val COLUMN_DESCRIPTION = "description"
const val COLUMN_THUMBNAIL = "thumbnail"
const val COLUMN_LAST_USED = "last_used"
const val COLUMN_TIMES_USED = "times_used"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
val ALL_FIELDS = arrayOf(
COLUMN_ID,
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_THUMBNAIL,
COLUMN_LAST_USED,
COLUMN_TIMES_USED
)
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" +
"$COLUMN_ID INTEGER PRIMARY KEY," +
"$COLUMN_NAME TEXT," +
"$COLUMN_DESCRIPTION TEXT," +
"$COLUMN_THUMBNAIL TEXT," +
"$COLUMN_LAST_USED INTEGER," +
"$COLUMN_TIMES_USED INTEGER" +
");"
fun uriForId(id: Int): Uri {
return Uri.parse("${BASE_URI}/$id")
}
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) return
if (from < 4) {
// doesn't exist yet
onUpdate(db, from + 1, to)
} else if (from == 4) {
// table added in version 5
onCreate(db)
onUpdate(db, from + 1, to)
} else if (from == 5) {
onUpdate(db, from + 1, to)
} else if (from == 17) {
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN description TEXT;")
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN thumbnail TEXT;")
onUpdate(db, from + 1, to)
}
}
// For URI matcher
private const val CATEGORIES = 1
private const val CATEGORIES_ID = 2
private const val BASE_PATH = "categories"
val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}")
}
}

View file

@ -1,209 +0,0 @@
package fr.free.nrw.commons.category;
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
public class CategoryDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
public void save(Category category) {
ContentProviderClient db = clientProvider.get();
try {
if (category.getContentUri() == null) {
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
} else {
db.update(category.getContentUri(), toContentValues(category), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find persisted category in database, based on its name.
*
* @param name Category's name
* @return category from database, or null if not found
*/
@Nullable
Category find(String name) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
CategoryContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return null;
}
/**
* Retrieve recently-used categories, ordered by descending date.
*
* @return a list containing recent categories
*/
@NonNull
List<CategoryItem> recentCategories(int limit) {
List<CategoryItem> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
CategoryContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < limit) {
if (fromCursor(cursor).getName() != null ) {
items.add(new CategoryItem(fromCursor(cursor).getName(),
fromCursor(cursor).getDescription(), fromCursor(cursor).getThumbnail(),
false));
}
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
@NonNull
@SuppressLint("Range")
Category fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new Category(
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_THUMBNAIL)),
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))),
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_TIMES_USED))
);
}
private ContentValues toContentValues(Category category) {
ContentValues cv = new ContentValues();
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
cv.put(Table.COLUMN_DESCRIPTION, category.getDescription());
cv.put(Table.COLUMN_THUMBNAIL, category.getThumbnail());
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
return cv;
}
public static class Table {
public static final String TABLE_NAME = "categories";
public static final String COLUMN_ID = "_id";
static final String COLUMN_NAME = "name";
static final String COLUMN_DESCRIPTION = "description";
static final String COLUMN_THUMBNAIL = "thumbnail";
static final String COLUMN_LAST_USED = "last_used";
static final String COLUMN_TIMES_USED = "times_used";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_THUMBNAIL,
COLUMN_LAST_USED,
COLUMN_TIMES_USED
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_THUMBNAIL + " STRING,"
+ COLUMN_LAST_USED + " INTEGER,"
+ COLUMN_TIMES_USED + " INTEGER"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from < 4) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 4) {
// table added in version 5
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from == 5) {
from++;
onUpdate(db, from, to);
return;
}
if (from == 17) {
db.execSQL("ALTER TABLE categories ADD COLUMN description STRING;");
db.execSQL("ALTER TABLE categories ADD COLUMN thumbnail STRING;");
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,194 @@
package fr.free.nrw.commons.category
import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.os.RemoteException
import java.util.ArrayList
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
class CategoryDao @Inject constructor(
@Named("category") private val clientProvider: Provider<ContentProviderClient>
) {
fun save(category: Category) {
val db = clientProvider.get()
try {
if (category.contentUri == null) {
category.contentUri = db.insert(
CategoryContentProvider.BASE_URI,
toContentValues(category)
)
} else {
db.update(
category.contentUri!!,
toContentValues(category),
null,
null
)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find persisted category in database, based on its name.
*
* @param name Category's name
* @return category from database, or null if not found
*/
fun find(name: String): Category? {
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
CategoryContentProvider.BASE_URI,
ALL_FIELDS,
"${COLUMN_NAME}=?",
arrayOf(name),
null
)
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return null
}
/**
* Retrieve recently-used categories, ordered by descending date.
*
* @return a list containing recent categories
*/
fun recentCategories(limit: Int): List<CategoryItem> {
val items = ArrayList<CategoryItem>()
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
CategoryContentProvider.BASE_URI,
ALL_FIELDS,
null,
emptyArray(),
"$COLUMN_LAST_USED DESC"
)
while (cursor != null && cursor.moveToNext() && cursor.position < limit) {
val category = fromCursor(cursor)
if (category.name != null) {
items.add(
CategoryItem(
category.name,
category.description,
category.thumbnail,
false
)
)
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return items
}
@SuppressLint("Range")
fun fromCursor(cursor: Cursor): Category {
// Hardcoding column positions!
return Category(
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
cursor.getString(cursor.getColumnIndex(COLUMN_DESCRIPTION)),
cursor.getString(cursor.getColumnIndex(COLUMN_THUMBNAIL)),
Date(cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_USED))),
cursor.getInt(cursor.getColumnIndex(COLUMN_TIMES_USED))
)
}
private fun toContentValues(category: Category): ContentValues {
return ContentValues().apply {
put(COLUMN_NAME, category.name)
put(COLUMN_DESCRIPTION, category.description)
put(COLUMN_THUMBNAIL, category.thumbnail)
put(COLUMN_LAST_USED, category.lastUsed?.time)
put(COLUMN_TIMES_USED, category.timesUsed)
}
}
companion object Table {
const val TABLE_NAME = "categories"
const val COLUMN_ID = "_id"
const val COLUMN_NAME = "name"
const val COLUMN_DESCRIPTION = "description"
const val COLUMN_THUMBNAIL = "thumbnail"
const val COLUMN_LAST_USED = "last_used"
const val COLUMN_TIMES_USED = "times_used"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
val ALL_FIELDS = arrayOf(
COLUMN_ID,
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_THUMBNAIL,
COLUMN_LAST_USED,
COLUMN_TIMES_USED
)
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" +
"$COLUMN_ID INTEGER PRIMARY KEY," +
"$COLUMN_NAME STRING," +
"$COLUMN_DESCRIPTION STRING," +
"$COLUMN_THUMBNAIL STRING," +
"$COLUMN_LAST_USED INTEGER," +
"$COLUMN_TIMES_USED INTEGER" +
");"
@SuppressLint("SQLiteString")
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
@SuppressLint("SQLiteString")
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) return
if (from < 4) {
// doesn't exist yet
onUpdate(db, from + 1, to)
} else if (from == 4) {
// table added in version 5
onCreate(db)
onUpdate(db, from + 1, to)
} else if (from == 5) {
onUpdate(db, from + 1, to)
} else if (from == 17) {
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN description STRING;")
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN thumbnail STRING;")
onUpdate(db, from + 1, to)
}
}
}
}

View file

@ -1,236 +0,0 @@
package fr.free.nrw.commons.category;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.FrameLayout;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
/**
* This activity displays details of a particular category
* Its generic and simply takes the name of category name in its start intent to load all images, subcategories in
* a particular category on wikimedia commons.
*/
public class CategoryDetailsActivity extends BaseActivity
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
private FragmentManager supportFragmentManager;
private CategoriesMediaFragment categoriesMediaFragment;
private MediaDetailPagerFragment mediaDetails;
private String categoryName;
ViewPagerAdapter viewPagerAdapter;
private ActivityCategoryDetailsBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityCategoryDetailsBinding.inflate(getLayoutInflater());
final View view = binding.getRoot();
setContentView(view);
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2);
binding.tabLayout.setupWithViewPager(binding.viewPager);
setSupportActionBar(binding.toolbarBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setTabs();
setPageTitle();
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
categoriesMediaFragment = new CategoriesMediaFragment();
SubCategoriesFragment subCategoryListFragment = new SubCategoriesFragment();
ParentCategoriesFragment parentCategoriesFragment = new ParentCategoriesFragment();
categoryName = getIntent().getStringExtra("categoryName");
if (getIntent() != null && categoryName != null) {
Bundle arguments = new Bundle();
arguments.putString("categoryName", categoryName);
categoriesMediaFragment.setArguments(arguments);
subCategoryListFragment.setArguments(arguments);
parentCategoriesFragment.setArguments(arguments);
}
fragmentList.add(categoriesMediaFragment);
titleList.add("MEDIA");
fragmentList.add(subCategoryListFragment);
titleList.add("SUBCATEGORIES");
fragmentList.add(parentCategoriesFragment);
titleList.add("PARENT CATEGORIES");
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Gets the passed categoryName from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("categoryName") != null) {
setTitle(getIntent().getStringExtra("categoryName"));
}
}
/**
* This method is called onClick of media inside category details (CategoryImageListFragment).
*/
@Override
public void onMediaClicked(int position) {
binding.tabLayout.setVisibility(View.GONE);
binding.viewPager.setVisibility(View.GONE);
binding.mediaContainer.setVisibility(View.VISIBLE);
if (mediaDetails == null || !mediaDetails.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(position);
}
/**
* Consumers should be simply using this method to use this activity.
* @param context A Context of the application package implementing this class.
* @param categoryName Name of the category for displaying its details
*/
public static void startYourself(Context context, String categoryName) {
Intent intent = new Intent(context, CategoryDetailsActivity.class);
intent.putExtra("categoryName", categoryName);
context.startActivity(intent);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
return categoriesMediaFragment.getMediaAtPosition(i);
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
return categoriesMediaFragment.getTotalMediaCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
onBackPressed();
onMediaClicked(index);
}
}
/**
* This method inflates the menu in the toolbar
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.fragment_category_detail, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* This method handles the logic on ItemSelect in toolbar menu
* Currently only 1 choice is available to open category details page in browser
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.menu_browser_current_category:
PageTitle title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName);
Utils.handleWebUrl(this, Uri.parse(title.getCanonicalUri()));
return true;
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
binding.tabLayout.setVisibility(View.VISIBLE);
binding.viewPager.setVisibility(View.VISIBLE);
binding.mediaContainer.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* This method is called on success of API call for Images inside a category.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails!=null){
mediaDetails.notifyDataSetChanged();
}
}
}

View file

@ -0,0 +1,216 @@
package fr.free.nrw.commons.category
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.theme.BaseActivity
/**
* This activity displays details of a particular category
* Its generic and simply takes the name of category name in its start intent to load all images, subcategories in
* a particular category on wikimedia commons.
*/
class CategoryDetailsActivity : BaseActivity(),
MediaDetailPagerFragment.MediaDetailProvider,
CategoryImagesCallback {
private lateinit var supportFragmentManager: FragmentManager
private lateinit var categoriesMediaFragment: CategoriesMediaFragment
private var mediaDetails: MediaDetailPagerFragment? = null
private var categoryName: String? = null
private lateinit var viewPagerAdapter: ViewPagerAdapter
private lateinit var binding: ActivityCategoryDetailsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCategoryDetailsBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
supportFragmentManager = getSupportFragmentManager()
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.viewPager.offscreenPageLimit = 2
binding.tabLayout.setupWithViewPager(binding.viewPager)
setSupportActionBar(binding.toolbarBinding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setTabs()
setPageTitle()
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private fun setTabs() {
val fragmentList = mutableListOf<Fragment>()
val titleList = mutableListOf<String>()
categoriesMediaFragment = CategoriesMediaFragment()
val subCategoryListFragment = SubCategoriesFragment()
val parentCategoriesFragment = ParentCategoriesFragment()
categoryName = intent?.getStringExtra("categoryName")
if (intent != null && categoryName != null) {
val arguments = Bundle().apply {
putString("categoryName", categoryName)
}
categoriesMediaFragment.arguments = arguments
subCategoryListFragment.arguments = arguments
parentCategoriesFragment.arguments = arguments
}
fragmentList.add(categoriesMediaFragment)
titleList.add("MEDIA")
fragmentList.add(subCategoryListFragment)
titleList.add("SUBCATEGORIES")
fragmentList.add(parentCategoriesFragment)
titleList.add("PARENT CATEGORIES")
viewPagerAdapter.setTabData(fragmentList, titleList)
viewPagerAdapter.notifyDataSetChanged()
}
/**
* Gets the passed categoryName from the intents and displays it as the page title
*/
private fun setPageTitle() {
intent?.getStringExtra("categoryName")?.let {
title = it
}
}
/**
* This method is called onClick of media inside category details (CategoryImageListFragment).
*/
override fun onMediaClicked(position: Int) {
binding.tabLayout.visibility = View.GONE
binding.viewPager.visibility = View.GONE
binding.mediaContainer.visibility = View.VISIBLE
if (mediaDetails == null || mediaDetails?.isVisible == false) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
supportFragmentManager.beginTransaction()
.replace(R.id.mediaContainer, mediaDetails!!)
.addToBackStack(null)
.commit()
supportFragmentManager.executePendingTransactions()
}
mediaDetails?.showImage(position)
}
companion object {
/**
* Consumers should be simply using this method to use this activity.
* @param context A Context of the application package implementing this class.
* @param categoryName Name of the category for displaying its details
*/
fun startYourself(context: Context?, categoryName: String) {
val intent = Intent(context, CategoryDetailsActivity::class.java).apply {
putExtra("categoryName", categoryName)
}
context?.startActivity(intent)
}
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
override fun getMediaAtPosition(i: Int): Media? {
return categoriesMediaFragment.getMediaAtPosition(i)
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
override fun getTotalMediaCount(): Int {
return categoriesMediaFragment.getTotalMediaCount()
}
override fun getContributionStateAt(position: Int): Int? {
return null
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (supportFragmentManager.backStackEntryCount == 1) {
onBackPressed()
onMediaClicked(index)
}
}
/**
* This method inflates the menu in the toolbar
*/
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.fragment_category_detail, menu)
return super.onCreateOptionsMenu(menu)
}
/**
* This method handles the logic on ItemSelect in toolbar menu
* Currently only 1 choice is available to open category details page in browser
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_browser_current_category -> {
val title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName)
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
true
}
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Deprecated("This method has been deprecated in favor of using the" +
"{@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}." +
"The OnBackPressedDispatcher controls how back button events are dispatched" +
"to one or more {@link OnBackPressedCallback} objects.")
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount == 1) {
binding.tabLayout.visibility = View.VISIBLE
binding.viewPager.visibility = View.VISIBLE
binding.mediaContainer.visibility = View.GONE
}
super.onBackPressed()
}
/**
* This method is called on success of API call for Images inside a category.
* The viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
mediaDetails?.notifyDataSetChanged()
}
}

View file

@ -1,123 +0,0 @@
package fr.free.nrw.commons.category;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_CATEGORY;
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 java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class CategoryEditHelper {
private final NotificationHelper notificationHelper;
public final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
private final String username;
@Inject
public CategoryEditHelper(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 edit categories
* @param context
* @param media
* @param categories
* @return
*/
public Single<Boolean> makeCategoryEdit(Context context, Media media, List<String> categories,
final String wikiText) {
viewUtil.showShortToast(context, context.getString(R.string.category_edit_helper_make_edit_toast));
return addCategory(media, categories, wikiText)
.flatMapSingle(result -> Single.just(showCategoryEditNotification(context, media, result)))
.firstOrError();
}
/**
* Rebuilds the WikiText with new categpries and post it on server
*
* @param media
* @param categories to be added
* @return
*/
private Observable<Boolean> addCategory(Media media, List<String> categories,
final String wikiText) {
Timber.d("thread is category adding %s", Thread.currentThread().getName());
String summary = "Adding categories";
final StringBuilder buffer = new StringBuilder();
final String wikiTextWithoutCategory;
//If the picture was uploaded without a category, the wikitext will contain "Uncategorized" instead of "[[Category"
if (wikiText.contains("Uncategorized")) {
wikiTextWithoutCategory = wikiText.substring(0, wikiText.indexOf("Uncategorized"));
} else if (wikiText.contains("[[Category")) {
wikiTextWithoutCategory = wikiText.substring(0, wikiText.indexOf("[[Category"));
} else {
wikiTextWithoutCategory = "";
}
if (categories != null && !categories.isEmpty()) {
//If the categories list is empty, when reading the categories of a picture,
// the code will add "None selected" to categories list in order to see in picture's categories with "None selected".
// So that after selected some category,"None selected" should be removed from list
for (int i = 0; i < categories.size(); i++) {
if (!categories.get(i).equals("None selected")//Not to add "None selected" as category to wikiText
|| !wikiText.contains("Uncategorized")) {
buffer.append("[[Category:").append(categories.get(i)).append("]]\n");
}
}
categories.remove("None selected");
} else {
buffer.append("{{subst:unc}}");
}
final String appendText = wikiTextWithoutCategory + buffer;
return pageEditClient.edit(media.getFilename(), appendText + "\n", summary);
}
private boolean showCategoryEditNotification(Context context, Media media, boolean result) {
String message;
String title = context.getString(R.string.category_edit_helper_show_edit_title);
if (result) {
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title_success);
StringBuilder categoriesInMessage = new StringBuilder();
List<String> mediaCategoryList = media.getCategories();
for (String category : mediaCategoryList) {
categoriesInMessage.append(category);
if (category.equals(mediaCategoryList.get(mediaCategoryList.size()-1))) {
continue;
}
categoriesInMessage.append(",");
}
message = context.getResources().getQuantityString(R.plurals.category_edit_helper_show_edit_message_if, mediaCategoryList.size(), categoriesInMessage.toString());
} else {
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title);
message = context.getString(R.string.category_edit_helper_edit_message_else) ;
}
String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_CATEGORY, browserIntent);
return result;
}
public interface Callback {
boolean updateCategoryDisplay(List<String> categories);
}
}

View file

@ -0,0 +1,144 @@
package fr.free.nrw.commons.category
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 javax.inject.Inject
import javax.inject.Named
import timber.log.Timber
class CategoryEditHelper @Inject constructor(
private val notificationHelper: NotificationHelper,
@Named("commons-page-edit") val pageEditClient: PageEditClient,
private val viewUtil: ViewUtilWrapper,
@Named("username") private val username: String
) {
/**
* Public interface to edit categories
* @param context
* @param media
* @param categories
* @return
*/
fun makeCategoryEdit(
context: Context,
media: Media,
categories: List<String>,
wikiText: String
): Single<Boolean> {
viewUtil.showShortToast(
context,
context.getString(R.string.category_edit_helper_make_edit_toast)
)
return addCategory(media, categories, wikiText)
.flatMapSingle { result ->
Single.just(showCategoryEditNotification(context, media, result))
}
.firstOrError()
}
/**
* Rebuilds the WikiText with new categories and post it on server
*
* @param media
* @param categories to be added
* @return
*/
private fun addCategory(
media: Media,
categories: List<String>?,
wikiText: String
): Observable<Boolean> {
Timber.d("thread is category adding %s", Thread.currentThread().name)
val summary = "Adding categories"
val buffer = StringBuilder()
// If the picture was uploaded without a category, the wikitext will contain "Uncategorized" instead of "[[Category"
val wikiTextWithoutCategory: String = when {
wikiText.contains("Uncategorized") -> wikiText.substring(0, wikiText.indexOf("Uncategorized"))
wikiText.contains("[[Category") -> wikiText.substring(0, wikiText.indexOf("[[Category"))
else -> ""
}
if (!categories.isNullOrEmpty()) {
// If the categories list is empty, when reading the categories of a picture,
// the code will add "None selected" to categories list in order to see in picture's categories with "None selected".
// So that after selecting some category, "None selected" should be removed from list
for (category in categories) {
if (category != "None selected" || !wikiText.contains("Uncategorized")) {
buffer.append("[[Category:").append(category).append("]]\n")
}
}
categories.dropWhile {
it == "None selected"
}
} else {
buffer.append("{{subst:unc}}")
}
val appendText = wikiTextWithoutCategory + buffer
return pageEditClient.edit(media.filename!!, "$appendText\n", summary)
}
private fun showCategoryEditNotification(
context: Context,
media: Media,
result: Boolean
): Boolean {
val title: String
val message: String
if (result) {
title = context.getString(R.string.category_edit_helper_show_edit_title) + ": " +
context.getString(R.string.category_edit_helper_show_edit_title_success)
val categoriesInMessage = StringBuilder()
val mediaCategoryList = media.categories
for ((index, category) in mediaCategoryList?.withIndex()!!) {
categoriesInMessage.append(category)
if (index != mediaCategoryList.size - 1) {
categoriesInMessage.append(",")
}
}
message = context.resources.getQuantityString(
R.plurals.category_edit_helper_show_edit_message_if,
mediaCategoryList.size,
categoriesInMessage.toString()
)
} else {
title = context.getString(R.string.category_edit_helper_show_edit_title) + ": " +
context.getString(R.string.category_edit_helper_show_edit_title)
message = context.getString(R.string.category_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_CATEGORY,
browserIntent
)
return result
}
interface Callback {
fun updateCategoryDisplay(categories: List<String>?): Boolean
}
companion object {
const val NOTIFICATION_EDIT_CATEGORY = 1
}
}

View file

@ -1,13 +0,0 @@
package fr.free.nrw.commons.category;
/**
* Callback for notifying the viewpager that the number of items have changed
* and for requesting more images when the viewpager has been scrolled to its end.
*/
public interface CategoryImagesCallback {
void viewPagerNotifyDataSetChanged();
void onMediaClicked(int position);
}

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.category
interface CategoryImagesCallback {
fun viewPagerNotifyDataSetChanged()
fun onMediaClicked(position: Int)
}

View file

@ -1,119 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
/**
* This is created to only display UI implementation. Needs to be changed in real implementation
*/
public class GridViewAdapter extends ArrayAdapter {
private List<Media> data;
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
super(context, layoutResourceId, data);
this.data = data;
}
/**
* Adds more item to the list
* Its triggered on scrolling down in the list
* @param images
*/
public void addItems(List<Media> images) {
if (data == null) {
data = new ArrayList<>();
}
data.addAll(images);
notifyDataSetChanged();
}
/**
* Check the first item in the new list with old list and returns true if they are same
* Its triggered on successful response of the fetch images API.
* @param images
*/
public boolean containsAll(List<Media> images){
if (images == null || images.isEmpty()) {
return false;
}
if (data == null) {
data = new ArrayList<>();
return false;
}
if (data.isEmpty()) {
return false;
}
String fileName = data.get(0).getFilename();
String imageName = images.get(0).getFilename();
return imageName.equals(fileName);
}
@Override
public boolean isEmpty() {
return data == null || data.isEmpty();
}
/**
* Sets up the UI for the category image item
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_category_images, null);
}
Media item = data.get(position);
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView uploader = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getMostRelevantCaption());
setUploaderView(item, uploader);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
/**
* @return the Media item at the given position
*/
@Nullable
@Override
public Media getItem(int position) {
return data.get(position);
}
/**
* Shows author information if its present
* @param item
* @param uploader
*/
private void setUploaderView(Media item, TextView uploader) {
if (!TextUtils.isEmpty(item.getAuthor())) {
uploader.setVisibility(View.VISIBLE);
uploader.setText(getContext().getString(R.string.image_uploaded_by, item.getUser()));
} else {
uploader.setVisibility(View.GONE);
}
}
}

View file

@ -0,0 +1,111 @@
package fr.free.nrw.commons.category
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.facebook.drawee.view.SimpleDraweeView
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
/**
* This is created to only display UI implementation. Needs to be changed in real implementation
*/
class GridViewAdapter(
context: Context,
layoutResourceId: Int,
private var data: MutableList<Media>?
) : ArrayAdapter<Media>(context, layoutResourceId, data ?: mutableListOf()) {
/**
* Adds more items to the list
* It's triggered on scrolling down in the list
* @param images
*/
fun addItems(images: List<Media>) {
if (data == null) {
data = mutableListOf()
}
data?.addAll(images)
notifyDataSetChanged()
}
/**
* Checks the first item in the new list with the old list and returns true if they are the same
* It's triggered on a successful response of the fetch images API.
* @param images
*/
fun containsAll(images: List<Media>?): Boolean {
if (images.isNullOrEmpty()) {
return false
}
if (data.isNullOrEmpty()) {
data = mutableListOf()
return false
}
val fileName = data?.get(0)?.filename
val imageName = images[0].filename
return imageName == fileName
}
override fun isEmpty(): Boolean {
return data.isNullOrEmpty()
}
/**
* Sets up the UI for the category image item
* @param position
* @param convertView
* @param parent
* @return
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(
R.layout.layout_category_images,
parent,
false
)
val item = data?.get(position)
val imageView = view.findViewById<SimpleDraweeView>(R.id.categoryImageView)
val fileName = view.findViewById<TextView>(R.id.categoryImageTitle)
val uploader = view.findViewById<TextView>(R.id.categoryImageAuthor)
item?.let {
fileName.text = it.mostRelevantCaption
setUploaderView(it, uploader)
imageView.setImageURI(it.thumbUrl)
}
return view
}
/**
* @return the Media item at the given position
*/
override fun getItem(position: Int): Media? {
return data?.get(position)
}
/**
* Shows author information if it's present
* @param item
* @param uploader
*/
@SuppressLint("StringFormatInvalid")
private fun setUploaderView(item: Media, uploader: TextView) {
if (!item.author.isNullOrEmpty()) {
uploader.visibility = View.VISIBLE
uploader.text = context.getString(
R.string.image_uploaded_by,
item.user
)
} else {
uploader.visibility = View.GONE
}
}
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons.category;
import java.util.List;
public interface OnCategoriesSaveHandler {
void onCategoriesSave(List<String> categories);
}

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.category
interface OnCategoriesSaveHandler {
fun onCategoriesSave(categories: List<String>)
}

View file

@ -170,8 +170,8 @@ public class ContributionController {
},
() -> locationPermissionCallback.onLocationPermissionDenied(
activity.getString(R.string.in_app_camera_location_permission_denied)),
null,
false);
null
);
}
/**
@ -203,8 +203,8 @@ public class ContributionController {
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
initiateCameraUpload(activity, resultLauncher);
},
null,
true);
null
);
}
/**

View file

@ -566,8 +566,8 @@ public class ContributionsFragment
getString(R.string.nearby_card_permission_explanation),
this::requestLocationPermission,
this::displayYouWontSeeNearbyMessage,
checkBoxView,
false);
checkBoxView
);
}
private void displayYouWontSeeNearbyMessage() {

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,334 @@
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.setCancelable(false)
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

@ -149,8 +149,7 @@ class DescriptionEditActivity :
getString(titleStringID),
getString(messageStringId),
getString(android.R.string.ok),
null,
true,
null
)
}
@ -272,7 +271,7 @@ class DescriptionEditActivity :
.addCaption(
applicationContext,
media,
mediaDetail.languageCode,
mediaDetail.languageCode!!,
mediaDetail.captionText,
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -304,7 +303,7 @@ class DescriptionEditActivity :
progressDialog!!.isIndeterminate = true
progressDialog!!.setTitle(getString(R.string.updating_caption_title))
progressDialog!!.setMessage(getString(R.string.updating_caption_message))
progressDialog!!.setCanceledOnTouchOutside(false)
progressDialog!!.setCancelable(false)
progressDialog!!.show()
}

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

@ -133,8 +133,8 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
askForLocationPermission();
},
null,
null,
false);
null
);
} else {
if (isPermissionDenied) {
locationPermissionsHelper.showAppSettingsDialog(getActivity(),

View file

@ -78,8 +78,7 @@ class LocationPermissionsHelper(
activity.getString(R.string.upload_map_location_access)
)
},
null,
false
null
)
} else {
ActivityCompat.requestPermissions(

View file

@ -1596,8 +1596,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
getString(R.string.about_translate_cancel),
{ onDeleteClicked(spinner) },
{},
spinner,
true
spinner
)
if (isDeleted) {
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
@ -1616,8 +1615,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
onDeleteClickeddialogtext(reason)
},
{},
input,
true
input
)
input.addTextChangedListener(object : TextWatcher {
fun handleText() {

View file

@ -283,6 +283,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
sendReportEmail(media, values[which]);
});
builder.setCancelable(false);
builder.show();
}

View file

@ -196,6 +196,7 @@ class ZoomableActivity : BaseActivity() {
val dialog = Dialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.full_screen_mode_info_dialog)
dialog.setCancelable(false)
(dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() }
dialog.show()
}

View file

@ -291,8 +291,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
askForLocationPermission();
},
null,
null,
false);
null
);
} else {
if (isPermissionDenied) {
locationPermissionsHelper.showAppSettingsDialog(getActivity(),

View file

@ -206,8 +206,8 @@ public class ProfileActivity extends BaseActivity {
getString(R.string.cancel),
() -> shareScreen(screenshot),
() -> {},
view,
true);
view
);
}
/**

View file

@ -323,8 +323,7 @@ class AchievementsFragment : CommonsDaggerSupportFragment(){
null,
message,
getString(R.string.ok),
{},
true
{}
)
// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE);
@ -510,8 +509,7 @@ class AchievementsFragment : CommonsDaggerSupportFragment(){
title,
message,
getString(R.string.ok),
{},
true
{}
)
}
@ -527,8 +525,7 @@ class AchievementsFragment : CommonsDaggerSupportFragment(){
getString(R.string.read_help_link),
{},
{ Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) },
null,
true
null
)
}
/**

View file

@ -66,6 +66,7 @@ class QuizActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle(getString(R.string.warning))
.setMessage(getString(R.string.quiz_back_button))
.setCancelable(false)
.setPositiveButton(R.string.continue_message) { dialog, _ ->
val intent = Intent(this, QuizResultActivity::class.java)
dialog.dismiss()
@ -137,6 +138,7 @@ class QuizActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.continue_message) { dialog, _ ->
questionIndex++
if (questionIndex == quiz.size) {

View file

@ -181,6 +181,7 @@ class QuizResultActivity : AppCompatActivity() {
val shareMessage = view.findViewById<TextView>(R.id.alert_text)
shareMessage.setText(R.string.quiz_result_share_message)
alertadd.setView(view)
alertadd.setCancelable(false)
alertadd.setPositiveButton(R.string.about_translate_proceed) { dialog, _ ->
shareScreen(screenshot)
}

View file

@ -273,8 +273,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
getString(R.string.read_help_link),
{ },
{ Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) },
null,
true
null
)
}
@ -333,7 +332,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val dialog = Dialog(requireActivity())
dialog.setContentView(R.layout.dialog_select_language)
dialog.setCanceledOnTouchOutside(true)
dialog.setCancelable(false)
dialog.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.90).toInt(),
(resources.displayMetrics.heightPixels * 0.90).toInt()

View file

@ -270,8 +270,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
getString(R.string.block_notification_title),
getString(R.string.block_notification),
getString(R.string.ok),
this::finish,
true)));
this::finish)));
}
public void checkStoragePermissions() {
@ -418,16 +417,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
getString(R.string.storage_permissions_denied),
getString(R.string.unable_to_share_upload_item),
getString(android.R.string.ok),
this::finish,
false);
this::finish);
} else {
DialogUtil.showAlertDialog(this,
getString(R.string.storage_permission_title),
getString(
R.string.write_storage_permission_rationale_for_image_share),
getString(android.R.string.ok),
this::checkStoragePermissions,
false);
this::checkStoragePermissions);
}
}
}
@ -754,8 +751,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
"",
getString(messageResourceId),
getString(R.string.ok),
onPositiveClick,
false);
onPositiveClick);
}
@Override

View file

@ -346,7 +346,7 @@ public class UploadMediaDetailAdapter extends
public void onClick(View view) {
Dialog dialog = new Dialog(view.getContext());
dialog.setContentView(R.layout.dialog_select_language);
dialog.setCanceledOnTouchOutside(true);
dialog.setCancelable(false);
dialog.getWindow().setLayout(
(int) (view.getContext().getResources().getDisplayMetrics().widthPixels
* 0.90),

View file

@ -105,7 +105,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
binding.tooltip.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
DialogUtil.showAlertDialog(getActivity(), getString(R.string.categories_activity_title), getString(R.string.categories_tooltip), getString(android.R.string.ok), null, true);
DialogUtil.showAlertDialog(getActivity(), getString(R.string.categories_activity_title), getString(R.string.categories_tooltip), getString(android.R.string.ok), null);
}
});
if (media == null) {

View file

@ -114,7 +114,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
setDepictsSubTitle();
binding.tooltip.setOnClickListener(v -> DialogUtil
.showAlertDialog(getActivity(), getString(R.string.depicts_step_title),
getString(R.string.depicts_tooltip), getString(android.R.string.ok), null, true));
getString(R.string.depicts_tooltip), getString(android.R.string.ok), null));
if (media == null) {
presenter.onAttachView(this);
} else {

View file

@ -69,7 +69,7 @@ public class MediaLicenseFragment extends UploadBaseFragment implements MediaLic
getString(R.string.license_step_title),
getString(R.string.license_tooltip),
getString(android.R.string.ok),
null, true)
null)
);
initPresenter();

View file

@ -309,7 +309,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
*/
private void showInfoAlert(int titleStringID, int messageStringId) {
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID),
getString(messageStringId), getString(android.R.string.ok), null, true);
getString(messageStringId), getString(android.R.string.ok), null);
}
@ -336,6 +336,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
BasicKvStore basicKvStore = new BasicKvStore(getActivity(), "IsAnyImageCancelled");
if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) {
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
newFragment.setCancelable(false);
newFragment.setCallback(new SimilarImageDialogFragment.Callback() {
@Override
public void onPositiveResponse() {
@ -450,7 +451,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
// Execute when user cancels the upload of the specified place
UploadActivity.nearbyPopupAnswers.put(place, false);
},
customLayout, true);
customLayout
);
}
}
@ -526,8 +528,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onImageValidationSuccess();
}, null,
checkBoxView,
false);
checkBoxView);
} else {
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onImageValidationSuccess();
@ -588,8 +589,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
basicKvStore.putBoolean(keyForShowingAlertDialog, false);
activity.finish();
},
null,
false
null
);
}
} catch (Exception e) {

View file

@ -79,7 +79,6 @@ object DialogUtil {
onPositiveBtnClick: Runnable?,
onNegativeBtnClick: Runnable?,
customView: View?,
cancelable: Boolean,
): AlertDialog? =
createAndShowDialogSafely(
activity = activity,
@ -90,7 +89,6 @@ object DialogUtil {
onPositiveBtnClick = onPositiveBtnClick,
onNegativeBtnClick = onNegativeBtnClick,
customView = customView,
cancelable = cancelable,
)
@JvmStatic
@ -103,7 +101,6 @@ object DialogUtil {
onPositiveBtnClick: Runnable?,
onNegativeBtnClick: Runnable?,
customView: View?,
cancelable: Boolean,
): AlertDialog? =
createAndShowDialogSafely(
activity = activity,
@ -114,7 +111,6 @@ object DialogUtil {
onPositiveBtnClick = onPositiveBtnClick,
onNegativeBtnClick = onNegativeBtnClick,
customView = customView,
cancelable = cancelable,
)
@JvmStatic
@ -124,7 +120,6 @@ object DialogUtil {
message: String?,
positiveButtonText: String?,
onPositiveBtnClick: Runnable?,
cancelable: Boolean,
): AlertDialog? =
createAndShowDialogSafely(
activity = activity,
@ -132,7 +127,6 @@ object DialogUtil {
message = message,
positiveButtonText = positiveButtonText,
onPositiveBtnClick = onPositiveBtnClick,
cancelable = cancelable,
)
/**
@ -156,7 +150,7 @@ object DialogUtil {
onPositiveBtnClick: Runnable? = null,
onNegativeBtnClick: Runnable? = null,
customView: View? = null,
cancelable: Boolean = true,
cancelable: Boolean = false,
): AlertDialog? {
/* If the custom view already has a parent, there is already a dialog showing with the view
* This happens for on resume - return to avoid creating a second dialog - the first one

View file

@ -283,7 +283,8 @@ object ImageUtils {
context,
context.getString(R.string.setting_wallpaper_dialog_title),
context.getString(R.string.setting_wallpaper_dialog_message),
true
true,
false
)
}
@ -293,7 +294,8 @@ object ImageUtils {
context,
context.getString(R.string.setting_avatar_dialog_title),
context.getString(R.string.setting_avatar_dialog_message),
true
true,
false
)
}

View file

@ -186,7 +186,7 @@ object PermissionUtils {
activity.isShowPermissionsDialog = true
}
},
null, null, activity !is UploadActivity
null, null
)
}
else -> Thread(onPermissionDenied).start()
@ -223,7 +223,7 @@ object PermissionUtils {
activity.finish()
}
},
null, false
null
)
}
}).onSameThread().check()

View file

@ -108,7 +108,7 @@ class BookmarkPicturesFragmentUnitTests {
GridViewAdapter(
context,
0,
listOf(media()),
mutableListOf(media()),
),
)
Whitebox.setInternalState(fragment, "binding", binding)

View file

@ -17,8 +17,6 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI
import fr.free.nrw.commons.category.CategoryContentProvider.uriForId
import fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS
import fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_DESCRIPTION
import fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID
@ -31,6 +29,7 @@ import fr.free.nrw.commons.category.CategoryDao.Table.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.category.CategoryDao.Table.onCreate
import fr.free.nrw.commons.category.CategoryDao.Table.onDelete
import fr.free.nrw.commons.category.CategoryDao.Table.onUpdate
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@ -85,21 +84,21 @@ class CategoryDaoTest {
@Test
fun migrateTableVersionFrom_v1_to_v2() {
onUpdate(database, 1, 2)
// Table didnt exist before v5
// Table didn't exist before v5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v2_to_v3() {
onUpdate(database, 2, 3)
// Table didnt exist before v5
// Table didn't exist before v5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v3_to_v4() {
onUpdate(database, 3, 4)
// Table didnt exist before v5
// Table didn't exist before v5
verifyNoInteractions(database)
}
@ -112,21 +111,21 @@ class CategoryDaoTest {
@Test
fun migrateTableVersionFrom_v5_to_v6() {
onUpdate(database, 5, 6)
// Table didnt change in version 6
// Table didn't change in version 6
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v6_to_v7() {
onUpdate(database, 6, 7)
// Table didnt change in version 7
// Table didn't change in version 7
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v7_to_v8() {
onUpdate(database, 7, 8)
// Table didnt change in version 8
// Table didn't change in version 8
verifyNoInteractions(database)
}
@ -135,9 +134,9 @@ class CategoryDaoTest {
createCursor(1).let { cursor ->
cursor.moveToFirst()
testObject.fromCursor(cursor).let {
assertEquals(uriForId(1), it.contentUri)
assertEquals(CategoryContentProvider.uriForId(1), it.contentUri)
assertEquals("showImageWithItem", it.name)
assertEquals(123, it.lastUsed.time)
assertEquals(123L, it.lastUsed?.time)
assertEquals(2, it.timesUsed)
}
}
@ -150,13 +149,18 @@ class CategoryDaoTest {
testObject.save(category)
verify(client).update(eq(category.contentUri), captor.capture(), isNull(), isNull())
verify(client).update(
eq(category.contentUri)!!,
captor.capture(),
isNull(),
isNull()
)
captor.firstValue.let { cv ->
assertEquals(5, cv.size())
assertEquals(category.name, cv.getAsString(COLUMN_NAME))
assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL))
assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.lastUsed?.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED))
}
}
@ -164,7 +168,7 @@ class CategoryDaoTest {
@Test
fun saveNewCategory() {
val contentUri = CategoryContentProvider.uriForId(111)
val contentUri = uriForId(111)
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
val category =
Category(
@ -178,13 +182,13 @@ class CategoryDaoTest {
testObject.save(category)
verify(client).insert(eq(BASE_URI), captor.capture())
verify(client).insert(eq(CategoryContentProvider.BASE_URI), captor.capture())
captor.firstValue.let { cv ->
assertEquals(5, cv.size())
assertEquals(category.name, cv.getAsString(COLUMN_NAME))
assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL))
assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.lastUsed?.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED))
assertEquals(contentUri, category.contentUri)
}
@ -226,7 +230,7 @@ class CategoryDaoTest {
val category = testObject.find("showImageWithItem")
assertNotNull(category)
assertEquals(uriForId(1), category?.contentUri)
assertEquals(CategoryContentProvider.uriForId(1), category?.contentUri)
assertEquals("showImageWithItem", category?.name)
assertEquals("description", category?.description)
assertEquals("image", category?.thumbnail)
@ -234,7 +238,7 @@ class CategoryDaoTest {
assertEquals(2, category?.timesUsed)
verify(client).query(
eq(BASE_URI),
eq(CategoryContentProvider.BASE_URI),
eq(ALL_FIELDS),
eq("$COLUMN_NAME=?"),
queryCaptor.capture(),
@ -288,7 +292,7 @@ class CategoryDaoTest {
assertEquals("showImageWithItem", result[0].name)
verify(client).query(
eq(BASE_URI),
eq(CategoryContentProvider.BASE_URI),
eq(ALL_FIELDS),
isNull(),
queryCaptor.capture(),

View file

@ -38,7 +38,7 @@ class GridViewAdapterUnitTest {
private lateinit var parent: ViewGroup
@Mock
private lateinit var images: List<Media>
private lateinit var images: MutableList<Media>
@Mock
private lateinit var textView: TextView
@ -82,20 +82,20 @@ class GridViewAdapterUnitTest {
@Test
fun testContainsAllDataEmpty() {
gridViewAdapter = GridViewAdapter(context, 0, listOf())
gridViewAdapter = GridViewAdapter(context, 0, mutableListOf())
Assert.assertEquals(gridViewAdapter.containsAll(images), false)
}
@Test
fun testContainsAll() {
gridViewAdapter = GridViewAdapter(context, 0, listOf(media1))
gridViewAdapter = GridViewAdapter(context, 0, mutableListOf(media1))
`when`(media1.filename).thenReturn("")
Assert.assertEquals(gridViewAdapter.containsAll(listOf(media1)), true)
}
@Test
fun testGetItem() {
gridViewAdapter = GridViewAdapter(context, 0, listOf(media1))
gridViewAdapter = GridViewAdapter(context, 0, mutableListOf(media1))
Assert.assertEquals(gridViewAdapter.getItem(0), media1)
}
@ -107,7 +107,7 @@ class GridViewAdapterUnitTest {
@Test
fun testGetView() {
gridViewAdapter = GridViewAdapter(context, 0, listOf(media1))
gridViewAdapter = GridViewAdapter(context, 0, mutableListOf(media1))
`when`(media1.mostRelevantCaption).thenReturn("")
Assert.assertEquals(gridViewAdapter.getView(0, convertView, parent), convertView)
}

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(