mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +01:00
Merge branch 'main' into jetpack-custom-selector
This commit is contained in:
commit
4ebb945308
256 changed files with 7222 additions and 6600 deletions
|
|
@ -100,12 +100,12 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||
|
||||
//Mocking
|
||||
testImplementation("io.mockk:mockk:1.13.4")
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
||||
testImplementation("io.mockk:mockk:1.13.5")
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
|
@ -374,11 +374,11 @@ android {
|
|||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildToolsVersion buildToolsVersion
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ public class LocationPickerActivity extends BaseActivity implements
|
|||
*/
|
||||
private void removeLocationFromImage() {
|
||||
if (media != null) {
|
||||
compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
|
||||
getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
|
||||
, media, "0.0", "0.0", "0.0f")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
@ -479,7 +479,7 @@ public class LocationPickerActivity extends BaseActivity implements
|
|||
}
|
||||
|
||||
try {
|
||||
compositeDisposable.add(
|
||||
getCompositeDisposable().add(
|
||||
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
|
||||
Latitude, Longitude, Accuracy)
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ import androidx.annotation.Nullable;
|
|||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.utils.SwipableCardView;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.campaigns;
|
|||
import android.annotation.SuppressLint;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import java.text.ParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
|
@ -14,7 +15,6 @@ import javax.inject.Singleton;
|
|||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ class CategoryClient
|
|||
}.map {
|
||||
it
|
||||
.filter { page ->
|
||||
!page.categoryInfo().isHidden
|
||||
// Null check is not redundant because some values could be null
|
||||
// for mocks when running unit tests
|
||||
page.categoryInfo()?.isHidden != true
|
||||
}.map {
|
||||
CategoryItem(
|
||||
it.title().replace(CATEGORY_PREFIX, ""),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public class ContributionController {
|
|||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -224,7 +224,7 @@ public class ContributionController {
|
|||
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
|||
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
||||
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||
|
||||
|
|
@ -23,12 +22,10 @@ import android.view.LayoutInflater;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MenuItem.OnMenuItemClickListener;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
|
|
@ -39,7 +36,6 @@ import androidx.annotation.Nullable;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||
|
|
@ -293,7 +289,7 @@ public class ContributionsFragment
|
|||
});
|
||||
}
|
||||
notification.setOnClickListener(view -> {
|
||||
NotificationActivity.startYourself(getContext(), "unread");
|
||||
NotificationActivity.Companion.startYourself(getContext(), "unread");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
|
@ -16,10 +13,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import fr.free.nrw.commons.databinding.MainBinding;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
|
|
@ -41,7 +36,6 @@ import fr.free.nrw.commons.notification.NotificationController;
|
|||
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.upload.UploadProgressActivity;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
|
|
@ -420,7 +414,7 @@ public class MainActivity extends BaseActivity
|
|||
return true;
|
||||
case R.id.notifications:
|
||||
// Starts notification activity on click to notification icon
|
||||
NotificationActivity.startYourself(this, "unread");
|
||||
NotificationActivity.Companion.startYourself(this, "unread");
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
package fr.free.nrw.commons.customselector.helper
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import fr.free.nrw.commons.R
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
object FolderDeletionHelper {
|
||||
|
||||
/**
|
||||
* Prompts the user to confirm deletion of a specified folder and, if confirmed, deletes it.
|
||||
*
|
||||
* @param context The context used to show the confirmation dialog and manage deletion.
|
||||
* @param folder The folder to be deleted.
|
||||
* @param onDeletionComplete Callback invoked with `true` if the folder was
|
||||
* @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
|
||||
* successfully deleted, `false` otherwise.
|
||||
*/
|
||||
fun confirmAndDeleteFolder(
|
||||
context: Context,
|
||||
folder: File,
|
||||
trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>,
|
||||
onDeletionComplete: (Boolean) -> Unit) {
|
||||
val itemCount = countItemsInFolder(context, folder)
|
||||
val folderPath = folder.absolutePath
|
||||
|
||||
//don't show this dialog on API 30+, it's handled automatically using MediaStore
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val success = deleteFolderMain(context, folder, trashFolderLauncher)
|
||||
onDeletionComplete(success)
|
||||
|
||||
} else {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
||||
.setMessage(context.getString(R.string.custom_selector_confirm_deletion_message, folderPath, itemCount))
|
||||
.setPositiveButton(context.getString(R.string.custom_selector_delete)) { _, _ ->
|
||||
|
||||
//proceed with deletion if user confirms
|
||||
val success = deleteFolderMain(context, folder, trashFolderLauncher)
|
||||
onDeletionComplete(success)
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.custom_selector_cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDeletionComplete(false)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified folder, handling different Android storage models based on the API
|
||||
*
|
||||
* @param context The context used to manage storage operations.
|
||||
* @param folder The folder to delete.
|
||||
* @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
|
||||
* @return `true` if the folder deletion was successful, `false` otherwise.
|
||||
*/
|
||||
private fun deleteFolderMain(
|
||||
context: Context,
|
||||
folder: File,
|
||||
trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>): Boolean
|
||||
{
|
||||
return when {
|
||||
//for API 30 and above, use MediaStore
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> trashFolderContents(context, folder, trashFolderLauncher)
|
||||
|
||||
//for API 29 ('requestLegacyExternalStorage' is set to true in Manifest)
|
||||
// and below use file system
|
||||
else -> deleteFolderLegacy(folder)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves all contents of a specified folder to the trash on devices running
|
||||
* Android 11 (API level 30) and above.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder whose contents are to be moved to the trash.
|
||||
* @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
|
||||
* @return `true` if the trash request was initiated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun trashFolderContents(
|
||||
context: Context,
|
||||
folder: File,
|
||||
trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>): Boolean
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
val urisToTrash = mutableListOf<Uri>()
|
||||
|
||||
// Use URIs specific to media items
|
||||
val mediaUris = listOf(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
)
|
||||
|
||||
for (mediaUri in mediaUris) {
|
||||
val selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%")
|
||||
|
||||
contentResolver.query(mediaUri, arrayOf(MediaStore.MediaColumns._ID), selection,
|
||||
selectionArgs, null)
|
||||
?.use{ cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val fileUri = ContentUris.withAppendedId(mediaUri, id)
|
||||
urisToTrash.add(fileUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//proceed with trashing if we have valid URIs
|
||||
if (urisToTrash.isNotEmpty()) {
|
||||
try {
|
||||
val trashRequest = MediaStore.createTrashRequest(contentResolver, urisToTrash, true)
|
||||
val intentSenderRequest = IntentSenderRequest.Builder(trashRequest.intentSender).build()
|
||||
trashFolderLauncher.launch(intentSenderRequest)
|
||||
return true
|
||||
} catch (e: SecurityException) {
|
||||
Timber.tag("DeleteFolder").e(context.getString(R.string.custom_selector_error_trashing_folder_contents, e.message))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Counts the number of items in a specified folder, including items in subfolders.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder in which to count items.
|
||||
* @return The total number of items in the folder.
|
||||
*/
|
||||
private fun countItemsInFolder(context: Context, folder: File): Int {
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
val selection = "${MediaStore.Images.Media.DATA} LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%")
|
||||
|
||||
return contentResolver.query(
|
||||
uri,
|
||||
arrayOf(MediaStore.Images.Media._ID),
|
||||
selection,
|
||||
selectionArgs,
|
||||
null)?.use { cursor ->
|
||||
cursor.count
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Refreshes the MediaStore for a specified folder, updating the system to recognize any changes
|
||||
*
|
||||
* @param context The context used to access the MediaScannerConnection.
|
||||
* @param folder The folder to refresh in the MediaStore.
|
||||
*/
|
||||
fun refreshMediaStore(context: Context, folder: File) {
|
||||
MediaScannerConnection.scanFile(
|
||||
context,
|
||||
arrayOf(folder.absolutePath),
|
||||
null
|
||||
) { _, _ -> }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a specified folder and all of its contents on devices running
|
||||
* Android 10 (API level 29) and below.
|
||||
*
|
||||
* @param folder The `File` object representing the folder to be deleted.
|
||||
* @return `true` if the folder and all contents were deleted successfully; `false` otherwise.
|
||||
*/
|
||||
private fun deleteFolderLegacy(folder: File): Boolean {
|
||||
return folder.deleteRecursively()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the absolute path of a folder given its unique identifier (bucket ID).
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folderId The unique identifier (bucket ID) of the folder.
|
||||
* @return The absolute path of the folder as a `String`, or `null` if the folder is not found.
|
||||
*/
|
||||
fun getFolderPath(context: Context, folderId: Long): String? {
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
val selection = "${MediaStore.Images.Media.BUCKET_ID} = ?"
|
||||
val selectionArgs = arrayOf(folderId.toString())
|
||||
|
||||
context.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val fullPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
|
||||
return File(fullPath).parent
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message to the user and logs it for debugging purposes.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The error message to display and log.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showError(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_failure, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").e(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a success message to the user.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The success message to display.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showSuccess(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_success, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").d(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,11 +13,26 @@ import android.view.View
|
|||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
|
@ -34,6 +49,7 @@ import fr.free.nrw.commons.customselector.data.MediaReader
|
|||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
|
|
@ -136,10 +152,23 @@ class CustomSelectorActivity :
|
|||
|
||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||
|
||||
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
|
||||
/**
|
||||
* Show delete button in folder
|
||||
*/
|
||||
private var showOverflowMenu = false
|
||||
|
||||
/**
|
||||
* Waits for confirmation of delete folder
|
||||
*/
|
||||
private val startForFolderDeletionResult = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){
|
||||
result -> onDeleteFolderResultReceived(result)
|
||||
}
|
||||
|
||||
private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
|
||||
onFullScreenDataReceived(result)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||
*/
|
||||
|
|
@ -264,6 +293,15 @@ class CustomSelectorActivity :
|
|||
}
|
||||
}
|
||||
|
||||
private fun onDeleteFolderResultReceived(result: ActivityResult){
|
||||
if (result.resultCode == Activity.RESULT_OK){
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show Custom Selector Welcome Dialog.
|
||||
*/
|
||||
|
|
@ -445,10 +483,97 @@ class CustomSelectorActivity :
|
|||
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
||||
limitError.visibility = View.INVISIBLE
|
||||
limitError.setOnClickListener { displayUploadLimitWarning() }
|
||||
|
||||
val overflowMenu: ImageButton = findViewById(R.id.menu_overflow)
|
||||
if(defaultKvStore.getBoolean("displayDeletionButton")) {
|
||||
overflowMenu.visibility = if (showOverflowMenu) View.VISIBLE else View.INVISIBLE
|
||||
overflowMenu.setOnClickListener { showPopupMenu(overflowMenu) }
|
||||
}else{
|
||||
overflowMenu.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showPopupMenu(anchorView: View) {
|
||||
val popupMenu = PopupMenu(this, anchorView)
|
||||
popupMenu.menuInflater.inflate(R.menu.menu_custom_selector, popupMenu.menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_delete_folder -> {
|
||||
deleteFolder()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* override on folder click, change the toolbar title on folder click.
|
||||
* Deletes folder based on Android API version.
|
||||
*/
|
||||
private fun deleteFolder() {
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: run {
|
||||
FolderDeletionHelper.showError(this, "Failed to retrieve folder path", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
val folder = File(folderPath)
|
||||
if (!folder.exists() || !folder.isDirectory) {
|
||||
FolderDeletionHelper.showError(this,"Folder not found or is not a directory", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
FolderDeletionHelper.confirmAndDeleteFolder(this, folder, startForFolderDeletionResult) { success ->
|
||||
if (success) {
|
||||
//for API 30+, navigation is handled in 'onDeleteFolderResultReceived'
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
} else {
|
||||
FolderDeletionHelper.showError(this, "Failed to delete folder", bucketName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Navigates back to the main `FolderFragment`, refreshes the MediaStore, resets UI states,
|
||||
* and reloads folder data.
|
||||
*/
|
||||
private fun navigateToCustomSelector() {
|
||||
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: ""
|
||||
val folder = File(folderPath)
|
||||
|
||||
supportFragmentManager.popBackStack(null,
|
||||
androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
//refresh MediaStore for the deleted folder path to ensure metadata updates
|
||||
FolderDeletionHelper.refreshMediaStore(this, folder)
|
||||
|
||||
//replace the current fragment with FolderFragment to go back to the main screen
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||
.commitAllowingStateLoss()
|
||||
|
||||
//reset toolbar and flags
|
||||
isImageFragmentOpen = false
|
||||
showOverflowMenu = false
|
||||
setUpToolbar()
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
|
||||
//fetch updated folder data
|
||||
fetchData()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* override on folder click,
|
||||
* change the toolbar title on folder click, make overflow menu visible
|
||||
*/
|
||||
override fun onFolderClick(
|
||||
folderId: Long,
|
||||
|
|
@ -466,6 +591,11 @@ class CustomSelectorActivity :
|
|||
bucketId = folderId
|
||||
bucketName = folderName
|
||||
isImageFragmentOpen = true
|
||||
|
||||
//show the overflow menu only when a folder is clicked
|
||||
showOverflowMenu = true
|
||||
setUpToolbar()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -581,6 +711,10 @@ class CustomSelectorActivity :
|
|||
isImageFragmentOpen = false
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
}
|
||||
|
||||
//hide overflow menu when not in folder
|
||||
showOverflowMenu = false
|
||||
setUpToolbar()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.free.nrw.commons.customselector.ui.selector
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
|
|
@ -346,7 +347,7 @@ class ImageFragment :
|
|||
context
|
||||
.getSharedPreferences(
|
||||
"CustomSelector",
|
||||
BaseActivity.MODE_PRIVATE,
|
||||
MODE_PRIVATE,
|
||||
)?.let { prefs ->
|
||||
prefs.edit()?.let { editor ->
|
||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileE
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -65,7 +66,7 @@ class ImageLoader
|
|||
/**
|
||||
* Coroutine Scope.
|
||||
*/
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
private val scope: CoroutineScope = MainScope()
|
||||
|
||||
/**
|
||||
* Query image and setUp the view.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
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 static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
|
@ -16,6 +16,7 @@ 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;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.di;
|
|||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.explore.categories.CategoriesModule;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ public class SearchActivity extends BaseActivity
|
|||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox)
|
||||
getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox)
|
||||
.takeUntil(RxView.detaches(binding.searchBox))
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
@ -284,7 +284,7 @@ public class SearchActivity extends BaseActivity
|
|||
@Override protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//Dispose the disposables when the activity is destroyed
|
||||
compositeDisposable.dispose();
|
||||
getCompositeDisposable().dispose();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChange
|
|||
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
||||
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
||||
|
||||
import android.Manifest;
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
|
|
@ -21,22 +19,17 @@ import android.location.Location;
|
|||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.BaseMarker;
|
||||
import fr.free.nrw.commons.MapController;
|
||||
|
|
@ -48,7 +41,6 @@ import fr.free.nrw.commons.databinding.FragmentExploreMapBinding;
|
|||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
||||
import fr.free.nrw.commons.explore.paging.LiveDataConverter;
|
||||
import fr.free.nrw.commons.filepicker.Constants;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
||||
|
|
@ -60,7 +52,6 @@ import fr.free.nrw.commons.nearby.Place;
|
|||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.MapUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
|
|
@ -310,7 +301,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
|||
}
|
||||
|
||||
private void startMapWithoutPermission() {
|
||||
lastKnownLocation = MapUtils.defaultLatLng;
|
||||
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||
moveCameraToPosition(
|
||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||
presenter.onMapReady(exploreMapController);
|
||||
|
|
@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
|||
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||
isPermissionDenied = true;
|
||||
}
|
||||
lastKnownLocation = MapUtils.defaultLatLng;
|
||||
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||
moveCameraToPosition(
|
||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||
presenter.onMapReady(exploreMapController);
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ public interface Constants {
|
|||
String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos";
|
||||
String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ import fr.free.nrw.commons.Utils;
|
|||
import fr.free.nrw.commons.actions.ThanksClient;
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.category.CategoryClient;
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
|
|
@ -272,6 +271,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
|||
|
||||
if (!sessionManager.isUserLoggedIn()) {
|
||||
binding.categoryEditButton.setVisibility(GONE);
|
||||
binding.descriptionEdit.setVisibility(GONE);
|
||||
binding.depictionsEditButton.setVisibility(GONE);
|
||||
} else {
|
||||
binding.categoryEditButton.setVisibility(VISIBLE);
|
||||
binding.descriptionEdit.setVisibility(VISIBLE);
|
||||
binding.depictionsEditButton.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if(applicationKvStore.getBoolean("login_skipped")){
|
||||
|
|
@ -313,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
|||
}
|
||||
|
||||
public void launchZoomActivity(final View view) {
|
||||
final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE);
|
||||
final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE());
|
||||
if (hasPermission) {
|
||||
launchZoomActivityAfterPermissionCheck(view);
|
||||
} else {
|
||||
|
|
@ -323,7 +328,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
|||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.read_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE
|
||||
PermissionUtils.getPERMISSIONS_STORAGE()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -400,7 +405,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
|||
}
|
||||
);
|
||||
binding.progressBarEdit.setVisibility(GONE);
|
||||
binding.descriptionEdit.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -678,7 +682,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
|||
// Stick in a filler element.
|
||||
allCategories.add(getString(R.string.detail_panel_cats_none));
|
||||
}
|
||||
binding.categoryEditButton.setVisibility(VISIBLE);
|
||||
if(sessionManager.isUserLoggedIn()) {
|
||||
binding.categoryEditButton.setVisibility(VISIBLE);
|
||||
}
|
||||
rebuildCatList(allCategories);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ public class OkHttpJsonApiClient {
|
|||
throws Exception {
|
||||
|
||||
Timber.d("Fetching nearby items at radius %s", radius);
|
||||
Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null));
|
||||
Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null));
|
||||
final String wikidataQuery;
|
||||
if (customQuery != null) {
|
||||
wikidataQuery = customQuery;
|
||||
|
|
@ -344,7 +344,7 @@ public class OkHttpJsonApiClient {
|
|||
final boolean shouldQueryForMonuments, final String customQuery)
|
||||
throws Exception {
|
||||
|
||||
Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null));
|
||||
Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null));
|
||||
|
||||
final String wikidataQuery;
|
||||
if (customQuery != null) {
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
|
|||
}
|
||||
|
||||
protected void onPeerReviewClicked() {
|
||||
ReviewActivity.startYourself(getActivity(), getString(R.string.title_activity_review));
|
||||
ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import android.view.ViewGroup;
|
|||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
|
|
@ -701,7 +700,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
= new LatLng(Double.parseDouble(locationLatLng[0]),
|
||||
Double.parseDouble(locationLatLng[1]), 1f);
|
||||
} else {
|
||||
lastKnownLocation = MapUtils.defaultLatLng;
|
||||
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||
}
|
||||
if (binding.map != null) {
|
||||
moveCameraToPosition(
|
||||
|
|
@ -793,7 +792,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
hideBottomSheet();
|
||||
binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener(
|
||||
(v, hasFocus) -> {
|
||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25,
|
||||
LayoutUtils.setLayoutHeightAlignedToWidth(1.25,
|
||||
binding.nearbyFilterList.getRoot());
|
||||
if (hasFocus) {
|
||||
binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE);
|
||||
|
|
@ -834,7 +833,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(),
|
||||
0.75);
|
||||
binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter);
|
||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
||||
LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
||||
compositeDisposable.add(
|
||||
RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView)
|
||||
.takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView))
|
||||
|
|
|
|||
|
|
@ -11,13 +11,10 @@ import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
|
|||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
|
||||
import android.location.Location;
|
||||
import android.view.View;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import fr.free.nrw.commons.BaseMarker;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
||||
|
|
@ -26,14 +23,10 @@ import fr.free.nrw.commons.nearby.CheckBoxTriStates;
|
|||
import fr.free.nrw.commons.nearby.Label;
|
||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
||||
import fr.free.nrw.commons.nearby.NearbyController;
|
||||
import fr.free.nrw.commons.nearby.NearbyFilterState;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.nearby.PlaceDao;
|
||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
import fr.free.nrw.commons.utils.LocationUtils;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import timber.log.Timber;
|
||||
|
|
|
|||
|
|
@ -1,288 +0,0 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
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 androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.databinding.ActivityNotificationBinding;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.notification.models.Notification;
|
||||
import fr.free.nrw.commons.notification.models.NotificationType;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Unit;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Created by root on 18.12.2017.
|
||||
*/
|
||||
|
||||
public class NotificationActivity extends BaseActivity {
|
||||
private ActivityNotificationBinding binding;
|
||||
|
||||
@Inject
|
||||
NotificationController controller;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
|
||||
private NotificationWorkerFragment mNotificationWorkerFragment;
|
||||
private NotificatinAdapter adapter;
|
||||
private List<Notification> notificationList;
|
||||
MenuItem notificationMenuItem;
|
||||
/**
|
||||
* Boolean isRead is true if this notification activity is for read section of notification.
|
||||
*/
|
||||
private boolean isRead;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
isRead = getIntent().getStringExtra("title").equals("read");
|
||||
binding = ActivityNotificationBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager()
|
||||
.findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT);
|
||||
initListView();
|
||||
setPageTitle();
|
||||
setSupportActionBar(binding.toolbar.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is unread section of the notifications, removeNotification method
|
||||
* Marks the notification as read,
|
||||
* Removes the notification from unread,
|
||||
* Displays the Snackbar.
|
||||
*
|
||||
* Otherwise returns (read section).
|
||||
*
|
||||
* @param notification
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void removeNotification(Notification notification) {
|
||||
if (isRead) {
|
||||
return;
|
||||
}
|
||||
Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
|
||||
() -> controller.markAsRead(notification))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
if (result) {
|
||||
notificationList.remove(notification);
|
||||
setItems(notificationList);
|
||||
adapter.notifyDataSetChanged();
|
||||
ViewUtil.showLongSnackbar(binding.container,getString(R.string.notification_mark_read));
|
||||
if (notificationList.size() == 0) {
|
||||
setEmptyView();
|
||||
binding.container.setVisibility(View.GONE);
|
||||
binding.noNotificationBackground.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
adapter.notifyDataSetChanged();
|
||||
setItems(notificationList);
|
||||
ViewUtil.showLongToast(this,getString(R.string.some_error));
|
||||
}
|
||||
}, throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
final String username = sessionManager.getUserName();
|
||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
);
|
||||
|
||||
CommonsApplication.getInstance().clearApplicationData(
|
||||
this, logoutListener);
|
||||
} else {
|
||||
Timber.e(throwable, "Error occurred while loading notifications");
|
||||
throwable.printStackTrace();
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
|
||||
}
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
});
|
||||
compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void initListView() {
|
||||
binding.listView.setLayoutManager(new LinearLayoutManager(this));
|
||||
DividerItemDecoration itemDecor = new DividerItemDecoration(binding.listView.getContext(), DividerItemDecoration.VERTICAL);
|
||||
binding.listView.addItemDecoration(itemDecor);
|
||||
if (isRead) {
|
||||
refresh(true);
|
||||
} else {
|
||||
refresh(false);
|
||||
}
|
||||
adapter = new NotificatinAdapter(item -> {
|
||||
Timber.d("Notification clicked %s", item.getLink());
|
||||
if (item.getNotificationType() == NotificationType.EMAIL){
|
||||
ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox));
|
||||
} else {
|
||||
handleUrl(item.getLink());
|
||||
}
|
||||
removeNotification(item);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
binding.listView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void refresh(boolean archived) {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.retry, view -> refresh(archived)).show();
|
||||
} else {
|
||||
addNotifications(archived);
|
||||
}
|
||||
binding.progressBar.setVisibility(View.VISIBLE);
|
||||
binding.noNotificationBackground.setVisibility(View.GONE);
|
||||
binding.container.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void addNotifications(boolean archived) {
|
||||
Timber.d("Add notifications");
|
||||
if (mNotificationWorkerFragment == null) {
|
||||
binding.progressBar.setVisibility(View.VISIBLE);
|
||||
compositeDisposable.add(controller.getNotifications(archived)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(notificationList -> {
|
||||
Collections.reverse(notificationList);
|
||||
Timber.d("Number of notifications is %d", notificationList.size());
|
||||
this.notificationList = notificationList;
|
||||
if (notificationList.size()==0){
|
||||
setEmptyView();
|
||||
binding.container.setVisibility(View.GONE);
|
||||
binding.noNotificationBackground.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
setItems(notificationList);
|
||||
}
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
}, throwable -> {
|
||||
Timber.e(throwable, "Error occurred while loading notifications ");
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
}));
|
||||
} else {
|
||||
notificationList = mNotificationWorkerFragment.getNotificationList();
|
||||
setItems(notificationList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_notifications, menu);
|
||||
notificationMenuItem = menu.findItem(R.id.archived);
|
||||
setMenuItemTitle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle item selection
|
||||
switch (item.getItemId()) {
|
||||
case R.id.archived:
|
||||
if (item.getTitle().equals(getString(R.string.menu_option_read))) {
|
||||
NotificationActivity.startYourself(NotificationActivity.this, "read");
|
||||
}else if (item.getTitle().equals(getString(R.string.menu_option_unread))) {
|
||||
onBackPressed();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUrl(String url) {
|
||||
if (url == null || url.equals("")) {
|
||||
return;
|
||||
}
|
||||
Utils.handleWebUrl(this, Uri.parse(url));
|
||||
}
|
||||
|
||||
private void setItems(List<Notification> notificationList) {
|
||||
if (notificationList == null || notificationList.isEmpty()) {
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications);
|
||||
/*progressBar.setVisibility(View.GONE);
|
||||
recyclerView.setVisibility(View.GONE);*/
|
||||
binding.container.setVisibility(View.GONE);
|
||||
setEmptyView();
|
||||
binding.noNotificationBackground.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
binding.container.setVisibility(View.VISIBLE);
|
||||
binding.noNotificationBackground.setVisibility(View.GONE);
|
||||
adapter.setItems(notificationList);
|
||||
}
|
||||
|
||||
public static void startYourself(Context context, String title) {
|
||||
Intent intent = new Intent(context, NotificationActivity.class);
|
||||
intent.putExtra("title", title);
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void setPageTitle() {
|
||||
if (getSupportActionBar() != null) {
|
||||
if (isRead) {
|
||||
getSupportActionBar().setTitle(R.string.read_notifications);
|
||||
} else {
|
||||
getSupportActionBar().setTitle(R.string.notifications);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setEmptyView() {
|
||||
if (isRead) {
|
||||
binding.noNotificationText.setText(R.string.no_read_notification);
|
||||
}else {
|
||||
binding.noNotificationText.setText(R.string.no_notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMenuItemTitle() {
|
||||
if (isRead) {
|
||||
notificationMenuItem.setTitle(R.string.menu_option_unread);
|
||||
|
||||
}else {
|
||||
notificationMenuItem.setTitle(R.string.menu_option_read);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package fr.free.nrw.commons.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.databinding.ActivityNotificationBinding
|
||||
import fr.free.nrw.commons.notification.models.Notification
|
||||
import fr.free.nrw.commons.notification.models.NotificationType
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.NetworkUtils
|
||||
import fr.free.nrw.commons.utils.ViewUtil
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by root on 18.12.2017.
|
||||
*/
|
||||
class NotificationActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityNotificationBinding
|
||||
|
||||
@Inject
|
||||
lateinit var controller: NotificationController
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
private val tagNotificationWorkerFragment = "NotificationWorkerFragment"
|
||||
private var mNotificationWorkerFragment: NotificationWorkerFragment? = null
|
||||
private lateinit var adapter: NotificationAdapter
|
||||
private var notificationList: MutableList<Notification> = mutableListOf()
|
||||
private var notificationMenuItem: MenuItem? = null
|
||||
|
||||
/**
|
||||
* Boolean isRead is true if this notification activity is for read section of notification.
|
||||
*/
|
||||
private var isRead: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
isRead = intent.getStringExtra("title") == "read"
|
||||
binding = ActivityNotificationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag(
|
||||
tagNotificationWorkerFragment
|
||||
) as? NotificationWorkerFragment
|
||||
initListView()
|
||||
setPageTitle()
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult", "NotifyDataSetChanged")
|
||||
fun removeNotification(notification: Notification) {
|
||||
if (isRead) return
|
||||
|
||||
val disposable = Observable.defer { controller.markAsRead(notification) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result ->
|
||||
if (result) {
|
||||
notificationList.remove(notification)
|
||||
setItems(notificationList)
|
||||
adapter.notifyDataSetChanged()
|
||||
ViewUtil.showLongSnackbar(binding.container, getString(R.string.notification_mark_read))
|
||||
if (notificationList.isEmpty()) {
|
||||
setEmptyView()
|
||||
binding.container.visibility = View.GONE
|
||||
binding.noNotificationBackground.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
adapter.notifyDataSetChanged()
|
||||
setItems(notificationList)
|
||||
ViewUtil.showLongToast(this, getString(R.string.some_error))
|
||||
}
|
||||
}, { throwable ->
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
val username = sessionManager.getUserName()
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
CommonsApplication.instance.clearApplicationData(this, logoutListener)
|
||||
} else {
|
||||
Timber.e(throwable, "Error occurred while loading notifications")
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
})
|
||||
compositeDisposable.add(disposable)
|
||||
}
|
||||
|
||||
private fun initListView() {
|
||||
binding.listView.layoutManager = LinearLayoutManager(this)
|
||||
val itemDecor = DividerItemDecoration(binding.listView.context, DividerItemDecoration.VERTICAL)
|
||||
binding.listView.addItemDecoration(itemDecor)
|
||||
refresh(isRead)
|
||||
adapter = NotificationAdapter { item ->
|
||||
Timber.d("Notification clicked %s", item.link)
|
||||
if (item.notificationType == NotificationType.EMAIL) {
|
||||
ViewUtil.showLongSnackbar(binding.container, getString(R.string.check_your_email_inbox))
|
||||
} else {
|
||||
handleUrl(item.link)
|
||||
}
|
||||
removeNotification(item)
|
||||
}
|
||||
binding.listView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun refresh(archived: Boolean) {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.retry) { refresh(archived) }
|
||||
.show()
|
||||
} else {
|
||||
addNotifications(archived)
|
||||
}
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.noNotificationBackground.visibility = View.GONE
|
||||
binding.container.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun addNotifications(archived: Boolean) {
|
||||
Timber.d("Add notifications")
|
||||
if (mNotificationWorkerFragment == null) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
compositeDisposable.add(controller.getNotifications(archived)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ notificationList ->
|
||||
notificationList.reversed()
|
||||
Timber.d("Number of notifications is %d", notificationList.size)
|
||||
this.notificationList = notificationList.toMutableList()
|
||||
if (notificationList.isEmpty()) {
|
||||
setEmptyView()
|
||||
binding.container.visibility = View.GONE
|
||||
binding.noNotificationBackground.visibility = View.VISIBLE
|
||||
} else {
|
||||
setItems(notificationList)
|
||||
}
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}, { throwable ->
|
||||
Timber.e(throwable, "Error occurred while loading notifications")
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications)
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}))
|
||||
} else {
|
||||
notificationList = mNotificationWorkerFragment?.notificationList?.toMutableList() ?: mutableListOf()
|
||||
setItems(notificationList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_notifications, menu)
|
||||
notificationMenuItem = menu.findItem(R.id.archived)
|
||||
setMenuItemTitle()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.archived -> {
|
||||
if (item.title == getString(R.string.menu_option_read)) {
|
||||
startYourself(this, "read")
|
||||
} else if (item.title == getString(R.string.menu_option_unread)) {
|
||||
onBackPressed()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUrl(url: String?) {
|
||||
if (url.isNullOrEmpty()) return
|
||||
Utils.handleWebUrl(this, Uri.parse(url))
|
||||
}
|
||||
|
||||
private fun setItems(notificationList: List<Notification>?) {
|
||||
if (notificationList.isNullOrEmpty()) {
|
||||
ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications)
|
||||
binding.container.visibility = View.GONE
|
||||
setEmptyView()
|
||||
binding.noNotificationBackground.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
binding.container.visibility = View.VISIBLE
|
||||
binding.noNotificationBackground.visibility = View.GONE
|
||||
adapter.items = notificationList
|
||||
}
|
||||
|
||||
private fun setPageTitle() {
|
||||
supportActionBar?.title = if (isRead) {
|
||||
getString(R.string.read_notifications)
|
||||
} else {
|
||||
getString(R.string.notifications)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEmptyView() {
|
||||
binding.noNotificationText.text = if (isRead) {
|
||||
getString(R.string.no_read_notification)
|
||||
} else {
|
||||
getString(R.string.no_notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMenuItemTitle() {
|
||||
notificationMenuItem?.title = if (isRead) {
|
||||
getString(R.string.menu_option_unread)
|
||||
} else {
|
||||
getString(R.string.menu_option_read)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun startYourself(context: Context, title: String) {
|
||||
val intent = Intent(context, NotificationActivity::class.java)
|
||||
intent.putExtra("title", title)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification
|
|||
import fr.free.nrw.commons.notification.models.Notification
|
||||
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
||||
|
||||
internal class NotificatinAdapter(
|
||||
internal class NotificationAdapter(
|
||||
onNotificationClicked: (Notification) -> Unit,
|
||||
) : BaseDelegateAdapter<Notification>(
|
||||
notificationDelegate(onNotificationClicked),
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import fr.free.nrw.commons.notification.models.Notification;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
*/
|
||||
@Singleton
|
||||
public class NotificationController {
|
||||
|
||||
private NotificationClient notificationClient;
|
||||
|
||||
|
||||
@Inject
|
||||
public NotificationController(NotificationClient notificationClient) {
|
||||
this.notificationClient = notificationClient;
|
||||
}
|
||||
|
||||
public Single<List<Notification>> getNotifications(boolean archived) {
|
||||
return notificationClient.getNotifications(archived);
|
||||
}
|
||||
|
||||
Observable<Boolean> markAsRead(Notification notification) {
|
||||
return notificationClient.markNotificationAsRead(notification.getNotificationId());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package fr.free.nrw.commons.notification
|
||||
|
||||
import fr.free.nrw.commons.notification.models.Notification
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
*/
|
||||
@Singleton
|
||||
class NotificationController @Inject constructor(
|
||||
private val notificationClient: NotificationClient
|
||||
) {
|
||||
|
||||
fun getNotifications(archived: Boolean): Single<List<Notification>> {
|
||||
return notificationClient.getNotifications(archived)
|
||||
}
|
||||
|
||||
fun markAsRead(notification: Notification): Observable<Boolean> {
|
||||
return notificationClient.markNotificationAsRead(notification.notificationId)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
|
||||
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
|
||||
|
||||
/**
|
||||
* Helper class that can be used to build a generic notification
|
||||
* Going forward all notifications should be built using this helper class
|
||||
*/
|
||||
@Singleton
|
||||
public class NotificationHelper {
|
||||
|
||||
public static final int NOTIFICATION_DELETE = 1;
|
||||
public static final int NOTIFICATION_EDIT_CATEGORY = 2;
|
||||
public static final int NOTIFICATION_EDIT_COORDINATES = 3;
|
||||
public static final int NOTIFICATION_EDIT_DESCRIPTION = 4;
|
||||
public static final int NOTIFICATION_EDIT_DEPICTIONS = 5;
|
||||
|
||||
private final NotificationManager notificationManager;
|
||||
private final NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
@Inject
|
||||
public NotificationHelper(final Context context) {
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationBuilder = new NotificationCompat
|
||||
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||
.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to build and show a notification in the notification bar
|
||||
* @param context passed context
|
||||
* @param notificationTitle title of the notification
|
||||
* @param notificationMessage message to be displayed in the notification
|
||||
* @param notificationId the notificationID
|
||||
* @param intent the intent to be fired when the notification is clicked
|
||||
*/
|
||||
public void showNotification(
|
||||
final Context context,
|
||||
final String notificationTitle,
|
||||
final String notificationMessage,
|
||||
final int notificationId,
|
||||
final Intent intent
|
||||
) {
|
||||
notificationBuilder.setDefaults(DEFAULT_ALL)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(notificationMessage))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setPriority(PRIORITY_HIGH);
|
||||
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23
|
||||
}
|
||||
|
||||
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags);
|
||||
notificationBuilder.setContentIntent(pendingIntent);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package fr.free.nrw.commons.notification
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.R
|
||||
import androidx.core.app.NotificationCompat.DEFAULT_ALL
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
|
||||
|
||||
/**
|
||||
* Helper class that can be used to build a generic notification
|
||||
* Going forward all notifications should be built using this helper class
|
||||
*/
|
||||
@Singleton
|
||||
class NotificationHelper @Inject constructor(
|
||||
context: Context
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val NOTIFICATION_DELETE = 1
|
||||
const val NOTIFICATION_EDIT_CATEGORY = 2
|
||||
const val NOTIFICATION_EDIT_COORDINATES = 3
|
||||
const val NOTIFICATION_EDIT_DESCRIPTION = 4
|
||||
const val NOTIFICATION_EDIT_DEPICTIONS = 5
|
||||
}
|
||||
|
||||
private val notificationManager: NotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder = NotificationCompat
|
||||
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
/**
|
||||
* Public interface to build and show a notification in the notification bar
|
||||
* @param context passed context
|
||||
* @param notificationTitle title of the notification
|
||||
* @param notificationMessage message to be displayed in the notification
|
||||
* @param notificationId the notificationID
|
||||
* @param intent the intent to be fired when the notification is clicked
|
||||
*/
|
||||
fun showNotification(
|
||||
context: Context,
|
||||
notificationTitle: String,
|
||||
notificationMessage: String,
|
||||
notificationId: Int,
|
||||
intent: Intent
|
||||
) {
|
||||
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationMessage))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(context, 1, intent, flags)
|
||||
notificationBuilder.setContentIntent(pendingIntent)
|
||||
notificationManager.notify(notificationId, notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.notification.models.Notification;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by knightshade on 25/2/18.
|
||||
*/
|
||||
|
||||
public class NotificationWorkerFragment extends Fragment {
|
||||
private List<Notification> notificationList;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
public void setNotificationList(List<Notification> notificationList){
|
||||
this.notificationList = notificationList;
|
||||
}
|
||||
|
||||
public List<Notification> getNotificationList(){
|
||||
return notificationList;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package fr.free.nrw.commons.notification
|
||||
|
||||
import android.app.Fragment
|
||||
import android.os.Bundle
|
||||
|
||||
import fr.free.nrw.commons.notification.models.Notification
|
||||
|
||||
|
||||
/**
|
||||
* Created by knightshade on 25/2/18.
|
||||
*/
|
||||
class NotificationWorkerFragment : Fragment() {
|
||||
|
||||
var notificationList: List<Notification>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package fr.free.nrw.commons.notification.models;
|
||||
|
||||
public enum NotificationType {
|
||||
THANK_YOU_EDIT("thank-you-edit"),
|
||||
EDIT_USER_TALK("edit-user-talk"),
|
||||
MENTION("mention"),
|
||||
EMAIL("email"),
|
||||
WELCOME("welcome"),
|
||||
UNKNOWN("unknown");
|
||||
private String type;
|
||||
|
||||
NotificationType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static NotificationType handledValueOf(String name) {
|
||||
for (NotificationType e : values()) {
|
||||
if (e.getType().equals(name)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package fr.free.nrw.commons.notification.models
|
||||
|
||||
enum class NotificationType(private val type: String) {
|
||||
THANK_YOU_EDIT("thank-you-edit"),
|
||||
|
||||
EDIT_USER_TALK("edit-user-talk"),
|
||||
|
||||
MENTION("mention"),
|
||||
|
||||
EMAIL("email"),
|
||||
|
||||
WELCOME("welcome"),
|
||||
|
||||
UNKNOWN("unknown");
|
||||
|
||||
// Getter for the type property
|
||||
fun getType(): String {
|
||||
return type
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Returns the corresponding NotificationType for a given name or UNKNOWN
|
||||
// if no match is found
|
||||
fun handledValueOf(name: String): NotificationType {
|
||||
for (e in values()) {
|
||||
if (e.type == name) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ public class ProfileActivity extends BaseActivity {
|
|||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
compositeDisposable.clear();
|
||||
getCompositeDisposable().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,104 +1,45 @@
|
|||
package fr.free.nrw.commons.profile.achievements
|
||||
|
||||
/**
|
||||
* Represents Achievements class and stores all the parameters
|
||||
* Represents Achievements data class and stores all the parameters.
|
||||
* Immutable version with default values for optional properties.
|
||||
*/
|
||||
class Achievements {
|
||||
data class Achievements(
|
||||
val uniqueUsedImages: Int = 0,
|
||||
val articlesUsingImages: Int = 0,
|
||||
val thanksReceived: Int = 0,
|
||||
val featuredImages: Int = 0,
|
||||
val qualityImages: Int = 0,
|
||||
val imagesUploaded: Int = 0,
|
||||
val revertCount: Int = 0
|
||||
) {
|
||||
/**
|
||||
* The count of unique images used by the wiki.
|
||||
* @return The count of unique images used.
|
||||
* @param uniqueUsedImages The count to set for unique images used.
|
||||
*/
|
||||
var uniqueUsedImages = 0
|
||||
private var articlesUsingImages = 0
|
||||
|
||||
/**
|
||||
* The count of thanks received.
|
||||
* @return The count of thanks received.
|
||||
* @param thanksReceived The count to set for thanks received.
|
||||
*/
|
||||
var thanksReceived = 0
|
||||
|
||||
/**
|
||||
* The count of featured images.
|
||||
* @return The count of featured images.
|
||||
* @param featuredImages The count to set for featured images.
|
||||
*/
|
||||
var featuredImages = 0
|
||||
|
||||
/**
|
||||
* The count of quality images.
|
||||
* @return The count of quality images.
|
||||
* @param qualityImages The count to set for quality images.
|
||||
*/
|
||||
var qualityImages = 0
|
||||
|
||||
/**
|
||||
* The count of images uploaded.
|
||||
* @return The count of images uploaded.
|
||||
* @param imagesUploaded The count to set for images uploaded.
|
||||
*/
|
||||
var imagesUploaded = 0
|
||||
private var revertCount = 0
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* constructor for achievements class to set its data members
|
||||
* @param uniqueUsedImages
|
||||
* @param articlesUsingImages
|
||||
* @param thanksReceived
|
||||
* @param featuredImages
|
||||
* @param imagesUploaded
|
||||
* @param revertCount
|
||||
*/
|
||||
constructor(
|
||||
uniqueUsedImages: Int,
|
||||
articlesUsingImages: Int,
|
||||
thanksReceived: Int,
|
||||
featuredImages: Int,
|
||||
qualityImages: Int,
|
||||
imagesUploaded: Int,
|
||||
revertCount: Int,
|
||||
) {
|
||||
this.uniqueUsedImages = uniqueUsedImages
|
||||
this.articlesUsingImages = articlesUsingImages
|
||||
this.thanksReceived = thanksReceived
|
||||
this.featuredImages = featuredImages
|
||||
this.qualityImages = qualityImages
|
||||
this.imagesUploaded = imagesUploaded
|
||||
this.revertCount = revertCount
|
||||
}
|
||||
|
||||
/**
|
||||
* used to calculate the percentages of images that haven't been reverted
|
||||
* @return
|
||||
* Used to calculate the percentages of images that haven't been reverted.
|
||||
* Returns 100 if imagesUploaded is 0 to avoid division by zero.
|
||||
*/
|
||||
val notRevertPercentage: Int
|
||||
get() =
|
||||
try {
|
||||
(imagesUploaded - revertCount) * 100 / imagesUploaded
|
||||
} catch (divideByZero: ArithmeticException) {
|
||||
100
|
||||
}
|
||||
get() = if (imagesUploaded > 0) {
|
||||
(imagesUploaded - revertCount) * 100 / imagesUploaded
|
||||
} else {
|
||||
100
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get Achievements object from FeedbackResponse
|
||||
* Get Achievements object from FeedbackResponse.
|
||||
*
|
||||
* @param response
|
||||
* @return
|
||||
* @param response The feedback response to convert.
|
||||
* @return An Achievements object with values from the response.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun from(response: FeedbackResponse): Achievements =
|
||||
Achievements(
|
||||
response.uniqueUsedImages,
|
||||
response.articlesUsingImages,
|
||||
response.thanksReceived,
|
||||
response.featuredImages.featuredPicturesOnWikimediaCommons,
|
||||
response.featuredImages.qualityImages,
|
||||
0,
|
||||
response.deletedUploads,
|
||||
)
|
||||
fun from(response: FeedbackResponse): Achievements = Achievements(
|
||||
uniqueUsedImages = response.uniqueUsedImages,
|
||||
articlesUsingImages = response.articlesUsingImages,
|
||||
thanksReceived = response.thanksReceived,
|
||||
featuredImages = response.featuredImages.featuredPicturesOnWikimediaCommons,
|
||||
qualityImages = response.featuredImages.qualityImages,
|
||||
imagesUploaded = 0, // Assuming imagesUploaded should be 0
|
||||
revertCount = response.deletedUploads
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
// Used for the setting the size of imageView at runtime
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams)
|
||||
binding.achievementBadgeImage.getLayoutParams();
|
||||
binding.achievementBadgeImage.getLayoutParams();
|
||||
params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO);
|
||||
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
|
||||
binding.achievementBadgeImage.requestLayout();
|
||||
|
|
@ -186,37 +186,37 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
try{
|
||||
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getAchievements(Objects.requireNonNull(userName))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
if (response != null) {
|
||||
setUploadCount(Achievements.from(response));
|
||||
} else {
|
||||
Timber.d("success");
|
||||
binding.layoutImageReverts.setVisibility(View.INVISIBLE);
|
||||
binding.achievementBadgeImage.setVisibility(View.INVISIBLE);
|
||||
// If the number of edits made by the user are more than 150,000
|
||||
// in some cases such high number of wiki edit counts cause the
|
||||
// achievements calculator to fail in some cases, for more details
|
||||
// refer Issue: #3295
|
||||
if (numberOfEdits <= 150000) {
|
||||
showSnackBarWithRetry(false);
|
||||
} else {
|
||||
showSnackBarWithRetry(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
Timber.e(t, "Fetching achievements statistics failed");
|
||||
if (numberOfEdits <= 150000) {
|
||||
showSnackBarWithRetry(false);
|
||||
} else {
|
||||
showSnackBarWithRetry(true);
|
||||
}
|
||||
.getAchievements(Objects.requireNonNull(userName))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
if (response != null) {
|
||||
setUploadCount(Achievements.from(response));
|
||||
} else {
|
||||
Timber.d("success");
|
||||
binding.layoutImageReverts.setVisibility(View.INVISIBLE);
|
||||
binding.achievementBadgeImage.setVisibility(View.INVISIBLE);
|
||||
// If the number of edits made by the user are more than 150,000
|
||||
// in some cases such high number of wiki edit counts cause the
|
||||
// achievements calculator to fail in some cases, for more details
|
||||
// refer Issue: #3295
|
||||
if (numberOfEdits <= 150000) {
|
||||
showSnackBarWithRetry(false);
|
||||
} else {
|
||||
showSnackBarWithRetry(true);
|
||||
}
|
||||
));
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
Timber.e(t, "Fetching achievements statistics failed");
|
||||
if (numberOfEdits <= 150000) {
|
||||
showSnackBarWithRetry(false);
|
||||
} else {
|
||||
showSnackBarWithRetry(true);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
catch (Exception e){
|
||||
Timber.d(e+"success");
|
||||
|
|
@ -233,15 +233,15 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
return;
|
||||
}
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getWikidataEdits(userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(edits -> {
|
||||
numberOfEdits = edits;
|
||||
binding.wikidataEdits.setText(String.valueOf(edits));
|
||||
}, e -> {
|
||||
Timber.e("Error:" + e);
|
||||
}));
|
||||
.getWikidataEdits(userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(edits -> {
|
||||
numberOfEdits = edits;
|
||||
binding.wikidataEdits.setText(String.valueOf(edits));
|
||||
}, e -> {
|
||||
Timber.e("Error:" + e);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -255,11 +255,11 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
if (tooManyAchievements) {
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
|
||||
R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements());
|
||||
R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements());
|
||||
} else {
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
|
||||
R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements());
|
||||
R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,16 +277,16 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
private void setUploadCount(Achievements achievements) {
|
||||
if (checkAccount()) {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getUploadCount(Objects.requireNonNull(userName))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
uploadCount -> setAchievementsUploadCount(achievements, uploadCount),
|
||||
t -> {
|
||||
Timber.e(t, "Fetching upload count failed");
|
||||
onError();
|
||||
}
|
||||
));
|
||||
.getUploadCount(Objects.requireNonNull(userName))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
uploadCount -> setAchievementsUploadCount(achievements, uploadCount),
|
||||
t -> {
|
||||
Timber.e(t, "Fetching upload count failed");
|
||||
onError();
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,8 +295,18 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
* @param uploadCount
|
||||
*/
|
||||
private void setAchievementsUploadCount(Achievements achievements, int uploadCount) {
|
||||
achievements.setImagesUploaded(uploadCount);
|
||||
hideProgressBar(achievements);
|
||||
// Create a new instance of Achievements with updated imagesUploaded
|
||||
Achievements updatedAchievements = new Achievements(
|
||||
achievements.getUniqueUsedImages(),
|
||||
achievements.getArticlesUsingImages(),
|
||||
achievements.getThanksReceived(),
|
||||
achievements.getFeaturedImages(),
|
||||
achievements.getQualityImages(),
|
||||
uploadCount, // Update imagesUploaded with new value
|
||||
achievements.getRevertCount()
|
||||
);
|
||||
|
||||
hideProgressBar(updatedAchievements);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -309,7 +319,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
}else {
|
||||
binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE);
|
||||
binding.imagesUploadedProgressbar.setProgress
|
||||
(100*uploadCount/levelInfo.getMaxUploadCount());
|
||||
(100*uploadCount/levelInfo.getMaxUploadCount());
|
||||
binding.tvUploadedImages.setText
|
||||
(uploadCount + "/" + levelInfo.getMaxUploadCount());
|
||||
}
|
||||
|
|
@ -318,8 +328,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
|
||||
private void setZeroAchievements() {
|
||||
String message = !Objects.equals(sessionManager.getUserName(), userName) ?
|
||||
getString(R.string.no_achievements_yet, userName) :
|
||||
getString(R.string.you_have_no_achievements_yet);
|
||||
getString(R.string.no_achievements_yet, userName) :
|
||||
getString(R.string.you_have_no_achievements_yet);
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
null,
|
||||
message,
|
||||
|
|
@ -357,7 +367,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE);
|
||||
binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived()));
|
||||
binding.imagesUsedByWikiProgressBar.setProgress
|
||||
(100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages());
|
||||
(100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages());
|
||||
binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/"
|
||||
+ levelInfo.getMaxUniqueImages());
|
||||
binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages()));
|
||||
|
|
@ -366,7 +376,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
levelUpInfoString += " " + levelInfo.getLevelNumber();
|
||||
binding.achievementLevel.setText(levelUpInfoString);
|
||||
binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge,
|
||||
new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme()));
|
||||
new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme()));
|
||||
binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber()));
|
||||
BasicKvStore store = new BasicKvStore(this.getContext(), userName);
|
||||
store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber()));
|
||||
|
|
@ -378,8 +388,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
private void hideProgressBar(Achievements achievements) {
|
||||
if (binding.progressBar != null) {
|
||||
levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),
|
||||
achievements.getUniqueUsedImages(),
|
||||
achievements.getNotRevertPercentage());
|
||||
achievements.getUniqueUsedImages(),
|
||||
achievements.getNotRevertPercentage());
|
||||
inflateAchievements(achievements);
|
||||
setUploadProgress(achievements.getImagesUploaded());
|
||||
setImageRevertPercentage(achievements.getNotRevertPercentage());
|
||||
|
|
@ -479,4 +489,4 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName
|
|||
* Represents Featured Images on WikiMedia Commons platform
|
||||
* Used by Achievements and FeedbackResponse (objects) of the user
|
||||
*/
|
||||
class FeaturedImages(
|
||||
data class FeaturedImages(
|
||||
@field:SerializedName("Quality_images") val qualityImages: Int,
|
||||
@field:SerializedName("Featured_pictures_on_Wikimedia_Commons") val featuredPicturesOnWikimediaCommons: Int,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
package fr.free.nrw.commons.quiz;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import com.facebook.drawee.drawable.ProgressBarDrawable;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
|
||||
import fr.free.nrw.commons.databinding.ActivityQuizBinding;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
public class QuizActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityQuizBinding binding;
|
||||
private final QuizController quizController = new QuizController();
|
||||
private ArrayList<QuizQuestion> quiz = new ArrayList<>();
|
||||
private int questionIndex = 0;
|
||||
private int score;
|
||||
/**
|
||||
* isPositiveAnswerChecked : represents yes click event
|
||||
*/
|
||||
private boolean isPositiveAnswerChecked;
|
||||
/**
|
||||
* isNegativeAnswerChecked : represents no click event
|
||||
*/
|
||||
private boolean isNegativeAnswerChecked;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityQuizBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
quizController.initialize(this);
|
||||
setSupportActionBar(binding.toolbar.toolbar);
|
||||
binding.nextButton.setOnClickListener(view -> notKnowAnswer());
|
||||
displayQuestion();
|
||||
}
|
||||
|
||||
/**
|
||||
* to move to next question and check whether answer is selected or not
|
||||
*/
|
||||
public void setNextQuestion(){
|
||||
if ( questionIndex <= quiz.size() && (isPositiveAnswerChecked || isNegativeAnswerChecked)) {
|
||||
evaluateScore();
|
||||
}
|
||||
}
|
||||
|
||||
public void notKnowAnswer(){
|
||||
customAlert("Information", quiz.get(questionIndex).getAnswerMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* to give warning before ending quiz
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(getResources().getString(R.string.warning))
|
||||
.setMessage(getResources().getString(R.string.quiz_back_button))
|
||||
.setPositiveButton(R.string.continue_message, (dialog, which) -> {
|
||||
final Intent intent = new Intent(this, QuizResultActivity.class);
|
||||
dialog.dismiss();
|
||||
intent.putExtra("QuizResult", score);
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.dismiss())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* to display the question
|
||||
*/
|
||||
public void displayQuestion() {
|
||||
quiz = quizController.getQuiz();
|
||||
binding.question.questionText.setText(quiz.get(questionIndex).getQuestion());
|
||||
binding.questionTitle.setText(
|
||||
getResources().getString(R.string.question) +
|
||||
quiz.get(questionIndex).getQuestionNumber()
|
||||
);
|
||||
binding.question.questionImage.setHierarchy(GenericDraweeHierarchyBuilder
|
||||
.newInstance(getResources())
|
||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
||||
R.drawable.ic_error_outline_black_24dp, getTheme()))
|
||||
.setProgressBarImage(new ProgressBarDrawable())
|
||||
.build());
|
||||
|
||||
binding.question.questionImage.setImageURI(quiz.get(questionIndex).getUrl());
|
||||
isPositiveAnswerChecked = false;
|
||||
isNegativeAnswerChecked = false;
|
||||
binding.answer.quizPositiveAnswer.setOnClickListener(view -> {
|
||||
isPositiveAnswerChecked = true;
|
||||
setNextQuestion();
|
||||
});
|
||||
binding.answer.quizNegativeAnswer.setOnClickListener(view -> {
|
||||
isNegativeAnswerChecked = true;
|
||||
setNextQuestion();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* to evaluate score and check whether answer is correct or wrong
|
||||
*/
|
||||
public void evaluateScore() {
|
||||
if ((quiz.get(questionIndex).isAnswer() && isPositiveAnswerChecked) ||
|
||||
(!quiz.get(questionIndex).isAnswer() && isNegativeAnswerChecked) ){
|
||||
customAlert(getResources().getString(R.string.correct),
|
||||
quiz.get(questionIndex).getAnswerMessage());
|
||||
score++;
|
||||
} else {
|
||||
customAlert(getResources().getString(R.string.wrong),
|
||||
quiz.get(questionIndex).getAnswerMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* to display explanation after each answer, update questionIndex and move to next question
|
||||
* @param title the alert title
|
||||
* @param Message the alert message
|
||||
*/
|
||||
public void customAlert(final String title, final String Message) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(Message)
|
||||
.setPositiveButton(R.string.continue_message, (dialog, which) -> {
|
||||
questionIndex++;
|
||||
if (questionIndex == quiz.size()) {
|
||||
final Intent intent = new Intent(this, QuizResultActivity.class);
|
||||
dialog.dismiss();
|
||||
intent.putExtra("QuizResult", score);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
displayQuestion();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
154
app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt
Normal file
154
app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package fr.free.nrw.commons.quiz
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
|
||||
import com.facebook.drawee.drawable.ProgressBarDrawable
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||||
|
||||
import fr.free.nrw.commons.databinding.ActivityQuizBinding
|
||||
import java.util.ArrayList
|
||||
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
|
||||
class QuizActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityQuizBinding
|
||||
private val quizController = QuizController()
|
||||
private var quiz = ArrayList<QuizQuestion>()
|
||||
private var questionIndex = 0
|
||||
private var score = 0
|
||||
|
||||
/**
|
||||
* isPositiveAnswerChecked : represents yes click event
|
||||
*/
|
||||
private var isPositiveAnswerChecked = false
|
||||
|
||||
/**
|
||||
* isNegativeAnswerChecked : represents no click event
|
||||
*/
|
||||
private var isNegativeAnswerChecked = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityQuizBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
quizController.initialize(this)
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
binding.nextButton.setOnClickListener { notKnowAnswer() }
|
||||
displayQuestion()
|
||||
}
|
||||
|
||||
/**
|
||||
* To move to next question and check whether answer is selected or not
|
||||
*/
|
||||
fun setNextQuestion() {
|
||||
if (questionIndex <= quiz.size && (isPositiveAnswerChecked || isNegativeAnswerChecked)) {
|
||||
evaluateScore()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notKnowAnswer() {
|
||||
customAlert("Information", quiz[questionIndex].answerMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* To give warning before ending quiz
|
||||
*/
|
||||
override fun onBackPressed() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.warning))
|
||||
.setMessage(getString(R.string.quiz_back_button))
|
||||
.setPositiveButton(R.string.continue_message) { dialog, _ ->
|
||||
val intent = Intent(this, QuizResultActivity::class.java)
|
||||
dialog.dismiss()
|
||||
intent.putExtra("QuizResult", score)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialogInterface, _ -> dialogInterface.dismiss() }
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* To display the question
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun displayQuestion() {
|
||||
quiz = quizController.getQuiz()
|
||||
binding.question.questionText.text = quiz[questionIndex].question
|
||||
binding.questionTitle.text = getString(R.string.question) + quiz[questionIndex].questionNumber
|
||||
|
||||
binding.question.questionImage.hierarchy = GenericDraweeHierarchyBuilder
|
||||
.newInstance(resources)
|
||||
.setFailureImage(VectorDrawableCompat.create(resources, R.drawable.ic_error_outline_black_24dp, theme))
|
||||
.setProgressBarImage(ProgressBarDrawable())
|
||||
.build()
|
||||
|
||||
binding.question.questionImage.setImageURI(quiz[questionIndex].getUrl())
|
||||
isPositiveAnswerChecked = false
|
||||
isNegativeAnswerChecked = false
|
||||
|
||||
binding.answer.quizPositiveAnswer.setOnClickListener {
|
||||
isPositiveAnswerChecked = true
|
||||
setNextQuestion()
|
||||
}
|
||||
binding.answer.quizNegativeAnswer.setOnClickListener {
|
||||
isNegativeAnswerChecked = true
|
||||
setNextQuestion()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To evaluate score and check whether answer is correct or wrong
|
||||
*/
|
||||
fun evaluateScore() {
|
||||
if (
|
||||
(quiz[questionIndex].isAnswer && isPositiveAnswerChecked)
|
||||
||
|
||||
(!quiz[questionIndex].isAnswer && isNegativeAnswerChecked)
|
||||
) {
|
||||
customAlert(
|
||||
getString(R.string.correct),
|
||||
quiz[questionIndex].answerMessage
|
||||
)
|
||||
score++
|
||||
} else {
|
||||
customAlert(
|
||||
getString(R.string.wrong),
|
||||
quiz[questionIndex].answerMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To display explanation after each answer, update questionIndex and move to next question
|
||||
* @param title The alert title
|
||||
* @param message The alert message
|
||||
*/
|
||||
fun customAlert(title: String, message: String) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_message) { dialog, _ ->
|
||||
questionIndex++
|
||||
if (questionIndex == quiz.size) {
|
||||
val intent = Intent(this, QuizResultActivity::class.java)
|
||||
dialog.dismiss()
|
||||
intent.putExtra("QuizResult", score)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
displayQuestion()
|
||||
}
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
package fr.free.nrw.commons.quiz;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* fetches the number of images uploaded and number of images reverted.
|
||||
* Then it calculates the percentage of the images reverted
|
||||
* if the percentage of images reverted after last quiz exceeds 50% and number of images uploaded is
|
||||
* greater than 50, then quiz is popped up
|
||||
*/
|
||||
@Singleton
|
||||
public class QuizChecker {
|
||||
|
||||
private int revertCount ;
|
||||
private int totalUploadCount ;
|
||||
private boolean isRevertCountFetched;
|
||||
private boolean isUploadCountFetched;
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private final SessionManager sessionManager;
|
||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private final JsonKvStore revertKvStore;
|
||||
|
||||
private static final int UPLOAD_COUNT_THRESHOLD = 5;
|
||||
private static final String REVERT_PERCENTAGE_FOR_MESSAGE = "50%";
|
||||
private final String REVERT_SHARED_PREFERENCE = "revertCount";
|
||||
private final String UPLOAD_SHARED_PREFERENCE = "uploadCount";
|
||||
|
||||
/**
|
||||
* constructor to set the parameters for quiz
|
||||
* @param sessionManager
|
||||
* @param okHttpJsonApiClient
|
||||
*/
|
||||
@Inject
|
||||
public QuizChecker(SessionManager sessionManager,
|
||||
OkHttpJsonApiClient okHttpJsonApiClient,
|
||||
@Named("default_preferences") JsonKvStore revertKvStore) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.revertKvStore = revertKvStore;
|
||||
}
|
||||
|
||||
public void initQuizCheck(Activity activity) {
|
||||
calculateRevertParameterAndShowQuiz(activity);
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* to fet the total number of images uploaded
|
||||
*/
|
||||
private void setUploadCount() {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getUploadCount(sessionManager.getUserName())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setTotalUploadCount,
|
||||
t -> Timber.e(t, "Fetching upload count failed")
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* set the sub Title of Contibutions Activity and
|
||||
* call function to check for quiz
|
||||
* @param uploadCount user's upload count
|
||||
*/
|
||||
private void setTotalUploadCount(int uploadCount) {
|
||||
totalUploadCount = uploadCount - revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0);
|
||||
if ( totalUploadCount < 0){
|
||||
totalUploadCount = 0;
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0);
|
||||
}
|
||||
isUploadCountFetched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* To call the API to get reverts count in form of JSONObject
|
||||
*/
|
||||
private void setRevertCount() {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getAchievements(sessionManager.getUserName())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
if (response != null) {
|
||||
setRevertParameter(response.getDeletedUploads());
|
||||
}
|
||||
}, throwable -> Timber.e(throwable, "Fetching feedback failed"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* to calculate the number of images reverted after previous quiz
|
||||
* @param revertCountFetched count of deleted uploads
|
||||
*/
|
||||
private void setRevertParameter(int revertCountFetched) {
|
||||
revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0);
|
||||
if (revertCount < 0){
|
||||
revertCount = 0;
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0);
|
||||
}
|
||||
isRevertCountFetched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* to check whether the criterion to call quiz is satisfied
|
||||
*/
|
||||
private void calculateRevertParameterAndShowQuiz(Activity activity) {
|
||||
setUploadCount();
|
||||
setRevertCount();
|
||||
if ( revertCount < 0 || totalUploadCount < 0){
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0);
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0);
|
||||
return;
|
||||
}
|
||||
if (isRevertCountFetched && isUploadCountFetched &&
|
||||
totalUploadCount >= UPLOAD_COUNT_THRESHOLD &&
|
||||
(revertCount * 100) / totalUploadCount >= 50) {
|
||||
callQuiz(activity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert which prompts to quiz
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private void callQuiz(Activity activity) {
|
||||
DialogUtil.showAlertDialog(activity,
|
||||
activity.getString(R.string.quiz),
|
||||
activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE),
|
||||
activity.getString(R.string.about_translate_proceed),
|
||||
activity.getString(android.R.string.cancel),
|
||||
() -> startQuizActivity(activity),
|
||||
null);
|
||||
}
|
||||
|
||||
private void startQuizActivity(Activity activity) {
|
||||
int newRevetSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0);
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs);
|
||||
int newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0);
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount);
|
||||
Intent i = new Intent(activity, WelcomeActivity.class);
|
||||
i.putExtra("isQuiz", true);
|
||||
activity.startActivity(i);
|
||||
}
|
||||
}
|
||||
175
app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt
Normal file
175
app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package fr.free.nrw.commons.quiz
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.WelcomeActivity
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* Fetches the number of images uploaded and number of images reverted.
|
||||
* Then it calculates the percentage of the images reverted.
|
||||
* If the percentage of images reverted after the last quiz exceeds 50% and number of images uploaded is
|
||||
* greater than 50, then the quiz is popped up.
|
||||
*/
|
||||
@Singleton
|
||||
class QuizChecker @Inject constructor(
|
||||
private val sessionManager: SessionManager,
|
||||
private val okHttpJsonApiClient: OkHttpJsonApiClient,
|
||||
@Named("default_preferences") private val revertKvStore: JsonKvStore
|
||||
) {
|
||||
|
||||
private var revertCount = 0
|
||||
private var totalUploadCount = 0
|
||||
private var isRevertCountFetched = false
|
||||
private var isUploadCountFetched = false
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
private val UPLOAD_COUNT_THRESHOLD = 5
|
||||
private val REVERT_PERCENTAGE_FOR_MESSAGE = "50%"
|
||||
private val REVERT_SHARED_PREFERENCE = "revertCount"
|
||||
private val UPLOAD_SHARED_PREFERENCE = "uploadCount"
|
||||
|
||||
/**
|
||||
* Initializes quiz check by calculating revert parameters and showing quiz if necessary
|
||||
*/
|
||||
fun initQuizCheck(activity: Activity) {
|
||||
calculateRevertParameterAndShowQuiz(activity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears disposables to avoid memory leaks
|
||||
*/
|
||||
fun cleanup() {
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the total number of images uploaded
|
||||
*/
|
||||
private fun setUploadCount() {
|
||||
compositeDisposable.add(
|
||||
okHttpJsonApiClient.getUploadCount(sessionManager.userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ uploadCount -> setTotalUploadCount(uploadCount) },
|
||||
{ t -> Timber.e(t, "Fetching upload count failed") }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total upload count after subtracting stored preference
|
||||
* @param uploadCount User's upload count
|
||||
*/
|
||||
private fun setTotalUploadCount(uploadCount: Int) {
|
||||
totalUploadCount = uploadCount - revertKvStore.getInt(
|
||||
UPLOAD_SHARED_PREFERENCE,
|
||||
0
|
||||
)
|
||||
if (totalUploadCount < 0) {
|
||||
totalUploadCount = 0
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
}
|
||||
isUploadCountFetched = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the revert count using the API
|
||||
*/
|
||||
private fun setRevertCount() {
|
||||
compositeDisposable.add(
|
||||
okHttpJsonApiClient.getAchievements(sessionManager.userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
response?.let { setRevertParameter(it.deletedUploads) }
|
||||
},
|
||||
{ throwable -> Timber.e(throwable, "Fetching feedback failed") }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of images reverted after the previous quiz
|
||||
* @param revertCountFetched Count of deleted uploads
|
||||
*/
|
||||
private fun setRevertParameter(revertCountFetched: Int) {
|
||||
revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
if (revertCount < 0) {
|
||||
revertCount = 0
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
}
|
||||
isRevertCountFetched = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the criteria for calling the quiz are satisfied
|
||||
*/
|
||||
private fun calculateRevertParameterAndShowQuiz(activity: Activity) {
|
||||
setUploadCount()
|
||||
setRevertCount()
|
||||
|
||||
if (revertCount < 0 || totalUploadCount < 0) {
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (isRevertCountFetched && isUploadCountFetched &&
|
||||
totalUploadCount >= UPLOAD_COUNT_THRESHOLD &&
|
||||
(revertCount * 100) / totalUploadCount >= 50
|
||||
) {
|
||||
callQuiz(activity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an alert prompting the user to take the quiz
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun callQuiz(activity: Activity) {
|
||||
DialogUtil.showAlertDialog(
|
||||
activity,
|
||||
activity.getString(R.string.quiz),
|
||||
activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE),
|
||||
activity.getString(R.string.about_translate_proceed),
|
||||
activity.getString(android.R.string.cancel),
|
||||
{ startQuizActivity(activity) },
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the quiz activity and updates preferences for revert and upload counts
|
||||
*/
|
||||
private fun startQuizActivity(activity: Activity) {
|
||||
val newRevertSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevertSharedPrefs)
|
||||
|
||||
val newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0)
|
||||
revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount)
|
||||
|
||||
val intent = Intent(activity, WelcomeActivity::class.java).apply {
|
||||
putExtra("isQuiz", true)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.quiz;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
/**
|
||||
* controls the quiz in the Activity
|
||||
*/
|
||||
public class QuizController {
|
||||
|
||||
ArrayList<QuizQuestion> quiz = new ArrayList<>();
|
||||
|
||||
private final String URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg";
|
||||
private final String URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg";
|
||||
private final String URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg";
|
||||
private final String URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png";
|
||||
private final String URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg";
|
||||
|
||||
public void initialize(Context context){
|
||||
QuizQuestion q1 = new QuizQuestion(1,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_SELFIE,
|
||||
false,
|
||||
context.getString(R.string.selfie_answer));
|
||||
quiz.add(q1);
|
||||
|
||||
QuizQuestion q2 = new QuizQuestion(2,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_TAJ_MAHAL,
|
||||
true,
|
||||
context.getString(R.string.taj_mahal_answer));
|
||||
quiz.add(q2);
|
||||
|
||||
QuizQuestion q3 = new QuizQuestion(3,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_BLURRY_IMAGE,
|
||||
false,
|
||||
context.getString(R.string.blurry_image_answer));
|
||||
quiz.add(q3);
|
||||
|
||||
QuizQuestion q4 = new QuizQuestion(4,
|
||||
context.getString(R.string.quiz_screenshot_question),
|
||||
URL_FOR_SCREENSHOT,
|
||||
false,
|
||||
context.getString(R.string.screenshot_answer));
|
||||
quiz.add(q4);
|
||||
|
||||
QuizQuestion q5 = new QuizQuestion(5,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_EVENT,
|
||||
true,
|
||||
context.getString(R.string.construction_event_answer));
|
||||
quiz.add(q5);
|
||||
|
||||
}
|
||||
|
||||
public ArrayList<QuizQuestion> getQuiz() {
|
||||
return quiz;
|
||||
}
|
||||
}
|
||||
76
app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt
Normal file
76
app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package fr.free.nrw.commons.quiz
|
||||
|
||||
import android.content.Context
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
|
||||
/**
|
||||
* Controls the quiz in the Activity
|
||||
*/
|
||||
class QuizController {
|
||||
|
||||
private val quiz: ArrayList<QuizQuestion> = ArrayList()
|
||||
|
||||
private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"
|
||||
private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"
|
||||
private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"
|
||||
private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"
|
||||
private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"
|
||||
|
||||
fun initialize(context: Context) {
|
||||
val q1 = QuizQuestion(
|
||||
1,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_SELFIE,
|
||||
false,
|
||||
context.getString(R.string.selfie_answer)
|
||||
)
|
||||
quiz.add(q1)
|
||||
|
||||
val q2 = QuizQuestion(
|
||||
2,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_TAJ_MAHAL,
|
||||
true,
|
||||
context.getString(R.string.taj_mahal_answer)
|
||||
)
|
||||
quiz.add(q2)
|
||||
|
||||
val q3 = QuizQuestion(
|
||||
3,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_BLURRY_IMAGE,
|
||||
false,
|
||||
context.getString(R.string.blurry_image_answer)
|
||||
)
|
||||
quiz.add(q3)
|
||||
|
||||
val q4 = QuizQuestion(
|
||||
4,
|
||||
context.getString(R.string.quiz_screenshot_question),
|
||||
URL_FOR_SCREENSHOT,
|
||||
false,
|
||||
context.getString(R.string.screenshot_answer)
|
||||
)
|
||||
quiz.add(q4)
|
||||
|
||||
val q5 = QuizQuestion(
|
||||
5,
|
||||
context.getString(R.string.quiz_question_string),
|
||||
URL_FOR_EVENT,
|
||||
true,
|
||||
context.getString(R.string.construction_event_answer)
|
||||
)
|
||||
quiz.add(q5)
|
||||
}
|
||||
|
||||
fun getQuiz(): ArrayList<QuizQuestion> {
|
||||
return quiz
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
package fr.free.nrw.commons.quiz;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import fr.free.nrw.commons.databinding.ActivityQuizResultBinding;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
|
||||
|
||||
/**
|
||||
* Displays the final score of quiz and congratulates the user
|
||||
*/
|
||||
public class QuizResultActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityQuizResultBinding binding;
|
||||
private final int NUMBER_OF_QUESTIONS = 5;
|
||||
private final int MULTIPLIER_TO_GET_PERCENTAGE = 20;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityQuizResultBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbar.toolbar);
|
||||
|
||||
binding.quizResultNext.setOnClickListener(view -> launchContributionActivity());
|
||||
|
||||
if ( getIntent() != null) {
|
||||
Bundle extras = getIntent().getExtras();
|
||||
int score = extras.getInt("QuizResult");
|
||||
setScore(score);
|
||||
}else{
|
||||
startActivityWithFlags(
|
||||
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
binding = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* to calculate and display percentage and score
|
||||
* @param score
|
||||
*/
|
||||
public void setScore(int score) {
|
||||
final int scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE;
|
||||
binding.resultProgressBar.setProgress(scorePercent);
|
||||
binding.tvResultProgress.setText(score +" / " + NUMBER_OF_QUESTIONS);
|
||||
final String message = getResources().getString(R.string.congratulatory_message_quiz,scorePercent + "%");
|
||||
binding.congratulatoryMessage.setText(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* to go to Contibutions Activity
|
||||
*/
|
||||
public void launchContributionActivity(){
|
||||
startActivityWithFlags(
|
||||
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
startActivityWithFlags(
|
||||
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call intent to an activity
|
||||
* @param context
|
||||
* @param cls
|
||||
* @param flags
|
||||
* @param <T>
|
||||
*/
|
||||
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
|
||||
Intent intent = new Intent(context, cls);
|
||||
for (int flag: flags) {
|
||||
intent.addFlags(flag);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* to inflate menu
|
||||
* @param menu
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_about, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* if share option selected then take screenshot and launch alert
|
||||
* @param item
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.share_app_icon) {
|
||||
View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
Bitmap screenShot = getScreenShot(rootView);
|
||||
showAlert(screenShot);
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* to store the screenshot of image in bitmap variable temporarily
|
||||
* @param view
|
||||
* @return
|
||||
*/
|
||||
public static Bitmap getScreenShot(View view) {
|
||||
View screenView = view.getRootView();
|
||||
screenView.setDrawingCacheEnabled(true);
|
||||
Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache());
|
||||
screenView.setDrawingCacheEnabled(false);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* share the screenshot through social media
|
||||
* @param bitmap
|
||||
*/
|
||||
void shareScreen(Bitmap bitmap) {
|
||||
try {
|
||||
File file = new File(this.getExternalCacheDir(),"screen.png");
|
||||
FileOutputStream fOut = new FileOutputStream(file);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
|
||||
fOut.flush();
|
||||
fOut.close();
|
||||
file.setReadable(true, false);
|
||||
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
|
||||
intent.setType("image/png");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_image_via)));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It display the alertDialog with Image of screenshot
|
||||
* @param screenshot
|
||||
*/
|
||||
public void showAlert(Bitmap screenshot) {
|
||||
AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this);
|
||||
LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this);
|
||||
final View view = factory.inflate(R.layout.image_alert_layout, null);
|
||||
ImageView screenShotImage = view.findViewById(R.id.alert_image);
|
||||
screenShotImage.setImageBitmap(screenshot);
|
||||
TextView shareMessage = view.findViewById(R.id.alert_text);
|
||||
shareMessage.setText(R.string.quiz_result_share_message);
|
||||
alertadd.setView(view);
|
||||
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
|
||||
alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
|
||||
alertadd.show();
|
||||
}
|
||||
}
|
||||
192
app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt
Normal file
192
app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package fr.free.nrw.commons.quiz
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
import fr.free.nrw.commons.databinding.ActivityQuizResultBinding
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
|
||||
|
||||
/**
|
||||
* Displays the final score of quiz and congratulates the user
|
||||
*/
|
||||
class QuizResultActivity : AppCompatActivity() {
|
||||
|
||||
private var binding: ActivityQuizResultBinding? = null
|
||||
private val NUMBER_OF_QUESTIONS = 5
|
||||
private val MULTIPLIER_TO_GET_PERCENTAGE = 20
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityQuizResultBinding.inflate(layoutInflater)
|
||||
setContentView(binding?.root)
|
||||
|
||||
setSupportActionBar(binding?.toolbar?.toolbar)
|
||||
|
||||
binding?.quizResultNext?.setOnClickListener {
|
||||
launchContributionActivity()
|
||||
}
|
||||
|
||||
intent?.extras?.let { extras ->
|
||||
val score = extras.getInt("QuizResult", 0)
|
||||
setScore(score)
|
||||
} ?: run {
|
||||
startActivityWithFlags(
|
||||
this, MainActivity::class.java,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
)
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* To calculate and display percentage and score
|
||||
* @param score
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid", "SetTextI18n")
|
||||
fun setScore(score: Int) {
|
||||
val scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE
|
||||
binding?.resultProgressBar?.progress = scorePercent
|
||||
binding?.tvResultProgress?.text = "$score / $NUMBER_OF_QUESTIONS"
|
||||
val message = resources.getString(R.string.congratulatory_message_quiz, "$scorePercent%")
|
||||
binding?.congratulatoryMessage?.text = message
|
||||
}
|
||||
|
||||
/**
|
||||
* To go to Contributions Activity
|
||||
*/
|
||||
fun launchContributionActivity() {
|
||||
startActivityWithFlags(
|
||||
this, MainActivity::class.java,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
startActivityWithFlags(
|
||||
this, MainActivity::class.java,
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
)
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call intent to an activity
|
||||
* @param context
|
||||
* @param cls
|
||||
* @param flags
|
||||
*/
|
||||
companion object {
|
||||
fun <T> startActivityWithFlags(context: Context, cls: Class<T>, vararg flags: Int) {
|
||||
val intent = Intent(context, cls)
|
||||
flags.forEach { flag -> intent.addFlags(flag) }
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To inflate menu
|
||||
* @param menu
|
||||
* @return
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
menuInflater.inflate(R.menu.menu_about, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* If share option selected then take screenshot and launch alert
|
||||
* @param item
|
||||
* @return
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.share_app_icon) {
|
||||
val rootView = window.decorView.findViewById<View>(android.R.id.content)
|
||||
val screenShot = getScreenShot(rootView)
|
||||
showAlert(screenShot)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* To store the screenshot of image in bitmap variable temporarily
|
||||
* @param view
|
||||
* @return
|
||||
*/
|
||||
fun getScreenShot(view: View): Bitmap {
|
||||
val screenView = view.rootView
|
||||
screenView.isDrawingCacheEnabled = true
|
||||
val bitmap = Bitmap.createBitmap(screenView.drawingCache)
|
||||
screenView.isDrawingCacheEnabled = false
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the screenshot through social media
|
||||
* @param bitmap
|
||||
*/
|
||||
@SuppressLint("SetWorldReadable")
|
||||
fun shareScreen(bitmap: Bitmap) {
|
||||
try {
|
||||
val file = File(this.externalCacheDir, "screen.png")
|
||||
FileOutputStream(file).use { fOut ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut)
|
||||
fOut.flush()
|
||||
}
|
||||
file.setReadable(true, false)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file))
|
||||
type = "image/png"
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_image_via)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It displays the AlertDialog with Image of screenshot
|
||||
* @param screenshot
|
||||
*/
|
||||
fun showAlert(screenshot: Bitmap) {
|
||||
val alertadd = AlertDialog.Builder(this)
|
||||
val factory = LayoutInflater.from(this)
|
||||
val view = factory.inflate(R.layout.image_alert_layout, null)
|
||||
val screenShotImage = view.findViewById<ImageView>(R.id.alert_image)
|
||||
screenShotImage.setImageBitmap(screenshot)
|
||||
val shareMessage = view.findViewById<TextView>(R.id.alert_text)
|
||||
shareMessage.setText(R.string.quiz_result_share_message)
|
||||
alertadd.setView(view)
|
||||
alertadd.setPositiveButton(R.string.about_translate_proceed) { dialog, _ ->
|
||||
shareScreen(screenshot)
|
||||
}
|
||||
alertadd.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
alertadd.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package fr.free.nrw.commons.quiz;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Used to group to or more radio buttons to ensure
|
||||
* that at a particular time only one of them is selected
|
||||
*/
|
||||
public class RadioGroupHelper {
|
||||
|
||||
public List<CompoundButton> radioButtons = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param radios
|
||||
*/
|
||||
public RadioGroupHelper(RadioButton... radios) {
|
||||
super();
|
||||
for (RadioButton rb : radios) {
|
||||
add(rb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param activity
|
||||
* @param radiosIDs
|
||||
*/
|
||||
public RadioGroupHelper(Activity activity, int... radiosIDs) {
|
||||
this(activity.findViewById(android.R.id.content),radiosIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param rootView
|
||||
* @param radiosIDs
|
||||
*/
|
||||
public RadioGroupHelper(View rootView, int... radiosIDs) {
|
||||
super();
|
||||
for (int radioButtonID : radiosIDs) {
|
||||
add(rootView.findViewById(radioButtonID));
|
||||
}
|
||||
}
|
||||
|
||||
private void add(CompoundButton button){
|
||||
this.radioButtons.add(button);
|
||||
button.setOnClickListener(onClickListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* listener to ensure only one of the radio button is selected
|
||||
*/
|
||||
View.OnClickListener onClickListener = v -> {
|
||||
for (CompoundButton rb : radioButtons) {
|
||||
if (rb != v) rb.setChecked(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package fr.free.nrw.commons.quiz
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioButton
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
* Used to group to or more radio buttons to ensure
|
||||
* that at a particular time only one of them is selected
|
||||
*/
|
||||
class RadioGroupHelper {
|
||||
|
||||
val radioButtons: MutableList<CompoundButton> = ArrayList()
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param radios
|
||||
*/
|
||||
constructor(vararg radios: RadioButton) {
|
||||
for (rb in radios) {
|
||||
add(rb)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param activity
|
||||
* @param radiosIDs
|
||||
*/
|
||||
constructor(activity: Activity, vararg radiosIDs: Int) : this(
|
||||
*radiosIDs.map { id -> activity.findViewById<RadioButton>(id) }.toTypedArray()
|
||||
)
|
||||
|
||||
/**
|
||||
* Constructor to group radio buttons
|
||||
* @param rootView
|
||||
* @param radiosIDs
|
||||
*/
|
||||
constructor(rootView: View, vararg radiosIDs: Int) {
|
||||
for (radioButtonID in radiosIDs) {
|
||||
add(rootView.findViewById(radioButtonID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun add(button: CompoundButton) {
|
||||
radioButtons.add(button)
|
||||
button.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* listener to ensure only one of the radio button is selected
|
||||
*/
|
||||
private val onClickListener = View.OnClickListener { v ->
|
||||
for (rb in radioButtons) {
|
||||
if (rb != v) rb.isChecked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package fr.free.nrw.commons.recentlanguages;
|
||||
|
||||
import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME;
|
||||
import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME;
|
||||
|
||||
import android.content.ContentValues;
|
||||
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 javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Content provider of recently used languages
|
||||
*/
|
||||
public class RecentLanguagesContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final String BASE_PATH = "recent_languages";
|
||||
public static final Uri BASE_URI =
|
||||
Uri.parse("content://" + BuildConfig.RECENT_LANGUAGE_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
|
||||
/**
|
||||
* Append language code to the base uri
|
||||
* @param languageCode Code of a language
|
||||
*/
|
||||
public static Uri uriForCode(final String languageCode) {
|
||||
return Uri.parse(BASE_URI + "/" + languageCode);
|
||||
}
|
||||
|
||||
@Inject
|
||||
DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull final Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the SQLite database for the recently used languages
|
||||
* @param uri : contains the uri for recently used languages
|
||||
* @param projection : contains the all fields of the table
|
||||
* @param selection : handles Where
|
||||
* @param selectionArgs : the condition of Where clause
|
||||
* @param sortOrder : ascending or descending
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
|
||||
final String[] selectionArgs, final String sortOrder) {
|
||||
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
final Cursor cursor = queryBuilder.query(db, projection, selection,
|
||||
selectionArgs, null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the update query of local SQLite Database
|
||||
* @param uri : contains the uri for recently used languages
|
||||
* @param contentValues : new values to be entered to db
|
||||
* @param selection : handles Where
|
||||
* @param selectionArgs : the condition of Where clause
|
||||
*/
|
||||
@Override
|
||||
public int update(@NonNull final Uri uri, final ContentValues contentValues,
|
||||
final String selection, final String[] selectionArgs) {
|
||||
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
final int rowsUpdated;
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
final int id = Integer.parseInt(uri.getLastPathSegment());
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
COLUMN_NAME + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the insertion of new recently used languages record to local SQLite Database
|
||||
* @param uri : contains the uri for recently used languages
|
||||
* @param contentValues : new values to be entered to db
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) {
|
||||
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
final long id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deletion of new recently used languages record to local SQLite Database
|
||||
* @param uri : contains the uri for recently used languages
|
||||
*/
|
||||
@Override
|
||||
public int delete(@NonNull final Uri uri, final String s, final String[] strings) {
|
||||
final int rows;
|
||||
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Timber.d("Deleting recently used language %s", uri.getLastPathSegment());
|
||||
rows = db.delete(
|
||||
TABLE_NAME,
|
||||
"language_code = ?",
|
||||
new String[]{uri.getLastPathSegment()}
|
||||
);
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package fr.free.nrw.commons.recentlanguages
|
||||
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteQueryBuilder
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.data.DBOpenHelper
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* Content provider of recently used languages
|
||||
*/
|
||||
class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
|
||||
|
||||
companion object {
|
||||
private const val BASE_PATH = "recent_languages"
|
||||
val BASE_URI: Uri =
|
||||
Uri.parse(
|
||||
"content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH"
|
||||
)
|
||||
|
||||
/**
|
||||
* Append language code to the base URI
|
||||
* @param languageCode Code of a language
|
||||
*/
|
||||
@JvmStatic
|
||||
fun uriForCode(languageCode: String): Uri {
|
||||
return Uri.parse("$BASE_URI/$languageCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var dbOpenHelper: DBOpenHelper
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the SQLite database for the recently used languages
|
||||
* @param uri : contains the URI for recently used languages
|
||||
* @param projection : contains all fields of the table
|
||||
* @param selection : handles WHERE
|
||||
* @param selectionArgs : the condition of WHERE clause
|
||||
* @param sortOrder : ascending or descending
|
||||
*/
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?,
|
||||
sortOrder: String?
|
||||
): Cursor? {
|
||||
val queryBuilder = SQLiteQueryBuilder()
|
||||
queryBuilder.tables = TABLE_NAME
|
||||
val db = dbOpenHelper.readableDatabase
|
||||
val cursor = queryBuilder.query(
|
||||
db,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)
|
||||
cursor.setNotificationUri(context?.contentResolver, uri)
|
||||
return cursor
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the update query of local SQLite Database
|
||||
* @param uri : contains the URI for recently used languages
|
||||
* @param contentValues : new values to be entered to the database
|
||||
* @param selection : handles WHERE
|
||||
* @param selectionArgs : the condition of WHERE clause
|
||||
*/
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
contentValues: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
): Int {
|
||||
val sqlDB = dbOpenHelper.writableDatabase
|
||||
val rowsUpdated: Int
|
||||
if (selection.isNullOrEmpty()) {
|
||||
val id = uri.lastPathSegment?.toInt()
|
||||
?: throw IllegalArgumentException("Invalid URI: $uri")
|
||||
rowsUpdated = sqlDB.update(
|
||||
TABLE_NAME,
|
||||
contentValues,
|
||||
"$COLUMN_NAME = ?",
|
||||
arrayOf(id.toString())
|
||||
)
|
||||
} else {
|
||||
throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID")
|
||||
}
|
||||
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return rowsUpdated
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the insertion of new recently used languages record to local SQLite Database
|
||||
* @param uri : contains the URI for recently used languages
|
||||
* @param contentValues : new values to be entered to the database
|
||||
*/
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
|
||||
val sqlDB = dbOpenHelper.writableDatabase
|
||||
val id = sqlDB.insert(
|
||||
TABLE_NAME,
|
||||
null,
|
||||
contentValues
|
||||
)
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return Uri.parse("$BASE_URI/$id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deletion of a recently used languages record from local SQLite Database
|
||||
* @param uri : contains the URI for recently used languages
|
||||
*/
|
||||
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
|
||||
val db = dbOpenHelper.readableDatabase
|
||||
Timber.d("Deleting recently used language %s", uri.lastPathSegment)
|
||||
val rows = db.delete(
|
||||
TABLE_NAME,
|
||||
"language_code = ?",
|
||||
arrayOf(uri.lastPathSegment)
|
||||
)
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
package fr.free.nrw.commons.recentlanguages;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Handles database operations for recently used languages
|
||||
*/
|
||||
@Singleton
|
||||
public class RecentLanguagesDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public RecentLanguagesDao
|
||||
(@Named("recent_languages") final Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all persisted recently used languages on database
|
||||
* @return list of recently used languages
|
||||
*/
|
||||
public List<Language> getRecentLanguages() {
|
||||
final List<Language> languages = new ArrayList<>();
|
||||
final ContentProviderClient db = clientProvider.get();
|
||||
try (final Cursor cursor = db.query(
|
||||
RecentLanguagesContentProvider.BASE_URI,
|
||||
RecentLanguagesDao.Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
null)) {
|
||||
if(cursor != null && cursor.moveToLast()) {
|
||||
do {
|
||||
languages.add(fromCursor(cursor));
|
||||
} while (cursor.moveToPrevious());
|
||||
}
|
||||
} catch (final RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Language to database
|
||||
* @param language : Language to add
|
||||
*/
|
||||
public void addRecentLanguage(final Language language) {
|
||||
final ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
db.insert(RecentLanguagesContentProvider.BASE_URI, toContentValues(language));
|
||||
} catch (final RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a language from database
|
||||
* @param languageCode : code of the Language to delete
|
||||
*/
|
||||
public void deleteRecentLanguage(final String languageCode) {
|
||||
final ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
db.delete(RecentLanguagesContentProvider.uriForCode(languageCode), null, null);
|
||||
} catch (final RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a language from database based on its name
|
||||
* @param languageCode : code of the Language to find
|
||||
* @return boolean : is language in database ?
|
||||
*/
|
||||
public boolean findRecentLanguage(final String languageCode) {
|
||||
if (languageCode == null) { //Avoiding NPE's
|
||||
return false;
|
||||
}
|
||||
final ContentProviderClient db = clientProvider.get();
|
||||
try (final Cursor cursor = db.query(
|
||||
RecentLanguagesContentProvider.BASE_URI,
|
||||
RecentLanguagesDao.Table.ALL_FIELDS,
|
||||
Table.COLUMN_CODE + "=?",
|
||||
new String[]{languageCode},
|
||||
null
|
||||
)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return true;
|
||||
}
|
||||
} catch (final RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* It creates an Recent Language object from data stored in the SQLite DB by using cursor
|
||||
* @param cursor cursor
|
||||
* @return Language object
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressLint("Range")
|
||||
Language fromCursor(final Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
|
||||
final String languageCode = cursor.getString(cursor.getColumnIndex(Table.COLUMN_CODE));
|
||||
return new Language(languageName, languageCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes data from Language and create a content value object
|
||||
* @param recentLanguage recently used language
|
||||
* @return ContentValues
|
||||
*/
|
||||
private ContentValues toContentValues(final Language recentLanguage) {
|
||||
final ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_NAME, recentLanguage.getLanguageName());
|
||||
cv.put(Table.COLUMN_CODE, recentLanguage.getLanguageCode());
|
||||
return cv;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class contains the database table architecture for recently used languages,
|
||||
* It also contains queries and logic necessary to the create, update, delete this table.
|
||||
*/
|
||||
public static final class Table {
|
||||
public static final String TABLE_NAME = "recent_languages";
|
||||
static final String COLUMN_NAME = "language_name";
|
||||
static final String COLUMN_CODE = "language_code";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_NAME,
|
||||
COLUMN_CODE
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_NAME + " STRING,"
|
||||
+ COLUMN_CODE + " STRING PRIMARY KEY"
|
||||
+ ");";
|
||||
|
||||
/**
|
||||
* This method creates a LanguagesTable in SQLiteDatabase
|
||||
* @param db SQLiteDatabase
|
||||
*/
|
||||
public static void onCreate(final SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes LanguagesTable from SQLiteDatabase
|
||||
* @param db SQLiteDatabase
|
||||
*/
|
||||
public static void onDelete(final SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on migrating from a older version to a newer version
|
||||
* @param db SQLiteDatabase
|
||||
* @param from Version from which we are migrating
|
||||
* @param to Version to which we are migrating
|
||||
*/
|
||||
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 19) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 19) {
|
||||
// table added in version 20
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
package fr.free.nrw.commons.recentlanguages
|
||||
|
||||
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 javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
/**
|
||||
* Handles database operations for recently used languages
|
||||
*/
|
||||
@Singleton
|
||||
class RecentLanguagesDao @Inject constructor(
|
||||
@Named("recent_languages")
|
||||
private val clientProvider: Provider<ContentProviderClient>
|
||||
) {
|
||||
|
||||
/**
|
||||
* Find all persisted recently used languages on database
|
||||
* @return list of recently used languages
|
||||
*/
|
||||
fun getRecentLanguages(): List<Language> {
|
||||
val languages = mutableListOf<Language>()
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
db.query(
|
||||
RecentLanguagesContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
arrayOf(),
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToLast()) {
|
||||
do {
|
||||
languages.add(fromCursor(cursor))
|
||||
} while (cursor.moveToPrevious())
|
||||
}
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
db.release()
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Language to database
|
||||
* @param language : Language to add
|
||||
*/
|
||||
fun addRecentLanguage(language: Language) {
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
db.insert(
|
||||
RecentLanguagesContentProvider.BASE_URI,
|
||||
toContentValues(language)
|
||||
)
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
db.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a language from database
|
||||
* @param languageCode : code of the Language to delete
|
||||
*/
|
||||
fun deleteRecentLanguage(languageCode: String) {
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
db.delete(
|
||||
RecentLanguagesContentProvider.uriForCode(languageCode),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
db.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a language from database based on its name
|
||||
* @param languageCode : code of the Language to find
|
||||
* @return boolean : is language in database ?
|
||||
*/
|
||||
fun findRecentLanguage(languageCode: String?): Boolean {
|
||||
if (languageCode == null) { // Avoiding NPEs
|
||||
return false
|
||||
}
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
db.query(
|
||||
RecentLanguagesContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
"${Table.COLUMN_CODE}=?",
|
||||
arrayOf(languageCode),
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
db.release()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* It creates an Recent Language object from data stored in the SQLite DB by using cursor
|
||||
* @param cursor cursor
|
||||
* @return Language object
|
||||
*/
|
||||
@SuppressLint("Range")
|
||||
fun fromCursor(cursor: Cursor): Language {
|
||||
// Hardcoding column positions!
|
||||
val languageName = cursor.getString(
|
||||
cursor.getColumnIndex(Table.COLUMN_NAME)
|
||||
)
|
||||
val languageCode = cursor.getString(
|
||||
cursor.getColumnIndex(Table.COLUMN_CODE)
|
||||
)
|
||||
return Language(languageName, languageCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes data from Language and create a content value object
|
||||
* @param recentLanguage recently used language
|
||||
* @return ContentValues
|
||||
*/
|
||||
private fun toContentValues(recentLanguage: Language): ContentValues {
|
||||
return ContentValues().apply {
|
||||
put(Table.COLUMN_NAME, recentLanguage.languageName)
|
||||
put(Table.COLUMN_CODE, recentLanguage.languageCode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class contains the database table architecture for recently used languages,
|
||||
* It also contains queries and logic necessary to the create, update, delete this table.
|
||||
*/
|
||||
object Table {
|
||||
const val TABLE_NAME = "recent_languages"
|
||||
const val COLUMN_NAME = "language_name"
|
||||
const val COLUMN_CODE = "language_code"
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
@JvmStatic
|
||||
val ALL_FIELDS = arrayOf(
|
||||
COLUMN_NAME,
|
||||
COLUMN_CODE
|
||||
)
|
||||
|
||||
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
|
||||
|
||||
const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" +
|
||||
"$COLUMN_NAME STRING," +
|
||||
"$COLUMN_CODE STRING PRIMARY KEY" +
|
||||
");"
|
||||
|
||||
/**
|
||||
* This method creates a LanguagesTable in SQLiteDatabase
|
||||
* @param db SQLiteDatabase
|
||||
*/
|
||||
@SuppressLint("SQLiteString")
|
||||
@JvmStatic
|
||||
fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes LanguagesTable from SQLiteDatabase
|
||||
* @param db SQLiteDatabase
|
||||
*/
|
||||
@JvmStatic
|
||||
fun onDelete(db: SQLiteDatabase) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT)
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on migrating from a older version to a newer version
|
||||
* @param db SQLiteDatabase
|
||||
* @param from Version from which we are migrating
|
||||
* @param to Version to which we are migrating
|
||||
*/
|
||||
@JvmStatic
|
||||
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
|
||||
if (from == to) {
|
||||
return
|
||||
}
|
||||
if (from < 19) {
|
||||
// doesn't exist yet
|
||||
onUpdate(db, from + 1, to)
|
||||
return
|
||||
}
|
||||
if (from == 19) {
|
||||
// table added in version 20
|
||||
onCreate(db)
|
||||
onUpdate(db, from + 1, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
package fr.free.nrw.commons.repository;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.category.CategoriesModel;
|
||||
import fr.free.nrw.commons.category.CategoryItem;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates;
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface;
|
||||
import fr.free.nrw.commons.upload.UploadController;
|
||||
import fr.free.nrw.commons.upload.UploadItem;
|
||||
import fr.free.nrw.commons.upload.UploadModel;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* The repository class for UploadActivity
|
||||
*/
|
||||
@Singleton
|
||||
public class UploadRepository {
|
||||
|
||||
private final UploadModel uploadModel;
|
||||
private final UploadController uploadController;
|
||||
private final CategoriesModel categoriesModel;
|
||||
private final NearbyPlaces nearbyPlaces;
|
||||
private final DepictModel depictModel;
|
||||
|
||||
private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters
|
||||
private final ContributionDao contributionDao;
|
||||
|
||||
@Inject
|
||||
public UploadRepository(UploadModel uploadModel,
|
||||
UploadController uploadController,
|
||||
CategoriesModel categoriesModel,
|
||||
NearbyPlaces nearbyPlaces,
|
||||
DepictModel depictModel,
|
||||
ContributionDao contributionDao) {
|
||||
this.uploadModel = uploadModel;
|
||||
this.uploadController = uploadController;
|
||||
this.categoriesModel = categoriesModel;
|
||||
this.nearbyPlaces = nearbyPlaces;
|
||||
this.depictModel = depictModel;
|
||||
this.contributionDao=contributionDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* asks the RemoteDataSource to build contributions
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Observable<Contribution> buildContributions() {
|
||||
return uploadModel.buildContributions();
|
||||
}
|
||||
|
||||
/**
|
||||
* asks the RemoteDataSource to start upload for the contribution
|
||||
*
|
||||
* @param contribution
|
||||
*/
|
||||
|
||||
public void prepareMedia(Contribution contribution) {
|
||||
uploadController.prepareMedia(contribution);
|
||||
}
|
||||
|
||||
|
||||
public void saveContribution(Contribution contribution) {
|
||||
contributionDao.save(contribution).blockingAwait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns all the Upload Items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<UploadItem> getUploads() {
|
||||
return uploadModel.getUploads();
|
||||
}
|
||||
|
||||
/**
|
||||
*Prepare for a fresh upload
|
||||
*/
|
||||
public void cleanup() {
|
||||
uploadModel.cleanUp();
|
||||
//This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
|
||||
categoriesModel.cleanUp();
|
||||
depictModel.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns the selected categories for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<CategoryItem> getSelectedCategories() {
|
||||
return categoriesModel.getSelectedCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* all categories from MWApi
|
||||
*
|
||||
* @param query
|
||||
* @param imageTitleList
|
||||
* @param selectedDepictions
|
||||
* @return
|
||||
*/
|
||||
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList,
|
||||
List<DepictedItem> selectedDepictions) {
|
||||
return categoriesModel.searchAll(query, imageTitleList, selectedDepictions);
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the list of selected categories for the current upload
|
||||
*
|
||||
* @param categoryStringList
|
||||
*/
|
||||
public void setSelectedCategories(List<String> categoryStringList) {
|
||||
uploadModel.setSelectedCategories(categoryStringList);
|
||||
}
|
||||
|
||||
/**
|
||||
* handles the category selection/deselection
|
||||
*
|
||||
* @param categoryItem
|
||||
*/
|
||||
public void onCategoryClicked(CategoryItem categoryItem, final Media media) {
|
||||
categoriesModel.onCategoryItemClicked(categoryItem, media);
|
||||
}
|
||||
|
||||
/**
|
||||
* prunes the category list for irrelevant categories see #750
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public boolean isSpammyCategory(String name) {
|
||||
return categoriesModel.isSpammyCategory(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* retursn the string list of available license from the LocalDataSource
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<String> getLicenses() {
|
||||
return uploadModel.getLicenses();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the selected license for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getSelectedLicense() {
|
||||
return uploadModel.getSelectedLicense();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the number of Upload Items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getCount() {
|
||||
return uploadModel.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* ask the RemoteDataSource to pre process the image
|
||||
*
|
||||
* @param uploadableFile
|
||||
* @param place
|
||||
* @param similarImageInterface
|
||||
* @return
|
||||
*/
|
||||
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
|
||||
SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) {
|
||||
return uploadModel.preProcessImage(uploadableFile, place,
|
||||
similarImageInterface, inAppPictureLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* query the RemoteDataSource for image quality
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of UploadItem
|
||||
*/
|
||||
public Single<Integer> getImageQuality(UploadItem uploadItem, LatLng location) {
|
||||
return uploadModel.getImageQuality(uploadItem, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* query the RemoteDataSource for image duplicity check
|
||||
*
|
||||
* @param filePath file to be checked
|
||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||
*/
|
||||
public Single<Integer> checkDuplicateImage(String filePath) {
|
||||
return uploadModel.checkDuplicateImage(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* query the RemoteDataSource for caption quality
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of caption of the UploadItem
|
||||
*/
|
||||
public Single<Integer> getCaptionQuality(UploadItem uploadItem) {
|
||||
return uploadModel.getCaptionQuality(uploadItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* asks the LocalDataSource to delete the file with the given file path
|
||||
*
|
||||
* @param filePath
|
||||
*/
|
||||
public void deletePicture(String filePath) {
|
||||
uploadModel.deletePicture(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetches and returns the upload item
|
||||
*
|
||||
* @param index
|
||||
* @return
|
||||
*/
|
||||
public UploadItem getUploadItem(int index) {
|
||||
if (index >= 0) {
|
||||
return uploadModel.getItems().get(index);
|
||||
}
|
||||
return null; //There is no item to copy details
|
||||
}
|
||||
|
||||
/**
|
||||
* set selected license for the current upload
|
||||
*
|
||||
* @param licenseName
|
||||
*/
|
||||
public void setSelectedLicense(String licenseName) {
|
||||
uploadModel.setSelectedLicense(licenseName);
|
||||
}
|
||||
|
||||
public void onDepictItemClicked(DepictedItem depictedItem, final Media media) {
|
||||
uploadModel.onDepictItemClicked(depictedItem, media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns the selected depictions for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
|
||||
public List<DepictedItem> getSelectedDepictions() {
|
||||
return uploadModel.getSelectedDepictions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing depicts
|
||||
*
|
||||
* @return selected existing depicts
|
||||
*/
|
||||
public List<String> getSelectedExistingDepictions() {
|
||||
return uploadModel.getSelectedExistingDepictions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize existing depicts
|
||||
*
|
||||
* @param selectedExistingDepictions existing depicts
|
||||
*/
|
||||
public void setSelectedExistingDepictions(final List<String> selectedExistingDepictions) {
|
||||
uploadModel.setSelectedExistingDepictions(selectedExistingDepictions);
|
||||
}
|
||||
/**
|
||||
* Search all depictions from
|
||||
*
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
|
||||
public Flowable<List<DepictedItem>> searchAllEntities(String query) {
|
||||
return depictModel.searchAllEntities(query, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the depiction for each unique {@link Place} associated with an {@link UploadItem}
|
||||
* from {@link #getUploads()}
|
||||
*
|
||||
* @return a single that provides the depictions
|
||||
*/
|
||||
public Single<List<DepictedItem>> getPlaceDepictions() {
|
||||
final Set<String> qids = new HashSet<>();
|
||||
for (final UploadItem item : getUploads()) {
|
||||
final Place place = item.getPlace();
|
||||
if (place != null) {
|
||||
qids.add(place.getWikiDataEntityId());
|
||||
}
|
||||
}
|
||||
return depictModel.getPlaceDepictions(new ArrayList<>(qids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category for each unique {@link Place} associated with an {@link UploadItem}
|
||||
* from {@link #getUploads()}
|
||||
*
|
||||
* @return a single that provides the categories
|
||||
*/
|
||||
public Single<List<CategoryItem>> getPlaceCategories() {
|
||||
final Set<String> qids = new HashSet<>();
|
||||
for (final UploadItem item : getUploads()) {
|
||||
final Place place = item.getPlace();
|
||||
if (place != null) {
|
||||
qids.add(place.getCategory());
|
||||
}
|
||||
}
|
||||
return Single.fromObservable(categoriesModel.getCategoriesByName(new ArrayList<>(qids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem
|
||||
* from the server
|
||||
*
|
||||
* @param depictionsQIDs IDs of Depiction
|
||||
* @return Flowable<List<DepictedItem>>
|
||||
*/
|
||||
public Flowable<List<DepictedItem>> getDepictions(final List<String> depictionsQIDs){
|
||||
final String ids = joinQIDs(depictionsQIDs);
|
||||
return depictModel.getDepictions(ids).toFlowable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a string by joining all IDs divided by "|"
|
||||
*
|
||||
* @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"]
|
||||
* @return string ex. "Q11023|Q1356"
|
||||
*/
|
||||
private String joinQIDs(final List<String> depictionsQIDs) {
|
||||
if (depictionsQIDs != null && !depictionsQIDs.isEmpty()) {
|
||||
final StringBuilder buffer = new StringBuilder(depictionsQIDs.get(0));
|
||||
|
||||
if (depictionsQIDs.size() > 1) {
|
||||
for (int i = 1; i < depictionsQIDs.size(); i++) {
|
||||
buffer.append("|");
|
||||
buffer.append(depictionsQIDs.get(i));
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nearest place matching the passed latitude and longitude
|
||||
*
|
||||
* @param decLatitude
|
||||
* @param decLongitude
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
public Place checkNearbyPlaces(final double decLatitude, final double decLongitude) {
|
||||
try {
|
||||
final List<Place> fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng(
|
||||
decLatitude, decLongitude, 0.0f),
|
||||
Locale.getDefault().getLanguage(),
|
||||
NEARBY_RADIUS_IN_KILO_METERS, null);
|
||||
return (fromWikidataQuery != null && fromWikidataQuery.size() > 0) ? fromWikidataQuery
|
||||
.get(0) : null;
|
||||
} catch (final Exception e) {
|
||||
Timber.e("Error fetching nearby places: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
|
||||
uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
|
||||
}
|
||||
|
||||
public boolean isWMLSupportedForThisPlace() {
|
||||
return uploadModel.getItems().get(0).isWLMUpload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing categories
|
||||
*
|
||||
* @return selected existing categories
|
||||
*/
|
||||
public List<String> getSelectedExistingCategories() {
|
||||
return categoriesModel.getSelectedExistingCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize existing categories
|
||||
*
|
||||
* @param selectedExistingCategories existing categories
|
||||
*/
|
||||
public void setSelectedExistingCategories(final List<String> selectedExistingCategories) {
|
||||
categoriesModel.setSelectedExistingCategories(selectedExistingCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes category names and Gets CategoryItem from the server
|
||||
*
|
||||
* @param categories names of Category
|
||||
* @return Observable<List<CategoryItem>>
|
||||
*/
|
||||
public Observable<List<CategoryItem>> getCategories(final List<String> categories){
|
||||
return categoriesModel.getCategoriesByName(categories);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
package fr.free.nrw.commons.repository
|
||||
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.category.CategoriesModel
|
||||
import fr.free.nrw.commons.category.CategoryItem
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.ContributionDao
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.NearbyPlaces
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.upload.ImageCoordinates
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface
|
||||
import fr.free.nrw.commons.upload.UploadController
|
||||
import fr.free.nrw.commons.upload.UploadItem
|
||||
import fr.free.nrw.commons.upload.UploadModel
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictModel
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* The repository class for UploadActivity
|
||||
*/
|
||||
@Singleton
|
||||
class UploadRepository @Inject constructor(
|
||||
private val uploadModel: UploadModel,
|
||||
private val uploadController: UploadController,
|
||||
private val categoriesModel: CategoriesModel,
|
||||
private val nearbyPlaces: NearbyPlaces,
|
||||
private val depictModel: DepictModel,
|
||||
private val contributionDao: ContributionDao
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val NEARBY_RADIUS_IN_KILO_METERS = 0.1 // 100 meters
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the RemoteDataSource to build contributions
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun buildContributions(): Observable<Contribution>? {
|
||||
return uploadModel.buildContributions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the RemoteDataSource to start upload for the contribution
|
||||
*
|
||||
* @param contribution
|
||||
*/
|
||||
fun prepareMedia(contribution: Contribution) {
|
||||
uploadController.prepareMedia(contribution)
|
||||
}
|
||||
|
||||
fun saveContribution(contribution: Contribution) {
|
||||
contributionDao.save(contribution).blockingAwait()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns all the Upload Items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getUploads(): List<UploadItem> {
|
||||
return uploadModel.getUploads()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for a fresh upload
|
||||
*/
|
||||
fun cleanup() {
|
||||
uploadModel.cleanUp()
|
||||
// This needs further refactoring, this should not be here, right now the structure
|
||||
// won't support this
|
||||
categoriesModel.cleanUp()
|
||||
depictModel.cleanUp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns the selected categories for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedCategories(): List<CategoryItem> {
|
||||
return categoriesModel.getSelectedCategories()
|
||||
}
|
||||
|
||||
/**
|
||||
* All categories from MWApi
|
||||
*
|
||||
* @param query
|
||||
* @param imageTitleList
|
||||
* @param selectedDepictions
|
||||
* @return
|
||||
*/
|
||||
fun searchAll(
|
||||
query: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>
|
||||
): Observable<List<CategoryItem>> {
|
||||
return categoriesModel.searchAll(query, imageTitleList, selectedDepictions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of selected categories for the current upload
|
||||
*
|
||||
* @param categoryStringList
|
||||
*/
|
||||
fun setSelectedCategories(categoryStringList: List<String>) {
|
||||
uploadModel.setSelectedCategories(categoryStringList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the category selection/deselection
|
||||
*
|
||||
* @param categoryItem
|
||||
*/
|
||||
fun onCategoryClicked(categoryItem: CategoryItem, media: Media?) {
|
||||
categoriesModel.onCategoryItemClicked(categoryItem, media)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes the category list for irrelevant categories see #750
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
fun isSpammyCategory(name: String): Boolean {
|
||||
return categoriesModel.isSpammyCategory(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string list of available licenses from the LocalDataSource
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getLicenses(): List<String> {
|
||||
return uploadModel.licenses
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected license for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedLicense(): String? {
|
||||
return uploadModel.selectedLicense
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of Upload Items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getCount(): Int {
|
||||
return uploadModel.count
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the RemoteDataSource to preprocess the image
|
||||
*
|
||||
* @param uploadableFile
|
||||
* @param place
|
||||
* @param similarImageInterface
|
||||
* @param inAppPictureLocation
|
||||
* @return
|
||||
*/
|
||||
fun preProcessImage(
|
||||
uploadableFile: UploadableFile?,
|
||||
place: Place?,
|
||||
similarImageInterface: SimilarImageInterface?,
|
||||
inAppPictureLocation: LatLng?
|
||||
): Observable<UploadItem>? {
|
||||
return uploadModel.preProcessImage(
|
||||
uploadableFile,
|
||||
place,
|
||||
similarImageInterface,
|
||||
inAppPictureLocation
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the RemoteDataSource for image quality
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @param location Location of the image
|
||||
* @return Quality of UploadItem
|
||||
*/
|
||||
fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int>? {
|
||||
return uploadModel.getImageQuality(uploadItem, location)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the RemoteDataSource for image duplicity check
|
||||
*
|
||||
* @param filePath file to be checked
|
||||
* @return IMAGE_DUPLICATE or IMAGE_OK
|
||||
*/
|
||||
fun checkDuplicateImage(filePath: String): Single<Int> {
|
||||
return uploadModel.checkDuplicateImage(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* query the RemoteDataSource for caption quality
|
||||
*
|
||||
* @param uploadItem UploadItem whose caption is to be checked
|
||||
* @return Quality of caption of the UploadItem
|
||||
*/
|
||||
fun getCaptionQuality(uploadItem: UploadItem): Single<Int>? {
|
||||
return uploadModel.getCaptionQuality(uploadItem)
|
||||
}
|
||||
|
||||
/**
|
||||
* asks the LocalDataSource to delete the file with the given file path
|
||||
*
|
||||
* @param filePath
|
||||
*/
|
||||
fun deletePicture(filePath: String) {
|
||||
uploadModel.deletePicture(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* fetches and returns the upload item
|
||||
*
|
||||
* @param index
|
||||
* @return
|
||||
*/
|
||||
fun getUploadItem(index: Int): UploadItem? {
|
||||
return if (index >= 0) {
|
||||
uploadModel.items.getOrNull(index)
|
||||
} else null //There is no item to copy details
|
||||
}
|
||||
|
||||
/**
|
||||
* set selected license for the current upload
|
||||
*
|
||||
* @param licenseName
|
||||
*/
|
||||
fun setSelectedLicense(licenseName: String) {
|
||||
uploadModel.selectedLicense = licenseName
|
||||
}
|
||||
|
||||
fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) {
|
||||
uploadModel.onDepictItemClicked(depictedItem, media)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns the selected depictions for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedDepictions(): List<DepictedItem> {
|
||||
return uploadModel.selectedDepictions
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing depicts
|
||||
*
|
||||
* @return selected existing depicts
|
||||
*/
|
||||
fun getSelectedExistingDepictions(): List<String> {
|
||||
return uploadModel.selectedExistingDepictions
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize existing depicts
|
||||
*
|
||||
* @param selectedExistingDepictions existing depicts
|
||||
*/
|
||||
fun setSelectedExistingDepictions(selectedExistingDepictions: List<String>) {
|
||||
uploadModel.selectedExistingDepictions = selectedExistingDepictions
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all depictions from
|
||||
*
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
fun searchAllEntities(query: String): Flowable<List<DepictedItem>> {
|
||||
return depictModel.searchAllEntities(query, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the depiction for each unique {@link Place} associated with an {@link UploadItem}
|
||||
* from {@link #getUploads()}
|
||||
*
|
||||
* @return a single that provides the depictions
|
||||
*/
|
||||
fun getPlaceDepictions(): Single<List<DepictedItem>> {
|
||||
val qids = mutableSetOf<String>()
|
||||
getUploads().forEach { item ->
|
||||
item.place?.let {
|
||||
it.wikiDataEntityId?.let { it1 ->
|
||||
qids.add(it1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return depictModel.getPlaceDepictions(qids.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category for each unique {@link Place} associated with an {@link UploadItem}
|
||||
* from {@link #getUploads()}
|
||||
*
|
||||
* @return a single that provides the categories
|
||||
*/
|
||||
fun getPlaceCategories(): Single<List<CategoryItem>> {
|
||||
val qids = mutableSetOf<String>()
|
||||
getUploads().forEach { item ->
|
||||
item.place?.category?.let { qids.add(it) }
|
||||
}
|
||||
return Single.fromObservable(categoriesModel.getCategoriesByName(qids.toList()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem
|
||||
* from the server
|
||||
*
|
||||
* @param depictionsQIDs IDs of Depiction
|
||||
* @return Flowable<List<DepictedItem>>
|
||||
*/
|
||||
fun getDepictions(depictionsQIDs: List<String>): Flowable<List<DepictedItem>> {
|
||||
val ids = joinQIDs(depictionsQIDs) ?: ""
|
||||
return depictModel.getDepictions(ids).toFlowable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a string by joining all IDs divided by "|"
|
||||
*
|
||||
* @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"]
|
||||
* @return string ex. "Q11023|Q1356"
|
||||
*/
|
||||
private fun joinQIDs(depictionsQIDs: List<String>?): String? {
|
||||
return depictionsQIDs?.takeIf {
|
||||
it.isNotEmpty()
|
||||
}?.joinToString("|")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nearest place matching the passed latitude and longitude
|
||||
*
|
||||
* @param decLatitude
|
||||
* @param decLongitude
|
||||
* @return
|
||||
*/
|
||||
fun checkNearbyPlaces(decLatitude: Double, decLongitude: Double): Place? {
|
||||
return try {
|
||||
val fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(
|
||||
LatLng(decLatitude, decLongitude, 0.0f),
|
||||
Locale.getDefault().language,
|
||||
NEARBY_RADIUS_IN_KILO_METERS,
|
||||
null
|
||||
)
|
||||
fromWikidataQuery?.firstOrNull()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error fetching nearby places: %s", e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) {
|
||||
uploadModel.useSimilarPictureCoordinates(
|
||||
imageCoordinates,
|
||||
uploadItemIndex
|
||||
)
|
||||
}
|
||||
|
||||
fun isWMLSupportedForThisPlace(): Boolean {
|
||||
return uploadModel.items.firstOrNull()?.isWLMUpload == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing categories
|
||||
*
|
||||
* @return selected existing categories
|
||||
*/
|
||||
fun getSelectedExistingCategories(): List<String> {
|
||||
return categoriesModel.getSelectedExistingCategories()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize existing categories
|
||||
*
|
||||
* @param selectedExistingCategories existing categories
|
||||
*/
|
||||
fun setSelectedExistingCategories(selectedExistingCategories: List<String>) {
|
||||
categoriesModel.setSelectedExistingCategories(
|
||||
selectedExistingCategories.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes category names and Gets CategoryItem from the server
|
||||
*
|
||||
* @param categories names of Category
|
||||
* @return Observable<List<CategoryItem>>
|
||||
*/
|
||||
fun getCategories(categories: List<String>): Observable<List<CategoryItem>> {
|
||||
return categoriesModel.getCategoriesByName(categories)
|
||||
?.map { it.toList() } ?: Observable.empty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
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.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.databinding.ActivityReviewBinding;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.media.MediaDetailFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Locale;
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ReviewActivity extends BaseActivity {
|
||||
|
||||
|
||||
private ActivityReviewBinding binding;
|
||||
|
||||
MediaDetailFragment mediaDetailFragment;
|
||||
public ReviewPagerAdapter reviewPagerAdapter;
|
||||
public ReviewController reviewController;
|
||||
@Inject
|
||||
ReviewHelper reviewHelper;
|
||||
@Inject
|
||||
DeleteHelper deleteHelper;
|
||||
/**
|
||||
* Represent fragment for ReviewImage
|
||||
* Use to call some methods of ReviewImage fragment
|
||||
*/
|
||||
private ReviewImageFragment reviewImageFragment;
|
||||
|
||||
/**
|
||||
* Flag to check whether there are any non-hidden categories in the File
|
||||
*/
|
||||
private boolean hasNonHiddenCategories = false;
|
||||
|
||||
final String SAVED_MEDIA = "saved_media";
|
||||
private Media media;
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (media != null) {
|
||||
outState.putParcelable(SAVED_MEDIA, media);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
*
|
||||
* @param context
|
||||
* @param title Page title
|
||||
*/
|
||||
public static void startYourself(Context context, String title) {
|
||||
Intent reviewActivity = new Intent(context, ReviewActivity.class);
|
||||
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
context.startActivity(reviewActivity);
|
||||
}
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
public Media getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityReviewBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
reviewController = new ReviewController(deleteHelper, this);
|
||||
|
||||
reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager());
|
||||
binding.viewPagerReview.setAdapter(reviewPagerAdapter);
|
||||
binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview);
|
||||
binding.pbReviewImage.setVisibility(View.VISIBLE);
|
||||
|
||||
Drawable d[]=binding.skipImage.getCompoundDrawablesRelative();
|
||||
d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN);
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) {
|
||||
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)); // Use existing media if we have one
|
||||
setUpMediaDetailOnOrientation();
|
||||
} else {
|
||||
runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added
|
||||
}
|
||||
|
||||
binding.skipImage.setOnClickListener(view -> {
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment();
|
||||
reviewImageFragment.disableButtons();
|
||||
runRandomizer();
|
||||
});
|
||||
|
||||
binding.reviewImageView.setOnClickListener(view ->setUpMediaDetailFragment());
|
||||
|
||||
binding.skipImage.setOnTouchListener((view, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= (
|
||||
binding.skipImage.getRight() - binding.skipImage
|
||||
.getCompoundDrawables()[2].getBounds().width())) {
|
||||
showSkipImageInfo();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public boolean runRandomizer() {
|
||||
hasNonHiddenCategories = false;
|
||||
binding.pbReviewImage.setVisibility(View.VISIBLE);
|
||||
binding.viewPagerReview.setCurrentItem(0);
|
||||
// Finds non-hidden categories from Media instance
|
||||
compositeDisposable.add(reviewHelper.getRandomMedia()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::checkWhetherFileIsUsedInWikis));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether media is used or not in any Wiki Page
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private void checkWhetherFileIsUsedInWikis(final Media media) {
|
||||
compositeDisposable.add(reviewHelper.checkFileUsage(media.getFilename())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
// result false indicates media is not used in any wiki
|
||||
if (!result) {
|
||||
// Finds non-hidden categories from Media instance
|
||||
findNonHiddenCategories(media);
|
||||
} else {
|
||||
runRandomizer();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds non-hidden categories and updates current image
|
||||
*/
|
||||
private void findNonHiddenCategories(Media media) {
|
||||
for(String key : media.getCategoriesHiddenStatus().keySet()) {
|
||||
Boolean value = media.getCategoriesHiddenStatus().get(key);
|
||||
// If non-hidden category is found then set hasNonHiddenCategories to true
|
||||
// so that category review cannot be skipped
|
||||
if(!value) {
|
||||
hasNonHiddenCategories = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment();
|
||||
reviewImageFragment.disableButtons();
|
||||
updateImage(media);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void updateImage(Media media) {
|
||||
reviewHelper.addViewedImagesToDB(media.getPageId());
|
||||
this.media = media;
|
||||
String fileName = media.getFilename();
|
||||
if (fileName.length() == 0) {
|
||||
ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review);
|
||||
return;
|
||||
}
|
||||
|
||||
//If The Media User and Current Session Username is same then Skip the Image
|
||||
if (media.getUser() != null && media.getUser().equals(AccountUtil.getUserName(getApplicationContext()))) {
|
||||
runRandomizer();
|
||||
return;
|
||||
}
|
||||
|
||||
binding.reviewImageView.setImageURI(media.getImageUrl());
|
||||
|
||||
reviewController.onImageRefreshed(media); //file name is updated
|
||||
compositeDisposable.add(reviewHelper.getFirstRevisionOfFile(fileName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(revision -> {
|
||||
reviewController.firstRevision = revision;
|
||||
reviewPagerAdapter.updateFileInformation();
|
||||
@SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String caption = String.format(getString(R.string.review_is_uploaded_by), fileName, revision.getUser());
|
||||
binding.tvImageCaption.setText(caption);
|
||||
binding.pbReviewImage.setVisibility(View.GONE);
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment();
|
||||
reviewImageFragment.enableButtons();
|
||||
}));
|
||||
binding.viewPagerReview.setCurrentItem(0);
|
||||
}
|
||||
|
||||
public void swipeToNext() {
|
||||
int nextPos = binding.viewPagerReview.getCurrentItem() + 1;
|
||||
// If currently at category fragment, then check whether the media has any non-hidden category
|
||||
if (nextPos <= 3) {
|
||||
binding.viewPagerReview.setCurrentItem(nextPos);
|
||||
if (nextPos == 2) {
|
||||
// The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually.
|
||||
if (!hasNonHiddenCategories) {
|
||||
swipeToNext();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runRandomizer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
compositeDisposable.clear();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
public void showSkipImageInfo(){
|
||||
DialogUtil.showAlertDialog(ReviewActivity.this,
|
||||
getString(R.string.skip_image).toUpperCase(Locale.ROOT),
|
||||
getString(R.string.skip_image_explanation),
|
||||
getString(android.R.string.ok),
|
||||
"",
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
public void showReviewImageInfo() {
|
||||
DialogUtil.showAlertDialog(ReviewActivity.this,
|
||||
getString(R.string.title_activity_review),
|
||||
getString(R.string.review_image_explanation),
|
||||
getString(android.R.string.ok),
|
||||
"",
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_review_activty, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_image_info:
|
||||
showReviewImageInfo();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* this function return the instance of reviewImageFragment
|
||||
*/
|
||||
public ReviewImageFragment getInstanceOfReviewImageFragment(){
|
||||
int currentItemOfReviewPager = binding.viewPagerReview.getCurrentItem();
|
||||
reviewImageFragment = (ReviewImageFragment) reviewPagerAdapter.instantiateItem(binding.viewPagerReview, currentItemOfReviewPager);
|
||||
return reviewImageFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* set up the media detail fragment when click on the review image
|
||||
*/
|
||||
private void setUpMediaDetailFragment() {
|
||||
if (binding.mediaDetailContainer.getVisibility() == View.GONE && media != null) {
|
||||
binding.mediaDetailContainer.setVisibility(View.VISIBLE);
|
||||
binding.reviewActivityContainer.setVisibility(View.INVISIBLE);
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
mediaDetailFragment = new MediaDetailFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable("media", media);
|
||||
mediaDetailFragment.setArguments(bundle);
|
||||
fragmentManager.beginTransaction().add(R.id.mediaDetailContainer, mediaDetailFragment).
|
||||
addToBackStack("MediaDetail").commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle the back pressed event of this activity
|
||||
* this function call every time when back button is pressed
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (binding.mediaDetailContainer.getVisibility() == View.VISIBLE) {
|
||||
binding.mediaDetailContainer.setVisibility(View.GONE);
|
||||
binding.reviewActivityContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* set up media detail fragment after orientation change
|
||||
*/
|
||||
private void setUpMediaDetailOnOrientation() {
|
||||
Fragment mediaDetailFragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.mediaDetailContainer);
|
||||
if (mediaDetailFragment != null) {
|
||||
binding.mediaDetailContainer.setVisibility(View.VISIBLE);
|
||||
binding.reviewActivityContainer.setVisibility(View.INVISIBLE);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediaDetailContainer, mediaDetailFragment).commit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
336
app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt
Normal file
336
app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.AccountUtil
|
||||
import fr.free.nrw.commons.databinding.ActivityReviewBinding
|
||||
import fr.free.nrw.commons.delete.DeleteHelper
|
||||
import fr.free.nrw.commons.media.MediaDetailFragment
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import fr.free.nrw.commons.utils.ViewUtil
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReviewActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityReviewBinding
|
||||
|
||||
private var mediaDetailFragment: MediaDetailFragment? = null
|
||||
lateinit var reviewPagerAdapter: ReviewPagerAdapter
|
||||
lateinit var reviewController: ReviewController
|
||||
|
||||
@Inject
|
||||
lateinit var reviewHelper: ReviewHelper
|
||||
|
||||
@Inject
|
||||
lateinit var deleteHelper: DeleteHelper
|
||||
|
||||
/**
|
||||
* Represent fragment for ReviewImage
|
||||
* Use to call some methods of ReviewImage fragment
|
||||
*/
|
||||
private var reviewImageFragment: ReviewImageFragment? = null
|
||||
private var hasNonHiddenCategories = false
|
||||
var media: Media? = null
|
||||
|
||||
private val SAVED_MEDIA = "saved_media"
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
media?.let {
|
||||
outState.putParcelable(SAVED_MEDIA, it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
*
|
||||
* @param context
|
||||
* @param title Page title
|
||||
*/
|
||||
companion object {
|
||||
fun startYourself(context: Context, title: String) {
|
||||
val reviewActivity = Intent(context, ReviewActivity::class.java)
|
||||
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
context.startActivity(reviewActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityReviewBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbarBinding?.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
reviewController = ReviewController(deleteHelper, this)
|
||||
|
||||
reviewPagerAdapter = ReviewPagerAdapter(supportFragmentManager)
|
||||
binding.viewPagerReview.adapter = reviewPagerAdapter
|
||||
binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview)
|
||||
binding.pbReviewImage.visibility = View.VISIBLE
|
||||
|
||||
binding.skipImage.compoundDrawablesRelative[2]?.setColorFilter(
|
||||
resources.getColor(R.color.button_blue),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
|
||||
if (savedInstanceState?.getParcelable<Media>(SAVED_MEDIA) != null) {
|
||||
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!)
|
||||
setUpMediaDetailOnOrientation()
|
||||
} else {
|
||||
runRandomizer()
|
||||
}
|
||||
|
||||
binding.skipImage.setOnClickListener {
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment()
|
||||
reviewImageFragment?.disableButtons()
|
||||
runRandomizer()
|
||||
}
|
||||
|
||||
binding.reviewImageView.setOnClickListener {
|
||||
setUpMediaDetailFragment()
|
||||
}
|
||||
|
||||
binding.skipImage.setOnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_UP &&
|
||||
event.rawX >= (binding.skipImage.right - binding.skipImage.compoundDrawables[2].bounds.width())
|
||||
) {
|
||||
showSkipImageInfo()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun runRandomizer(): Boolean {
|
||||
hasNonHiddenCategories = false
|
||||
binding.pbReviewImage.visibility = View.VISIBLE
|
||||
binding.viewPagerReview.currentItem = 0
|
||||
|
||||
compositeDisposable.add(
|
||||
reviewHelper.getRandomMedia()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(::checkWhetherFileIsUsedInWikis)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether media is used or not in any Wiki Page
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private fun checkWhetherFileIsUsedInWikis(media: Media) {
|
||||
compositeDisposable.add(
|
||||
reviewHelper.checkFileUsage(media.filename)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
if (!result) {
|
||||
findNonHiddenCategories(media)
|
||||
} else {
|
||||
runRandomizer()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds non-hidden categories and updates current image
|
||||
*/
|
||||
private fun findNonHiddenCategories(media: Media) {
|
||||
this.media = media
|
||||
// If non-hidden category is found then set hasNonHiddenCategories to true
|
||||
// so that category review cannot be skipped
|
||||
hasNonHiddenCategories = media.categoriesHiddenStatus.values.any { !it }
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment()
|
||||
reviewImageFragment?.disableButtons()
|
||||
updateImage(media)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun updateImage(media: Media) {
|
||||
reviewHelper.addViewedImagesToDB(media.pageId)
|
||||
this.media = media
|
||||
val fileName = media.filename
|
||||
|
||||
if (fileName.isNullOrEmpty()) {
|
||||
ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review)
|
||||
return
|
||||
}
|
||||
|
||||
//If The Media User and Current Session Username is same then Skip the Image
|
||||
if (media.user == AccountUtil.getUserName(applicationContext)) {
|
||||
runRandomizer()
|
||||
return
|
||||
}
|
||||
|
||||
binding.reviewImageView.setImageURI(media.imageUrl)
|
||||
|
||||
reviewController.onImageRefreshed(media) // filename is updated
|
||||
compositeDisposable.add(
|
||||
reviewHelper.getFirstRevisionOfFile(fileName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { revision ->
|
||||
reviewController.firstRevision = revision
|
||||
reviewPagerAdapter.updateFileInformation()
|
||||
val caption = getString(
|
||||
R.string.review_is_uploaded_by,
|
||||
fileName,
|
||||
revision.user
|
||||
)
|
||||
binding.tvImageCaption.text = caption
|
||||
binding.pbReviewImage.visibility = View.GONE
|
||||
reviewImageFragment = getInstanceOfReviewImageFragment()
|
||||
reviewImageFragment?.enableButtons()
|
||||
}
|
||||
)
|
||||
binding.viewPagerReview.currentItem = 0
|
||||
}
|
||||
|
||||
fun swipeToNext() {
|
||||
val nextPos = binding.viewPagerReview.currentItem + 1
|
||||
|
||||
// If currently at category fragment, then check whether the media has any non-hidden category
|
||||
if (nextPos <= 3) {
|
||||
binding.viewPagerReview.currentItem = nextPos
|
||||
if (nextPos == 2 && !hasNonHiddenCategories)
|
||||
{
|
||||
// The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually.
|
||||
swipeToNext()
|
||||
}
|
||||
} else {
|
||||
runRandomizer()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
fun showSkipImageInfo() {
|
||||
DialogUtil.showAlertDialog(
|
||||
this,
|
||||
getString(R.string.skip_image).uppercase(Locale.ROOT),
|
||||
getString(R.string.skip_image_explanation),
|
||||
getString(android.R.string.ok),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun showReviewImageInfo() {
|
||||
DialogUtil.showAlertDialog(
|
||||
this,
|
||||
getString(R.string.title_activity_review),
|
||||
getString(R.string.review_image_explanation),
|
||||
getString(android.R.string.ok),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_review_activty, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_image_info -> {
|
||||
showReviewImageInfo()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this function return the instance of reviewImageFragment
|
||||
*/
|
||||
private fun getInstanceOfReviewImageFragment(): ReviewImageFragment? {
|
||||
val currentItemOfReviewPager = binding.viewPagerReview.currentItem
|
||||
return reviewPagerAdapter.instantiateItem(
|
||||
binding.viewPagerReview,
|
||||
currentItemOfReviewPager
|
||||
) as? ReviewImageFragment
|
||||
}
|
||||
|
||||
/**
|
||||
* set up the media detail fragment when click on the review image
|
||||
*/
|
||||
private fun setUpMediaDetailFragment() {
|
||||
if (binding.mediaDetailContainer.visibility == View.GONE && media != null) {
|
||||
binding.mediaDetailContainer.visibility = View.VISIBLE
|
||||
binding.reviewActivityContainer.visibility = View.INVISIBLE
|
||||
val fragmentManager = supportFragmentManager
|
||||
mediaDetailFragment = MediaDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable("media", media)
|
||||
}
|
||||
}
|
||||
fragmentManager.beginTransaction()
|
||||
.add(R.id.mediaDetailContainer, mediaDetailFragment!!)
|
||||
.addToBackStack("MediaDetail")
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle the back pressed event of this activity
|
||||
* this function call every time when back button is pressed
|
||||
*/
|
||||
@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 (binding.mediaDetailContainer.visibility == View.VISIBLE) {
|
||||
binding.mediaDetailContainer.visibility = View.GONE
|
||||
binding.reviewActivityContainer.visibility = View.VISIBLE
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* set up media detail fragment after orientation change
|
||||
*/
|
||||
private fun setUpMediaDetailOnOrientation() {
|
||||
val fragment = supportFragmentManager.findFragmentById(R.id.mediaDetailContainer)
|
||||
fragment?.let {
|
||||
binding.mediaDetailContainer.visibility = View.VISIBLE
|
||||
binding.reviewActivityContainer.visibility = View.INVISIBLE
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.mediaDetailContainer, it)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.actions.ThanksClient;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Singleton
|
||||
public class ReviewController {
|
||||
private static final int NOTIFICATION_SEND_THANK = 0x102;
|
||||
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
|
||||
protected static ArrayList<String> categories;
|
||||
@Inject
|
||||
ThanksClient thanksClient;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
private final DeleteHelper deleteHelper;
|
||||
@Nullable
|
||||
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
|
||||
@Inject
|
||||
@Named("commons-page-edit")
|
||||
PageEditClient pageEditClient;
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
private Media media;
|
||||
|
||||
ReviewController(DeleteHelper deleteHelper, Context context) {
|
||||
this.deleteHelper = deleteHelper;
|
||||
CommonsApplication.createNotificationChannel(context.getApplicationContext());
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
|
||||
}
|
||||
|
||||
void onImageRefreshed(Media media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public Media getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
public enum DeleteReason {
|
||||
SPAM,
|
||||
COPYRIGHT_VIOLATION
|
||||
}
|
||||
|
||||
void reportSpam(@NonNull Activity activity, ReviewCallback reviewCallback) {
|
||||
Timber.d("Report spam for %s", media.getFilename());
|
||||
deleteHelper.askReasonAndExecute(media,
|
||||
activity,
|
||||
activity.getResources().getString(R.string.review_spam_report_question),
|
||||
DeleteReason.SPAM,
|
||||
reviewCallback);
|
||||
}
|
||||
|
||||
void reportPossibleCopyRightViolation(@NonNull Activity activity, ReviewCallback reviewCallback) {
|
||||
Timber.d("Report spam for %s", media.getFilename());
|
||||
deleteHelper.askReasonAndExecute(media,
|
||||
activity,
|
||||
activity.getResources().getString(R.string.review_c_violation_report_question),
|
||||
DeleteReason.COPYRIGHT_VIOLATION,
|
||||
reviewCallback);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallback) {
|
||||
Context context = activity.getApplicationContext();
|
||||
ApplicationlessInjection
|
||||
.getInstance(context)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()));
|
||||
|
||||
publishProgress(context, 0);
|
||||
String summary = context.getString(R.string.check_category_edit_summary);
|
||||
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
|
||||
pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((result) -> {
|
||||
publishProgress(context, 2);
|
||||
String message;
|
||||
String title;
|
||||
|
||||
if (result) {
|
||||
title = context.getString(R.string.check_category_success_title);
|
||||
message = context.getString(R.string.check_category_success_message, media.getDisplayTitle());
|
||||
reviewCallback.onSuccess();
|
||||
} else {
|
||||
title = context.getString(R.string.check_category_failure_title);
|
||||
message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle());
|
||||
reviewCallback.onFailure();
|
||||
}
|
||||
|
||||
showNotification(title, message);
|
||||
|
||||
}, Timber::e);
|
||||
}
|
||||
|
||||
private void publishProgress(@NonNull Context context, int i) {
|
||||
int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template};
|
||||
String message = "";
|
||||
if (0 < i && i < messages.length) {
|
||||
message = context.getString(messages[i]);
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle()))
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(messages.length, i, false)
|
||||
.setOngoing(true);
|
||||
notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build());
|
||||
}
|
||||
|
||||
@SuppressLint({"CheckResult", "StringFormatInvalid"})
|
||||
void sendThanks(@NonNull Activity activity) {
|
||||
Context context = activity.getApplicationContext();
|
||||
ApplicationlessInjection
|
||||
.getInstance(context)
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()));
|
||||
|
||||
if (firstRevision == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Observable.defer((Callable<ObservableSource<Boolean>>) () -> thanksClient.thank(firstRevision.getRevisionId()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
displayThanksToast(context, result);
|
||||
}, throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
final String username = sessionManager.getUserName();
|
||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||
activity,
|
||||
activity.getString(R.string.invalid_login_message),
|
||||
username
|
||||
);
|
||||
|
||||
CommonsApplication.getInstance().clearApplicationData(
|
||||
activity, logoutListener);
|
||||
} else {
|
||||
Timber.e(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private void displayThanksToast(final Context context, final boolean result){
|
||||
final String message;
|
||||
final String title;
|
||||
if (result) {
|
||||
title = context.getString(R.string.send_thank_success_title);
|
||||
message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle());
|
||||
} else {
|
||||
title = context.getString(R.string.send_thank_failure_title);
|
||||
message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle());
|
||||
}
|
||||
|
||||
ViewUtil.showShortToast(context,message);
|
||||
}
|
||||
|
||||
private void showNotification(String title, String message) {
|
||||
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setContentTitle(title)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
|
||||
}
|
||||
|
||||
public interface ReviewCallback {
|
||||
void onSuccess();
|
||||
|
||||
void onFailure();
|
||||
|
||||
void onTokenException(Exception e);
|
||||
|
||||
void disableButtons();
|
||||
|
||||
void enableButtons();
|
||||
}
|
||||
}
|
||||
231
app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt
Normal file
231
app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.actions.PageEditClient
|
||||
import fr.free.nrw.commons.actions.ThanksClient
|
||||
import fr.free.nrw.commons.delete.DeleteHelper
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||
import fr.free.nrw.commons.utils.ViewUtil
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.ObservableSource
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@Singleton
|
||||
class ReviewController @Inject constructor(
|
||||
private val deleteHelper: DeleteHelper,
|
||||
context: Context
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_SEND_THANK = 0x102
|
||||
private const val NOTIFICATION_CHECK_CATEGORY = 0x101
|
||||
protected var categories: ArrayList<String> = ArrayList()
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var thanksClient: ThanksClient
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
@Inject
|
||||
@field: Named("commons-page-edit")
|
||||
lateinit var pageEditClient: PageEditClient
|
||||
|
||||
var firstRevision: MwQueryPage.Revision? = null // TODO: maybe we can expand this class to include fileName
|
||||
|
||||
private val notificationManager: NotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
private val notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||
|
||||
var media: Media? = null
|
||||
|
||||
init {
|
||||
CommonsApplication.createNotificationChannel(context.applicationContext)
|
||||
}
|
||||
|
||||
fun onImageRefreshed(media: Media) {
|
||||
this.media = media
|
||||
}
|
||||
|
||||
enum class DeleteReason {
|
||||
SPAM,
|
||||
COPYRIGHT_VIOLATION
|
||||
}
|
||||
|
||||
fun reportSpam(activity: Activity, reviewCallback: ReviewCallback) {
|
||||
Timber.d("Report spam for %s", media?.filename)
|
||||
deleteHelper.askReasonAndExecute(
|
||||
media,
|
||||
activity,
|
||||
activity.resources.getString(R.string.review_spam_report_question),
|
||||
DeleteReason.SPAM,
|
||||
reviewCallback
|
||||
)
|
||||
}
|
||||
|
||||
fun reportPossibleCopyRightViolation(activity: Activity, reviewCallback: ReviewCallback) {
|
||||
Timber.d("Report copyright violation for %s", media?.filename)
|
||||
deleteHelper.askReasonAndExecute(
|
||||
media,
|
||||
activity,
|
||||
activity.resources.getString(R.string.review_c_violation_report_question),
|
||||
DeleteReason.COPYRIGHT_VIOLATION,
|
||||
reviewCallback
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun reportWrongCategory(activity: Activity, reviewCallback: ReviewCallback) {
|
||||
val context = activity.applicationContext
|
||||
ApplicationlessInjection
|
||||
.getInstance(context)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
|
||||
ViewUtil.showShortToast(
|
||||
context,
|
||||
context.getString(R.string.check_category_toast, media?.displayTitle)
|
||||
)
|
||||
|
||||
publishProgress(context, 0)
|
||||
val summary = context.getString(R.string.check_category_edit_summary)
|
||||
|
||||
Observable.defer {
|
||||
pageEditClient.appendEdit(media?.filename ?: "", "\n{{subst:chc}}\n", summary)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result ->
|
||||
publishProgress(context, 2)
|
||||
val (title, message) = if (result) {
|
||||
reviewCallback.onSuccess()
|
||||
context.getString(R.string.check_category_success_title) to
|
||||
context.getString(R.string.check_category_success_message, media?.displayTitle)
|
||||
} else {
|
||||
reviewCallback.onFailure()
|
||||
context.getString(R.string.check_category_failure_title) to
|
||||
context.getString(R.string.check_category_failure_message, media?.displayTitle)
|
||||
}
|
||||
showNotification(title, message)
|
||||
}, Timber::e)
|
||||
}
|
||||
|
||||
private fun publishProgress(context: Context, progress: Int) {
|
||||
val messages = arrayOf(
|
||||
R.string.getting_edit_token,
|
||||
R.string.check_category_adding_template
|
||||
)
|
||||
|
||||
val message = if (progress in 1 until messages.size) {
|
||||
context.getString(messages[progress])
|
||||
} else ""
|
||||
|
||||
notificationBuilder.setContentTitle(
|
||||
context.getString(
|
||||
R.string.check_category_notification_title,
|
||||
media?.displayTitle
|
||||
)
|
||||
)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(messages.size, progress, false)
|
||||
.setOngoing(true)
|
||||
|
||||
notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build())
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun sendThanks(activity: Activity) {
|
||||
val context = activity.applicationContext
|
||||
ApplicationlessInjection
|
||||
.getInstance(context)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
|
||||
ViewUtil.showShortToast(
|
||||
context,
|
||||
context.getString(R.string.send_thank_toast, media?.displayTitle)
|
||||
)
|
||||
|
||||
if (firstRevision == null) return
|
||||
|
||||
Observable.defer {
|
||||
thanksClient.thank(firstRevision!!.revisionId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result ->
|
||||
displayThanksToast(context, result)
|
||||
}, { throwable ->
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
val username = sessionManager.userName
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
activity,
|
||||
activity.getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
CommonsApplication.instance.clearApplicationData(activity, logoutListener)
|
||||
} else {
|
||||
Timber.e(throwable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun displayThanksToast(context: Context, result: Boolean) {
|
||||
val (title, message) = if (result) {
|
||||
context.getString(R.string.send_thank_success_title) to
|
||||
context.getString(R.string.send_thank_success_message, media?.displayTitle)
|
||||
} else {
|
||||
context.getString(R.string.send_thank_failure_title) to
|
||||
context.getString(R.string.send_thank_failure_message, media?.displayTitle)
|
||||
}
|
||||
|
||||
ViewUtil.showShortToast(context, message)
|
||||
}
|
||||
|
||||
private fun showNotification(title: String, message: String) {
|
||||
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setContentTitle(title)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build())
|
||||
}
|
||||
|
||||
interface ReviewCallback {
|
||||
fun onSuccess()
|
||||
fun onFailure()
|
||||
fun onTokenException(e: Exception)
|
||||
fun disableButtons()
|
||||
fun enableButtons()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
package fr.free.nrw.commons.review
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
/**
|
||||
* Dao interface for reviewed images database
|
||||
*/
|
||||
@Dao
|
||||
public interface ReviewDao {
|
||||
interface ReviewDao {
|
||||
|
||||
/**
|
||||
* Inserts reviewed/skipped image identifier into the database
|
||||
|
|
@ -17,7 +17,7 @@ public interface ReviewDao {
|
|||
* @param reviewEntity
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
void insert(ReviewEntity reviewEntity);
|
||||
fun insert(reviewEntity: ReviewEntity)
|
||||
|
||||
/**
|
||||
* Checks if the image has already been reviewed/skipped by the user
|
||||
|
|
@ -26,7 +26,6 @@ public interface ReviewDao {
|
|||
* @param imageId
|
||||
* @return
|
||||
*/
|
||||
@Query( "SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
|
||||
Boolean isReviewedAlready(String imageId);
|
||||
|
||||
}
|
||||
@Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
|
||||
fun isReviewedAlready(imageId: String): Boolean
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
/**
|
||||
* Entity to store reviewed/skipped images identifier
|
||||
*/
|
||||
@Entity(tableName = "reviewed-images")
|
||||
public class ReviewEntity {
|
||||
@PrimaryKey
|
||||
@NonNull
|
||||
String imageId;
|
||||
|
||||
public ReviewEntity(String imageId) {
|
||||
this.imageId = imageId;
|
||||
}
|
||||
}
|
||||
13
app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt
Normal file
13
app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity to store reviewed/skipped images identifier
|
||||
*/
|
||||
@Entity(tableName = "reviewed-images")
|
||||
data class ReviewEntity(
|
||||
@PrimaryKey
|
||||
val imageId: String
|
||||
)
|
||||
|
|
@ -77,7 +77,7 @@ class ReviewHelper
|
|||
* @param image
|
||||
* @return
|
||||
*/
|
||||
fun getReviewStatus(image: String?): Boolean = dao?.isReviewedAlready(image) ?: false
|
||||
fun getReviewStatus(image: String?): Boolean = image?.let { dao?.isReviewedAlready(it) } ?: false
|
||||
|
||||
/**
|
||||
* Gets the first revision of the file from filename
|
||||
|
|
@ -132,7 +132,7 @@ class ReviewHelper
|
|||
*/
|
||||
fun addViewedImagesToDB(imageId: String?) {
|
||||
Completable
|
||||
.fromAction { dao!!.insert(ReviewEntity(imageId)) }
|
||||
.fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
|
|
|
|||
|
|
@ -1,262 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.databinding.FragmentReviewImageBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ReviewImageFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
static final int CATEGORY = 2;
|
||||
private static final int SPAM = 0;
|
||||
private static final int COPYRIGHT = 1;
|
||||
private static final int THANKS = 3;
|
||||
|
||||
private int position;
|
||||
|
||||
private FragmentReviewImageBinding binding;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
|
||||
// Constant variable used to store user's key name for onSaveInstanceState method
|
||||
private final String SAVED_USER = "saved_user";
|
||||
|
||||
// Variable that stores the value of user
|
||||
private String user;
|
||||
|
||||
public void update(final int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
private String updateCategoriesQuestion() {
|
||||
final Media media = getReviewActivity().getMedia();
|
||||
if (media != null && media.getCategoriesHiddenStatus() != null && isAdded()) {
|
||||
// Filter category name attribute from all categories
|
||||
final List<String> categories = new ArrayList<>();
|
||||
for(final String key : media.getCategoriesHiddenStatus().keySet()) {
|
||||
String value = String.valueOf(key);
|
||||
// Each category returned has a format like "Category:<some-category-name>"
|
||||
// so remove the prefix "Category:"
|
||||
final int index = key.indexOf("Category:");
|
||||
if(index == 0) {
|
||||
value = key.substring(9);
|
||||
}
|
||||
categories.add(value);
|
||||
}
|
||||
String catString = TextUtils.join(", ", categories);
|
||||
if (catString != null && !catString.equals("") && binding.tvReviewQuestionContext != null) {
|
||||
catString = "<b>" + catString + "</b>";
|
||||
final String stringToConvertHtml = String.format(getResources().getString(R.string.review_category_explanation), catString);
|
||||
return Html.fromHtml(stringToConvertHtml).toString();
|
||||
}
|
||||
}
|
||||
return getResources().getString(R.string.review_no_category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
position = getArguments().getInt("position");
|
||||
binding = FragmentReviewImageBinding.inflate(inflater, container, false);
|
||||
|
||||
final String question;
|
||||
String explanation=null;
|
||||
String yesButtonText;
|
||||
final String noButtonText;
|
||||
|
||||
binding.buttonYes.setOnClickListener(view -> onYesButtonClicked());
|
||||
|
||||
switch (position) {
|
||||
case SPAM:
|
||||
question = getString(R.string.review_spam);
|
||||
explanation = getString(R.string.review_spam_explanation);
|
||||
yesButtonText = getString(R.string.yes);
|
||||
noButtonText = getString(R.string.no);
|
||||
binding.buttonNo.setOnClickListener(view -> getReviewActivity()
|
||||
.reviewController.reportSpam(requireActivity(), getReviewCallback()));
|
||||
break;
|
||||
case COPYRIGHT:
|
||||
enableButtons();
|
||||
question = getString(R.string.review_copyright);
|
||||
explanation = getString(R.string.review_copyright_explanation);
|
||||
yesButtonText = getString(R.string.yes);
|
||||
noButtonText = getString(R.string.no);
|
||||
binding.buttonNo.setOnClickListener(view -> getReviewActivity()
|
||||
.reviewController
|
||||
.reportPossibleCopyRightViolation(requireActivity(), getReviewCallback()));
|
||||
break;
|
||||
case CATEGORY:
|
||||
enableButtons();
|
||||
question = getString(R.string.review_category);
|
||||
explanation = updateCategoriesQuestion();
|
||||
yesButtonText = getString(R.string.yes);
|
||||
noButtonText = getString(R.string.no);
|
||||
binding.buttonNo.setOnClickListener(view -> {
|
||||
getReviewActivity()
|
||||
.reviewController
|
||||
.reportWrongCategory(requireActivity(), getReviewCallback());
|
||||
getReviewActivity().swipeToNext();
|
||||
});
|
||||
break;
|
||||
case THANKS:
|
||||
enableButtons();
|
||||
question = getString(R.string.review_thanks);
|
||||
|
||||
if (getReviewActivity().reviewController.firstRevision != null) {
|
||||
user = getReviewActivity().reviewController.firstRevision.getUser();
|
||||
} else {
|
||||
if(savedInstanceState != null) {
|
||||
user = savedInstanceState.getString(SAVED_USER);
|
||||
}
|
||||
}
|
||||
|
||||
//if the user is null because of whatsoever reason, review will not be sent anyways
|
||||
if (!TextUtils.isEmpty(user)) {
|
||||
explanation = getString(R.string.review_thanks_explanation, user);
|
||||
}
|
||||
|
||||
// Note that the yes and no buttons are swapped in this section
|
||||
yesButtonText = getString(R.string.review_thanks_yes_button_text);
|
||||
noButtonText = getString(R.string.review_thanks_no_button_text);
|
||||
binding.buttonYes.setTextColor(Color.parseColor("#116aaa"));
|
||||
binding.buttonNo.setTextColor(Color.parseColor("#228b22"));
|
||||
binding.buttonNo.setOnClickListener(view -> {
|
||||
getReviewActivity().reviewController.sendThanks(getReviewActivity());
|
||||
getReviewActivity().swipeToNext();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
enableButtons();
|
||||
question = "How did we get here?";
|
||||
explanation = "No idea.";
|
||||
yesButtonText = "yes";
|
||||
noButtonText = "no";
|
||||
}
|
||||
|
||||
binding.tvReviewQuestion.setText(question);
|
||||
binding.tvReviewQuestionContext.setText(explanation);
|
||||
binding.buttonYes.setText(yesButtonText);
|
||||
binding.buttonNo.setText(noButtonText);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will be called when configuration changes happen
|
||||
*
|
||||
* @param outState
|
||||
*/
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
//Save user name when configuration changes happen
|
||||
outState.putString(SAVED_USER, user);
|
||||
}
|
||||
|
||||
private ReviewController.ReviewCallback getReviewCallback() {
|
||||
return new ReviewController
|
||||
.ReviewCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
getReviewActivity().runRandomizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTokenException(final Exception e) {
|
||||
if (e instanceof InvalidLoginTokenException){
|
||||
final String username = sessionManager.getUserName();
|
||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||
getActivity(),
|
||||
requireActivity().getString(R.string.invalid_login_message),
|
||||
username
|
||||
);
|
||||
|
||||
CommonsApplication.getInstance().clearApplicationData(
|
||||
requireActivity(), logoutListener);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image is being loaded
|
||||
* to disable the review buttons
|
||||
*/
|
||||
@Override
|
||||
public void disableButtons() {
|
||||
ReviewImageFragment.this.disableButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image has
|
||||
* been loaded to enable the review buttons.
|
||||
*/
|
||||
@Override
|
||||
public void enableButtons() {
|
||||
ReviewImageFragment.this.enableButtons();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image has
|
||||
* been loaded to enable the review buttons.
|
||||
*/
|
||||
public void enableButtons() {
|
||||
binding.buttonYes.setEnabled(true);
|
||||
binding.buttonYes.setAlpha(1);
|
||||
binding.buttonNo.setEnabled(true);
|
||||
binding.buttonNo.setAlpha(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image is being loaded
|
||||
* to disable the review buttons
|
||||
*/
|
||||
public void disableButtons() {
|
||||
binding.buttonYes.setEnabled(false);
|
||||
binding.buttonYes.setAlpha(0.5f);
|
||||
binding.buttonNo.setEnabled(false);
|
||||
binding.buttonNo.setAlpha(0.5f);
|
||||
}
|
||||
|
||||
void onYesButtonClicked() {
|
||||
getReviewActivity().swipeToNext();
|
||||
}
|
||||
|
||||
private ReviewActivity getReviewActivity() {
|
||||
return (ReviewActivity) requireActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.databinding.FragmentReviewImageBinding
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ReviewImageFragment : CommonsDaggerSupportFragment() {
|
||||
|
||||
companion object {
|
||||
const val CATEGORY = 2
|
||||
private const val SPAM = 0
|
||||
private const val COPYRIGHT = 1
|
||||
private const val THANKS = 3
|
||||
}
|
||||
|
||||
private var position: Int = 0
|
||||
private var binding: FragmentReviewImageBinding? = null
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
// Constant variable used to store user's key name for onSaveInstanceState method
|
||||
private val SAVED_USER = "saved_user"
|
||||
|
||||
// Variable that stores the value of user
|
||||
private var user: String? = null
|
||||
|
||||
fun update(position: Int) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
private fun updateCategoriesQuestion(): String {
|
||||
val media = reviewActivity.media
|
||||
if (media?.categoriesHiddenStatus != null && isAdded) {
|
||||
// Filter category name attribute from all categories
|
||||
val categories = media.categoriesHiddenStatus.keys.map { key ->
|
||||
var value = key
|
||||
// Each category returned has a format like "Category:<some-category-name>"
|
||||
// so remove the prefix "Category:"
|
||||
if (key.startsWith("Category:")) {
|
||||
value = key.substring(9)
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
val catString = categories.joinToString(", ")
|
||||
if (catString.isNotEmpty() && binding?.tvReviewQuestionContext != null) {
|
||||
val formattedCatString = "<b>$catString</b>"
|
||||
val stringToConvertHtml = getString(
|
||||
R.string.review_category_explanation,
|
||||
formattedCatString
|
||||
)
|
||||
return Html.fromHtml(stringToConvertHtml).toString()
|
||||
}
|
||||
}
|
||||
return getString(R.string.review_no_category)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
position = requireArguments().getInt("position")
|
||||
binding = FragmentReviewImageBinding.inflate(inflater, container, false)
|
||||
|
||||
val question: String
|
||||
var explanation: String? = null
|
||||
val yesButtonText: String
|
||||
val noButtonText: String
|
||||
|
||||
binding?.buttonYes?.setOnClickListener { onYesButtonClicked() }
|
||||
|
||||
when (position) {
|
||||
SPAM -> {
|
||||
question = getString(R.string.review_spam)
|
||||
explanation = getString(R.string.review_spam_explanation)
|
||||
yesButtonText = getString(R.string.yes)
|
||||
noButtonText = getString(R.string.no)
|
||||
binding?.buttonNo?.setOnClickListener {
|
||||
reviewActivity.reviewController.reportSpam(requireActivity(), reviewCallback)
|
||||
}
|
||||
}
|
||||
COPYRIGHT -> {
|
||||
enableButtons()
|
||||
question = getString(R.string.review_copyright)
|
||||
explanation = getString(R.string.review_copyright_explanation)
|
||||
yesButtonText = getString(R.string.yes)
|
||||
noButtonText = getString(R.string.no)
|
||||
binding?.buttonNo?.setOnClickListener {
|
||||
reviewActivity.reviewController.reportPossibleCopyRightViolation(
|
||||
requireActivity(),
|
||||
reviewCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
CATEGORY -> {
|
||||
enableButtons()
|
||||
question = getString(R.string.review_category)
|
||||
explanation = updateCategoriesQuestion()
|
||||
yesButtonText = getString(R.string.yes)
|
||||
noButtonText = getString(R.string.no)
|
||||
binding?.buttonNo?.setOnClickListener {
|
||||
reviewActivity.reviewController.reportWrongCategory(
|
||||
requireActivity(),
|
||||
reviewCallback
|
||||
)
|
||||
reviewActivity.swipeToNext()
|
||||
}
|
||||
}
|
||||
THANKS -> {
|
||||
enableButtons()
|
||||
question = getString(R.string.review_thanks)
|
||||
|
||||
user = reviewActivity.reviewController.firstRevision?.user
|
||||
?: savedInstanceState?.getString(SAVED_USER)
|
||||
|
||||
//if the user is null because of whatsoever reason, review will not be sent anyways
|
||||
if (!user.isNullOrEmpty()) {
|
||||
explanation = getString(R.string.review_thanks_explanation, user)
|
||||
}
|
||||
|
||||
// Note that the yes and no buttons are swapped in this section
|
||||
yesButtonText = getString(R.string.review_thanks_yes_button_text)
|
||||
noButtonText = getString(R.string.review_thanks_no_button_text)
|
||||
binding?.buttonYes?.setTextColor(Color.parseColor("#116aaa"))
|
||||
binding?.buttonNo?.setTextColor(Color.parseColor("#228b22"))
|
||||
binding?.buttonNo?.setOnClickListener {
|
||||
reviewActivity.reviewController.sendThanks(requireActivity())
|
||||
reviewActivity.swipeToNext()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
enableButtons()
|
||||
question = "How did we get here?"
|
||||
explanation = "No idea."
|
||||
yesButtonText = "yes"
|
||||
noButtonText = "no"
|
||||
}
|
||||
}
|
||||
|
||||
binding?.apply {
|
||||
tvReviewQuestion.text = question
|
||||
tvReviewQuestionContext.text = explanation
|
||||
buttonYes.text = yesButtonText
|
||||
buttonNo.text = noButtonText
|
||||
}
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be called when configuration changes happen
|
||||
*
|
||||
* @param outState
|
||||
*/
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
//Save user name when configuration changes happen
|
||||
outState.putString(SAVED_USER, user)
|
||||
}
|
||||
|
||||
private val reviewCallback: ReviewController.ReviewCallback
|
||||
get() = object : ReviewController.ReviewCallback {
|
||||
override fun onSuccess() {
|
||||
reviewActivity.runRandomizer()
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
override fun onTokenException(e: Exception) {
|
||||
if (e is InvalidLoginTokenException) {
|
||||
val username = sessionManager.userName
|
||||
val logoutListener = activity?.let {
|
||||
CommonsApplication.BaseLogoutListener(
|
||||
it,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
}
|
||||
|
||||
if (logoutListener != null) {
|
||||
CommonsApplication.instance.clearApplicationData(
|
||||
requireActivity(), logoutListener
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disableButtons() {
|
||||
this@ReviewImageFragment.disableButtons()
|
||||
}
|
||||
|
||||
override fun enableButtons() {
|
||||
this@ReviewImageFragment.enableButtons()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image has
|
||||
* been loaded to enable the review buttons.
|
||||
*/
|
||||
fun enableButtons() {
|
||||
binding?.apply {
|
||||
buttonYes.isEnabled = true
|
||||
buttonYes.alpha = 1f
|
||||
buttonNo.isEnabled = true
|
||||
buttonNo.alpha = 1f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an image is being loaded
|
||||
* to disable the review buttons
|
||||
*/
|
||||
fun disableButtons() {
|
||||
binding?.apply {
|
||||
buttonYes.isEnabled = false
|
||||
buttonYes.alpha = 0.5f
|
||||
buttonNo.isEnabled = false
|
||||
buttonNo.alpha = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
fun onYesButtonClicked() {
|
||||
reviewActivity.swipeToNext()
|
||||
}
|
||||
|
||||
private val reviewActivity: ReviewActivity
|
||||
get() = requireActivity() as ReviewActivity
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
|
||||
public class ReviewPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private ReviewImageFragment[] reviewImageFragments;
|
||||
|
||||
/**
|
||||
* this function return the instance of ReviewviewPage current item
|
||||
*/
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
return super.instantiateItem(container, position);
|
||||
}
|
||||
|
||||
ReviewPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
reviewImageFragments = new ReviewImageFragment[]{
|
||||
new ReviewImageFragment(),
|
||||
new ReviewImageFragment(),
|
||||
new ReviewImageFragment(),
|
||||
new ReviewImageFragment()
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return reviewImageFragments.length;
|
||||
}
|
||||
|
||||
void updateFileInformation() {
|
||||
for (int i = 0; i < getCount(); i++) {
|
||||
ReviewImageFragment fragment = reviewImageFragments[i];
|
||||
fragment.update(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("position", position);
|
||||
reviewImageFragments[position].setArguments(bundle);
|
||||
return reviewImageFragments[position];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
|
||||
|
||||
class ReviewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
|
||||
private val reviewImageFragments: Array<ReviewImageFragment> = arrayOf(
|
||||
ReviewImageFragment(),
|
||||
ReviewImageFragment(),
|
||||
ReviewImageFragment(),
|
||||
ReviewImageFragment()
|
||||
)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return reviewImageFragments.size
|
||||
}
|
||||
|
||||
fun updateFileInformation() {
|
||||
for (i in 0 until count) {
|
||||
val fragment = reviewImageFragments[i]
|
||||
fragment.update(i)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
val bundle = Bundle().apply {
|
||||
putInt("position", position)
|
||||
}
|
||||
reviewImageFragments[position].arguments = bundle
|
||||
return reviewImageFragments[position]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package fr.free.nrw.commons.review;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
public class ReviewViewPager extends ViewPager {
|
||||
|
||||
public ReviewViewPager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ReviewViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
// Never allow swiping to switch between pages
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// Never allow swiping to switch between pages
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package fr.free.nrw.commons.review
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
|
||||
class ReviewViewPager @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : ViewPager(context, attrs) {
|
||||
|
||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||
// Never allow swiping to switch between pages
|
||||
return false
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
// Never allow swiping to switch between pages
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package fr.free.nrw.commons.settings;
|
||||
|
||||
public class Prefs {
|
||||
public static String GLOBAL_PREFS = "fr.free.nrw.commons.preferences";
|
||||
|
||||
public static String TRACKING_ENABLED = "eventLogging";
|
||||
public static final String DEFAULT_LICENSE = "defaultLicense";
|
||||
public static final String UPLOADS_SHOWING = "uploadsshowing";
|
||||
public static final String MANAGED_EXIF_TAGS = "managed_exif_tags";
|
||||
public static final String DESCRIPTION_LANGUAGE = "languageDescription";
|
||||
public static final String APP_UI_LANGUAGE = "appUiLanguage";
|
||||
public static final String KEY_THEME_VALUE = "appThemePref";
|
||||
|
||||
public static class Licenses {
|
||||
public static final String CC_BY_SA_3 = "CC BY-SA 3.0";
|
||||
public static final String CC_BY_3 = "CC BY 3.0";
|
||||
public static final String CC_BY_SA_4 = "CC BY-SA 4.0";
|
||||
public static final String CC_BY_4 = "CC BY 4.0";
|
||||
public static final String CC0 = "CC0";
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt
Normal file
21
app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package fr.free.nrw.commons.settings
|
||||
|
||||
object Prefs {
|
||||
const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences"
|
||||
|
||||
const val TRACKING_ENABLED = "eventLogging"
|
||||
const val DEFAULT_LICENSE = "defaultLicense"
|
||||
const val UPLOADS_SHOWING = "uploadsShowing"
|
||||
const val MANAGED_EXIF_TAGS = "managed_exif_tags"
|
||||
const val DESCRIPTION_LANGUAGE = "languageDescription"
|
||||
const val APP_UI_LANGUAGE = "appUiLanguage"
|
||||
const val KEY_THEME_VALUE = "appThemePref"
|
||||
|
||||
object Licenses {
|
||||
const val CC_BY_SA_3 = "CC BY-SA 3.0"
|
||||
const val CC_BY_3 = "CC BY 3.0"
|
||||
const val CC_BY_SA_4 = "CC BY-SA 4.0"
|
||||
const val CC_BY_4 = "CC BY 4.0"
|
||||
const val CC0 = "CC0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package fr.free.nrw.commons.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import fr.free.nrw.commons.databinding.ActivitySettingsBinding;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
|
||||
/**
|
||||
* allows the user to change the settings
|
||||
*/
|
||||
public class SettingsActivity extends BaseActivity {
|
||||
|
||||
private ActivitySettingsBinding binding;
|
||||
// private AppCompatDelegate settingsDelegate;
|
||||
/**
|
||||
* to be called when the activity starts
|
||||
* @param savedInstanceState the previously saved state
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivitySettingsBinding.inflate(getLayoutInflater());
|
||||
final View view = binding.getRoot();
|
||||
setContentView(view);
|
||||
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
// Get an action bar
|
||||
/**
|
||||
* takes care of actions taken after the creation has happened
|
||||
* @param savedInstanceState the saved state
|
||||
*/
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
// if (settingsDelegate == null) {
|
||||
// settingsDelegate = AppCompatDelegate.create(this, null);
|
||||
// }
|
||||
// settingsDelegate.onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action-bar clicks
|
||||
* @param item the selected item
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package fr.free.nrw.commons.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import fr.free.nrw.commons.databinding.ActivitySettingsBinding
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
|
||||
|
||||
/**
|
||||
* allows the user to change the settings
|
||||
*/
|
||||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
// private var settingsDelegate: AppCompatDelegate? = null
|
||||
|
||||
/**
|
||||
* to be called when the activity starts
|
||||
* @param savedInstanceState the previously saved state
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
// Get an action bar
|
||||
/**
|
||||
* takes care of actions taken after the creation has happened
|
||||
* @param savedInstanceState the saved state
|
||||
*/
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
// if (settingsDelegate == null) {
|
||||
// settingsDelegate = AppCompatDelegate.create(this, null)
|
||||
// }
|
||||
// settingsDelegate?.onPostCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action-bar clicks
|
||||
* @param item the selected item
|
||||
* @return true on success, false on failure
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,561 +0,0 @@
|
|||
package fr.free.nrw.commons.settings;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.Preference.OnPreferenceClickListener;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceGroupAdapter;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import com.karumi.dexter.Dexter;
|
||||
import com.karumi.dexter.MultiplePermissionsReport;
|
||||
import com.karumi.dexter.PermissionToken;
|
||||
import com.karumi.dexter.listener.PermissionRequest;
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.campaigns.CampaignView;
|
||||
import fr.free.nrw.commons.contributions.ContributionController;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.logging.CommonsLogSender;
|
||||
import fr.free.nrw.commons.recentlanguages.Language;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
|
||||
import fr.free.nrw.commons.upload.LanguagesAdapter;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore defaultKvStore;
|
||||
|
||||
@Inject
|
||||
CommonsLogSender commonsLogSender;
|
||||
|
||||
@Inject
|
||||
RecentLanguagesDao recentLanguagesDao;
|
||||
|
||||
@Inject
|
||||
ContributionController contributionController;
|
||||
|
||||
@Inject
|
||||
LocationServiceManager locationManager;
|
||||
|
||||
private ListPreference themeListPreference;
|
||||
private Preference descriptionLanguageListPreference;
|
||||
private Preference appUiLanguageListPreference;
|
||||
private String keyLanguageListPreference;
|
||||
private TextView recentLanguagesTextView;
|
||||
private View separator;
|
||||
private ListView languageHistoryListView;
|
||||
private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content";
|
||||
|
||||
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
@Override
|
||||
public void onActivityResult(Map<String, Boolean> result) {
|
||||
boolean areAllGranted = true;
|
||||
for (final boolean b : result.values()) {
|
||||
areAllGranted = areAllGranted && b;
|
||||
}
|
||||
if (!areAllGranted && shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
ApplicationlessInjection
|
||||
.getInstance(getActivity().getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
// Set the preferences from an XML resource
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey);
|
||||
|
||||
themeListPreference = findPreference(Prefs.KEY_THEME_VALUE);
|
||||
prepareTheme();
|
||||
|
||||
MultiSelectListPreference multiSelectListPref = findPreference(Prefs.MANAGED_EXIF_TAGS);
|
||||
if (multiSelectListPref != null) {
|
||||
multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (newValue instanceof HashSet && !((HashSet) newValue).contains(getString(R.string.exif_tag_location))) {
|
||||
defaultKvStore.putBoolean("has_user_manually_removed_location", true);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Preference inAppCameraLocationPref = findPreference("inAppCameraLocationPref");
|
||||
|
||||
inAppCameraLocationPref.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
boolean isInAppCameraLocationTurnedOn = (boolean) newValue;
|
||||
if (isInAppCameraLocationTurnedOn) {
|
||||
createDialogsAndHandleLocationPermissions(getActivity());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// Gets current language code from shared preferences
|
||||
String languageCode;
|
||||
|
||||
appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref");
|
||||
assert appUiLanguageListPreference != null;
|
||||
keyLanguageListPreference = appUiLanguageListPreference.getKey();
|
||||
languageCode = getCurrentLanguageCode(keyLanguageListPreference);
|
||||
assert languageCode != null;
|
||||
if (languageCode.equals("")) {
|
||||
// If current language code is empty, means none selected by user yet so use phone local
|
||||
appUiLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage());
|
||||
} else {
|
||||
// If any language is selected by user previously, use it
|
||||
Locale defLocale = createLocale(languageCode);
|
||||
appUiLanguageListPreference.setSummary((defLocale).getDisplayLanguage(defLocale));
|
||||
}
|
||||
appUiLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
prepareAppLanguages(appUiLanguageListPreference.getKey());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref");
|
||||
assert descriptionLanguageListPreference != null;
|
||||
keyLanguageListPreference = descriptionLanguageListPreference.getKey();
|
||||
languageCode = getCurrentLanguageCode(keyLanguageListPreference);
|
||||
assert languageCode != null;
|
||||
if (languageCode.equals("")) {
|
||||
// If current language code is empty, means none selected by user yet so use phone local
|
||||
descriptionLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage());
|
||||
} else {
|
||||
// If any language is selected by user previously, use it
|
||||
Locale defLocale = createLocale(languageCode);
|
||||
descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale));
|
||||
}
|
||||
descriptionLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
prepareAppLanguages(descriptionLanguageListPreference.getKey());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Preference betaTesterPreference = findPreference("becomeBetaTester");
|
||||
betaTesterPreference.setOnPreferenceClickListener(preference -> {
|
||||
Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link)));
|
||||
return true;
|
||||
});
|
||||
Preference sendLogsPreference = findPreference("sendLogFile");
|
||||
sendLogsPreference.setOnPreferenceClickListener(preference -> {
|
||||
checkPermissionsAndSendLogs();
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference documentBasedPickerPreference = findPreference("openDocumentPhotoPickerPref");
|
||||
documentBasedPickerPreference.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
boolean isGetContentPickerTurnedOn = !(boolean) newValue;
|
||||
if (isGetContentPickerTurnedOn) {
|
||||
showLocationLossWarning();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
// Disable some settings when not logged in.
|
||||
if (defaultKvStore.getBoolean("login_skipped", false)) {
|
||||
findPreference("useExternalStorage").setEnabled(false);
|
||||
findPreference("useAuthorName").setEnabled(false);
|
||||
findPreference("displayNearbyCardView").setEnabled(false);
|
||||
findPreference("descriptionDefaultLanguagePref").setEnabled(false);
|
||||
findPreference("displayLocationPermissionForCardView").setEnabled(false);
|
||||
findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false);
|
||||
findPreference("managed_exif_tags").setEnabled(false);
|
||||
findPreference("openDocumentPhotoPickerPref").setEnabled(false);
|
||||
findPreference("inAppCameraLocationPref").setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks users to provide location access
|
||||
*
|
||||
* @param activity
|
||||
*/
|
||||
private void createDialogsAndHandleLocationPermissions(Activity activity) {
|
||||
inAppCameraLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION});
|
||||
}
|
||||
|
||||
/**
|
||||
* On some devices, the new Photo Picker with GET_CONTENT takeover
|
||||
* redacts location tags from EXIF metadata
|
||||
*
|
||||
* Show warning to the user when ACTION_GET_CONTENT intent is enabled
|
||||
*/
|
||||
private void showLocationLossWarning() {
|
||||
DialogUtil.showAlertDialog(
|
||||
getActivity(),
|
||||
null,
|
||||
getString(R.string.location_loss_warning),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.read_help_link),
|
||||
() -> {},
|
||||
() -> Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)),
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Adapter onCreateAdapter(final PreferenceScreen preferenceScreen) {
|
||||
return new PreferenceGroupAdapter(preferenceScreen) {
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
Preference preference = getItem(position);
|
||||
View iconFrame = holder.itemView.findViewById(R.id.icon_frame);
|
||||
if (iconFrame != null) {
|
||||
iconFrame.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme pref
|
||||
*/
|
||||
private void prepareTheme() {
|
||||
themeListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and Show language selection dialog box
|
||||
* Uses previously saved language if there is any, if not uses phone locale as initial language.
|
||||
* Disable default/already selected language from dialog box
|
||||
* Get ListPreference key and act accordingly for each ListPreference.
|
||||
* saves value chosen by user to shared preferences
|
||||
* to remember later and recall MainActivity to reflect language changes
|
||||
* @param keyListPreference
|
||||
*/
|
||||
private void prepareAppLanguages(final String keyListPreference) {
|
||||
|
||||
// Gets current language code from shared preferences
|
||||
final String languageCode = getCurrentLanguageCode(keyListPreference);
|
||||
final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages();
|
||||
HashMap<Integer, String> selectedLanguages = new HashMap<>();
|
||||
|
||||
if (keyListPreference.equals("appUiDefaultLanguagePref")) {
|
||||
|
||||
assert languageCode != null;
|
||||
if (languageCode.equals("")) {
|
||||
selectedLanguages.put(0, Locale.getDefault().getLanguage());
|
||||
} else {
|
||||
selectedLanguages.put(0, languageCode);
|
||||
}
|
||||
} else if (keyListPreference.equals("descriptionDefaultLanguagePref")) {
|
||||
|
||||
assert languageCode != null;
|
||||
if (languageCode.equals("")) {
|
||||
selectedLanguages.put(0, Locale.getDefault().getLanguage());
|
||||
|
||||
} else {
|
||||
selectedLanguages.put(0, languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
LanguagesAdapter languagesAdapter = new LanguagesAdapter(
|
||||
getActivity(),
|
||||
selectedLanguages
|
||||
);
|
||||
|
||||
Dialog dialog = new Dialog(getActivity());
|
||||
dialog.setContentView(R.layout.dialog_select_language);
|
||||
dialog.setCanceledOnTouchOutside(true);
|
||||
dialog.getWindow().setLayout((int)(getActivity().getResources().getDisplayMetrics().widthPixels*0.90),
|
||||
(int)(getActivity().getResources().getDisplayMetrics().heightPixels*0.90));
|
||||
dialog.show();
|
||||
|
||||
EditText editText = dialog.findViewById(R.id.search_language);
|
||||
ListView listView = dialog.findViewById(R.id.language_list);
|
||||
languageHistoryListView = dialog.findViewById(R.id.language_history_list);
|
||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches);
|
||||
separator = dialog.findViewById(R.id.separator);
|
||||
|
||||
setUpRecentLanguagesSection(recentLanguages, selectedLanguages);
|
||||
|
||||
listView.setAdapter(languagesAdapter);
|
||||
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1,
|
||||
int i2) {
|
||||
hideRecentLanguagesSection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1,
|
||||
int i2) {
|
||||
languagesAdapter.getFilter().filter(charSequence);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
languageHistoryListView.setOnItemClickListener((adapterView, view, position, id) -> {
|
||||
onRecentLanguageClicked(keyListPreference, dialog, adapterView, position);
|
||||
});
|
||||
|
||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i,
|
||||
long l) {
|
||||
String languageCode = ((LanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageCode(i);
|
||||
final String languageName = ((LanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageName(i);
|
||||
final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode);
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(languageCode);
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode));
|
||||
saveLanguageValue(languageCode, keyListPreference);
|
||||
Locale defLocale = createLocale(languageCode);
|
||||
if(keyListPreference.equals("appUiDefaultLanguagePref")) {
|
||||
appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale));
|
||||
setLocale(requireActivity(), languageCode);
|
||||
getActivity().recreate();
|
||||
final Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}else {
|
||||
descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale));
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.setOnDismissListener(
|
||||
dialogInterface -> languagesAdapter.getFilter().filter(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up recent languages section
|
||||
*
|
||||
* @param recentLanguages recently used languages
|
||||
* @param selectedLanguages selected languages
|
||||
*/
|
||||
private void setUpRecentLanguagesSection(List<Language> recentLanguages,
|
||||
HashMap<Integer, String> selectedLanguages) {
|
||||
if (recentLanguages.isEmpty()) {
|
||||
languageHistoryListView.setVisibility(View.GONE);
|
||||
recentLanguagesTextView.setVisibility(View.GONE);
|
||||
separator.setVisibility(View.GONE);
|
||||
} else {
|
||||
if (recentLanguages.size() > 5) {
|
||||
for (int i = recentLanguages.size()-1; i >=5; i--) {
|
||||
recentLanguagesDao
|
||||
.deleteRecentLanguage(recentLanguages.get(i).getLanguageCode());
|
||||
}
|
||||
}
|
||||
languageHistoryListView.setVisibility(View.VISIBLE);
|
||||
recentLanguagesTextView.setVisibility(View.VISIBLE);
|
||||
separator.setVisibility(View.VISIBLE);
|
||||
final RecentLanguagesAdapter recentLanguagesAdapter
|
||||
= new RecentLanguagesAdapter(
|
||||
getActivity(),
|
||||
recentLanguagesDao.getRecentLanguages(),
|
||||
selectedLanguages);
|
||||
languageHistoryListView.setAdapter(recentLanguagesAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click event for recent language section
|
||||
*/
|
||||
private void onRecentLanguageClicked(String keyListPreference, Dialog dialog, AdapterView<?> adapterView,
|
||||
int position) {
|
||||
final String recentLanguageCode = ((RecentLanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageCode(position);
|
||||
final String recentLanguageName = ((RecentLanguagesAdapter) adapterView.getAdapter())
|
||||
.getLanguageName(position);
|
||||
final boolean isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode);
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(recentLanguageCode);
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(
|
||||
new Language(recentLanguageName, recentLanguageCode));
|
||||
saveLanguageValue(recentLanguageCode, keyListPreference);
|
||||
final Locale defLocale = createLocale(recentLanguageCode);
|
||||
if (keyListPreference.equals("appUiDefaultLanguagePref")) {
|
||||
appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale));
|
||||
setLocale(requireActivity(), recentLanguageCode);
|
||||
getActivity().recreate();
|
||||
final Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale));
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the section of recent languages
|
||||
*/
|
||||
private void hideRecentLanguagesSection() {
|
||||
languageHistoryListView.setVisibility(View.GONE);
|
||||
recentLanguagesTextView.setVisibility(View.GONE);
|
||||
separator.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changing the default app language with selected one and save it to SharedPreferences
|
||||
*/
|
||||
public void setLocale(final Activity activity, String userSelectedValue) {
|
||||
if (userSelectedValue.equals("")) {
|
||||
userSelectedValue = Locale.getDefault().getLanguage();
|
||||
}
|
||||
final Locale locale = createLocale(userSelectedValue);
|
||||
Locale.setDefault(locale);
|
||||
final Configuration configuration = new Configuration();
|
||||
configuration.locale = locale;
|
||||
activity.getBaseContext().getResources().updateConfiguration(configuration,
|
||||
activity.getBaseContext().getResources().getDisplayMetrics());
|
||||
|
||||
final SharedPreferences.Editor editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit();
|
||||
editor.putString("language", userSelectedValue);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Locale based on different types of language codes
|
||||
* @param languageCode
|
||||
* @return Locale and throws error for invalid language codes
|
||||
*/
|
||||
public static Locale createLocale(String languageCode) {
|
||||
String[] parts = languageCode.split("-");
|
||||
switch (parts.length) {
|
||||
case 1:
|
||||
return new Locale(parts[0]);
|
||||
case 2:
|
||||
return new Locale(parts[0], parts[1]);
|
||||
case 3:
|
||||
return new Locale(parts[0], parts[1], parts[2]);
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid language code: " + languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save userselected language in List Preference
|
||||
* @param userSelectedValue
|
||||
* @param preferenceKey
|
||||
*/
|
||||
private void saveLanguageValue(final String userSelectedValue, final String preferenceKey) {
|
||||
if (preferenceKey.equals("appUiDefaultLanguagePref")) {
|
||||
defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue);
|
||||
} else if (preferenceKey.equals("descriptionDefaultLanguagePref")) {
|
||||
defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current language code from shared preferences
|
||||
* @param preferenceKey
|
||||
* @return
|
||||
*/
|
||||
private String getCurrentLanguageCode(final String preferenceKey) {
|
||||
if (preferenceKey.equals("appUiDefaultLanguagePref")) {
|
||||
return defaultKvStore.getString(Prefs.APP_UI_LANGUAGE, "");
|
||||
}
|
||||
if (preferenceKey.equals("descriptionDefaultLanguagePref")) {
|
||||
return defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* First checks for external storage permissions and then sends logs via email
|
||||
*/
|
||||
private void checkPermissionsAndSendLogs() {
|
||||
if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE)) {
|
||||
commonsLogSender.send(getActivity(), null);
|
||||
} else {
|
||||
requestExternalStoragePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests external storage permissions and shows a toast stating that log collection has
|
||||
* started
|
||||
*/
|
||||
private void requestExternalStoragePermissions() {
|
||||
Dexter.withActivity(getActivity())
|
||||
.withPermissions(PermissionUtils.PERMISSIONS_STORAGE)
|
||||
.withListener(new MultiplePermissionsListener() {
|
||||
@Override
|
||||
public void onPermissionsChecked(MultiplePermissionsReport report) {
|
||||
ViewUtil.showLongToast(getActivity(),
|
||||
getResources().getString(R.string.log_collection_started));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRationaleShouldBeShown(
|
||||
List<PermissionRequest> permissions, PermissionToken token) {
|
||||
|
||||
}
|
||||
})
|
||||
.onSameThread()
|
||||
.check();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,558 @@
|
|||
package fr.free.nrw.commons.settings
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroupAdapter
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.MultiplePermissionsReport
|
||||
import com.karumi.dexter.PermissionToken
|
||||
import com.karumi.dexter.listener.PermissionRequest
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.campaigns.CampaignView
|
||||
import fr.free.nrw.commons.contributions.ContributionController
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.logging.CommonsLogSender
|
||||
import fr.free.nrw.commons.recentlanguages.Language
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||
import fr.free.nrw.commons.upload.LanguagesAdapter
|
||||
import fr.free.nrw.commons.utils.DialogUtil
|
||||
import fr.free.nrw.commons.utils.PermissionUtils
|
||||
import fr.free.nrw.commons.utils.ViewUtil
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@Inject
|
||||
@field: Named("default_preferences")
|
||||
lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@Inject
|
||||
lateinit var commonsLogSender: CommonsLogSender
|
||||
|
||||
@Inject
|
||||
lateinit var recentLanguagesDao: RecentLanguagesDao
|
||||
|
||||
@Inject
|
||||
lateinit var contributionController: ContributionController
|
||||
|
||||
@Inject
|
||||
lateinit var locationManager: LocationServiceManager
|
||||
|
||||
private var themeListPreference: ListPreference? = null
|
||||
private var descriptionLanguageListPreference: Preference? = null
|
||||
private var appUiLanguageListPreference: Preference? = null
|
||||
private var showDeletionButtonPreference: Preference? = null
|
||||
private var keyLanguageListPreference: String? = null
|
||||
private var recentLanguagesTextView: TextView? = null
|
||||
private var separator: View? = null
|
||||
private var languageHistoryListView: ListView? = null
|
||||
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
|
||||
private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
|
||||
|
||||
private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> =
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks ->
|
||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* to be called when the fragment creates preferences
|
||||
* @param savedInstanceState the previously saved state
|
||||
* @param rootKey the root key for preferences
|
||||
*/
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
ApplicationlessInjection
|
||||
.getInstance(requireActivity().applicationContext)
|
||||
.commonsApplicationComponent
|
||||
.inject(this)
|
||||
|
||||
// Set the preferences from an XML resource
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
themeListPreference = findPreference(Prefs.KEY_THEME_VALUE)
|
||||
prepareTheme()
|
||||
|
||||
val multiSelectListPref: MultiSelectListPreference? = findPreference(
|
||||
Prefs.MANAGED_EXIF_TAGS
|
||||
)
|
||||
multiSelectListPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is HashSet<*> && !newValue.contains(getString(R.string.exif_tag_location)))
|
||||
{
|
||||
defaultKvStore.putBoolean("has_user_manually_removed_location", true)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val inAppCameraLocationPref: Preference? = findPreference("inAppCameraLocationPref")
|
||||
inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val isInAppCameraLocationTurnedOn = newValue as Boolean
|
||||
if (isInAppCameraLocationTurnedOn) {
|
||||
createDialogsAndHandleLocationPermissions(requireActivity())
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
inAppCameraLocationPermissionLauncher = registerForActivityResult(
|
||||
RequestMultiplePermissions()
|
||||
) { result ->
|
||||
var areAllGranted = true
|
||||
for (b in result.values) {
|
||||
areAllGranted = areAllGranted && b
|
||||
}
|
||||
if (
|
||||
!areAllGranted
|
||||
&&
|
||||
shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)
|
||||
) {
|
||||
contributionController.handleShowRationaleFlowCameraLocation(
|
||||
requireActivity(),
|
||||
inAppCameraLocationPermissionLauncher,
|
||||
cameraPickLauncherForResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Gets current language code from shared preferences
|
||||
var languageCode: String?
|
||||
|
||||
appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref")
|
||||
appUiLanguageListPreference?.let { appUiLanguageListPreference ->
|
||||
keyLanguageListPreference = appUiLanguageListPreference.key
|
||||
languageCode = getCurrentLanguageCode(keyLanguageListPreference!!)
|
||||
|
||||
languageCode?.let { code ->
|
||||
if (code.isEmpty()) {
|
||||
// If current language code is empty, means none selected by user yet so use
|
||||
// phone locale
|
||||
appUiLanguageListPreference.summary = Locale.getDefault().displayLanguage
|
||||
} else {
|
||||
// If any language is selected by user previously, use it
|
||||
val defLocale = createLocale(code)
|
||||
appUiLanguageListPreference.summary = defLocale.getDisplayLanguage(defLocale)
|
||||
}
|
||||
}
|
||||
|
||||
appUiLanguageListPreference.setOnPreferenceClickListener {
|
||||
prepareAppLanguages(keyLanguageListPreference!!)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref")
|
||||
descriptionLanguageListPreference?.let { descriptionLanguageListPreference ->
|
||||
languageCode = getCurrentLanguageCode(descriptionLanguageListPreference.key)
|
||||
|
||||
languageCode?.let { code ->
|
||||
if (code.isEmpty()) {
|
||||
// If current language code is empty, means none selected by user yet so use
|
||||
// phone locale
|
||||
descriptionLanguageListPreference.summary = Locale.getDefault().displayLanguage
|
||||
} else {
|
||||
// If any language is selected by user previously, use it
|
||||
val defLocale = createLocale(code)
|
||||
descriptionLanguageListPreference.summary = defLocale.getDisplayLanguage(
|
||||
defLocale
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
descriptionLanguageListPreference.setOnPreferenceClickListener {
|
||||
prepareAppLanguages(it.key)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
showDeletionButtonPreference = findPreference("displayDeletionButton")
|
||||
showDeletionButtonPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val isEnabled = newValue as Boolean
|
||||
// Save preference when user toggles the button
|
||||
defaultKvStore.putBoolean("displayDeletionButton", isEnabled)
|
||||
true
|
||||
}
|
||||
|
||||
val betaTesterPreference: Preference? = findPreference("becomeBetaTester")
|
||||
betaTesterPreference?.setOnPreferenceClickListener {
|
||||
Utils.handleWebUrl(requireActivity(), Uri.parse(getString(R.string.beta_opt_in_link)))
|
||||
true
|
||||
}
|
||||
|
||||
val sendLogsPreference: Preference? = findPreference("sendLogFile")
|
||||
sendLogsPreference?.setOnPreferenceClickListener {
|
||||
checkPermissionsAndSendLogs()
|
||||
true
|
||||
}
|
||||
|
||||
val documentBasedPickerPreference: Preference? = findPreference(
|
||||
"openDocumentPhotoPickerPref"
|
||||
)
|
||||
documentBasedPickerPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val isGetContentPickerTurnedOn = newValue as Boolean
|
||||
if (!isGetContentPickerTurnedOn) {
|
||||
showLocationLossWarning()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Disable some settings when not logged in.
|
||||
if (defaultKvStore.getBoolean("login_skipped", false)) {
|
||||
findPreference<Preference>("useExternalStorage")?.isEnabled = false
|
||||
findPreference<Preference>("useAuthorName")?.isEnabled = false
|
||||
findPreference<Preference>("displayNearbyCardView")?.isEnabled = false
|
||||
findPreference<Preference>("descriptionDefaultLanguagePref")?.isEnabled = false
|
||||
findPreference<Preference>("displayLocationPermissionForCardView")?.isEnabled = false
|
||||
findPreference<Preference>(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE)?.isEnabled = false
|
||||
findPreference<Preference>("managed_exif_tags")?.isEnabled = false
|
||||
findPreference<Preference>("openDocumentPhotoPickerPref")?.isEnabled = false
|
||||
findPreference<Preference>("inAppCameraLocationPref")?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks users to provide location access
|
||||
*
|
||||
* @param activity
|
||||
*/
|
||||
private fun createDialogsAndHandleLocationPermissions(activity: Activity) {
|
||||
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
|
||||
}
|
||||
|
||||
/**
|
||||
* On some devices, the new Photo Picker with GET_CONTENT takeover
|
||||
* redacts location tags from EXIF metadata
|
||||
*
|
||||
* Show warning to the user when ACTION_GET_CONTENT intent is enabled
|
||||
*/
|
||||
private fun showLocationLossWarning() {
|
||||
DialogUtil.showAlertDialog(
|
||||
requireActivity(),
|
||||
null,
|
||||
getString(R.string.location_loss_warning),
|
||||
getString(R.string.ok),
|
||||
getString(R.string.read_help_link),
|
||||
{ },
|
||||
{ Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) },
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter<PreferenceViewHolder>
|
||||
{
|
||||
return object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
val preference = getItem(position)
|
||||
val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame)
|
||||
iconFrame?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme pref
|
||||
*/
|
||||
private fun prepareTheme() {
|
||||
themeListPreference?.setOnPreferenceChangeListener { _, _ ->
|
||||
requireActivity().recreate()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and Show language selection dialog box
|
||||
* Uses previously saved language if there is any, if not uses phone locale as initial language.
|
||||
* Disable default/already selected language from dialog box
|
||||
* Get ListPreference key and act accordingly for each ListPreference.
|
||||
* saves value chosen by user to shared preferences
|
||||
* to remember later and recall MainActivity to reflect language changes
|
||||
* @param keyListPreference
|
||||
*/
|
||||
private fun prepareAppLanguages(keyListPreference: String) {
|
||||
// Gets current language code from shared preferences
|
||||
val languageCode = getCurrentLanguageCode(keyListPreference)
|
||||
val recentLanguages = recentLanguagesDao.getRecentLanguages()
|
||||
val selectedLanguages = hashMapOf<Int, String>()
|
||||
|
||||
if (keyListPreference == "appUiDefaultLanguagePref") {
|
||||
if (languageCode.isNullOrEmpty()) {
|
||||
selectedLanguages[0] = Locale.getDefault().language
|
||||
} else {
|
||||
selectedLanguages[0] = languageCode
|
||||
}
|
||||
} else if (keyListPreference == "descriptionDefaultLanguagePref") {
|
||||
if (languageCode.isNullOrEmpty()) {
|
||||
selectedLanguages[0] = Locale.getDefault().language
|
||||
} else {
|
||||
selectedLanguages[0] = languageCode
|
||||
}
|
||||
}
|
||||
|
||||
val languagesAdapter = LanguagesAdapter(requireActivity(), selectedLanguages)
|
||||
|
||||
val dialog = Dialog(requireActivity())
|
||||
dialog.setContentView(R.layout.dialog_select_language)
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
dialog.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.90).toInt(),
|
||||
(resources.displayMetrics.heightPixels * 0.90).toInt()
|
||||
)
|
||||
dialog.show()
|
||||
|
||||
val editText: EditText = dialog.findViewById(R.id.search_language)
|
||||
val listView: ListView = dialog.findViewById(R.id.language_list)
|
||||
languageHistoryListView = dialog.findViewById(R.id.language_history_list)
|
||||
recentLanguagesTextView = dialog.findViewById(R.id.recent_searches)
|
||||
separator = dialog.findViewById(R.id.separator)
|
||||
|
||||
setUpRecentLanguagesSection(recentLanguages, selectedLanguages)
|
||||
|
||||
listView.adapter = languagesAdapter
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
|
||||
hideRecentLanguagesSection()
|
||||
}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
|
||||
languagesAdapter.filter.filter(charSequence)
|
||||
}
|
||||
|
||||
override fun afterTextChanged(editable: Editable?) {}
|
||||
})
|
||||
|
||||
languageHistoryListView?.setOnItemClickListener { adapterView, _, position, _ ->
|
||||
onRecentLanguageClicked(keyListPreference, dialog, adapterView, position)
|
||||
}
|
||||
|
||||
listView.setOnItemClickListener { adapterView, _, position, _ ->
|
||||
val lCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(position)
|
||||
val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(position)
|
||||
val isExists = recentLanguagesDao.findRecentLanguage(lCode)
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(lCode)
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(Language(languageName, lCode))
|
||||
saveLanguageValue(lCode, keyListPreference)
|
||||
val defLocale = createLocale(lCode)
|
||||
if (keyListPreference == "appUiDefaultLanguagePref") {
|
||||
appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||
setLocale(requireActivity(), lCode)
|
||||
requireActivity().recreate()
|
||||
val intent = Intent(requireActivity(), MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener { languagesAdapter.filter.filter("") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up recent languages section
|
||||
*
|
||||
* @param recentLanguages recently used languages
|
||||
* @param selectedLanguages selected languages
|
||||
*/
|
||||
private fun setUpRecentLanguagesSection(
|
||||
recentLanguages: List<Language>,
|
||||
selectedLanguages: HashMap<Int, String>
|
||||
) {
|
||||
if (recentLanguages.isEmpty()) {
|
||||
languageHistoryListView?.visibility = View.GONE
|
||||
recentLanguagesTextView?.visibility = View.GONE
|
||||
separator?.visibility = View.GONE
|
||||
} else {
|
||||
if (recentLanguages.size > 5) {
|
||||
for (i in recentLanguages.size - 1 downTo 5) {
|
||||
recentLanguagesDao.deleteRecentLanguage(recentLanguages[i].languageCode)
|
||||
}
|
||||
}
|
||||
languageHistoryListView?.visibility = View.VISIBLE
|
||||
recentLanguagesTextView?.visibility = View.VISIBLE
|
||||
separator?.visibility = View.VISIBLE
|
||||
val recentLanguagesAdapter = RecentLanguagesAdapter(
|
||||
requireActivity(),
|
||||
recentLanguagesDao.getRecentLanguages(),
|
||||
selectedLanguages
|
||||
)
|
||||
languageHistoryListView?.adapter = recentLanguagesAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click event for recent language section
|
||||
*/
|
||||
private fun onRecentLanguageClicked(
|
||||
keyListPreference: String,
|
||||
dialog: Dialog,
|
||||
adapterView: AdapterView<*>,
|
||||
position: Int
|
||||
) {
|
||||
val recentLanguageCode = (adapterView.adapter as RecentLanguagesAdapter).getLanguageCode(position)
|
||||
val recentLanguageName = (adapterView.adapter as RecentLanguagesAdapter).getLanguageName(position)
|
||||
val isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode)
|
||||
if (isExists) {
|
||||
recentLanguagesDao.deleteRecentLanguage(recentLanguageCode)
|
||||
}
|
||||
recentLanguagesDao.addRecentLanguage(Language(recentLanguageName, recentLanguageCode))
|
||||
saveLanguageValue(recentLanguageCode, keyListPreference)
|
||||
val defLocale = createLocale(recentLanguageCode)
|
||||
if (keyListPreference == "appUiDefaultLanguagePref") {
|
||||
appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||
setLocale(requireActivity(), recentLanguageCode)
|
||||
requireActivity().recreate()
|
||||
val intent = Intent(requireActivity(), MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the section of recent languages
|
||||
*/
|
||||
private fun hideRecentLanguagesSection() {
|
||||
languageHistoryListView?.visibility = View.GONE
|
||||
recentLanguagesTextView?.visibility = View.GONE
|
||||
separator?.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Changing the default app language with selected one and save it to SharedPreferences
|
||||
*/
|
||||
fun setLocale(activity: Activity, userSelectedValue: String) {
|
||||
var selectedLanguage = userSelectedValue
|
||||
if (selectedLanguage == "") {
|
||||
selectedLanguage = Locale.getDefault().language
|
||||
}
|
||||
val locale = createLocale(selectedLanguage)
|
||||
Locale.setDefault(locale)
|
||||
val configuration = Configuration()
|
||||
configuration.locale = locale
|
||||
activity.baseContext.resources.updateConfiguration(configuration, activity.baseContext.resources.displayMetrics)
|
||||
|
||||
val editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit()
|
||||
editor.putString("language", selectedLanguage)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create Locale based on different types of language codes
|
||||
* @param languageCode
|
||||
* @return Locale and throws error for invalid language codes
|
||||
*/
|
||||
fun createLocale(languageCode: String): Locale {
|
||||
val parts = languageCode.split("-")
|
||||
return when (parts.size) {
|
||||
1 -> Locale(parts[0])
|
||||
2 -> Locale(parts[0], parts[1])
|
||||
3 -> Locale(parts[0], parts[1], parts[2])
|
||||
else -> throw IllegalArgumentException("Invalid language code: $languageCode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save userSelected language in List Preference
|
||||
* @param userSelectedValue
|
||||
* @param preferenceKey
|
||||
*/
|
||||
private fun saveLanguageValue(userSelectedValue: String, preferenceKey: String) {
|
||||
when (preferenceKey) {
|
||||
"appUiDefaultLanguagePref" -> defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue)
|
||||
"descriptionDefaultLanguagePref" -> defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current language code from shared preferences
|
||||
* @param preferenceKey
|
||||
* @return
|
||||
*/
|
||||
private fun getCurrentLanguageCode(preferenceKey: String): String? {
|
||||
return when (preferenceKey) {
|
||||
"appUiDefaultLanguagePref" -> defaultKvStore.getString(
|
||||
Prefs.APP_UI_LANGUAGE, ""
|
||||
)
|
||||
"descriptionDefaultLanguagePref" -> defaultKvStore.getString(
|
||||
Prefs.DESCRIPTION_LANGUAGE, ""
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First checks for external storage permissions and then sends logs via email
|
||||
*/
|
||||
private fun checkPermissionsAndSendLogs() {
|
||||
if (
|
||||
PermissionUtils.hasPermission(
|
||||
requireActivity(),
|
||||
PermissionUtils.PERMISSIONS_STORAGE
|
||||
)
|
||||
) {
|
||||
commonsLogSender.send(requireActivity(), null)
|
||||
} else {
|
||||
requestExternalStoragePermissions()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests external storage permissions and shows a toast stating that log collection has
|
||||
* started
|
||||
*/
|
||||
private fun requestExternalStoragePermissions() {
|
||||
Dexter.withActivity(requireActivity())
|
||||
.withPermissions(*PermissionUtils.PERMISSIONS_STORAGE)
|
||||
.withListener(object : MultiplePermissionsListener {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
|
||||
ViewUtil.showLongToast(requireActivity(), getString(R.string.log_collection_started))
|
||||
}
|
||||
|
||||
override fun onPermissionRationaleShouldBeShown(
|
||||
permissions: List<PermissionRequest>, token: PermissionToken
|
||||
) {
|
||||
// No action needed
|
||||
}
|
||||
})
|
||||
.onSameThread()
|
||||
.check()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package fr.free.nrw.commons.theme;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.WindowManager;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
public JsonKvStore defaultKvStore;
|
||||
|
||||
@Inject
|
||||
SystemThemeUtils systemThemeUtils;
|
||||
|
||||
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
protected boolean wasPreviouslyDarkTheme;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
||||
setTheme(wasPreviouslyDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
||||
float fontScale = android.provider.Settings.System.getFloat(
|
||||
getBaseContext().getContentResolver(),
|
||||
android.provider.Settings.System.FONT_SCALE,
|
||||
1f);
|
||||
adjustFontScale(getResources().getConfiguration(), fontScale);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
// Restart activity if theme is changed
|
||||
if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) {
|
||||
recreate();
|
||||
}
|
||||
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply fontScale on device
|
||||
*/
|
||||
public void adjustFontScale(Configuration configuration, float scale) {
|
||||
configuration.fontScale = scale;
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getMetrics(metrics);
|
||||
metrics.scaledDensity = configuration.fontScale * metrics.density;
|
||||
getBaseContext().getResources().updateConfiguration(configuration, metrics);
|
||||
}
|
||||
|
||||
}
|
||||
65
app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt
Normal file
65
app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package fr.free.nrw.commons.theme
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.utils.SystemThemeUtils
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
|
||||
|
||||
abstract class BaseActivity : CommonsDaggerAppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
@field:Named("default_preferences")
|
||||
lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@Inject
|
||||
lateinit var systemThemeUtils: SystemThemeUtils
|
||||
|
||||
protected val compositeDisposable = CompositeDisposable()
|
||||
protected var wasPreviouslyDarkTheme: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode()
|
||||
setTheme(if (wasPreviouslyDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme)
|
||||
|
||||
val fontScale = android.provider.Settings.System.getFloat(
|
||||
baseContext.contentResolver,
|
||||
android.provider.Settings.System.FONT_SCALE,
|
||||
1f
|
||||
)
|
||||
adjustFontScale(resources.configuration, fontScale)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
// Restart activity if theme is changed
|
||||
if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) {
|
||||
recreate()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply fontScale on device
|
||||
*/
|
||||
fun adjustFontScale(configuration: Configuration, scale: Float) {
|
||||
configuration.fontScale = scale
|
||||
val metrics = resources.displayMetrics
|
||||
val wm = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
wm.defaultDisplay.getMetrics(metrics)
|
||||
metrics.scaledDensity = configuration.fontScale * metrics.density
|
||||
baseContext.resources.updateConfiguration(configuration, metrics)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package fr.free.nrw.commons.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Build.VERSION;
|
||||
import android.util.AttributeSet;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
public class PasteSensitiveTextInputEditText extends TextInputEditText {
|
||||
|
||||
private boolean formattingAllowed = true;
|
||||
|
||||
public PasteSensitiveTextInputEditText(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public PasteSensitiveTextInputEditText(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
formattingAllowed = extractFormattingAttribute(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTextContextMenuItem(int id) {
|
||||
|
||||
// if not paste command, or formatting is allowed, return default
|
||||
if(id != android.R.id.paste || formattingAllowed){
|
||||
return super.onTextContextMenuItem(id);
|
||||
}
|
||||
|
||||
// if its paste and formatting not allowed
|
||||
boolean proceeded;
|
||||
if(VERSION.SDK_INT >= 23) {
|
||||
proceeded = super.onTextContextMenuItem(android.R.id.pasteAsPlainText);
|
||||
}else {
|
||||
proceeded = super.onTextContextMenuItem(id);
|
||||
if (proceeded && getText() != null) {
|
||||
// rewrite with plain text so formatting is lost
|
||||
setText(getText().toString());
|
||||
setSelection(getText().length());
|
||||
}
|
||||
}
|
||||
return proceeded;
|
||||
}
|
||||
|
||||
private boolean extractFormattingAttribute(Context context, AttributeSet attrs){
|
||||
|
||||
boolean formatAllowed = true;
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(
|
||||
attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0);
|
||||
|
||||
try {
|
||||
formatAllowed = a.getBoolean(
|
||||
R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
return formatAllowed;
|
||||
}
|
||||
|
||||
public void setFormattingAllowed(boolean formattingAllowed){
|
||||
this.formattingAllowed = formattingAllowed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package fr.free.nrw.commons.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
|
||||
class PasteSensitiveTextInputEditText @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : TextInputEditText(context, attrs) {
|
||||
|
||||
private var formattingAllowed: Boolean = true
|
||||
|
||||
init {
|
||||
if (attrs != null) {
|
||||
formattingAllowed = extractFormattingAttribute(context, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextContextMenuItem(id: Int): Boolean {
|
||||
// if not paste command, or formatting is allowed, return default
|
||||
if (id != android.R.id.paste || formattingAllowed) {
|
||||
return super.onTextContextMenuItem(id)
|
||||
}
|
||||
|
||||
// if it's paste and formatting not allowed
|
||||
val proceeded: Boolean = if (VERSION.SDK_INT >= 23) {
|
||||
super.onTextContextMenuItem(android.R.id.pasteAsPlainText)
|
||||
} else {
|
||||
val success = super.onTextContextMenuItem(id)
|
||||
if (success && text != null) {
|
||||
// rewrite with plain text so formatting is lost
|
||||
setText(text.toString())
|
||||
setSelection(text?.length ?: 0)
|
||||
}
|
||||
success
|
||||
}
|
||||
return proceeded
|
||||
}
|
||||
|
||||
private fun extractFormattingAttribute(context: Context, attrs: AttributeSet): Boolean {
|
||||
val a = context.theme.obtainStyledAttributes(
|
||||
attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0
|
||||
)
|
||||
return try {
|
||||
a.getBoolean(R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true)
|
||||
} finally {
|
||||
a.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun setFormattingAllowed(formattingAllowed: Boolean) {
|
||||
this.formattingAllowed = formattingAllowed
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import fr.free.nrw.commons.utils.StringUtil;
|
||||
|
||||
/**
|
||||
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
|
||||
* links clickable.
|
||||
*/
|
||||
public class HtmlTextView extends AppCompatTextView {
|
||||
|
||||
/**
|
||||
* Constructs a new instance of HtmlTextView
|
||||
* @param context the context of the view
|
||||
* @param attrs the set of attributes for the view
|
||||
*/
|
||||
public HtmlTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setMovementMethod(LinkMovementMethod.getInstance());
|
||||
setText(StringUtil.fromHtml(getText().toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed
|
||||
* @param newText the text to be displayed
|
||||
*/
|
||||
public void setHtmlText(String newText) {
|
||||
setText(StringUtil.fromHtml(newText));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package fr.free.nrw.commons.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
|
||||
import fr.free.nrw.commons.utils.StringUtil
|
||||
|
||||
/**
|
||||
* An [AppCompatTextView] which formats the text to HTML displayable text and makes any
|
||||
* links clickable.
|
||||
*/
|
||||
class HtmlTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AppCompatTextView(context, attrs) {
|
||||
|
||||
init {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = StringUtil.fromHtml(text.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed
|
||||
* @param newText the text to be displayed
|
||||
*/
|
||||
fun setHtmlText(newText: String) {
|
||||
text = StringUtil.fromHtml(newText)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
/**
|
||||
* a formatted dialog fragment
|
||||
* This class is used by NearbyInfoDialog
|
||||
*/
|
||||
public abstract class OverlayDialog extends DialogFragment {
|
||||
|
||||
/**
|
||||
* creates a DialogFragment with the correct style and theme
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the view is created, sets the dialog layout to full screen
|
||||
*
|
||||
* @param view the view being used
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
setDialogLayoutToFullScreen();
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the dialog layout to fullscreen
|
||||
*/
|
||||
private void setDialogLayoutToFullScreen() {
|
||||
Window window = getDialog().getWindow();
|
||||
WindowManager.LayoutParams wlp = window.getAttributes();
|
||||
window.requestFeature(Window.FEATURE_NO_TITLE);
|
||||
wlp.gravity = Gravity.BOTTOM;
|
||||
wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||
wlp.height = WindowManager.LayoutParams.MATCH_PARENT;
|
||||
window.setAttributes(wlp);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds custom dialog container
|
||||
*
|
||||
* @param savedInstanceState the previously saved state
|
||||
* @return the dialog
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
Window window = dialog.getWindow();
|
||||
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package fr.free.nrw.commons.ui.widget
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
/**
|
||||
* A formatted dialog fragment
|
||||
* This class is used by NearbyInfoDialog
|
||||
*/
|
||||
abstract class OverlayDialog : DialogFragment() {
|
||||
|
||||
/**
|
||||
* Creates a DialogFragment with the correct style and theme
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light)
|
||||
}
|
||||
|
||||
/**
|
||||
* When the view is created, sets the dialog layout to full screen
|
||||
*
|
||||
* @param view the view being used
|
||||
* @param savedInstanceState bundle re-constructed from a previous saved state
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setDialogLayoutToFullScreen()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dialog layout to fullscreen
|
||||
*/
|
||||
private fun setDialogLayoutToFullScreen() {
|
||||
val window = dialog?.window ?: return
|
||||
val wlp = window.attributes
|
||||
window.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
wlp.gravity = Gravity.BOTTOM
|
||||
wlp.width = WindowManager.LayoutParams.MATCH_PARENT
|
||||
wlp.height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
window.attributes = wlp
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds custom dialog container
|
||||
*
|
||||
* @param savedInstanceState the previously saved state
|
||||
* @return the dialog
|
||||
*/
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
||||
import static fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE;
|
||||
import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction;
|
||||
import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
|
||||
|
|
@ -32,7 +32,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
|
|
@ -277,7 +276,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
|||
|
||||
public void checkStoragePermissions() {
|
||||
// Check if all required permissions are granted
|
||||
final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE);
|
||||
final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE());
|
||||
final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this);
|
||||
if (hasAllPermissions || hasPartialAccess) {
|
||||
// All required permissions are granted, so enable UI elements and perform actions
|
||||
|
|
@ -297,7 +296,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
|||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale_for_image_share,
|
||||
PERMISSIONS_STORAGE);
|
||||
getPERMISSIONS_STORAGE());
|
||||
}
|
||||
}
|
||||
/* If all permissions are not granted and a dialog is already showing on screen
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import android.widget.ListView;
|
|||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class CategoriesPresenter
|
|||
.doOnNext {
|
||||
view.showProgress(true)
|
||||
}.switchMap(::searchResults)
|
||||
.map { repository.selectedCategories + it }
|
||||
.map { repository.getSelectedCategories() + it }
|
||||
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(
|
||||
|
|
@ -89,7 +89,7 @@ class CategoriesPresenter
|
|||
private fun searchResults(term: String): Observable<List<CategoryItem>>? {
|
||||
if (media == null) {
|
||||
return repository
|
||||
.searchAll(term, getImageTitleList(), repository.selectedDepictions)
|
||||
.searchAll(term, getImageTitleList(), repository.getSelectedDepictions())
|
||||
.subscribeOn(ioScheduler)
|
||||
.map {
|
||||
it.filter { categoryItem ->
|
||||
|
|
@ -101,13 +101,13 @@ class CategoriesPresenter
|
|||
return Observable
|
||||
.zip(
|
||||
repository
|
||||
.getCategories(repository.selectedExistingCategories)
|
||||
.getCategories(repository.getSelectedExistingCategories())
|
||||
.map { list ->
|
||||
list.map {
|
||||
CategoryItem(it.name, it.description, it.thumbnail, true)
|
||||
}
|
||||
},
|
||||
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions),
|
||||
repository.searchAll(term, getImageTitleList(), repository.getSelectedDepictions()),
|
||||
) { it1, it2 ->
|
||||
it1 + it2
|
||||
}.subscribeOn(ioScheduler)
|
||||
|
|
@ -138,7 +138,7 @@ class CategoriesPresenter
|
|||
* @return
|
||||
*/
|
||||
private fun getImageTitleList(): List<String> =
|
||||
repository.uploads
|
||||
repository.getUploads()
|
||||
.map { it.uploadMediaDetails[0].captionText }
|
||||
.filterNot { TextUtils.isEmpty(it) }
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ class CategoriesPresenter
|
|||
* Verifies the number of categories selected, prompts the user if none selected
|
||||
*/
|
||||
override fun verifyCategories() {
|
||||
val selectedCategories = repository.selectedCategories
|
||||
val selectedCategories = repository.getSelectedCategories()
|
||||
if (selectedCategories.isNotEmpty()) {
|
||||
repository.setSelectedCategories(selectedCategories.map { it.name })
|
||||
view.goToNextScreen()
|
||||
|
|
@ -173,14 +173,14 @@ class CategoriesPresenter
|
|||
) {
|
||||
this.view = view
|
||||
this.media = media
|
||||
repository.selectedExistingCategories = view.existingCategories
|
||||
repository.setSelectedExistingCategories(view.existingCategories)
|
||||
compositeDisposable.add(
|
||||
searchTerms
|
||||
.observeOn(mainThreadScheduler)
|
||||
.doOnNext {
|
||||
view.showProgress(true)
|
||||
}.switchMap(::searchResults)
|
||||
.map { repository.selectedCategories + it }
|
||||
.map { repository.getSelectedCategories() + it }
|
||||
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(
|
||||
|
|
@ -218,13 +218,21 @@ class CategoriesPresenter
|
|||
wikiText: String,
|
||||
) {
|
||||
// check if view.existingCategories is null
|
||||
if (repository.selectedCategories.isNotEmpty() ||
|
||||
(view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size)
|
||||
if (
|
||||
repository.getSelectedCategories().isNotEmpty()
|
||||
||
|
||||
(
|
||||
view.existingCategories != null
|
||||
&&
|
||||
repository.getSelectedExistingCategories().size
|
||||
!=
|
||||
view.existingCategories.size
|
||||
)
|
||||
) {
|
||||
val selectedCategories: MutableList<String> =
|
||||
(
|
||||
repository.selectedCategories.map { it.name }.toMutableList() +
|
||||
repository.selectedExistingCategories
|
||||
repository.getSelectedCategories().map { it.name }.toMutableList() +
|
||||
repository.getSelectedExistingCategories()
|
||||
).toMutableList()
|
||||
|
||||
if (selectedCategories.isNotEmpty()) {
|
||||
|
|
@ -305,7 +313,7 @@ class CategoriesPresenter
|
|||
|
||||
override fun selectCategories() {
|
||||
compositeDisposable.add(
|
||||
repository.placeCategories
|
||||
repository.getPlaceCategories()
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(::selectNewCategories),
|
||||
|
|
|
|||
|
|
@ -93,14 +93,14 @@ class DepictsPresenter
|
|||
return repository
|
||||
.searchAllEntities(querystring)
|
||||
.subscribeOn(ioScheduler)
|
||||
.map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() }
|
||||
.map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() }
|
||||
.map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
|
||||
.map { it.distinctBy(DepictedItem::id) }
|
||||
} else {
|
||||
return Flowable
|
||||
.zip(
|
||||
repository
|
||||
.getDepictions(repository.selectedExistingDepictions)
|
||||
.getDepictions(repository.getSelectedExistingDepictions())
|
||||
.map { list ->
|
||||
list.map {
|
||||
DepictedItem(
|
||||
|
|
@ -118,7 +118,7 @@ class DepictsPresenter
|
|||
) { it1, it2 ->
|
||||
it1 + it2
|
||||
}.subscribeOn(ioScheduler)
|
||||
.map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() }
|
||||
.map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() }
|
||||
.map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
|
||||
.map { it.distinctBy(DepictedItem::id) }
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ class DepictsPresenter
|
|||
*/
|
||||
override fun selectPlaceDepictions() {
|
||||
compositeDisposable.add(
|
||||
repository.placeDepictions
|
||||
repository.getPlaceDepictions()
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(::selectNewDepictions),
|
||||
|
|
@ -188,10 +188,10 @@ class DepictsPresenter
|
|||
* from the depiction list
|
||||
*/
|
||||
override fun verifyDepictions() {
|
||||
if (repository.selectedDepictions.isNotEmpty()) {
|
||||
if (repository.getSelectedDepictions().isNotEmpty()) {
|
||||
if (::depictsDao.isInitialized) {
|
||||
// save all the selected Depicted item in room Database
|
||||
depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions)
|
||||
depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions())
|
||||
}
|
||||
view.goToNextScreen()
|
||||
} else {
|
||||
|
|
@ -205,20 +205,20 @@ class DepictsPresenter
|
|||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
override fun updateDepictions(media: Media) {
|
||||
if (repository.selectedDepictions.isNotEmpty() ||
|
||||
repository.selectedExistingDepictions.size != view.existingDepictions.size
|
||||
if (repository.getSelectedDepictions().isNotEmpty() ||
|
||||
repository.getSelectedExistingDepictions().size != view.existingDepictions.size
|
||||
) {
|
||||
view.showProgressDialog()
|
||||
val selectedDepictions: MutableList<String> =
|
||||
(
|
||||
repository.selectedDepictions.map { it.id }.toMutableList() +
|
||||
repository.selectedExistingDepictions
|
||||
repository.getSelectedDepictions().map { it.id }.toMutableList() +
|
||||
repository.getSelectedExistingDepictions()
|
||||
).toMutableList()
|
||||
|
||||
if (selectedDepictions.isNotEmpty()) {
|
||||
if (::depictsDao.isInitialized) {
|
||||
// save all the selected Depicted item in room Database
|
||||
depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions)
|
||||
depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions())
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
|
|
@ -254,7 +254,7 @@ class DepictsPresenter
|
|||
) {
|
||||
this.view = view
|
||||
this.media = media
|
||||
repository.selectedExistingDepictions = view.existingDepictions
|
||||
repository.setSelectedExistingDepictions(view.existingDepictions)
|
||||
compositeDisposable.add(
|
||||
searchTerm
|
||||
.observeOn(mainThreadScheduler)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
|
|
@ -45,6 +44,7 @@ import fr.free.nrw.commons.upload.UploadBaseFragment;
|
|||
import fr.free.nrw.commons.upload.UploadItem;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
|
||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
|
|
@ -208,7 +208,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
|
||||
try {
|
||||
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) {
|
||||
startActivityWithFlags(
|
||||
ActivityUtils.startActivityWithFlags(
|
||||
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class DepictModel
|
|||
for (place in places) {
|
||||
place.wikiDataEntityId?.let { qids.add(it) }
|
||||
}
|
||||
repository.uploads.forEach { item ->
|
||||
repository.getUploads().forEach { item ->
|
||||
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
||||
Coordinates2Country
|
||||
.countryQID(
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import java.util.Random
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -548,33 +549,30 @@ class UploadWorker(
|
|||
}
|
||||
|
||||
private fun findUniqueFileName(fileName: String): String {
|
||||
var sequenceFileName: String?
|
||||
var sequenceNumber = 1
|
||||
while (true) {
|
||||
var sequenceFileName: String? = fileName
|
||||
val random = Random()
|
||||
|
||||
// Loops until sequenceFileName does not match any existing file names
|
||||
while (mediaClient
|
||||
.checkPageExistsUsingTitle(
|
||||
String.format(
|
||||
"File:%s",
|
||||
sequenceFileName,
|
||||
),
|
||||
).blockingGet()) {
|
||||
|
||||
// Generate a random 5-character alphanumeric string
|
||||
val randomHash = (random.nextInt(90000) + 10000).toString()
|
||||
|
||||
sequenceFileName =
|
||||
if (sequenceNumber == 1) {
|
||||
fileName
|
||||
if (fileName.indexOf('.') == -1) {
|
||||
"$fileName #$randomHash"
|
||||
} else {
|
||||
if (fileName.indexOf('.') == -1) {
|
||||
"$fileName $sequenceNumber"
|
||||
} else {
|
||||
val regex =
|
||||
Pattern.compile("^(.*)(\\..+?)$")
|
||||
val regexMatcher = regex.matcher(fileName)
|
||||
regexMatcher.replaceAll("$1 $sequenceNumber$2")
|
||||
}
|
||||
val regex =
|
||||
Pattern.compile("^(.*)(\\..+?)$")
|
||||
val regexMatcher = regex.matcher(fileName)
|
||||
regexMatcher.replaceAll("$1 #$randomHash")
|
||||
}
|
||||
if (!mediaClient
|
||||
.checkPageExistsUsingTitle(
|
||||
String.format(
|
||||
"File:%s",
|
||||
sequenceFileName,
|
||||
),
|
||||
).blockingGet()
|
||||
) {
|
||||
break
|
||||
}
|
||||
sequenceNumber++
|
||||
}
|
||||
return sequenceFileName!!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class AbstractTextWatcher implements TextWatcher {
|
||||
private final TextChange textChange;
|
||||
|
||||
public AbstractTextWatcher(@NonNull TextChange textChange) {
|
||||
this.textChange = textChange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
textChange.onTextChanged(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
|
||||
public interface TextChange {
|
||||
void onTextChanged(String value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
||||
class AbstractTextWatcher(
|
||||
private val textChange: TextChange
|
||||
) : TextWatcher {
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
textChange.onTextChanged(s.toString())
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
interface TextChange {
|
||||
fun onTextChanged(value: String)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class ActivityUtils {
|
||||
|
||||
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
|
||||
Intent intent = new Intent(context, cls);
|
||||
for (int flag : flags) {
|
||||
intent.addFlags(flag);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt
Normal file
16
app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
object ActivityUtils {
|
||||
|
||||
@JvmStatic
|
||||
fun <T> startActivityWithFlags(context: Context, cls: Class<T>, vararg flags: Int) {
|
||||
val intent = Intent(context, cls)
|
||||
for (flag in flags) {
|
||||
intent.addFlags(flag)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Provides util functions for formatting date time
|
||||
* Most of our formatting needs are addressed by the data library's DateUtil class
|
||||
* Methods should be added here only if DateUtil class doesn't provide for it already
|
||||
*/
|
||||
public class CommonsDateUtil {
|
||||
|
||||
/**
|
||||
* Gets SimpleDateFormat for short date pattern
|
||||
* @return simpledateformat
|
||||
*/
|
||||
public static SimpleDateFormat getIso8601DateFormatShort() {
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT);
|
||||
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return simpleDateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets SimpleDateFormat for date pattern returned by Media object
|
||||
* @return simpledateformat
|
||||
*/
|
||||
public static SimpleDateFormat getMediaSimpleDateFormat() {
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT);
|
||||
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return simpleDateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp pattern for a date
|
||||
* @return timestamp
|
||||
*/
|
||||
public static SimpleDateFormat getIso8601DateFormatTimestamp() {
|
||||
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
Locale.ROOT);
|
||||
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return simpleDateFormat;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Provides util functions for formatting date time.
|
||||
* Most of our formatting needs are addressed by the data library's DateUtil class.
|
||||
* Methods should be added here only if DateUtil class doesn't provide for it already.
|
||||
*/
|
||||
object CommonsDateUtil {
|
||||
|
||||
/**
|
||||
* Gets SimpleDateFormat for short date pattern.
|
||||
* @return simpleDateFormat
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getIso8601DateFormatShort(): SimpleDateFormat {
|
||||
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return simpleDateFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets SimpleDateFormat for date pattern returned by Media object.
|
||||
* @return simpleDateFormat
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getMediaSimpleDateFormat(): SimpleDateFormat {
|
||||
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT)
|
||||
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return simpleDateFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp pattern for a date.
|
||||
* @return timestamp
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getIso8601DateFormatTimestamp(): SimpleDateFormat {
|
||||
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT)
|
||||
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return simpleDateFormat
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import static android.text.format.DateFormat.getBestDateTimePattern;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public final class DateUtil {
|
||||
private static Map<String, SimpleDateFormat> DATE_FORMATS = new HashMap<>();
|
||||
|
||||
// TODO: Switch to DateTimeFormatter when minSdk = 26.
|
||||
|
||||
public static synchronized String iso8601DateFormat(Date date) {
|
||||
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date);
|
||||
}
|
||||
|
||||
public static synchronized Date iso8601DateParse(String date) throws ParseException {
|
||||
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date);
|
||||
}
|
||||
|
||||
public static String getMonthOnlyDateString(@NonNull Date date) {
|
||||
return getDateStringWithSkeletonPattern(date, "MMMM d");
|
||||
}
|
||||
|
||||
public static String getExtraShortDateString(@NonNull Date date) {
|
||||
return getDateStringWithSkeletonPattern(date, "MMM d");
|
||||
}
|
||||
|
||||
public static synchronized String getDateStringWithSkeletonPattern(@NonNull Date date, @NonNull String pattern) {
|
||||
return getCachedDateFormat(getBestDateTimePattern(Locale.getDefault(), pattern), Locale.getDefault(), false).format(date);
|
||||
}
|
||||
|
||||
private static SimpleDateFormat getCachedDateFormat(String pattern, Locale locale, boolean utc) {
|
||||
if (!DATE_FORMATS.containsKey(pattern)) {
|
||||
SimpleDateFormat df = new SimpleDateFormat(pattern, locale);
|
||||
if (utc) {
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
DATE_FORMATS.put(pattern, df);
|
||||
}
|
||||
return DATE_FORMATS.get(pattern);
|
||||
}
|
||||
|
||||
private DateUtil() {
|
||||
}
|
||||
}
|
||||
62
app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt
Normal file
62
app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import android.text.format.DateFormat.getBestDateTimePattern
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.HashMap
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Utility class for date formatting and parsing.
|
||||
* TODO: Switch to DateTimeFormatter when minSdk = 26.
|
||||
*/
|
||||
object DateUtil {
|
||||
|
||||
private val DATE_FORMATS: MutableMap<String, SimpleDateFormat> = HashMap()
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun iso8601DateFormat(date: Date): String {
|
||||
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
@Throws(ParseException::class)
|
||||
fun iso8601DateParse(date: String): Date {
|
||||
return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getMonthOnlyDateString(date: Date): String {
|
||||
return getDateStringWithSkeletonPattern(date, "MMMM d")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getExtraShortDateString(date: Date): String {
|
||||
return getDateStringWithSkeletonPattern(date, "MMM d")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getDateStringWithSkeletonPattern(date: Date, pattern: String): String {
|
||||
return getCachedDateFormat(
|
||||
getBestDateTimePattern(Locale.getDefault(), pattern),
|
||||
Locale.getDefault(), false
|
||||
).format(date)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun getCachedDateFormat(pattern: String, locale: Locale, utc: Boolean): SimpleDateFormat {
|
||||
if (!DATE_FORMATS.containsKey(pattern)) {
|
||||
val df = SimpleDateFormat(pattern, locale)
|
||||
if (utc) {
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
DATE_FORMATS[pattern] = df
|
||||
}
|
||||
return DATE_FORMATS[pattern]!!
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import fr.free.nrw.commons.utils.model.ConnectionType;
|
||||
import fr.free.nrw.commons.utils.model.NetworkConnectionType;
|
||||
|
||||
import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR;
|
||||
import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_3G;
|
||||
import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_4G;
|
||||
import static fr.free.nrw.commons.utils.model.ConnectionType.NO_INTERNET;
|
||||
import static fr.free.nrw.commons.utils.model.ConnectionType.WIFI_NETWORK;
|
||||
import static fr.free.nrw.commons.utils.model.NetworkConnectionType.FOUR_G;
|
||||
import static fr.free.nrw.commons.utils.model.NetworkConnectionType.THREE_G;
|
||||
import static fr.free.nrw.commons.utils.model.NetworkConnectionType.TWO_G;
|
||||
import static fr.free.nrw.commons.utils.model.NetworkConnectionType.UNKNOWN;
|
||||
import static fr.free.nrw.commons.utils.model.NetworkConnectionType.WIFI;
|
||||
|
||||
/**
|
||||
* Util class to get any information about the user's device
|
||||
* Ensure that any sensitive information like IMEI is not fetched/shared without user's consent
|
||||
*/
|
||||
public class DeviceInfoUtil {
|
||||
private static final Map<NetworkConnectionType, ConnectionType> TYPE_MAPPING = new HashMap<>();
|
||||
|
||||
static {
|
||||
TYPE_MAPPING.put(TWO_G, CELLULAR);
|
||||
TYPE_MAPPING.put(THREE_G, CELLULAR_3G);
|
||||
TYPE_MAPPING.put(FOUR_G, CELLULAR_4G);
|
||||
TYPE_MAPPING.put(WIFI, WIFI_NETWORK);
|
||||
TYPE_MAPPING.put(UNKNOWN, CELLULAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network connection type
|
||||
* @param context
|
||||
* @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet
|
||||
*/
|
||||
public static ConnectionType getConnectionType(Context context) {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(context)) {
|
||||
return NO_INTERNET;
|
||||
}
|
||||
NetworkConnectionType networkType = NetworkUtils.getNetworkType(context);
|
||||
ConnectionType deviceNetworkType = TYPE_MAPPING.get(networkType);
|
||||
return deviceNetworkType == null ? CELLULAR : deviceNetworkType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device manufacturer
|
||||
* @return
|
||||
*/
|
||||
public static String getDeviceManufacturer() {
|
||||
return Build.MANUFACTURER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device model name
|
||||
* @return
|
||||
*/
|
||||
public static String getDeviceModel() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Android version. Eg. 4.4.2
|
||||
* @return
|
||||
*/
|
||||
public static String getAndroidVersion() {
|
||||
return Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API Level. Eg. 26
|
||||
* @return
|
||||
*/
|
||||
public static String getAPILevel() {
|
||||
return Build.VERSION.SDK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device.
|
||||
* @return
|
||||
*/
|
||||
public static String getDevice() {
|
||||
return Build.DEVICE;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import fr.free.nrw.commons.utils.model.ConnectionType
|
||||
import fr.free.nrw.commons.utils.model.NetworkConnectionType
|
||||
|
||||
/**
|
||||
* Util class to get any information about the user's device
|
||||
* Ensure that any sensitive information like IMEI is not fetched/shared without user's consent
|
||||
*/
|
||||
object DeviceInfoUtil {
|
||||
private val TYPE_MAPPING = mapOf(
|
||||
NetworkConnectionType.TWO_G to ConnectionType.CELLULAR,
|
||||
NetworkConnectionType.THREE_G to ConnectionType.CELLULAR_3G,
|
||||
NetworkConnectionType.FOUR_G to ConnectionType.CELLULAR_4G,
|
||||
NetworkConnectionType.WIFI to ConnectionType.WIFI_NETWORK,
|
||||
NetworkConnectionType.UNKNOWN to ConnectionType.CELLULAR
|
||||
)
|
||||
|
||||
/**
|
||||
* Get network connection type
|
||||
* @param context
|
||||
* @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getConnectionType(context: Context): ConnectionType {
|
||||
return if (!NetworkUtils.isInternetConnectionEstablished(context)) {
|
||||
ConnectionType.NO_INTERNET
|
||||
} else {
|
||||
val networkType = NetworkUtils.getNetworkType(context)
|
||||
TYPE_MAPPING[networkType] ?: ConnectionType.CELLULAR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device manufacturer
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDeviceManufacturer(): String {
|
||||
return Build.MANUFACTURER
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device model name
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDeviceModel(): String {
|
||||
return Build.MODEL
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Android version. Eg. 4.4.2
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getAndroidVersion(): String {
|
||||
return Build.VERSION.RELEASE
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API Level. Eg. 26
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getAPILevel(): String {
|
||||
return Build.VERSION.SDK
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Device.
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDevice(): String {
|
||||
return Build.DEVICE
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue