Skip to content

Commit 3e915f9

Browse files
authored
Upgrade to SDK 34 (commons-app#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
1 parent eb027b7 commit 3e915f9

17 files changed

+435
-202
lines changed

app/build.gradle

+24-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ dependencies {
5151
implementation 'com.karumi:dexter:5.0.0'
5252
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
5353

54+
// Jetpack Compose
55+
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
56+
57+
implementation "androidx.activity:activity-compose:1.9.1"
58+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
59+
implementation (composeBom)
60+
implementation "androidx.compose.runtime:runtime"
61+
implementation "androidx.compose.ui:ui"
62+
implementation "androidx.compose.ui:ui-graphics"
63+
implementation "androidx.compose.ui:ui-tooling"
64+
implementation "androidx.compose.foundation:foundation"
65+
implementation "androidx.compose.foundation:foundation-layout"
66+
implementation "androidx.compose.material3:material3"
67+
androidTestImplementation(composeBom)
68+
5469
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
5570
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
5671
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
@@ -186,7 +201,7 @@ project.gradle.taskGraph.whenReady {
186201
}
187202

188203
android {
189-
compileSdkVersion 33
204+
compileSdkVersion 34
190205

191206
defaultConfig {
192207
//applicationId 'fr.free.nrw.commons'
@@ -196,7 +211,7 @@ android {
196211
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
197212

198213
minSdkVersion 21
199-
targetSdkVersion 33
214+
targetSdkVersion 34
200215
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
201216
testInstrumentationRunnerArguments clearPackageData: 'true'
202217

@@ -253,11 +268,12 @@ android {
253268
}
254269
}
255270
debug {
256-
testCoverageEnabled true
257271
minifyEnabled false
258272
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
259273
testProguardFile 'test-proguard-rules.txt'
260274
versionNameSuffix "-debug-" + getBranchName()
275+
enableUnitTestCoverage true
276+
enableAndroidTestCoverage true
261277
}
262278
}
263279

@@ -354,13 +370,17 @@ android {
354370
targetCompatibility JavaVersion.VERSION_11
355371
}
356372
kotlinOptions {
357-
jvmTarget = "1.8"
373+
jvmTarget = "11"
358374
}
359375

360376
buildToolsVersion buildToolsVersion
361377

362378
buildFeatures {
363379
viewBinding true
380+
compose true
381+
}
382+
composeOptions {
383+
kotlinCompilerExtensionVersion '1.3.2'
364384
}
365385
namespace 'fr.free.nrw.commons'
366386
lint {

app/src/main/AndroidManifest.xml

+15-5
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,29 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
6+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
7+
android:maxSdkVersion="32" />
78
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
89
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
910
<uses-permission android:name="android.permission.REORDER_TASKS" />
1011
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
11-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
12+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
13+
android:maxSdkVersion="29"/>
1214
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
1315
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
1416
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
1517
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
1618
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
17-
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
18-
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
19+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
20+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
21+
android:minSdkVersion="33"/>
1922
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
2023
<uses-permission android:name="android.permission.SET_WALLPAPER" />
2124
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
22-
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
25+
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
26+
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
27+
android:minSdkVersion="34"/>
28+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
2329

2430
<queries>
2531

@@ -183,6 +189,10 @@
183189
android:name="org.acra.sender.SenderService"
184190
android:exported="false"
185191
android:process=":acra" />
192+
193+
<service
194+
android:name="androidx.work.impl.foreground.SystemForegroundService"
195+
android:foregroundServiceType="dataSync" />
186196

187197
<provider
188198
android:name=".filepicker.ExtendedFileProvider"

app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,16 @@ public void onCreate(Bundle savedInstanceState) {
165165
* so that location in the EXIF metadata of the images shared by the user
166166
* is retained on devices running Android 10 or above
167167
*/
168-
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
169-
PermissionUtils.checkPermissionsAndPerformAction(
170-
this,
171-
() -> {
172-
},
173-
R.string.media_location_permission_denied,
174-
R.string.add_location_manually,
175-
permission.ACCESS_MEDIA_LOCATION);
176-
}
168+
// if (VERSION.SDK_INT >= VERSION_CODES.Q) {
169+
// ActivityCompat.requestPermissions(this,
170+
// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0);
171+
// PermissionUtils.checkPermissionsAndPerformAction(
172+
// this,
173+
// () -> {},
174+
// R.string.media_location_permission_denied,
175+
// R.string.add_location_manually,
176+
// permission.ACCESS_MEDIA_LOCATION);
177+
// }
177178
checkAndResumeStuckUploads();
178179
}
179180
}

app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
4040
* Detects the gestures
4141
*/
4242
override fun onFling(
43-
event1: MotionEvent,
43+
event1: MotionEvent?,
4444
event2: MotionEvent,
4545
velocityX: Float,
4646
velocityY: Float
4747
): Boolean {
4848
try {
49-
val diffY: Float = event2.y - event1.y
50-
val diffX: Float = event2.x - event1.x
49+
val diffY: Float = event2.y - (event1?.y ?: event2.y)
50+
val diffX: Float = event2.x - (event1?.x ?: event2.x)
5151
if (abs(diffX) > abs(diffY)) {
5252
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
5353
SWIPE_VELOCITY_THRESHOLD) {

app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt

+6-4
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ class FolderAdapter(
6262
folder.images.removeAll(toBeRemoved)
6363
val count = folder.images.size
6464

65-
if(count == 0) {
65+
if(count == 0 && folders.size > 0) {
6666
// Folder is empty, remove folder from the adapter.
6767
holder.itemView.post{
6868
val updatePosition = folders.indexOf(folder)
69-
folders.removeAt(updatePosition)
70-
notifyItemRemoved(updatePosition)
71-
notifyItemRangeChanged(updatePosition, folders.size)
69+
if(updatePosition != -1) {
70+
folders.removeAt(updatePosition)
71+
notifyItemRemoved(updatePosition)
72+
notifyItemRangeChanged(updatePosition, folders.size)
73+
}
7274
}
7375
} else {
7476
val previewImage = folder.images[0]

app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class ImageAdapter(
122122
* Bind View holder, load image, selected view, click listeners.
123123
*/
124124
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
125-
125+
if(images.size == 0) { return }
126126
var image=images[position]
127127
holder.image.setImageDrawable (null)
128128
if (context.contentResolver.getType(image.uri) == null) {

app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt

+125-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
11
package fr.free.nrw.commons.customselector.ui.selector
22

3+
import android.Manifest
34
import android.app.Activity
45
import android.app.Dialog
56
import android.content.Intent
67
import android.content.SharedPreferences
8+
import android.content.pm.PackageManager
9+
import android.os.Build
710
import android.os.Bundle
11+
import android.util.Log
812
import android.view.View
913
import android.view.Window
1014
import android.widget.Button
1115
import android.widget.ImageButton
1216
import android.widget.TextView
17+
import androidx.compose.foundation.BorderStroke
18+
import androidx.compose.foundation.background
19+
import androidx.compose.foundation.clickable
20+
import androidx.compose.foundation.layout.Arrangement
21+
import androidx.compose.foundation.layout.Box
22+
import androidx.compose.foundation.layout.Row
23+
import androidx.compose.foundation.layout.fillMaxWidth
24+
import androidx.compose.foundation.layout.height
25+
import androidx.compose.foundation.layout.padding
26+
import androidx.compose.foundation.shape.RoundedCornerShape
27+
import androidx.compose.material3.Button
28+
import androidx.compose.material3.ButtonDefaults
29+
import androidx.compose.material3.CardDefaults
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.material3.OutlinedCard
32+
import androidx.compose.material3.Surface
33+
import androidx.compose.material3.Text
34+
import androidx.compose.material3.TextButton
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.setValue
39+
import androidx.compose.ui.Alignment
40+
import androidx.compose.ui.Modifier
41+
import androidx.compose.ui.draw.clip
42+
import androidx.compose.ui.res.colorResource
43+
import androidx.compose.ui.tooling.preview.Preview
44+
import androidx.compose.ui.unit.dp
1345
import androidx.constraintlayout.widget.ConstraintLayout
46+
import androidx.core.content.ContextCompat
1447
import androidx.lifecycle.ViewModelProvider
1548
import fr.free.nrw.commons.R
1649
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
@@ -24,10 +57,12 @@ import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
2457
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
2558
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
2659
import fr.free.nrw.commons.filepicker.Constants
60+
import fr.free.nrw.commons.filepicker.FilePicker
2761
import fr.free.nrw.commons.media.ZoomableActivity
2862
import fr.free.nrw.commons.theme.BaseActivity
2963
import fr.free.nrw.commons.upload.FileUtilsWrapper
3064
import fr.free.nrw.commons.utils.CustomSelectorUtils
65+
import fr.free.nrw.commons.utils.PermissionUtils
3166
import kotlinx.coroutines.*
3267
import java.io.File
3368
import java.lang.Integer.max
@@ -114,14 +149,37 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
114149

115150
private var progressDialogText:String=""
116151

152+
private var showPartialAccessIndicator by mutableStateOf(false)
153+
117154
/**
118155
* onCreate Activity, sets theme, initialises the view model, setup view.
119156
*/
120157
override fun onCreate(savedInstanceState: Bundle?) {
121158
super.onCreate(savedInstanceState)
159+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
160+
ContextCompat.checkSelfPermission(
161+
this, Manifest.permission.READ_MEDIA_IMAGES
162+
) == PackageManager.PERMISSION_DENIED
163+
) {
164+
showPartialAccessIndicator = true
165+
}
166+
122167
binding = ActivityCustomSelectorBinding.inflate(layoutInflater)
123168
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
124169
bottomSheetBinding = CustomSelectorBottomLayoutBinding.bind(binding.root)
170+
binding.partialAccessIndicator.setContent {
171+
PartialStorageAccessIndicator(
172+
isVisible = showPartialAccessIndicator,
173+
onManage = {
174+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
175+
requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1)
176+
}
177+
},
178+
modifier = Modifier
179+
.padding(vertical = 8.dp, horizontal = 4.dp)
180+
.fillMaxWidth()
181+
)
182+
}
125183
val view = binding.root
126184
setContentView(view)
127185

@@ -147,6 +205,24 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
147205
}
148206
}
149207

208+
override fun onRequestPermissionsResult(
209+
requestCode: Int,
210+
permissions: Array<out String>,
211+
grantResults: IntArray
212+
) {
213+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
214+
if(requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
215+
if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
216+
showPartialAccessIndicator = false
217+
}
218+
}
219+
}
220+
221+
override fun onResume() {
222+
super.onResume()
223+
fetchData()
224+
}
225+
150226
/**
151227
* When data will be send from full screen mode, it will be passed to fragment
152228
*/
@@ -181,7 +257,6 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
181257
supportFragmentManager.beginTransaction()
182258
.replace(R.id.fragment_container, FolderFragment.newInstance())
183259
.commit()
184-
fetchData()
185260
setUpToolbar()
186261
setUpBottomLayout()
187262
}
@@ -498,3 +573,52 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
498573
const val ITEM_ID: String = "ItemId"
499574
}
500575
}
576+
@Composable
577+
fun PartialStorageAccessIndicator(
578+
isVisible: Boolean,
579+
onManage: ()-> Unit,
580+
modifier: Modifier = Modifier
581+
) {
582+
if(isVisible) {
583+
OutlinedCard(
584+
modifier = modifier,
585+
colors = CardDefaults.cardColors(
586+
containerColor = colorResource(R.color.primarySuperLightColor)
587+
),
588+
border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)),
589+
shape = RoundedCornerShape(8.dp)
590+
) {
591+
Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
592+
Text(
593+
text = "You've given access to a select number of photos",
594+
modifier = Modifier.weight(1f)
595+
)
596+
TextButton(
597+
onClick = onManage,
598+
modifier = Modifier.align(Alignment.Bottom),
599+
colors = ButtonDefaults.buttonColors(
600+
containerColor = colorResource(R.color.primaryColor)
601+
),
602+
shape = RoundedCornerShape(8.dp)
603+
) {
604+
Text(
605+
text = "Manage",
606+
style = MaterialTheme.typography.labelMedium,
607+
color = colorResource(R.color.primaryTextColor)
608+
)
609+
}
610+
}
611+
}
612+
}
613+
}
614+
615+
@Preview
616+
@Composable
617+
fun PartialStorageAccessIndicatorPreview() {
618+
Surface {
619+
PartialStorageAccessIndicator(isVisible = true, onManage = {}, modifier = Modifier
620+
.padding(vertical = 8.dp, horizontal = 4.dp)
621+
.fillMaxWidth()
622+
)
623+
}
624+
}

app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,14 @@ class FolderFragment : CommonsDaggerSupportFragment() {
116116
private fun handleResult(result: Result) {
117117
if(result.status is CallbackStatus.SUCCESS){
118118
val images = result.images
119-
if(images.isNullOrEmpty())
120-
{
119+
if(images.isEmpty()){
121120
binding?.emptyText?.let {
122121
it.visibility = View.VISIBLE
123122
}
123+
} else {
124+
binding?.emptyText?.let {
125+
it.visibility = View.GONE
126+
}
124127
}
125128
folders = ImageHelper.folderListFromImages(result.images)
126129
folderAdapter.init(folders)

0 commit comments

Comments
 (0)