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"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||||
|
|
||||||
//Mocking
|
//Mocking
|
||||||
testImplementation("io.mockk:mockk:1.13.4")
|
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
||||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
||||||
|
testImplementation("io.mockk:mockk:1.13.5")
|
||||||
|
|
||||||
// Unit testing
|
// Unit testing
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
|
@ -374,11 +374,11 @@ android {
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
buildToolsVersion buildToolsVersion
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ public class LocationPickerActivity extends BaseActivity implements
|
||||||
*/
|
*/
|
||||||
private void removeLocationFromImage() {
|
private void removeLocationFromImage() {
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
|
getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
|
||||||
, media, "0.0", "0.0", "0.0f")
|
, media, "0.0", "0.0", "0.0f")
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
@ -479,7 +479,7 @@ public class LocationPickerActivity extends BaseActivity implements
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
compositeDisposable.add(
|
getCompositeDisposable().add(
|
||||||
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
|
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
|
||||||
Latitude, Longitude, Accuracy)
|
Latitude, Longitude, Accuracy)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,15 @@ import androidx.annotation.Nullable;
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
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.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
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.SwipableCardView;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.campaigns;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||||
|
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -14,7 +15,6 @@ import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
|
||||||
import io.reactivex.Scheduler;
|
import io.reactivex.Scheduler;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.SingleObserver;
|
import io.reactivex.SingleObserver;
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,9 @@ class CategoryClient
|
||||||
}.map {
|
}.map {
|
||||||
it
|
it
|
||||||
.filter { page ->
|
.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 {
|
}.map {
|
||||||
CategoryItem(
|
CategoryItem(
|
||||||
it.title().replace(CATEGORY_PREFIX, ""),
|
it.title().replace(CATEGORY_PREFIX, ""),
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ public class ContributionController {
|
||||||
},
|
},
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale,
|
R.string.write_storage_permission_rationale,
|
||||||
PermissionUtils.PERMISSIONS_STORAGE);
|
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -224,7 +224,7 @@ public class ContributionController {
|
||||||
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale,
|
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.contributions.Contribution.STATE_PAUSED;
|
||||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
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.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.computeBearing;
|
||||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||||
|
|
||||||
|
|
@ -23,12 +22,10 @@ import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.MenuItem.OnMenuItemClickListener;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
|
@ -39,7 +36,6 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||||
|
|
@ -293,7 +289,7 @@ public class ContributionsFragment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
notification.setOnClickListener(view -> {
|
notification.setOnClickListener(view -> {
|
||||||
NotificationActivity.startYourself(getContext(), "unread");
|
NotificationActivity.Companion.startYourself(getContext(), "unread");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
@ -16,10 +13,8 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import fr.free.nrw.commons.databinding.MainBinding;
|
import fr.free.nrw.commons.databinding.MainBinding;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
import fr.free.nrw.commons.WelcomeActivity;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
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.quiz.QuizChecker;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
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.UploadProgressActivity;
|
||||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||||
|
|
@ -420,7 +414,7 @@ public class MainActivity extends BaseActivity
|
||||||
return true;
|
return true;
|
||||||
case R.id.notifications:
|
case R.id.notifications:
|
||||||
// Starts notification activity on click to notification icon
|
// Starts notification activity on click to notification icon
|
||||||
NotificationActivity.startYourself(this, "unread");
|
NotificationActivity.Companion.startYourself(this, "unread");
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
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.view.Window
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
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.NotForUploadStatus
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
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.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
@ -136,10 +152,23 @@ class CustomSelectorActivity :
|
||||||
|
|
||||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
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)
|
onFullScreenDataReceived(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
* 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.
|
* Show Custom Selector Welcome Dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -445,10 +483,97 @@ class CustomSelectorActivity :
|
||||||
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
||||||
limitError.visibility = View.INVISIBLE
|
limitError.visibility = View.INVISIBLE
|
||||||
limitError.setOnClickListener { displayUploadLimitWarning() }
|
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(
|
override fun onFolderClick(
|
||||||
folderId: Long,
|
folderId: Long,
|
||||||
|
|
@ -466,6 +591,11 @@ class CustomSelectorActivity :
|
||||||
bucketId = folderId
|
bucketId = folderId
|
||||||
bucketName = folderName
|
bucketName = folderName
|
||||||
isImageFragmentOpen = true
|
isImageFragmentOpen = true
|
||||||
|
|
||||||
|
//show the overflow menu only when a folder is clicked
|
||||||
|
showOverflowMenu = true
|
||||||
|
setUpToolbar()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -581,6 +711,10 @@ class CustomSelectorActivity :
|
||||||
isImageFragmentOpen = false
|
isImageFragmentOpen = false
|
||||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
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
|
package fr.free.nrw.commons.customselector.ui.selector
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -346,7 +347,7 @@ class ImageFragment :
|
||||||
context
|
context
|
||||||
.getSharedPreferences(
|
.getSharedPreferences(
|
||||||
"CustomSelector",
|
"CustomSelector",
|
||||||
BaseActivity.MODE_PRIVATE,
|
MODE_PRIVATE,
|
||||||
)?.let { prefs ->
|
)?.let { prefs ->
|
||||||
prefs.edit()?.let { editor ->
|
prefs.edit()?.let { editor ->
|
||||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
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.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
@ -65,7 +66,7 @@ class ImageLoader
|
||||||
/**
|
/**
|
||||||
* Coroutine Scope.
|
* Coroutine Scope.
|
||||||
*/
|
*/
|
||||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
private val scope: CoroutineScope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query image and setUp the view.
|
* Query image and setUp the view.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package fr.free.nrw.commons.delete;
|
package fr.free.nrw.commons.delete;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
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.auth.csrf.InvalidLoginTokenException;
|
||||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||||
import fr.free.nrw.commons.review.ReviewController;
|
import fr.free.nrw.commons.review.ReviewController;
|
||||||
|
import fr.free.nrw.commons.utils.LangCodeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.delete;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import fr.free.nrw.commons.utils.DateUtil;
|
import fr.free.nrw.commons.utils.DateUtil;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
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.explore.categories.CategoriesModule;
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ public class SearchActivity extends BaseActivity
|
||||||
|
|
||||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||||
viewPagerAdapter.notifyDataSetChanged();
|
viewPagerAdapter.notifyDataSetChanged();
|
||||||
compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox)
|
getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox)
|
||||||
.takeUntil(RxView.detaches(binding.searchBox))
|
.takeUntil(RxView.detaches(binding.searchBox))
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
@ -284,7 +284,7 @@ public class SearchActivity extends BaseActivity
|
||||||
@Override protected void onDestroy() {
|
@Override protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
//Dispose the disposables when the activity is destroyed
|
//Dispose the disposables when the activity is destroyed
|
||||||
compositeDisposable.dispose();
|
getCompositeDisposable().dispose();
|
||||||
binding = null;
|
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.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
|
||||||
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.Manifest.permission;
|
import android.Manifest.permission;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
|
@ -21,22 +19,17 @@ import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.Settings;
|
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.MapController;
|
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.di.CommonsDaggerSupportFragment;
|
||||||
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
import fr.free.nrw.commons.explore.ExploreMapRootFragment;
|
||||||
import fr.free.nrw.commons.explore.paging.LiveDataConverter;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
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.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.MapUtils;
|
import fr.free.nrw.commons.utils.MapUtils;
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
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.SystemThemeUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
|
@ -310,7 +301,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startMapWithoutPermission() {
|
private void startMapWithoutPermission() {
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
|
|
@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
|
||||||
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
|
||||||
isPermissionDenied = true;
|
isPermissionDenied = true;
|
||||||
}
|
}
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude()));
|
||||||
presenter.onMapReady(exploreMapController);
|
presenter.onMapReady(exploreMapController);
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,4 @@ public interface Constants {
|
||||||
String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos";
|
String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos";
|
||||||
String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images";
|
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.actions.ThanksClient;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
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.auth.csrf.InvalidLoginTokenException;
|
||||||
import fr.free.nrw.commons.category.CategoryClient;
|
import fr.free.nrw.commons.category.CategoryClient;
|
||||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||||
|
|
@ -272,6 +271,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
|
|
||||||
if (!sessionManager.isUserLoggedIn()) {
|
if (!sessionManager.isUserLoggedIn()) {
|
||||||
binding.categoryEditButton.setVisibility(GONE);
|
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")){
|
if(applicationKvStore.getBoolean("login_skipped")){
|
||||||
|
|
@ -313,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void launchZoomActivity(final View view) {
|
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) {
|
if (hasPermission) {
|
||||||
launchZoomActivityAfterPermissionCheck(view);
|
launchZoomActivityAfterPermissionCheck(view);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -323,7 +328,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
},
|
},
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.read_storage_permission_rationale,
|
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.progressBarEdit.setVisibility(GONE);
|
||||||
binding.descriptionEdit.setVisibility(VISIBLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -678,7 +682,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
// Stick in a filler element.
|
// Stick in a filler element.
|
||||||
allCategories.add(getString(R.string.detail_panel_cats_none));
|
allCategories.add(getString(R.string.detail_panel_cats_none));
|
||||||
}
|
}
|
||||||
binding.categoryEditButton.setVisibility(VISIBLE);
|
if(sessionManager.isUserLoggedIn()) {
|
||||||
|
binding.categoryEditButton.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
rebuildCatList(allCategories);
|
rebuildCatList(allCategories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ public class OkHttpJsonApiClient {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
Timber.d("Fetching nearby items at radius %s", radius);
|
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;
|
final String wikidataQuery;
|
||||||
if (customQuery != null) {
|
if (customQuery != null) {
|
||||||
wikidataQuery = customQuery;
|
wikidataQuery = customQuery;
|
||||||
|
|
@ -344,7 +344,7 @@ public class OkHttpJsonApiClient {
|
||||||
final boolean shouldQueryForMonuments, final String customQuery)
|
final boolean shouldQueryForMonuments, final String customQuery)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null));
|
Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null));
|
||||||
|
|
||||||
final String wikidataQuery;
|
final String wikidataQuery;
|
||||||
if (customQuery != null) {
|
if (customQuery != null) {
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onPeerReviewClicked() {
|
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.ViewGroup.LayoutParams;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.activity.result.ActivityResultCallback;
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
|
@ -701,7 +700,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
= new LatLng(Double.parseDouble(locationLatLng[0]),
|
= new LatLng(Double.parseDouble(locationLatLng[0]),
|
||||||
Double.parseDouble(locationLatLng[1]), 1f);
|
Double.parseDouble(locationLatLng[1]), 1f);
|
||||||
} else {
|
} else {
|
||||||
lastKnownLocation = MapUtils.defaultLatLng;
|
lastKnownLocation = MapUtils.getDefaultLatLng();
|
||||||
}
|
}
|
||||||
if (binding.map != null) {
|
if (binding.map != null) {
|
||||||
moveCameraToPosition(
|
moveCameraToPosition(
|
||||||
|
|
@ -793,7 +792,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
hideBottomSheet();
|
hideBottomSheet();
|
||||||
binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener(
|
binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener(
|
||||||
(v, hasFocus) -> {
|
(v, hasFocus) -> {
|
||||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25,
|
LayoutUtils.setLayoutHeightAlignedToWidth(1.25,
|
||||||
binding.nearbyFilterList.getRoot());
|
binding.nearbyFilterList.getRoot());
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE);
|
binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
|
@ -834,7 +833,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(),
|
.getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(),
|
||||||
0.75);
|
0.75);
|
||||||
binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter);
|
binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter);
|
||||||
LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot());
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView)
|
RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView)
|
||||||
.takeUntil(RxView.detaches(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 static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.view.View;
|
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import fr.free.nrw.commons.BaseMarker;
|
import fr.free.nrw.commons.BaseMarker;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
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.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
import fr.free.nrw.commons.location.LatLng;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
|
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.Label;
|
||||||
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
|
||||||
import fr.free.nrw.commons.nearby.NearbyController;
|
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.Place;
|
||||||
import fr.free.nrw.commons.nearby.PlaceDao;
|
|
||||||
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
|
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.utils.LocationUtils;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import timber.log.Timber;
|
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.notification.models.Notification
|
||||||
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
||||||
|
|
||||||
internal class NotificatinAdapter(
|
internal class NotificationAdapter(
|
||||||
onNotificationClicked: (Notification) -> Unit,
|
onNotificationClicked: (Notification) -> Unit,
|
||||||
) : BaseDelegateAdapter<Notification>(
|
) : BaseDelegateAdapter<Notification>(
|
||||||
notificationDelegate(onNotificationClicked),
|
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
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
compositeDisposable.clear();
|
getCompositeDisposable().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,45 @@
|
||||||
package fr.free.nrw.commons.profile.achievements
|
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.
|
* Used to calculate the percentages of images that haven't been reverted.
|
||||||
* @return The count of unique images used.
|
* Returns 100 if imagesUploaded is 0 to avoid division by zero.
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
val notRevertPercentage: Int
|
val notRevertPercentage: Int
|
||||||
get() =
|
get() = if (imagesUploaded > 0) {
|
||||||
try {
|
(imagesUploaded - revertCount) * 100 / imagesUploaded
|
||||||
(imagesUploaded - revertCount) * 100 / imagesUploaded
|
} else {
|
||||||
} catch (divideByZero: ArithmeticException) {
|
100
|
||||||
100
|
}
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Get Achievements object from FeedbackResponse
|
* Get Achievements object from FeedbackResponse.
|
||||||
*
|
*
|
||||||
* @param response
|
* @param response The feedback response to convert.
|
||||||
* @return
|
* @return An Achievements object with values from the response.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun from(response: FeedbackResponse): Achievements =
|
fun from(response: FeedbackResponse): Achievements = Achievements(
|
||||||
Achievements(
|
uniqueUsedImages = response.uniqueUsedImages,
|
||||||
response.uniqueUsedImages,
|
articlesUsingImages = response.articlesUsingImages,
|
||||||
response.articlesUsingImages,
|
thanksReceived = response.thanksReceived,
|
||||||
response.thanksReceived,
|
featuredImages = response.featuredImages.featuredPicturesOnWikimediaCommons,
|
||||||
response.featuredImages.featuredPicturesOnWikimediaCommons,
|
qualityImages = response.featuredImages.qualityImages,
|
||||||
response.featuredImages.qualityImages,
|
imagesUploaded = 0, // Assuming imagesUploaded should be 0
|
||||||
0,
|
revertCount = response.deletedUploads
|
||||||
response.deletedUploads,
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +105,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
// Used for the setting the size of imageView at runtime
|
// Used for the setting the size of imageView at runtime
|
||||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams)
|
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams)
|
||||||
binding.achievementBadgeImage.getLayoutParams();
|
binding.achievementBadgeImage.getLayoutParams();
|
||||||
params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO);
|
params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO);
|
||||||
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
|
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
|
||||||
binding.achievementBadgeImage.requestLayout();
|
binding.achievementBadgeImage.requestLayout();
|
||||||
|
|
@ -186,37 +186,37 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
try{
|
try{
|
||||||
|
|
||||||
compositeDisposable.add(okHttpJsonApiClient
|
compositeDisposable.add(okHttpJsonApiClient
|
||||||
.getAchievements(Objects.requireNonNull(userName))
|
.getAchievements(Objects.requireNonNull(userName))
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
response -> {
|
response -> {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
setUploadCount(Achievements.from(response));
|
setUploadCount(Achievements.from(response));
|
||||||
} else {
|
} else {
|
||||||
Timber.d("success");
|
Timber.d("success");
|
||||||
binding.layoutImageReverts.setVisibility(View.INVISIBLE);
|
binding.layoutImageReverts.setVisibility(View.INVISIBLE);
|
||||||
binding.achievementBadgeImage.setVisibility(View.INVISIBLE);
|
binding.achievementBadgeImage.setVisibility(View.INVISIBLE);
|
||||||
// If the number of edits made by the user are more than 150,000
|
// 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
|
// in some cases such high number of wiki edit counts cause the
|
||||||
// achievements calculator to fail in some cases, for more details
|
// achievements calculator to fail in some cases, for more details
|
||||||
// refer Issue: #3295
|
// refer Issue: #3295
|
||||||
if (numberOfEdits <= 150000) {
|
if (numberOfEdits <= 150000) {
|
||||||
showSnackBarWithRetry(false);
|
showSnackBarWithRetry(false);
|
||||||
} else {
|
} else {
|
||||||
showSnackBarWithRetry(true);
|
showSnackBarWithRetry(true);
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
t -> {
|
|
||||||
Timber.e(t, "Fetching achievements statistics failed");
|
|
||||||
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){
|
catch (Exception e){
|
||||||
Timber.d(e+"success");
|
Timber.d(e+"success");
|
||||||
|
|
@ -233,15 +233,15 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
compositeDisposable.add(okHttpJsonApiClient
|
compositeDisposable.add(okHttpJsonApiClient
|
||||||
.getWikidataEdits(userName)
|
.getWikidataEdits(userName)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(edits -> {
|
.subscribe(edits -> {
|
||||||
numberOfEdits = edits;
|
numberOfEdits = edits;
|
||||||
binding.wikidataEdits.setText(String.valueOf(edits));
|
binding.wikidataEdits.setText(String.valueOf(edits));
|
||||||
}, e -> {
|
}, e -> {
|
||||||
Timber.e("Error:" + e);
|
Timber.e("Error:" + e);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -255,11 +255,11 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
if (tooManyAchievements) {
|
if (tooManyAchievements) {
|
||||||
binding.progressBar.setVisibility(View.GONE);
|
binding.progressBar.setVisibility(View.GONE);
|
||||||
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
|
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 {
|
} else {
|
||||||
binding.progressBar.setVisibility(View.GONE);
|
binding.progressBar.setVisibility(View.GONE);
|
||||||
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
|
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) {
|
private void setUploadCount(Achievements achievements) {
|
||||||
if (checkAccount()) {
|
if (checkAccount()) {
|
||||||
compositeDisposable.add(okHttpJsonApiClient
|
compositeDisposable.add(okHttpJsonApiClient
|
||||||
.getUploadCount(Objects.requireNonNull(userName))
|
.getUploadCount(Objects.requireNonNull(userName))
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
uploadCount -> setAchievementsUploadCount(achievements, uploadCount),
|
uploadCount -> setAchievementsUploadCount(achievements, uploadCount),
|
||||||
t -> {
|
t -> {
|
||||||
Timber.e(t, "Fetching upload count failed");
|
Timber.e(t, "Fetching upload count failed");
|
||||||
onError();
|
onError();
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,8 +295,18 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
* @param uploadCount
|
* @param uploadCount
|
||||||
*/
|
*/
|
||||||
private void setAchievementsUploadCount(Achievements achievements, int uploadCount) {
|
private void setAchievementsUploadCount(Achievements achievements, int uploadCount) {
|
||||||
achievements.setImagesUploaded(uploadCount);
|
// Create a new instance of Achievements with updated imagesUploaded
|
||||||
hideProgressBar(achievements);
|
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 {
|
}else {
|
||||||
binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE);
|
binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE);
|
||||||
binding.imagesUploadedProgressbar.setProgress
|
binding.imagesUploadedProgressbar.setProgress
|
||||||
(100*uploadCount/levelInfo.getMaxUploadCount());
|
(100*uploadCount/levelInfo.getMaxUploadCount());
|
||||||
binding.tvUploadedImages.setText
|
binding.tvUploadedImages.setText
|
||||||
(uploadCount + "/" + levelInfo.getMaxUploadCount());
|
(uploadCount + "/" + levelInfo.getMaxUploadCount());
|
||||||
}
|
}
|
||||||
|
|
@ -318,8 +328,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
|
|
||||||
private void setZeroAchievements() {
|
private void setZeroAchievements() {
|
||||||
String message = !Objects.equals(sessionManager.getUserName(), userName) ?
|
String message = !Objects.equals(sessionManager.getUserName(), userName) ?
|
||||||
getString(R.string.no_achievements_yet, userName) :
|
getString(R.string.no_achievements_yet, userName) :
|
||||||
getString(R.string.you_have_no_achievements_yet);
|
getString(R.string.you_have_no_achievements_yet);
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
DialogUtil.showAlertDialog(getActivity(),
|
||||||
null,
|
null,
|
||||||
message,
|
message,
|
||||||
|
|
@ -357,7 +367,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE);
|
// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE);
|
||||||
binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived()));
|
binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived()));
|
||||||
binding.imagesUsedByWikiProgressBar.setProgress
|
binding.imagesUsedByWikiProgressBar.setProgress
|
||||||
(100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages());
|
(100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages());
|
||||||
binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/"
|
binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/"
|
||||||
+ levelInfo.getMaxUniqueImages());
|
+ levelInfo.getMaxUniqueImages());
|
||||||
binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages()));
|
binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages()));
|
||||||
|
|
@ -366,7 +376,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
levelUpInfoString += " " + levelInfo.getLevelNumber();
|
levelUpInfoString += " " + levelInfo.getLevelNumber();
|
||||||
binding.achievementLevel.setText(levelUpInfoString);
|
binding.achievementLevel.setText(levelUpInfoString);
|
||||||
binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge,
|
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()));
|
binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber()));
|
||||||
BasicKvStore store = new BasicKvStore(this.getContext(), userName);
|
BasicKvStore store = new BasicKvStore(this.getContext(), userName);
|
||||||
store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber()));
|
store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber()));
|
||||||
|
|
@ -378,8 +388,8 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
private void hideProgressBar(Achievements achievements) {
|
private void hideProgressBar(Achievements achievements) {
|
||||||
if (binding.progressBar != null) {
|
if (binding.progressBar != null) {
|
||||||
levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),
|
levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),
|
||||||
achievements.getUniqueUsedImages(),
|
achievements.getUniqueUsedImages(),
|
||||||
achievements.getNotRevertPercentage());
|
achievements.getNotRevertPercentage());
|
||||||
inflateAchievements(achievements);
|
inflateAchievements(achievements);
|
||||||
setUploadProgress(achievements.getImagesUploaded());
|
setUploadProgress(achievements.getImagesUploaded());
|
||||||
setImageRevertPercentage(achievements.getNotRevertPercentage());
|
setImageRevertPercentage(achievements.getNotRevertPercentage());
|
||||||
|
|
@ -479,4 +489,4 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName
|
||||||
* Represents Featured Images on WikiMedia Commons platform
|
* Represents Featured Images on WikiMedia Commons platform
|
||||||
* Used by Achievements and FeedbackResponse (objects) of the user
|
* Used by Achievements and FeedbackResponse (objects) of the user
|
||||||
*/
|
*/
|
||||||
class FeaturedImages(
|
data class FeaturedImages(
|
||||||
@field:SerializedName("Quality_images") val qualityImages: Int,
|
@field:SerializedName("Quality_images") val qualityImages: Int,
|
||||||
@field:SerializedName("Featured_pictures_on_Wikimedia_Commons") val featuredPicturesOnWikimediaCommons: 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.Dao
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query;
|
import androidx.room.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dao interface for reviewed images database
|
* Dao interface for reviewed images database
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
public interface ReviewDao {
|
interface ReviewDao {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts reviewed/skipped image identifier into the database
|
* Inserts reviewed/skipped image identifier into the database
|
||||||
|
|
@ -17,7 +17,7 @@ public interface ReviewDao {
|
||||||
* @param reviewEntity
|
* @param reviewEntity
|
||||||
*/
|
*/
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
void insert(ReviewEntity reviewEntity);
|
fun insert(reviewEntity: ReviewEntity)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the image has already been reviewed/skipped by the user
|
* Checks if the image has already been reviewed/skipped by the user
|
||||||
|
|
@ -26,7 +26,6 @@ public interface ReviewDao {
|
||||||
* @param imageId
|
* @param imageId
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Query( "SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
|
@Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))")
|
||||||
Boolean isReviewedAlready(String 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
|
* @param image
|
||||||
* @return
|
* @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
|
* Gets the first revision of the file from filename
|
||||||
|
|
@ -132,7 +132,7 @@ class ReviewHelper
|
||||||
*/
|
*/
|
||||||
fun addViewedImagesToDB(imageId: String?) {
|
fun addViewedImagesToDB(imageId: String?) {
|
||||||
Completable
|
Completable
|
||||||
.fromAction { dao!!.insert(ReviewEntity(imageId)) }
|
.fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.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;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
|
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.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.PLACE_OBJECT;
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
|
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.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import androidx.work.ExistingWorkPolicy;
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
|
@ -277,7 +276,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
|
|
||||||
public void checkStoragePermissions() {
|
public void checkStoragePermissions() {
|
||||||
// Check if all required permissions are granted
|
// 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);
|
final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this);
|
||||||
if (hasAllPermissions || hasPartialAccess) {
|
if (hasAllPermissions || hasPartialAccess) {
|
||||||
// All required permissions are granted, so enable UI elements and perform actions
|
// 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.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale_for_image_share,
|
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
|
/* 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 android.widget.TextView;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class CategoriesPresenter
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
view.showProgress(true)
|
view.showProgress(true)
|
||||||
}.switchMap(::searchResults)
|
}.switchMap(::searchResults)
|
||||||
.map { repository.selectedCategories + it }
|
.map { repository.getSelectedCategories() + it }
|
||||||
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
@ -89,7 +89,7 @@ class CategoriesPresenter
|
||||||
private fun searchResults(term: String): Observable<List<CategoryItem>>? {
|
private fun searchResults(term: String): Observable<List<CategoryItem>>? {
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
return repository
|
return repository
|
||||||
.searchAll(term, getImageTitleList(), repository.selectedDepictions)
|
.searchAll(term, getImageTitleList(), repository.getSelectedDepictions())
|
||||||
.subscribeOn(ioScheduler)
|
.subscribeOn(ioScheduler)
|
||||||
.map {
|
.map {
|
||||||
it.filter { categoryItem ->
|
it.filter { categoryItem ->
|
||||||
|
|
@ -101,13 +101,13 @@ class CategoriesPresenter
|
||||||
return Observable
|
return Observable
|
||||||
.zip(
|
.zip(
|
||||||
repository
|
repository
|
||||||
.getCategories(repository.selectedExistingCategories)
|
.getCategories(repository.getSelectedExistingCategories())
|
||||||
.map { list ->
|
.map { list ->
|
||||||
list.map {
|
list.map {
|
||||||
CategoryItem(it.name, it.description, it.thumbnail, true)
|
CategoryItem(it.name, it.description, it.thumbnail, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions),
|
repository.searchAll(term, getImageTitleList(), repository.getSelectedDepictions()),
|
||||||
) { it1, it2 ->
|
) { it1, it2 ->
|
||||||
it1 + it2
|
it1 + it2
|
||||||
}.subscribeOn(ioScheduler)
|
}.subscribeOn(ioScheduler)
|
||||||
|
|
@ -138,7 +138,7 @@ class CategoriesPresenter
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private fun getImageTitleList(): List<String> =
|
private fun getImageTitleList(): List<String> =
|
||||||
repository.uploads
|
repository.getUploads()
|
||||||
.map { it.uploadMediaDetails[0].captionText }
|
.map { it.uploadMediaDetails[0].captionText }
|
||||||
.filterNot { TextUtils.isEmpty(it) }
|
.filterNot { TextUtils.isEmpty(it) }
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ class CategoriesPresenter
|
||||||
* Verifies the number of categories selected, prompts the user if none selected
|
* Verifies the number of categories selected, prompts the user if none selected
|
||||||
*/
|
*/
|
||||||
override fun verifyCategories() {
|
override fun verifyCategories() {
|
||||||
val selectedCategories = repository.selectedCategories
|
val selectedCategories = repository.getSelectedCategories()
|
||||||
if (selectedCategories.isNotEmpty()) {
|
if (selectedCategories.isNotEmpty()) {
|
||||||
repository.setSelectedCategories(selectedCategories.map { it.name })
|
repository.setSelectedCategories(selectedCategories.map { it.name })
|
||||||
view.goToNextScreen()
|
view.goToNextScreen()
|
||||||
|
|
@ -173,14 +173,14 @@ class CategoriesPresenter
|
||||||
) {
|
) {
|
||||||
this.view = view
|
this.view = view
|
||||||
this.media = media
|
this.media = media
|
||||||
repository.selectedExistingCategories = view.existingCategories
|
repository.setSelectedExistingCategories(view.existingCategories)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
searchTerms
|
searchTerms
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
view.showProgress(true)
|
view.showProgress(true)
|
||||||
}.switchMap(::searchResults)
|
}.switchMap(::searchResults)
|
||||||
.map { repository.selectedCategories + it }
|
.map { repository.getSelectedCategories() + it }
|
||||||
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
.map { it.distinctBy { categoryItem -> categoryItem.name } }
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
@ -218,13 +218,21 @@ class CategoriesPresenter
|
||||||
wikiText: String,
|
wikiText: String,
|
||||||
) {
|
) {
|
||||||
// check if view.existingCategories is null
|
// check if view.existingCategories is null
|
||||||
if (repository.selectedCategories.isNotEmpty() ||
|
if (
|
||||||
(view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size)
|
repository.getSelectedCategories().isNotEmpty()
|
||||||
|
||
|
||||||
|
(
|
||||||
|
view.existingCategories != null
|
||||||
|
&&
|
||||||
|
repository.getSelectedExistingCategories().size
|
||||||
|
!=
|
||||||
|
view.existingCategories.size
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val selectedCategories: MutableList<String> =
|
val selectedCategories: MutableList<String> =
|
||||||
(
|
(
|
||||||
repository.selectedCategories.map { it.name }.toMutableList() +
|
repository.getSelectedCategories().map { it.name }.toMutableList() +
|
||||||
repository.selectedExistingCategories
|
repository.getSelectedExistingCategories()
|
||||||
).toMutableList()
|
).toMutableList()
|
||||||
|
|
||||||
if (selectedCategories.isNotEmpty()) {
|
if (selectedCategories.isNotEmpty()) {
|
||||||
|
|
@ -305,7 +313,7 @@ class CategoriesPresenter
|
||||||
|
|
||||||
override fun selectCategories() {
|
override fun selectCategories() {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository.placeCategories
|
repository.getPlaceCategories()
|
||||||
.subscribeOn(ioScheduler)
|
.subscribeOn(ioScheduler)
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
.subscribe(::selectNewCategories),
|
.subscribe(::selectNewCategories),
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,14 @@ class DepictsPresenter
|
||||||
return repository
|
return repository
|
||||||
.searchAllEntities(querystring)
|
.searchAllEntities(querystring)
|
||||||
.subscribeOn(ioScheduler)
|
.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.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
|
||||||
.map { it.distinctBy(DepictedItem::id) }
|
.map { it.distinctBy(DepictedItem::id) }
|
||||||
} else {
|
} else {
|
||||||
return Flowable
|
return Flowable
|
||||||
.zip(
|
.zip(
|
||||||
repository
|
repository
|
||||||
.getDepictions(repository.selectedExistingDepictions)
|
.getDepictions(repository.getSelectedExistingDepictions())
|
||||||
.map { list ->
|
.map { list ->
|
||||||
list.map {
|
list.map {
|
||||||
DepictedItem(
|
DepictedItem(
|
||||||
|
|
@ -118,7 +118,7 @@ class DepictsPresenter
|
||||||
) { it1, it2 ->
|
) { it1, it2 ->
|
||||||
it1 + it2
|
it1 + it2
|
||||||
}.subscribeOn(ioScheduler)
|
}.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.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
|
||||||
.map { it.distinctBy(DepictedItem::id) }
|
.map { it.distinctBy(DepictedItem::id) }
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ class DepictsPresenter
|
||||||
*/
|
*/
|
||||||
override fun selectPlaceDepictions() {
|
override fun selectPlaceDepictions() {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository.placeDepictions
|
repository.getPlaceDepictions()
|
||||||
.subscribeOn(ioScheduler)
|
.subscribeOn(ioScheduler)
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
.subscribe(::selectNewDepictions),
|
.subscribe(::selectNewDepictions),
|
||||||
|
|
@ -188,10 +188,10 @@ class DepictsPresenter
|
||||||
* from the depiction list
|
* from the depiction list
|
||||||
*/
|
*/
|
||||||
override fun verifyDepictions() {
|
override fun verifyDepictions() {
|
||||||
if (repository.selectedDepictions.isNotEmpty()) {
|
if (repository.getSelectedDepictions().isNotEmpty()) {
|
||||||
if (::depictsDao.isInitialized) {
|
if (::depictsDao.isInitialized) {
|
||||||
// save all the selected Depicted item in room Database
|
// save all the selected Depicted item in room Database
|
||||||
depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions)
|
depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions())
|
||||||
}
|
}
|
||||||
view.goToNextScreen()
|
view.goToNextScreen()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -205,20 +205,20 @@ class DepictsPresenter
|
||||||
*/
|
*/
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
override fun updateDepictions(media: Media) {
|
override fun updateDepictions(media: Media) {
|
||||||
if (repository.selectedDepictions.isNotEmpty() ||
|
if (repository.getSelectedDepictions().isNotEmpty() ||
|
||||||
repository.selectedExistingDepictions.size != view.existingDepictions.size
|
repository.getSelectedExistingDepictions().size != view.existingDepictions.size
|
||||||
) {
|
) {
|
||||||
view.showProgressDialog()
|
view.showProgressDialog()
|
||||||
val selectedDepictions: MutableList<String> =
|
val selectedDepictions: MutableList<String> =
|
||||||
(
|
(
|
||||||
repository.selectedDepictions.map { it.id }.toMutableList() +
|
repository.getSelectedDepictions().map { it.id }.toMutableList() +
|
||||||
repository.selectedExistingDepictions
|
repository.getSelectedExistingDepictions()
|
||||||
).toMutableList()
|
).toMutableList()
|
||||||
|
|
||||||
if (selectedDepictions.isNotEmpty()) {
|
if (selectedDepictions.isNotEmpty()) {
|
||||||
if (::depictsDao.isInitialized) {
|
if (::depictsDao.isInitialized) {
|
||||||
// save all the selected Depicted item in room Database
|
// save all the selected Depicted item in room Database
|
||||||
depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions)
|
depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions())
|
||||||
}
|
}
|
||||||
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
|
|
@ -254,7 +254,7 @@ class DepictsPresenter
|
||||||
) {
|
) {
|
||||||
this.view = view
|
this.view = view
|
||||||
this.media = media
|
this.media = media
|
||||||
repository.selectedExistingDepictions = view.existingDepictions
|
repository.setSelectedExistingDepictions(view.existingDepictions)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
searchTerm
|
searchTerm
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons.upload.mediaDetails;
|
package fr.free.nrw.commons.upload.mediaDetails;
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
import static fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
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.UploadItem;
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
|
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.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||||
|
|
@ -208,7 +208,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) {
|
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) {
|
||||||
startActivityWithFlags(
|
ActivityUtils.startActivityWithFlags(
|
||||||
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class DepictModel
|
||||||
for (place in places) {
|
for (place in places) {
|
||||||
place.wikiDataEntityId?.let { qids.add(it) }
|
place.wikiDataEntityId?.let { qids.add(it) }
|
||||||
}
|
}
|
||||||
repository.uploads.forEach { item ->
|
repository.getUploads().forEach { item ->
|
||||||
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
||||||
Coordinates2Country
|
Coordinates2Country
|
||||||
.countryQID(
|
.countryQID(
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Random
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -548,33 +549,30 @@ class UploadWorker(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findUniqueFileName(fileName: String): String {
|
private fun findUniqueFileName(fileName: String): String {
|
||||||
var sequenceFileName: String?
|
var sequenceFileName: String? = fileName
|
||||||
var sequenceNumber = 1
|
val random = Random()
|
||||||
while (true) {
|
|
||||||
|
// 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 =
|
sequenceFileName =
|
||||||
if (sequenceNumber == 1) {
|
if (fileName.indexOf('.') == -1) {
|
||||||
fileName
|
"$fileName #$randomHash"
|
||||||
} else {
|
} else {
|
||||||
if (fileName.indexOf('.') == -1) {
|
val regex =
|
||||||
"$fileName $sequenceNumber"
|
Pattern.compile("^(.*)(\\..+?)$")
|
||||||
} else {
|
val regexMatcher = regex.matcher(fileName)
|
||||||
val regex =
|
regexMatcher.replaceAll("$1 #$randomHash")
|
||||||
Pattern.compile("^(.*)(\\..+?)$")
|
|
||||||
val regexMatcher = regex.matcher(fileName)
|
|
||||||
regexMatcher.replaceAll("$1 $sequenceNumber$2")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!mediaClient
|
|
||||||
.checkPageExistsUsingTitle(
|
|
||||||
String.format(
|
|
||||||
"File:%s",
|
|
||||||
sequenceFileName,
|
|
||||||
),
|
|
||||||
).blockingGet()
|
|
||||||
) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sequenceNumber++
|
|
||||||
}
|
}
|
||||||
return sequenceFileName!!
|
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