Migrated category module from Java to Kotlin (#6016)

* Rename .java to .kt

* Rebased category PR

* Resolved conflicts

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Saifuddin Adenwala 2024-12-11 07:47:05 +05:30 committed by GitHub
parent 3030a6fca7
commit c175a4ee03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 939 additions and 1021 deletions

View file

@ -78,7 +78,13 @@ class CategoriesModel
// Newly used category... // Newly used category...
if (category == null) { 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() category.incTimesUsed()
categoryDao.save(category) 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

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

View file

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