diff --git a/app/build.gradle b/app/build.gradle index c8124e42be..a9c0c498cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,21 @@ 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.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 +201,7 @@ project.gradle.taskGraph.whenReady { } android { - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { //applicationId 'fr.free.nrw.commons' @@ -196,7 +211,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 +268,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 +370,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/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87e182cc32..6a47a46447 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,23 +3,29 @@ xmlns:tools="http://schemas.android.com/tools"> - + - + - - + + - + + + @@ -183,6 +189,10 @@ android:name="org.acra.sender.SenderService" android:exported="false" android:process=":acra" /> + + = 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(); } } 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 f454a3af8c..961d511583 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) { 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 60d2994917..abebc89444 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 47784153ec..58f4c83850 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/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 7cf0229cb2..bcb7446d8e 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/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 0f546e7881..95f427f49e 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 842531dd2c..c5e5de4f64 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", 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 d4c08d7a53..b63d3a4c10 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.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 NotificationHelper(Context context) { * @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() @@ -65,14 +62,11 @@ public void showNotification(Context context, .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/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 707bf13639..eb180ec44b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -278,7 +278,8 @@ protected void checkBlockStatus() { public void checkStoragePermissions() { // Check if all required permissions are granted final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE); - if (hasAllPermissions) { + final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this); + if (hasAllPermissions || hasPartialAccess) { // All required permissions are granted, so enable UI elements and perform actions receiveSharedItems(); binding.cvContainerTopCard.setVisibility(View.VISIBLE); 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 4a06cafb61..ebf930915e 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 @@ -6,6 +6,7 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.graphics.BitmapFactory import android.os.Build import androidx.core.app.NotificationCompat @@ -46,13 +47,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,12 +83,11 @@ 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 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", @@ -100,7 +99,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) @@ -120,21 +119,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() @@ -248,10 +249,18 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : * Create new notification for foreground service */ private fun createForegroundInfo(): ForegroundInfo { - return ForegroundInfo( - 1, - createNotificationForForegroundService() - ) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ForegroundInfo( + 1, + createNotificationForForegroundService(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + ForegroundInfo( + 1, + createNotificationForForegroundService() + ) + } } override suspend fun getForegroundInfo(): ForegroundInfo { @@ -282,9 +291,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 @@ -294,7 +303,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build()!! + currentNotification.build() ) val filename = media.filename @@ -312,14 +321,16 @@ 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 @@ -335,7 +346,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" @@ -378,12 +389,15 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : } else -> { Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""") + contribution.state = Contribution.STATE_FAILED contribution.chunkInfo = null contribution.errorInfo = stashUploadResult.errorMessage showErrorNotification(contribution) 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") showInvalidLoginNotification(contribution) val username = sessionManager.userName @@ -475,7 +489,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : imageSha1 == modifiedSha1, true ) - ); + ) } } } @@ -519,8 +533,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : private fun showSuccessNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle contribution.state=Contribution.STATE_COMPLETED - curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) - curentNotification.setContentTitle( + currentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) + currentNotification.setContentTitle( appContext.getString( R.string.upload_completed_notification_title, displayTitle @@ -531,7 +545,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -542,8 +556,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : @SuppressLint("StringFormatInvalid") private fun showFailedNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) - curentNotification.setContentTitle( + currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) + currentNotification.setContentTitle( appContext.getString( R.string.upload_failed_notification_title, displayTitle @@ -554,13 +568,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 @@ -571,7 +585,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -581,7 +595,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : @SuppressLint("StringFormatInvalid") private fun showErrorNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentTitle( + currentNotification.setContentTitle( appContext.getString( R.string.upload_failed_notification_title, displayTitle @@ -592,7 +606,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager?.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -602,8 +616,9 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : */ private fun showPausedNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) - curentNotification.setContentTitle( + + currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) + currentNotification.setContentTitle( appContext.getString( R.string.upload_paused_notification_title, displayTitle @@ -614,7 +629,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager!!.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -624,8 +639,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : */ private fun showCancelledNotification(contribution: Contribution) { val displayTitle = contribution.media.displayTitle - curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) - curentNotification.setContentTitle( + currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) + currentNotification.setContentTitle( displayTitle ) .setContentText("Upload has been cancelled!") @@ -633,7 +648,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .setOngoing(false) notificationManager!!.notify( currentNotificationTag, currentNotificationID, - curentNotification.build() + currentNotification.build() ) } @@ -652,6 +667,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/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index b0a72eae16..9082c1f0fc 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,26 @@ 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, + Manifest.permission.ACCESS_MEDIA_LOCATION }; + } + if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { + return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, + Manifest. permission.ACCESS_MEDIA_LOCATION }; + } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION }; + } + return new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE }; } /** @@ -45,11 +48,11 @@ public static boolean isSDKVersionTiramisu() { * 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 +61,13 @@ private static void askUserToManuallyEnablePermissionFromSettings(Activity activ /** * 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 +75,17 @@ public static boolean hasPermission(Activity activity, String permissions[]) { 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 && ContextCompat.checkSelfPermission( + activity, permission.READ_MEDIA_IMAGES + ) == PackageManager.PERMISSION_DENIED; + } + 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 +112,17 @@ public static boolean hasPermission(Activity activity, String permissions[]) { * @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 +146,30 @@ public static void checkPermissionsAndPerformAction(Activity activity, * @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 +184,16 @@ public void onPermissionsChecked(MultiplePermissionsReport report) { } @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 +202,19 @@ public void onPermissionRationaleShouldBeShown(List permissio ((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(); } } 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 f336317847..2734520787 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.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.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; @@ -41,17 +38,28 @@ */ 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 +68,76 @@ void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int app /** * 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()); - - // 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); - - loadImageFromUrl(response.getThumbUrl(), context, views, appWidgetManager, appWidgetId); + response -> { + if (response != null) { + views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle()); + + // View in browser + final Intent viewIntent = new Intent(); + viewIntent.setAction(ACTION_VIEW); + viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri())); + + 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); @@ -122,32 +145,37 @@ protected void onNewResultImpl(@Nullable Bitmap tempBitmap) { } @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 } } diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index fbd036f94f..02c8644229 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" /> diff --git a/build.gradle b/build.gradle index 8e8c8911db..003163cb8f 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 d2e417be93..fb6a720531 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