mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +01:00 
			
		
		
		
	Merge branch 'main' into recent-search
This commit is contained in:
		
						commit
						5415471045
					
				
					 65 changed files with 2160 additions and 2056 deletions
				
			
		|  | @ -180,8 +180,8 @@ public class AboutActivity extends BaseActivity { | |||
|             getString(R.string.about_translate_cancel), | ||||
|             positiveButtonRunnable, | ||||
|             () -> {}, | ||||
|             spinner, | ||||
|             true); | ||||
|             spinner | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|     } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								app/src/main/java/fr/free/nrw/commons/category/Category.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/java/fr/free/nrw/commons/category/Category.kt
									
										
									
									
									
										Normal 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++ | ||||
|     } | ||||
| } | ||||
|  | @ -1,5 +0,0 @@ | |||
| package fr.free.nrw.commons.category; | ||||
| 
 | ||||
| public interface CategoryClickedListener { | ||||
|     void categoryClicked(CategoryItem item); | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| interface CategoryClickedListener { | ||||
|     fun categoryClicked(item: CategoryItem) | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -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}") | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										194
									
								
								app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt
									
										
									
									
									
										Normal 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) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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() | ||||
|     } | ||||
| } | ||||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
|  | @ -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); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,7 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| interface CategoryImagesCallback { | ||||
|     fun viewPagerNotifyDataSetChanged() | ||||
| 
 | ||||
|     fun onMediaClicked(position: Int) | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +0,0 @@ | |||
| package fr.free.nrw.commons.category; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| public interface OnCategoriesSaveHandler { | ||||
|     void onCategoriesSave(List<String> categories); | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| package fr.free.nrw.commons.category | ||||
| 
 | ||||
| interface OnCategoriesSaveHandler { | ||||
|     fun onCategoriesSave(categories: List<String>) | ||||
| } | ||||
|  | @ -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 | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -566,8 +566,8 @@ public class ContributionsFragment | |||
|             getString(R.string.nearby_card_permission_explanation), | ||||
|             this::requestLocationPermission, | ||||
|             this::displayYouWontSeeNearbyMessage, | ||||
|             checkBoxView, | ||||
|             false); | ||||
|             checkBoxView | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void displayYouWontSeeNearbyMessage() { | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
|  | @ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt
									
										
									
									
									
										Normal 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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										182
									
								
								app/src/main/java/fr/free/nrw/commons/db/Converters.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								app/src/main/java/fr/free/nrw/commons/db/Converters.kt
									
										
									
									
									
										Normal 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) } | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										334
									
								
								app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										334
									
								
								app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt
									
										
									
									
									
										Normal 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 | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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() | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
|  | @ -133,8 +133,8 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment | |||
|                             askForLocationPermission(); | ||||
|                         }, | ||||
|                         null, | ||||
|                         null, | ||||
|                         false); | ||||
|                         null | ||||
|                     ); | ||||
|                 } else { | ||||
|                     if (isPermissionDenied) { | ||||
|                         locationPermissionsHelper.showAppSettingsDialog(getActivity(), | ||||
|  |  | |||
|  | @ -78,8 +78,7 @@ class LocationPermissionsHelper( | |||
|                             activity.getString(R.string.upload_map_location_access) | ||||
|                         ) | ||||
|                     }, | ||||
|                     null, | ||||
|                     false | ||||
|                     null | ||||
|                 ) | ||||
|             } else { | ||||
|                 ActivityCompat.requestPermissions( | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -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(); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|     } | ||||
|  |  | |||
|  | @ -291,8 +291,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                             askForLocationPermission(); | ||||
|                         }, | ||||
|                         null, | ||||
|                         null, | ||||
|                         false); | ||||
|                         null | ||||
|                     ); | ||||
|                 } else { | ||||
|                     if (isPermissionDenied) { | ||||
|                         locationPermissionsHelper.showAppSettingsDialog(getActivity(), | ||||
|  |  | |||
|  | @ -206,8 +206,8 @@ public class ProfileActivity extends BaseActivity { | |||
|             getString(R.string.cancel), | ||||
|             () -> shareScreen(screenshot), | ||||
|             () -> {}, | ||||
|             view, | ||||
|             true); | ||||
|             view | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -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 | ||||
|         ) | ||||
|     } | ||||
|     /** | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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) | ||||
|         } | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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), | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ class BookmarkPicturesFragmentUnitTests { | |||
|             GridViewAdapter( | ||||
|                 context, | ||||
|                 0, | ||||
|                 listOf(media()), | ||||
|                 mutableListOf(media()), | ||||
|             ), | ||||
|         ) | ||||
|         Whitebox.setInternalState(fragment, "binding", binding) | ||||
|  |  | |||
|  | @ -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(), | ||||
|  |  | |||
|  | @ -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) | ||||
|     } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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)) | ||||
|     } | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Neel Doshi
						Neel Doshi