mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
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:
parent
eb027b74ce
commit
3e915f9848
17 changed files with 433 additions and 200 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
@ -184,6 +190,10 @@
|
|||
android:exported="false"
|
||||
android:process=":acra" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.ExtendedFileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -62,14 +62,16 @@ 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)
|
||||
if(updatePosition != -1) {
|
||||
folders.removeAt(updatePosition)
|
||||
notifyItemRemoved(updatePosition)
|
||||
notifyItemRangeChanged(updatePosition, folders.size)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val previewImage = folder.images[0]
|
||||
Glide.with(holder.image).load(previewImage.uri).into(holder.image)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,11 +249,19 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
|||
* Create new notification for foreground service
|
||||
*/
|
||||
private fun createForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
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 {
|
||||
return createForegroundInfo()
|
||||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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 };
|
||||
}
|
||||
|
||||
public static boolean isSDKVersionTiramisu() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
|
||||
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();
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
token.cancelPermissionRequest();
|
||||
if (activity instanceof UploadActivity) {
|
||||
activity.finish();
|
||||
}
|
||||
}, null, false
|
||||
);
|
||||
}
|
||||
,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
})
|
||||
.onSameThread()
|
||||
.check();
|
||||
}).onSameThread().check();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,15 +68,17 @@ 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())
|
||||
|
|
@ -78,13 +88,20 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
|||
views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle());
|
||||
|
||||
// View in browser
|
||||
Intent viewIntent = new Intent();
|
||||
final 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);
|
||||
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.appwidget_image, pendingIntent);
|
||||
loadImageFromUrl(response.getThumbUrl(),
|
||||
context, views, appWidgetManager, appWidgetId);
|
||||
}
|
||||
},
|
||||
t -> Timber.e(t, "Fetching picture of the day failed")
|
||||
|
|
@ -93,28 +110,34 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
|||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue