Upgrade to SDK 34 (#5790)

* change the overridden method signature as per API 34

* add version check condition to compare with API 23 before adding flag

* refactor: add final keywords, fix typo, and remove redundant spaces

For optimized code only

* upgrade: migrate to SDK 34 and upgrade APG

Additionally, add Jetpack Compose to the project

* AndroidManifest: add new permission for API 34

DescriptionActivity should not be exposed

* refactor: permission should not be check on onCreate for some cases

* add method to get correct storage permission and check partial access

Additionally, add final keywords to reduce compiler warnings

* refactor: prevent app from crashing for SDKs >= 34

* add new UI component to allows user to manage partially access photos

Implement using composeView

* change the overridden method signature as per API 34

* add version check condition to compare with API 23 before adding flag

* refactor: add final keywords, fix typo, and remove redundant spaces

For optimized code only

* upgrade: migrate to SDK 34 and upgrade APG

Additionally, add Jetpack Compose to the project

* AndroidManifest: add new permission for API 34

DescriptionActivity should not be exposed

* refactor: permission should not be check on onCreate for some cases

* add method to get correct storage permission and check partial access

Additionally, add final keywords to reduce compiler warnings

* refactor: prevent app from crashing for SDKs >= 34

* add new UI component to allows user to manage partially access photos

Implement using composeView

* replace deprecated circular progress bar with material progress bar

* remove redundant appcompat dependency

* add condition to check for partial access on API >= 34

It prevents invoking photo picker on UploadActivity.

* UploadWorker: add foreground service type

* fix typos in UploadWorker.kt

* add permission to access media location
This commit is contained in:
Rohit Verma 2024-09-17 20:28:44 +05:30 committed by GitHub
parent eb027b74ce
commit 3e915f9848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 433 additions and 200 deletions

View file

@ -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 {

View file

@ -3,23 +3,29 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
android:minSdkVersion="33"/>
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
android:minSdkVersion="34"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<queries>
@ -183,6 +189,10 @@
android:name="org.acra.sender.SenderService"
android:exported="false"
android:process=":acra" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync" />
<provider
android:name=".filepicker.ExtendedFileProvider"

View file

@ -165,15 +165,16 @@ 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();
}
}

View file

@ -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) {

View file

@ -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]

View file

@ -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) {

View file

@ -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<out String>,
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()
)
}
}

View file

@ -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)

View file

@ -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",

View file

@ -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()
@ -65,14 +62,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());
}

View file

@ -278,7 +278,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
public void checkStoragePermissions() {
// Check if all required permissions are granted
final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE);
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);

View file

@ -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<Int>()
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)
}
};
}
}
}

View file

@ -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.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,
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 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 +61,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 +75,17 @@ 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 && 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 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 +146,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 +184,16 @@ public class PermissionUtils {
}
@Override
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions,
PermissionToken token) {
public void onPermissionRationaleShouldBeShown(
final List<PermissionRequest> 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 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();
}
}

View file

@ -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;
@ -41,17 +38,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 +68,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<CloseableReference<CloseableImage>> 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<CloseableReference<CloseableImage>> 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 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
protected void onFailureImpl(
final DataSource<CloseableReference<CloseableImage>> 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
}
}

View file

@ -1,17 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/custom_selector_toolbar" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/partial_access_indicator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar_layout"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottom_layout"
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout" />
app:layout_constraintTop_toBottomOf="@+id/partial_access_indicator"
tools:layout_editor_absoluteX="-16dp" />
<include layout="@layout/custom_selector_bottom_layout" />

View file

@ -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'

View file

@ -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