From a81d48cc9d0dcff6beb0cd67241237ebfb6fd9aa Mon Sep 17 00:00:00 2001 From: Nicolas Raoul Date: Thu, 22 Aug 2024 22:41:27 +0900 Subject: [PATCH 01/11] Updating jraska/livedata-testing for GSoC (#5785) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d477b32ab..44834951b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ dependencies { testImplementation 'androidx.test.ext:junit:1.1.5' testImplementation "androidx.test:rules:1.5.0" testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION" - testImplementation "com.jraska.livedata:testing-ktx:1.1.2" + testImplementation "com.jraska.livedata:testing-ktx:1.2.0" testImplementation "androidx.arch.core:core-testing:2.2.0" testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0" From 096c07554813a96ae5a5bd5d0cefc3677f6e59d3 Mon Sep 17 00:00:00 2001 From: ujjwal2900 <65774017+ujjwal2900@users.noreply.github.com> Date: Sun, 25 Aug 2024 09:31:55 +0530 Subject: [PATCH 02/11] Removed duplicate code in addMarkersToMap method (#5783) --- .../fragments/NearbyParentFragment.java | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 03ba4a48d..7b23ad680 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -1992,51 +1992,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment * locations. */ private void addMarkersToMap(List nearbyBaseMarkers) { - for (int i = 0; i < nearbyBaseMarkers.size(); i++) { - Drawable icon = ContextCompat.getDrawable(getContext(), - getIconFor(nearbyBaseMarkers.get(i).getPlace(), false)); - GeoPoint point = new GeoPoint( - nearbyBaseMarkers.get(i).getPlace().location.getLatitude(), - nearbyBaseMarkers.get(i).getPlace().location.getLongitude()); - Marker marker = new Marker(binding.map); - marker.setPosition(point); - marker.setIcon(icon); - Place place = nearbyBaseMarkers.get(i).getPlace(); - if (!Objects.equals(place.name, "")) { - marker.setTitle(place.name); - marker.setSnippet( - containsParentheses(place.getLongDescription()) - ? getTextBetweenParentheses( - place.getLongDescription()) : place.getLongDescription()); - } - marker.setTextLabelFontSize(40); - marker.setId(String.valueOf(i)); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_TOP); - marker.setOnMarkerClickListener((marker1, mapView) -> { - marker1.showInfoWindow(); - if (clickedMarker != null) { - clickedMarker.closeInfoWindow(); - } - clickedMarker = marker1; - int index = Integer.parseInt(marker1.getId()); - Place updatedPlace = nearbyBaseMarkers.get(index).getPlace(); - binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.VISIBLE); - binding.bottomSheetDetails.icon.setVisibility(View.GONE); - binding.bottomSheetDetails.wikiDataLl.setVisibility(View.GONE); - if (Objects.equals(updatedPlace.name, "")) { - getPlaceData(updatedPlace.getWikiDataEntityId(), updatedPlace, marker1, false); - } else { - marker.showInfoWindow(); - binding.bottomSheetDetails.dataCircularProgress.setVisibility(View.GONE); - binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE); - binding.bottomSheetDetails.wikiDataLl.setVisibility(View.VISIBLE); - passInfoToSheet(place); - hideBottomSheet(); - } - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - return true; - }); - binding.map.getOverlays().add(marker); + + for(int i = 0; i< nearbyBaseMarkers.size(); i++){ + addMarkerToMap(nearbyBaseMarkers.get(i).getPlace(), false); } } From ba573edfcd014ad418119e4d9d45755016738244 Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Fri, 23 Aug 2024 15:09:28 +0530 Subject: [PATCH 03/11] change the overridden method signature as per API 34 --- .../commons/customselector/helper/OnSwipeTouchListener.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt index f454a3af8..961d51158 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt @@ -40,14 +40,14 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { * Detects the gestures */ override fun onFling( - event1: MotionEvent, + event1: MotionEvent?, event2: MotionEvent, velocityX: Float, velocityY: Float ): Boolean { try { - val diffY: Float = event2.y - event1.y - val diffX: Float = event2.x - event1.x + val diffY: Float = event2.y - (event1?.y ?: event2.y) + val diffX: Float = event2.x - (event1?.x ?: event2.x) if (abs(diffX) > abs(diffY)) { if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { From 21b21846ac85d104290d627b605dc45c1f66e2a3 Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Fri, 23 Aug 2024 15:14:27 +0530 Subject: [PATCH 04/11] add version check condition to compare with API 23 before adding flag --- .../notification/NotificationHelper.java | 9 +- .../nrw/commons/widget/PicOfDayAppWidget.java | 110 +++++++++++------- 2 files changed, 71 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java index d4c08d7a5..1a882194a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -65,14 +65,11 @@ public class NotificationHelper { .setPriority(PRIORITY_HIGH); int flags = PendingIntent.FLAG_UPDATE_CURRENT; - - // Check if the API level is 31 or higher to modify the flag - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // For API level 31 or above, PendingIntent requires either FLAG_IMMUTABLE or FLAG_MUTABLE to be set - flags |= PendingIntent.FLAG_IMMUTABLE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23 } - PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags); notificationBuilder.setContentIntent(pendingIntent); notificationManager.notify(notificationId, notificationBuilder.build()); } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java index f33631784..d7a7d0541 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -41,17 +41,28 @@ import static android.content.Intent.ACTION_VIEW; */ public class PicOfDayAppWidget extends AppWidgetProvider { - private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); @Inject MediaClient mediaClient; - void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget); + void updateAppWidget( + final Context context, + final AppWidgetManager appWidgetManager, + final int appWidgetId + ) { + final RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.pic_of_day_app_widget); // Launch App on Button Click - Intent viewIntent = new Intent(context, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, PendingIntent.FLAG_IMMUTABLE); + final Intent viewIntent = new Intent(context, MainActivity.class); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + final PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, viewIntent, flags); + views.setOnClickPendingIntent(R.id.camera_button, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); @@ -60,61 +71,76 @@ public class PicOfDayAppWidget extends AppWidgetProvider { /** * Loads the picture of the day using media wiki API - * @param context - * @param views - * @param appWidgetManager - * @param appWidgetId + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId he ID of the App Widget to update. */ - private void loadPictureOfTheDay(Context context, - RemoteViews views, - AppWidgetManager appWidgetManager, - int appWidgetId) { + private void loadPictureOfTheDay( + final Context context, + final RemoteViews views, + final AppWidgetManager appWidgetManager, + final int appWidgetId + ) { compositeDisposable.add(mediaClient.getPictureOfTheDay() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - response -> { - if (response != null) { - views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle()); + response -> { + if (response != null) { + views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle()); - // View in browser - Intent viewIntent = new Intent(); - viewIntent.setAction(ACTION_VIEW); - viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri())); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, PendingIntent.FLAG_IMMUTABLE); - views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent); + // View in browser + final Intent viewIntent = new Intent(); + viewIntent.setAction(ACTION_VIEW); + viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri())); - loadImageFromUrl(response.getThumbUrl(), context, views, appWidgetManager, appWidgetId); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; } - }, - t -> Timber.e(t, "Fetching picture of the day failed") + final PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, viewIntent, flags); + + views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent); + loadImageFromUrl(response.getThumbUrl(), + context, views, appWidgetManager, appWidgetId); + } + }, + t -> Timber.e(t, "Fetching picture of the day failed") )); } /** * Uses Fresco to load an image from Url - * @param imageUrl - * @param context - * @param views - * @param appWidgetManager - * @param appWidgetId + * @param imageUrl The URL of the image to load. + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId he ID of the App Widget to update. */ - private void loadImageFromUrl(String imageUrl, - Context context, - RemoteViews views, - AppWidgetManager appWidgetManager, - int appWidgetId) { - ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build(); - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - DataSource> dataSource - = imagePipeline.fetchDecodedImage(request, context); + private void loadImageFromUrl( + final String imageUrl, + final Context context, + final RemoteViews views, + final AppWidgetManager appWidgetManager, + final int appWidgetId + ) { + final ImageRequest request = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(imageUrl)).build(); + final ImagePipeline imagePipeline = Fresco.getImagePipeline(); + final DataSource> dataSource = imagePipeline + .fetchDecodedImage(request, context); + dataSource.subscribe(new BaseBitmapDataSubscriber() { @Override - protected void onNewResultImpl(@Nullable Bitmap tempBitmap) { + protected void onNewResultImpl(@Nullable final Bitmap tempBitmap) { Bitmap bitmap = null; if (tempBitmap != null) { - bitmap = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); + bitmap = Bitmap.createBitmap( + tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888 + ); + final Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint()); } views.setImageViewBitmap(R.id.appwidget_image, bitmap); From ee55b5933c9e138a112da2b45d9961eb90d100e2 Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Fri, 23 Aug 2024 15:16:09 +0530 Subject: [PATCH 05/11] refactor: add final keywords, fix typo, and remove redundant spaces For optimized code only --- .../notification/NotificationHelper.java | 23 +++--- .../nrw/commons/upload/worker/UploadWorker.kt | 74 ++++++++++--------- .../nrw/commons/widget/PicOfDayAppWidget.java | 24 +++--- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java index 1a882194a..b63d3a4c1 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java @@ -4,16 +4,12 @@ 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; @@ -30,11 +26,11 @@ public class NotificationHelper { public static final int NOTIFICATION_EDIT_DESCRIPTION = 4; public static final int NOTIFICATION_EDIT_DEPICTIONS = 5; - private NotificationManager notificationManager; - private NotificationCompat.Builder notificationBuilder; + private final NotificationManager notificationManager; + private final NotificationCompat.Builder notificationBuilder; @Inject - public NotificationHelper(Context context) { + public NotificationHelper(final Context context) { notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationBuilder = new NotificationCompat .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) @@ -49,12 +45,13 @@ public class NotificationHelper { * @param notificationId the notificationID * @param intent the intent to be fired when the notification is clicked */ - public void showNotification(Context context, - String notificationTitle, - String notificationMessage, - int notificationId, - Intent intent) { - + 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() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 296ffe6a1..1c6402c7b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -48,13 +48,12 @@ import java.util.* import java.util.regex.Pattern import javax.inject.Inject - -class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : - CoroutineWorker(appContext, workerParams) { +class UploadWorker( + private var appContext: Context, workerParams: WorkerParameters +): CoroutineWorker(appContext, workerParams) { private var notificationManager: NotificationManagerCompat? = null - @Inject lateinit var wikidataEditService: WikidataEditService @@ -83,14 +82,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : //Attributes of the current-upload notification - private var currentNotificationID: Int = -1// lateinit is not allowed with primitives + private var currentNotificationID: Int = -1// late init is not allowed with primitives private lateinit var currentNotificationTag: String - private var curentNotification: NotificationCompat.Builder + private var currentNotification: NotificationCompat.Builder private val statesToProcess= ArrayList() - private val STASH_ERROR_CODES = Arrays - .asList( + private val STASH_ERROR_CODES = listOf( "uploadstash-file-not-found", "stashfailed", "verification-error", @@ -102,7 +100,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .getInstance(appContext) .commonsApplicationComponent .inject(this) - curentNotification = + currentNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! statesToProcess.add(Contribution.STATE_QUEUED) @@ -123,21 +121,23 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : fun onProgress(transferred: Long, total: Long) { if (transferred == total) { // Completed! - curentNotification.setContentTitle(notificationFinishingTitle) + currentNotification.setContentTitle(notificationFinishingTitle) .setProgress(0, 100, true) } else { - curentNotification + currentNotification .setProgress( 100, (transferred.toDouble() / total.toDouble() * 100).toInt(), false ) } - notificationManager?.cancel(PROCESSING_UPLOADS_NOTIFICATION_TAG, PROCESSING_UPLOADS_NOTIFICATION_ID) + notificationManager?.cancel( + PROCESSING_UPLOADS_NOTIFICATION_TAG, PROCESSING_UPLOADS_NOTIFICATION_ID + ) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build()!! + currentNotification.build() ) contribution!!.transferred = transferred contributionDao.update(contribution).blockingAwait() @@ -188,7 +188,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .blockingGet() //Showing initial notification for the number of uploads being processed - Timber.e("Queued Contributions: " + queuedContributions.size) + Timber.e("Queued Contributions: %s", queuedContributions.size) processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads)) processingUploads.setContentText( @@ -249,7 +249,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : PROCESSING_UPLOADS_NOTIFICATION_ID ) } - // Trigger WorkManager to process any new contributions that may have been added to the queue + //Trigger WorkManager to process any new contributions that may have been added to the queue val updatedContributionQueue = withContext(Dispatchers.IO) { contributionDao.getContribution(statesToProcess).blockingGet() } @@ -311,9 +311,9 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : currentNotificationID = (contribution.localUri.toString() + contribution.media.filename).hashCode() - curentNotification + currentNotification getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! - curentNotification.setContentTitle( + currentNotification.setContentTitle( appContext.getString( R.string.upload_progress_notification_title_start, displayTitle @@ -323,7 +323,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build()!! + currentNotification.build() ) val filename = media.filename @@ -341,15 +341,17 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : val stashUploadResult = uploadClient.uploadFileToStash( filename!!, contribution, notificationProgressUpdater ).onErrorReturn{ - return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message) + return@onErrorReturn StashUploadResult( + StashUploadState.FAILED,fileKey = null,errorMessage = it.message + ) }.blockingSingle() when (stashUploadResult.state) { StashUploadState.SUCCESS -> { //If the stash upload succeeds Timber.d("Upload to stash success for fileName: $filename") - Timber.d("Ensure uniqueness of filename"); - val uniqueFileName = findUniqueFileName(filename!!) + Timber.d("Ensure uniqueness of filename") + val uniqueFileName = findUniqueFileName(filename) try { //Upload the file from stash @@ -365,7 +367,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : ) wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution) - .blockingSubscribe(); + .blockingSubscribe() if(contribution.wikidataPlace==null){ Timber.d( "WikiDataEdit not required, upload success" @@ -404,12 +406,14 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : contributionDao.saveSynchronous(contribution) } else -> { - Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""") + Timber.e("Upload file to stash failed with status: ${stashUploadResult.state}") showInvalidLoginNotification(contribution) contribution.state = Contribution.STATE_FAILED contribution.chunkInfo = null contributionDao.saveSynchronous(contribution) - if (stashUploadResult.errorMessage.equals(CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE)) { + if (stashUploadResult.errorMessage.equals( + CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE) + ) { Timber.e("Invalid Login, logging out") val username = sessionManager.userName var logoutListener = CommonsApplication.BaseLogoutListener( @@ -499,7 +503,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : imageSha1 == modifiedSha1, true ) - ); + ) } } } @@ -543,7 +547,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : private fun showSuccessNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle contribution.state=Contribution.STATE_COMPLETED - curentNotification.setContentTitle( + currentNotification.setContentTitle( appContext.getString( R.string.upload_completed_notification_title, displayTitle @@ -554,7 +558,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -565,8 +569,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : @SuppressLint("StringFormatInvalid") private fun showFailedNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) - curentNotification.setContentTitle( + currentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) + currentNotification.setContentTitle( appContext.getString( R.string.upload_failed_notification_title, displayTitle @@ -577,13 +581,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @SuppressLint("StringFormatInvalid") private fun showInvalidLoginNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentTitle( + currentNotification.setContentTitle( appContext.getString( R.string.upload_failed_notification_title, displayTitle @@ -594,7 +598,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -604,7 +608,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : */ private fun showPausedNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentTitle( + currentNotification.setContentTitle( appContext.getString( R.string.upload_paused_notification_title, displayTitle @@ -615,7 +619,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager!!.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -634,6 +638,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : } else { getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) } - }; + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java index d7a7d0541..273452078 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -9,10 +9,9 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.net.Uri; +import android.os.Build; import android.widget.RemoteViews; - import androidx.annotation.Nullable; - import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.references.CloseableReference; import com.facebook.datasource.DataSource; @@ -22,10 +21,8 @@ import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; - import fr.free.nrw.commons.media.MediaClient; import javax.inject.Inject; - import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; @@ -148,32 +145,37 @@ public class PicOfDayAppWidget extends AppWidgetProvider { } @Override - protected void onFailureImpl(DataSource> dataSource) { + protected void onFailureImpl( + final DataSource> dataSource + ) { // Ignore failure for now. } }, CallerThreadExecutor.getInstance()); } @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + public void onUpdate( + final Context context, + final AppWidgetManager appWidgetManager, + final int[] appWidgetIds + ) { ApplicationlessInjection - .getInstance(context - .getApplicationContext()) + .getInstance(context.getApplicationContext()) .getCommonsApplicationComponent() .inject(this); // There may be multiple widgets active, so update all of them - for (int appWidgetId : appWidgetIds) { + for (final int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } @Override - public void onEnabled(Context context) { + public void onEnabled(final Context context) { // Enter relevant functionality for when the first widget is created } @Override - public void onDisabled(Context context) { + public void onDisabled(final Context context) { // Enter relevant functionality for when the last widget is disabled } } From 84719284e788fa52b1064f3b3a393e2a4985420e Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Sat, 24 Aug 2024 20:40:49 +0530 Subject: [PATCH 06/11] upgrade: migrate to SDK 34 and upgrade APG Additionally, add Jetpack Compose to the project --- app/build.gradle | 29 ++++++++++++++++++++---- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 44834951b..d769b618c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,22 @@ dependencies { implementation 'com.karumi:dexter:5.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + // Jetpack Compose + def composeBom = platform('androidx.compose:compose-bom:2024.08.00') + + implementation "androidx.appcompat:appcompat:1.7.0" + implementation "androidx.activity:activity-compose:1.9.1" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4" + implementation (composeBom) + implementation "androidx.compose.runtime:runtime" + implementation "androidx.compose.ui:ui" + implementation "androidx.compose.ui:ui-graphics" + implementation "androidx.compose.ui:ui-tooling" + implementation "androidx.compose.foundation:foundation" + implementation "androidx.compose.foundation:foundation-layout" + implementation "androidx.compose.material3:material3" + androidTestImplementation(composeBom) + implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION" implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION" @@ -186,7 +202,7 @@ project.gradle.taskGraph.whenReady { } android { - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { //applicationId 'fr.free.nrw.commons' @@ -196,7 +212,7 @@ android { setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' @@ -253,11 +269,12 @@ android { } } debug { - testCoverageEnabled true minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' testProguardFile 'test-proguard-rules.txt' versionNameSuffix "-debug-" + getBranchName() + enableUnitTestCoverage true + enableAndroidTestCoverage true } } @@ -354,13 +371,17 @@ android { targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } buildToolsVersion buildToolsVersion buildFeatures { viewBinding true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.3.2' } namespace 'fr.free.nrw.commons' lint { diff --git a/build.gradle b/build.gradle index 242b06429..ec3d4f617 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.5.0' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath 'org.codehaus.groovy:groovy-all:2.4.15' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d2e417be9..fb6a72053 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Apr 23 18:22:54 IST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file From 1995c0a86cf726e0534bb504cebd5da38243e9fc Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Sat, 24 Aug 2024 20:45:46 +0530 Subject: [PATCH 07/11] AndroidManifest: add new permission for API 34 DescriptionActivity should not be exposed --- app/src/main/AndroidManifest.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 02f31185a..5d130f124 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,24 +1,30 @@ + - + - + - + + @@ -54,7 +60,7 @@ + android:exported="false" /> Date: Sat, 24 Aug 2024 20:49:08 +0530 Subject: [PATCH 08/11] refactor: permission should not be check on onCreate for some cases --- .../commons/contributions/MainActivity.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 63bde1be9..58e064552 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -162,14 +162,17 @@ public class MainActivity extends BaseActivity * so that location in the EXIF metadata of the images shared by the user * is retained on devices running Android 10 or above */ - if (VERSION.SDK_INT >= VERSION_CODES.Q) { - PermissionUtils.checkPermissionsAndPerformAction( - this, - () -> {}, - R.string.media_location_permission_denied, - R.string.add_location_manually, - permission.ACCESS_MEDIA_LOCATION); - } +// if (VERSION.SDK_INT >= VERSION_CODES.Q) { +// ActivityCompat.requestPermissions(this, +// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0); +// PermissionUtils.checkPermissionsAndPerformAction( +// this, +// () -> {}, +// R.string.media_location_permission_denied, +// R.string.add_location_manually, +// permission.ACCESS_MEDIA_LOCATION); +// } + checkAndResumeStuckUploads(); } } From f8d164b6a201c12b361c877a7ccd1665b73c7983 Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Sat, 24 Aug 2024 20:52:14 +0530 Subject: [PATCH 09/11] add method to get correct storage permission and check partial access Additionally, add final keywords to reduce compiler warnings --- .../nrw/commons/utils/PermissionUtils.java | 123 ++++++++++-------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index b0a72eae1..5fb444192 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.utils; import android.Manifest; +import android.Manifest.permission; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; @@ -20,24 +21,23 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.upload.UploadActivity; import java.util.List; - public class PermissionUtils { + public static String[] PERMISSIONS_STORAGE = getPermissionsStorage(); - public static String[] PERMISSIONS_STORAGE = isSDKVersionScopedStorageCompatible() ? - isSDKVersionTiramisu() ? new String[]{ - Manifest.permission.READ_MEDIA_IMAGES} : - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE} - : isSDKVersionTiramisu() ? new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_MEDIA_IMAGES} - : new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - - private static boolean isSDKVersionScopedStorageCompatible() { - return Build.VERSION.SDK_INT > Build.VERSION_CODES.P; - } - - public static boolean isSDKVersionTiramisu() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; + static String[] getPermissionsStorage() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return new String[]{ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_IMAGES }; + } + if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { + return new String[]{ Manifest.permission.READ_MEDIA_IMAGES }; + } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; + } + return new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE }; } /** @@ -45,11 +45,11 @@ public class PermissionUtils { * blocked(marked never ask again by the user) It open the app settings from where the user can * manually give us the required permission. * - * @param activity + * @param activity The Activity which requires a permission which has been blocked */ - private static void askUserToManuallyEnablePermissionFromSettings(Activity activity) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); + private static void askUserToManuallyEnablePermissionFromSettings(final Activity activity) { + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); intent.setData(uri); activity.startActivityForResult(intent, CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS); @@ -58,14 +58,13 @@ public class PermissionUtils { /** * Checks whether the app already has a particular permission * - * @param activity - * @param permissions permissions to be checked - * @return + * @param activity The Activity context to check permissions against + * @param permissions An array of permission strings to check + * @return `true if the app has all the specified permissions, `false` otherwise */ - public static boolean hasPermission(Activity activity, String permissions[]) { + public static boolean hasPermission(final Activity activity, final String[] permissions) { boolean hasPermission = true; - for (String permission : permissions - ) { + for(final String permission : permissions) { hasPermission = hasPermission && ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED; @@ -73,6 +72,15 @@ public class PermissionUtils { return hasPermission; } + public static boolean hasPartialAccess(final Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return ContextCompat.checkSelfPermission(activity, + permission.READ_MEDIA_VISUAL_USER_SELECTED + ) == PackageManager.PERMISSION_GRANTED; + } + return false; + } + /** * Checks for a particular permission and runs the runnable to perform an action when the * permission is granted Also, it shows a rationale if needed @@ -99,9 +107,17 @@ public class PermissionUtils { * @param rationaleMessage rationale message to be displayed when permission was denied. It * can be an invalid @StringRes */ - public static void checkPermissionsAndPerformAction(Activity activity, - Runnable onPermissionGranted, @StringRes int rationaleTitle, - @StringRes int rationaleMessage, String... permissions) { + public static void checkPermissionsAndPerformAction( + final Activity activity, + final Runnable onPermissionGranted, + final @StringRes int rationaleTitle, + final @StringRes int rationaleMessage, + final String... permissions + ) { + if (hasPartialAccess(activity)) { + onPermissionGranted.run(); + return; + } checkPermissionsAndPerformAction(activity, onPermissionGranted, null, rationaleTitle, rationaleMessage, permissions); } @@ -125,25 +141,30 @@ public class PermissionUtils { * @param rationaleTitle rationale title to be displayed when permission was denied * @param rationaleMessage rationale message to be displayed when permission was denied */ - public static void checkPermissionsAndPerformAction(Activity activity, - Runnable onPermissionGranted, Runnable onPermissionDenied, @StringRes int rationaleTitle, - @StringRes int rationaleMessage, String... permissions) { + public static void checkPermissionsAndPerformAction( + final Activity activity, + final Runnable onPermissionGranted, + final Runnable onPermissionDenied, + final @StringRes int rationaleTitle, + final @StringRes int rationaleMessage, + final String... permissions + ) { Dexter.withActivity(activity) .withPermissions(permissions) .withListener(new MultiplePermissionsListener() { @Override - public void onPermissionsChecked(MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted()) { + public void onPermissionsChecked(final MultiplePermissionsReport report) { + if (report.areAllPermissionsGranted() || hasPartialAccess(activity)) { onPermissionGranted.run(); return; } if (report.isAnyPermissionPermanentlyDenied()) { // permission is denied permanently, we will show user a dialog message. - DialogUtil.showAlertDialog(activity, activity.getString(rationaleTitle), + DialogUtil.showAlertDialog( + activity, activity.getString(rationaleTitle), activity.getString(rationaleMessage), activity.getString(R.string.navigation_item_settings), - null, - () -> { + null, () -> { askUserToManuallyEnablePermissionFromSettings(activity); if (activity instanceof UploadActivity) { ((UploadActivity) activity).setShowPermissionsDialog(true); @@ -158,13 +179,16 @@ public class PermissionUtils { } @Override - public void onPermissionRationaleShouldBeShown(List permissions, - PermissionToken token) { + public void onPermissionRationaleShouldBeShown( + final List permissions, + final PermissionToken token + ) { if (rationaleTitle == -1 && rationaleMessage == -1) { token.continuePermissionRequest(); return; } - DialogUtil.showAlertDialog(activity, activity.getString(rationaleTitle), + DialogUtil.showAlertDialog( + activity, activity.getString(rationaleTitle), activity.getString(rationaleMessage), activity.getString(android.R.string.ok), activity.getString(android.R.string.cancel), @@ -173,24 +197,19 @@ public class PermissionUtils { ((UploadActivity) activity).setShowPermissionsDialog(true); } token.continuePermissionRequest(); - } - , + }, () -> { Toast.makeText(activity.getApplicationContext(), - R.string.permissions_are_required_for_functionality, - Toast.LENGTH_LONG) - .show(); + R.string.permissions_are_required_for_functionality, + Toast.LENGTH_LONG + ).show(); token.cancelPermissionRequest(); if (activity instanceof UploadActivity) { activity.finish(); } - } - , - null, - false); + }, null, false + ); } - }) - .onSameThread() - .check(); + }).onSameThread().check(); } } From ad1074a84a6abac710b8cf822f0cf857af3db5bc Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Sat, 24 Aug 2024 20:56:31 +0530 Subject: [PATCH 10/11] refactor: prevent app from crashing for SDKs >= 34 --- .../commons/customselector/ui/adapter/FolderAdapter.kt | 10 ++++++---- .../commons/customselector/ui/adapter/ImageAdapter.kt | 2 +- .../customselector/ui/selector/FolderFragment.kt | 7 +++++-- .../customselector/ui/selector/ImageFragment.kt | 5 ++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 60d299491..abebc8944 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -62,13 +62,15 @@ class FolderAdapter( folder.images.removeAll(toBeRemoved) val count = folder.images.size - if(count == 0) { + if(count == 0 && folders.size > 0) { // Folder is empty, remove folder from the adapter. holder.itemView.post{ val updatePosition = folders.indexOf(folder) - folders.removeAt(updatePosition) - notifyItemRemoved(updatePosition) - notifyItemRangeChanged(updatePosition, folders.size) + if(updatePosition != -1) { + folders.removeAt(updatePosition) + notifyItemRemoved(updatePosition) + notifyItemRangeChanged(updatePosition, folders.size) + } } } else { val previewImage = folder.images[0] diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 47784153e..58f4c8385 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -122,7 +122,7 @@ class ImageAdapter( * Bind View holder, load image, selected view, click listeners. */ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { - + if(images.size == 0) { return } var image=images[position] holder.image.setImageDrawable (null) if (context.contentResolver.getType(image.uri) == null) { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index 0f546e788..95f427f49 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -116,11 +116,14 @@ class FolderFragment : CommonsDaggerSupportFragment() { private fun handleResult(result: Result) { if(result.status is CallbackStatus.SUCCESS){ val images = result.images - if(images.isNullOrEmpty()) - { + if(images.isEmpty()){ binding?.emptyText?.let { it.visibility = View.VISIBLE } + } else { + binding?.emptyText?.let { + it.visibility = View.GONE + } } folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index 842531dd2..c5e5de4f6 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -39,6 +39,7 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper import io.reactivex.schedulers.Schedulers import java.util.* import javax.inject.Inject +import kotlin.collections.ArrayList /** * Custom Selector Image Fragment. @@ -279,6 +280,8 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat } } } else { + filteredImages = ArrayList() + allImages = filteredImages binding?.emptyText?.let { it.visibility = View.VISIBLE } @@ -324,7 +327,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat .findFirstVisibleItemPosition() // Check for empty RecyclerView. - if (position != -1) { + if (position != -1 && filteredImages.size > 0) { context?.let { context -> context.getSharedPreferences( "CustomSelector", From bc621a085244843f128ca0da1d71442352cde647 Mon Sep 17 00:00:00 2001 From: Rohit Verma Date: Sat, 24 Aug 2024 21:00:11 +0530 Subject: [PATCH 11/11] add new UI component to allows user to manage partially access photos Implement using composeView --- .../ui/selector/CustomSelectorActivity.kt | 126 +++++++++++++++++- .../res/layout/activity_custom_selector.xml | 10 +- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 7cf0229cb..bcb7446d8 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -1,16 +1,49 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.Manifest import android.app.Activity import android.app.Dialog import android.content.Intent import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.View import android.view.Window import android.widget.Button import android.widget.ImageButton import android.widget.TextView +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.database.NotForUploadStatus @@ -24,10 +57,12 @@ import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding import fr.free.nrw.commons.filepicker.Constants +import fr.free.nrw.commons.filepicker.FilePicker import fr.free.nrw.commons.media.ZoomableActivity import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.utils.CustomSelectorUtils +import fr.free.nrw.commons.utils.PermissionUtils import kotlinx.coroutines.* import java.io.File import java.lang.Integer.max @@ -114,14 +149,37 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL private var progressDialogText:String="" + private var showPartialAccessIndicator by mutableStateOf(false) + /** * onCreate Activity, sets theme, initialises the view model, setup view. */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + ContextCompat.checkSelfPermission( + this, Manifest.permission.READ_MEDIA_IMAGES + ) == PackageManager.PERMISSION_DENIED + ) { + showPartialAccessIndicator = true + } + binding = ActivityCustomSelectorBinding.inflate(layoutInflater) toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root) bottomSheetBinding = CustomSelectorBottomLayoutBinding.bind(binding.root) + binding.partialAccessIndicator.setContent { + PartialStorageAccessIndicator( + isVisible = showPartialAccessIndicator, + onManage = { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1) + } + }, + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 4.dp) + .fillMaxWidth() + ) + } val view = binding.root setContentView(view) @@ -147,6 +205,24 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL } } + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if(requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showPartialAccessIndicator = false + } + } + } + + override fun onResume() { + super.onResume() + fetchData() + } + /** * When data will be send from full screen mode, it will be passed to fragment */ @@ -181,7 +257,6 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, FolderFragment.newInstance()) .commit() - fetchData() setUpToolbar() setUpBottomLayout() } @@ -498,3 +573,52 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL const val ITEM_ID: String = "ItemId" } } +@Composable +fun PartialStorageAccessIndicator( + isVisible: Boolean, + onManage: ()-> Unit, + modifier: Modifier = Modifier +) { + if(isVisible) { + OutlinedCard( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = colorResource(R.color.primarySuperLightColor) + ), + border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)), + shape = RoundedCornerShape(8.dp) + ) { + Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) { + Text( + text = "You've given access to a select number of photos", + modifier = Modifier.weight(1f) + ) + TextButton( + onClick = onManage, + modifier = Modifier.align(Alignment.Bottom), + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(R.color.primaryColor) + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Manage", + style = MaterialTheme.typography.labelMedium, + color = colorResource(R.color.primaryTextColor) + ) + } + } + } + } +} + +@Preview +@Composable +fun PartialStorageAccessIndicatorPreview() { + Surface { + PartialStorageAccessIndicator(isVisible = true, onManage = {}, modifier = Modifier + .padding(vertical = 8.dp, horizontal = 4.dp) + .fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index fbd036f94..02c864422 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -1,17 +1,25 @@ + + + app:layout_constraintTop_toBottomOf="@+id/partial_access_indicator" + tools:layout_editor_absoluteX="-16dp" />