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 'com.karumi:dexter:5.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.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-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
||||||
|
|
@ -186,7 +201,7 @@ project.gradle.taskGraph.whenReady {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
//applicationId 'fr.free.nrw.commons'
|
//applicationId 'fr.free.nrw.commons'
|
||||||
|
|
@ -196,7 +211,7 @@ android {
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
|
|
@ -253,11 +268,12 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
testCoverageEnabled true
|
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
testProguardFile 'test-proguard-rules.txt'
|
||||||
versionNameSuffix "-debug-" + getBranchName()
|
versionNameSuffix "-debug-" + getBranchName()
|
||||||
|
enableUnitTestCoverage true
|
||||||
|
enableAndroidTestCoverage true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,13 +370,17 @@ android {
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
buildToolsVersion buildToolsVersion
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion '1.3.2'
|
||||||
}
|
}
|
||||||
namespace 'fr.free.nrw.commons'
|
namespace 'fr.free.nrw.commons'
|
||||||
lint {
|
lint {
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,29 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
<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.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<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="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<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>
|
<queries>
|
||||||
|
|
||||||
|
|
@ -184,6 +190,10 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":acra" />
|
android:process=":acra" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".filepicker.ExtendedFileProvider"
|
android:name=".filepicker.ExtendedFileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
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
|
* so that location in the EXIF metadata of the images shared by the user
|
||||||
* is retained on devices running Android 10 or above
|
* is retained on devices running Android 10 or above
|
||||||
*/
|
*/
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
// if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||||
PermissionUtils.checkPermissionsAndPerformAction(
|
// ActivityCompat.requestPermissions(this,
|
||||||
this,
|
// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0);
|
||||||
() -> {
|
// PermissionUtils.checkPermissionsAndPerformAction(
|
||||||
},
|
// this,
|
||||||
R.string.media_location_permission_denied,
|
// () -> {},
|
||||||
R.string.add_location_manually,
|
// R.string.media_location_permission_denied,
|
||||||
permission.ACCESS_MEDIA_LOCATION);
|
// R.string.add_location_manually,
|
||||||
}
|
// permission.ACCESS_MEDIA_LOCATION);
|
||||||
|
// }
|
||||||
checkAndResumeStuckUploads();
|
checkAndResumeStuckUploads();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,14 +40,14 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
* Detects the gestures
|
* Detects the gestures
|
||||||
*/
|
*/
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
event1: MotionEvent,
|
event1: MotionEvent?,
|
||||||
event2: MotionEvent,
|
event2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
try {
|
||||||
val diffY: Float = event2.y - event1.y
|
val diffY: Float = event2.y - (event1?.y ?: event2.y)
|
||||||
val diffX: Float = event2.x - event1.x
|
val diffX: Float = event2.x - (event1?.x ?: event2.x)
|
||||||
if (abs(diffX) > abs(diffY)) {
|
if (abs(diffX) > abs(diffY)) {
|
||||||
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
|
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
|
||||||
SWIPE_VELOCITY_THRESHOLD) {
|
SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,16 @@ class FolderAdapter(
|
||||||
folder.images.removeAll(toBeRemoved)
|
folder.images.removeAll(toBeRemoved)
|
||||||
val count = folder.images.size
|
val count = folder.images.size
|
||||||
|
|
||||||
if(count == 0) {
|
if(count == 0 && folders.size > 0) {
|
||||||
// Folder is empty, remove folder from the adapter.
|
// Folder is empty, remove folder from the adapter.
|
||||||
holder.itemView.post{
|
holder.itemView.post{
|
||||||
val updatePosition = folders.indexOf(folder)
|
val updatePosition = folders.indexOf(folder)
|
||||||
|
if(updatePosition != -1) {
|
||||||
folders.removeAt(updatePosition)
|
folders.removeAt(updatePosition)
|
||||||
notifyItemRemoved(updatePosition)
|
notifyItemRemoved(updatePosition)
|
||||||
notifyItemRangeChanged(updatePosition, folders.size)
|
notifyItemRangeChanged(updatePosition, folders.size)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val previewImage = folder.images[0]
|
val previewImage = folder.images[0]
|
||||||
Glide.with(holder.image).load(previewImage.uri).into(holder.image)
|
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.
|
* Bind View holder, load image, selected view, click listeners.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||||
|
if(images.size == 0) { return }
|
||||||
var image=images[position]
|
var image=images[position]
|
||||||
holder.image.setImageDrawable (null)
|
holder.image.setImageDrawable (null)
|
||||||
if (context.contentResolver.getType(image.uri) == null) {
|
if (context.contentResolver.getType(image.uri) == null) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,49 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.selector
|
package fr.free.nrw.commons.customselector.ui.selector
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
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.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
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.CustomSelectorBottomLayoutBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||||
import fr.free.nrw.commons.filepicker.Constants
|
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.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
|
import fr.free.nrw.commons.utils.PermissionUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.Integer.max
|
import java.lang.Integer.max
|
||||||
|
|
@ -114,14 +149,37 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
|
|
||||||
private var progressDialogText:String=""
|
private var progressDialogText:String=""
|
||||||
|
|
||||||
|
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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)
|
binding = ActivityCustomSelectorBinding.inflate(layoutInflater)
|
||||||
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
|
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
|
||||||
bottomSheetBinding = CustomSelectorBottomLayoutBinding.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
|
val view = binding.root
|
||||||
setContentView(view)
|
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
|
* 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()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||||
.commit()
|
.commit()
|
||||||
fetchData()
|
|
||||||
setUpToolbar()
|
setUpToolbar()
|
||||||
setUpBottomLayout()
|
setUpBottomLayout()
|
||||||
}
|
}
|
||||||
|
|
@ -498,3 +573,52 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
const val ITEM_ID: String = "ItemId"
|
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) {
|
private fun handleResult(result: Result) {
|
||||||
if(result.status is CallbackStatus.SUCCESS){
|
if(result.status is CallbackStatus.SUCCESS){
|
||||||
val images = result.images
|
val images = result.images
|
||||||
if(images.isNullOrEmpty())
|
if(images.isEmpty()){
|
||||||
{
|
|
||||||
binding?.emptyText?.let {
|
binding?.emptyText?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
binding?.emptyText?.let {
|
||||||
|
it.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
folders = ImageHelper.folderListFromImages(result.images)
|
folders = ImageHelper.folderListFromImages(result.images)
|
||||||
folderAdapter.init(folders)
|
folderAdapter.init(folders)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector Image Fragment.
|
* Custom Selector Image Fragment.
|
||||||
|
|
@ -279,6 +280,8 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
filteredImages = ArrayList()
|
||||||
|
allImages = filteredImages
|
||||||
binding?.emptyText?.let {
|
binding?.emptyText?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +327,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
.findFirstVisibleItemPosition()
|
.findFirstVisibleItemPosition()
|
||||||
|
|
||||||
// Check for empty RecyclerView.
|
// Check for empty RecyclerView.
|
||||||
if (position != -1) {
|
if (position != -1 && filteredImages.size > 0) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
context.getSharedPreferences(
|
context.getSharedPreferences(
|
||||||
"CustomSelector",
|
"CustomSelector",
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,12 @@ import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
|
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
|
||||||
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
|
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_DESCRIPTION = 4;
|
||||||
public static final int NOTIFICATION_EDIT_DEPICTIONS = 5;
|
public static final int NOTIFICATION_EDIT_DEPICTIONS = 5;
|
||||||
|
|
||||||
private NotificationManager notificationManager;
|
private final NotificationManager notificationManager;
|
||||||
private NotificationCompat.Builder notificationBuilder;
|
private final NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public NotificationHelper(Context context) {
|
public NotificationHelper(final Context context) {
|
||||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationBuilder = new NotificationCompat
|
notificationBuilder = new NotificationCompat
|
||||||
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
|
||||||
|
|
@ -49,12 +45,13 @@ public class NotificationHelper {
|
||||||
* @param notificationId the notificationID
|
* @param notificationId the notificationID
|
||||||
* @param intent the intent to be fired when the notification is clicked
|
* @param intent the intent to be fired when the notification is clicked
|
||||||
*/
|
*/
|
||||||
public void showNotification(Context context,
|
public void showNotification(
|
||||||
String notificationTitle,
|
final Context context,
|
||||||
String notificationMessage,
|
final String notificationTitle,
|
||||||
int notificationId,
|
final String notificationMessage,
|
||||||
Intent intent) {
|
final int notificationId,
|
||||||
|
final Intent intent
|
||||||
|
) {
|
||||||
notificationBuilder.setDefaults(DEFAULT_ALL)
|
notificationBuilder.setDefaults(DEFAULT_ALL)
|
||||||
.setContentTitle(notificationTitle)
|
.setContentTitle(notificationTitle)
|
||||||
.setStyle(new NotificationCompat.BigTextStyle()
|
.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
|
@ -65,14 +62,11 @@ public class NotificationHelper {
|
||||||
.setPriority(PRIORITY_HIGH);
|
.setPriority(PRIORITY_HIGH);
|
||||||
|
|
||||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Check if the API level is 31 or higher to modify the flag
|
flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags);
|
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags);
|
||||||
notificationBuilder.setContentIntent(pendingIntent);
|
notificationBuilder.setContentIntent(pendingIntent);
|
||||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
public void checkStoragePermissions() {
|
public void checkStoragePermissions() {
|
||||||
// Check if all required permissions are granted
|
// Check if all required permissions are granted
|
||||||
final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE);
|
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
|
// All required permissions are granted, so enable UI elements and perform actions
|
||||||
receiveSharedItems();
|
receiveSharedItems();
|
||||||
binding.cvContainerTopCard.setVisibility(View.VISIBLE);
|
binding.cvContainerTopCard.setVisibility(View.VISIBLE);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.app.PendingIntent
|
||||||
import android.app.TaskStackBuilder
|
import android.app.TaskStackBuilder
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
@ -46,13 +47,12 @@ import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UploadWorker(
|
||||||
class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
private var appContext: Context, workerParams: WorkerParameters
|
||||||
CoroutineWorker(appContext, workerParams) {
|
): CoroutineWorker(appContext, workerParams) {
|
||||||
|
|
||||||
private var notificationManager: NotificationManagerCompat? = null
|
private var notificationManager: NotificationManagerCompat? = null
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var wikidataEditService: WikidataEditService
|
lateinit var wikidataEditService: WikidataEditService
|
||||||
|
|
||||||
|
|
@ -83,12 +83,11 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
//Attributes of the current-upload notification
|
//Attributes of the current-upload notification
|
||||||
private var currentNotificationID: Int = -1// lateinit is not allowed with primitives
|
private var currentNotificationID: Int = -1// lateinit is not allowed with primitives
|
||||||
private lateinit var currentNotificationTag: String
|
private lateinit var currentNotificationTag: String
|
||||||
private var curentNotification: NotificationCompat.Builder
|
private var currentNotification: NotificationCompat.Builder
|
||||||
|
|
||||||
private val statesToProcess= ArrayList<Int>()
|
private val statesToProcess= ArrayList<Int>()
|
||||||
|
|
||||||
private val STASH_ERROR_CODES = Arrays
|
private val STASH_ERROR_CODES = listOf(
|
||||||
.asList(
|
|
||||||
"uploadstash-file-not-found",
|
"uploadstash-file-not-found",
|
||||||
"stashfailed",
|
"stashfailed",
|
||||||
"verification-error",
|
"verification-error",
|
||||||
|
|
@ -100,7 +99,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.getInstance(appContext)
|
.getInstance(appContext)
|
||||||
.commonsApplicationComponent
|
.commonsApplicationComponent
|
||||||
.inject(this)
|
.inject(this)
|
||||||
curentNotification =
|
currentNotification =
|
||||||
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
|
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
|
||||||
|
|
||||||
statesToProcess.add(Contribution.STATE_QUEUED)
|
statesToProcess.add(Contribution.STATE_QUEUED)
|
||||||
|
|
@ -120,21 +119,23 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
fun onProgress(transferred: Long, total: Long) {
|
fun onProgress(transferred: Long, total: Long) {
|
||||||
if (transferred == total) {
|
if (transferred == total) {
|
||||||
// Completed!
|
// Completed!
|
||||||
curentNotification.setContentTitle(notificationFinishingTitle)
|
currentNotification.setContentTitle(notificationFinishingTitle)
|
||||||
.setProgress(0, 100, true)
|
.setProgress(0, 100, true)
|
||||||
} else {
|
} else {
|
||||||
curentNotification
|
currentNotification
|
||||||
.setProgress(
|
.setProgress(
|
||||||
100,
|
100,
|
||||||
(transferred.toDouble() / total.toDouble() * 100).toInt(),
|
(transferred.toDouble() / total.toDouble() * 100).toInt(),
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notificationManager?.cancel(PROCESSING_UPLOADS_NOTIFICATION_TAG, PROCESSING_UPLOADS_NOTIFICATION_ID)
|
notificationManager?.cancel(
|
||||||
|
PROCESSING_UPLOADS_NOTIFICATION_TAG, PROCESSING_UPLOADS_NOTIFICATION_ID
|
||||||
|
)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag,
|
currentNotificationTag,
|
||||||
currentNotificationID,
|
currentNotificationID,
|
||||||
curentNotification.build()!!
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
contribution!!.transferred = transferred
|
contribution!!.transferred = transferred
|
||||||
contributionDao.update(contribution).blockingAwait()
|
contributionDao.update(contribution).blockingAwait()
|
||||||
|
|
@ -248,11 +249,19 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
* Create new notification for foreground service
|
* Create new notification for foreground service
|
||||||
*/
|
*/
|
||||||
private fun createForegroundInfo(): ForegroundInfo {
|
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,
|
1,
|
||||||
createNotificationForForegroundService()
|
createNotificationForForegroundService()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
return createForegroundInfo()
|
return createForegroundInfo()
|
||||||
|
|
@ -282,9 +291,9 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
currentNotificationID =
|
currentNotificationID =
|
||||||
(contribution.localUri.toString() + contribution.media.filename).hashCode()
|
(contribution.localUri.toString() + contribution.media.filename).hashCode()
|
||||||
|
|
||||||
curentNotification
|
currentNotification
|
||||||
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
|
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_progress_notification_title_start,
|
R.string.upload_progress_notification_title_start,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -294,7 +303,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag,
|
currentNotificationTag,
|
||||||
currentNotificationID,
|
currentNotificationID,
|
||||||
curentNotification.build()!!
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
val filename = media.filename
|
val filename = media.filename
|
||||||
|
|
@ -312,14 +321,16 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
val stashUploadResult = uploadClient.uploadFileToStash(
|
val stashUploadResult = uploadClient.uploadFileToStash(
|
||||||
filename!!, contribution, notificationProgressUpdater
|
filename!!, contribution, notificationProgressUpdater
|
||||||
).onErrorReturn{
|
).onErrorReturn{
|
||||||
return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message)
|
return@onErrorReturn StashUploadResult(
|
||||||
|
StashUploadState.FAILED,fileKey = null,errorMessage = it.message
|
||||||
|
)
|
||||||
}.blockingSingle()
|
}.blockingSingle()
|
||||||
when (stashUploadResult.state) {
|
when (stashUploadResult.state) {
|
||||||
StashUploadState.SUCCESS -> {
|
StashUploadState.SUCCESS -> {
|
||||||
//If the stash upload succeeds
|
//If the stash upload succeeds
|
||||||
Timber.d("Upload to stash success for fileName: $filename")
|
Timber.d("Upload to stash success for fileName: $filename")
|
||||||
Timber.d("Ensure uniqueness of filename");
|
Timber.d("Ensure uniqueness of filename")
|
||||||
val uniqueFileName = findUniqueFileName(filename!!)
|
val uniqueFileName = findUniqueFileName(filename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//Upload the file from stash
|
//Upload the file from stash
|
||||||
|
|
@ -335,7 +346,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
)
|
)
|
||||||
|
|
||||||
wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)
|
wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)
|
||||||
.blockingSubscribe();
|
.blockingSubscribe()
|
||||||
if(contribution.wikidataPlace==null){
|
if(contribution.wikidataPlace==null){
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"WikiDataEdit not required, upload success"
|
"WikiDataEdit not required, upload success"
|
||||||
|
|
@ -378,12 +389,15 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""")
|
Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""")
|
||||||
|
|
||||||
contribution.state = Contribution.STATE_FAILED
|
contribution.state = Contribution.STATE_FAILED
|
||||||
contribution.chunkInfo = null
|
contribution.chunkInfo = null
|
||||||
contribution.errorInfo = stashUploadResult.errorMessage
|
contribution.errorInfo = stashUploadResult.errorMessage
|
||||||
showErrorNotification(contribution)
|
showErrorNotification(contribution)
|
||||||
contributionDao.saveSynchronous(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")
|
Timber.e("Invalid Login, logging out")
|
||||||
showInvalidLoginNotification(contribution)
|
showInvalidLoginNotification(contribution)
|
||||||
val username = sessionManager.userName
|
val username = sessionManager.userName
|
||||||
|
|
@ -475,7 +489,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
imageSha1 == modifiedSha1,
|
imageSha1 == modifiedSha1,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -519,8 +533,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
private fun showSuccessNotification(contribution: Contribution) {
|
private fun showSuccessNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
contribution.state=Contribution.STATE_COMPLETED
|
contribution.state=Contribution.STATE_COMPLETED
|
||||||
curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
|
currentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_completed_notification_title,
|
R.string.upload_completed_notification_title,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -531,7 +545,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -542,8 +556,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun showFailedNotification(contribution: Contribution) {
|
private fun showFailedNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_failed_notification_title,
|
R.string.upload_failed_notification_title,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -554,13 +568,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun showInvalidLoginNotification(contribution: Contribution) {
|
private fun showInvalidLoginNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_failed_notification_title,
|
R.string.upload_failed_notification_title,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -571,7 +585,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,7 +595,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun showErrorNotification(contribution: Contribution) {
|
private fun showErrorNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_failed_notification_title,
|
R.string.upload_failed_notification_title,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -592,7 +606,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager?.notify(
|
notificationManager?.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -602,8 +616,9 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
*/
|
*/
|
||||||
private fun showPausedNotification(contribution: Contribution) {
|
private fun showPausedNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
|
currentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_paused_notification_title,
|
R.string.upload_paused_notification_title,
|
||||||
displayTitle
|
displayTitle
|
||||||
|
|
@ -614,7 +629,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager!!.notify(
|
notificationManager!!.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -624,8 +639,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
*/
|
*/
|
||||||
private fun showCancelledNotification(contribution: Contribution) {
|
private fun showCancelledNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
currentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
currentNotification.setContentTitle(
|
||||||
displayTitle
|
displayTitle
|
||||||
)
|
)
|
||||||
.setContentText("Upload has been cancelled!")
|
.setContentText("Upload has been cancelled!")
|
||||||
|
|
@ -633,7 +648,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
notificationManager!!.notify(
|
notificationManager!!.notify(
|
||||||
currentNotificationTag, currentNotificationID,
|
currentNotificationTag, currentNotificationID,
|
||||||
curentNotification.build()
|
currentNotification.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,6 +667,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
} else {
|
} else {
|
||||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.utils;
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.Manifest.permission;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
|
@ -20,24 +21,26 @@ import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.upload.UploadActivity;
|
import fr.free.nrw.commons.upload.UploadActivity;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
public class PermissionUtils {
|
public class PermissionUtils {
|
||||||
|
public static String[] PERMISSIONS_STORAGE = getPermissionsStorage();
|
||||||
|
|
||||||
public static String[] PERMISSIONS_STORAGE = isSDKVersionScopedStorageCompatible() ?
|
static String[] getPermissionsStorage() {
|
||||||
isSDKVersionTiramisu() ? new String[]{
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
Manifest.permission.READ_MEDIA_IMAGES} :
|
return new String[]{ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
|
||||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}
|
Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
: isSDKVersionTiramisu() ? new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
|
||||||
public static boolean isSDKVersionTiramisu() {
|
return new String[]{ Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
|
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
|
* blocked(marked never ask again by the user) It open the app settings from where the user can
|
||||||
* manually give us the required permission.
|
* 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) {
|
private static void askUserToManuallyEnablePermissionFromSettings(final Activity activity) {
|
||||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
final Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
activity.startActivityForResult(intent,
|
activity.startActivityForResult(intent,
|
||||||
CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS);
|
CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS);
|
||||||
|
|
@ -58,14 +61,13 @@ public class PermissionUtils {
|
||||||
/**
|
/**
|
||||||
* Checks whether the app already has a particular permission
|
* Checks whether the app already has a particular permission
|
||||||
*
|
*
|
||||||
* @param activity
|
* @param activity The Activity context to check permissions against
|
||||||
* @param permissions permissions to be checked
|
* @param permissions An array of permission strings to check
|
||||||
* @return
|
* @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;
|
boolean hasPermission = true;
|
||||||
for (String permission : permissions
|
for(final String permission : permissions) {
|
||||||
) {
|
|
||||||
hasPermission = hasPermission &&
|
hasPermission = hasPermission &&
|
||||||
ContextCompat.checkSelfPermission(activity, permission)
|
ContextCompat.checkSelfPermission(activity, permission)
|
||||||
== PackageManager.PERMISSION_GRANTED;
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
@ -73,6 +75,17 @@ public class PermissionUtils {
|
||||||
return hasPermission;
|
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
|
* 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
|
* 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
|
* @param rationaleMessage rationale message to be displayed when permission was denied. It
|
||||||
* can be an invalid @StringRes
|
* can be an invalid @StringRes
|
||||||
*/
|
*/
|
||||||
public static void checkPermissionsAndPerformAction(Activity activity,
|
public static void checkPermissionsAndPerformAction(
|
||||||
Runnable onPermissionGranted, @StringRes int rationaleTitle,
|
final Activity activity,
|
||||||
@StringRes int rationaleMessage, String... permissions) {
|
final Runnable onPermissionGranted,
|
||||||
|
final @StringRes int rationaleTitle,
|
||||||
|
final @StringRes int rationaleMessage,
|
||||||
|
final String... permissions
|
||||||
|
) {
|
||||||
|
if (hasPartialAccess(activity)) {
|
||||||
|
onPermissionGranted.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
checkPermissionsAndPerformAction(activity, onPermissionGranted, null,
|
checkPermissionsAndPerformAction(activity, onPermissionGranted, null,
|
||||||
rationaleTitle, rationaleMessage, permissions);
|
rationaleTitle, rationaleMessage, permissions);
|
||||||
}
|
}
|
||||||
|
|
@ -125,25 +146,30 @@ public class PermissionUtils {
|
||||||
* @param rationaleTitle rationale title to be displayed when permission was denied
|
* @param rationaleTitle rationale title to be displayed when permission was denied
|
||||||
* @param rationaleMessage rationale message to be displayed when permission was denied
|
* @param rationaleMessage rationale message to be displayed when permission was denied
|
||||||
*/
|
*/
|
||||||
public static void checkPermissionsAndPerformAction(Activity activity,
|
public static void checkPermissionsAndPerformAction(
|
||||||
Runnable onPermissionGranted, Runnable onPermissionDenied, @StringRes int rationaleTitle,
|
final Activity activity,
|
||||||
@StringRes int rationaleMessage, String... permissions) {
|
final Runnable onPermissionGranted,
|
||||||
|
final Runnable onPermissionDenied,
|
||||||
|
final @StringRes int rationaleTitle,
|
||||||
|
final @StringRes int rationaleMessage,
|
||||||
|
final String... permissions
|
||||||
|
) {
|
||||||
Dexter.withActivity(activity)
|
Dexter.withActivity(activity)
|
||||||
.withPermissions(permissions)
|
.withPermissions(permissions)
|
||||||
.withListener(new MultiplePermissionsListener() {
|
.withListener(new MultiplePermissionsListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPermissionsChecked(MultiplePermissionsReport report) {
|
public void onPermissionsChecked(final MultiplePermissionsReport report) {
|
||||||
if (report.areAllPermissionsGranted()) {
|
if (report.areAllPermissionsGranted() || hasPartialAccess(activity)) {
|
||||||
onPermissionGranted.run();
|
onPermissionGranted.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (report.isAnyPermissionPermanentlyDenied()) {
|
if (report.isAnyPermissionPermanentlyDenied()) {
|
||||||
// permission is denied permanently, we will show user a dialog message.
|
// 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(rationaleMessage),
|
||||||
activity.getString(R.string.navigation_item_settings),
|
activity.getString(R.string.navigation_item_settings),
|
||||||
null,
|
null, () -> {
|
||||||
() -> {
|
|
||||||
askUserToManuallyEnablePermissionFromSettings(activity);
|
askUserToManuallyEnablePermissionFromSettings(activity);
|
||||||
if (activity instanceof UploadActivity) {
|
if (activity instanceof UploadActivity) {
|
||||||
((UploadActivity) activity).setShowPermissionsDialog(true);
|
((UploadActivity) activity).setShowPermissionsDialog(true);
|
||||||
|
|
@ -158,13 +184,16 @@ public class PermissionUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions,
|
public void onPermissionRationaleShouldBeShown(
|
||||||
PermissionToken token) {
|
final List<PermissionRequest> permissions,
|
||||||
|
final PermissionToken token
|
||||||
|
) {
|
||||||
if (rationaleTitle == -1 && rationaleMessage == -1) {
|
if (rationaleTitle == -1 && rationaleMessage == -1) {
|
||||||
token.continuePermissionRequest();
|
token.continuePermissionRequest();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DialogUtil.showAlertDialog(activity, activity.getString(rationaleTitle),
|
DialogUtil.showAlertDialog(
|
||||||
|
activity, activity.getString(rationaleTitle),
|
||||||
activity.getString(rationaleMessage),
|
activity.getString(rationaleMessage),
|
||||||
activity.getString(android.R.string.ok),
|
activity.getString(android.R.string.ok),
|
||||||
activity.getString(android.R.string.cancel),
|
activity.getString(android.R.string.cancel),
|
||||||
|
|
@ -173,24 +202,19 @@ public class PermissionUtils {
|
||||||
((UploadActivity) activity).setShowPermissionsDialog(true);
|
((UploadActivity) activity).setShowPermissionsDialog(true);
|
||||||
}
|
}
|
||||||
token.continuePermissionRequest();
|
token.continuePermissionRequest();
|
||||||
}
|
},
|
||||||
,
|
|
||||||
() -> {
|
() -> {
|
||||||
Toast.makeText(activity.getApplicationContext(),
|
Toast.makeText(activity.getApplicationContext(),
|
||||||
R.string.permissions_are_required_for_functionality,
|
R.string.permissions_are_required_for_functionality,
|
||||||
Toast.LENGTH_LONG)
|
Toast.LENGTH_LONG
|
||||||
.show();
|
).show();
|
||||||
token.cancelPermissionRequest();
|
token.cancelPermissionRequest();
|
||||||
if (activity instanceof UploadActivity) {
|
if (activity instanceof UploadActivity) {
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
|
}, null, false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
,
|
}).onSameThread().check();
|
||||||
null,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onSameThread()
|
|
||||||
.check();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.widget.RemoteViews;
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.facebook.common.executors.CallerThreadExecutor;
|
import com.facebook.common.executors.CallerThreadExecutor;
|
||||||
import com.facebook.common.references.CloseableReference;
|
import com.facebook.common.references.CloseableReference;
|
||||||
import com.facebook.datasource.DataSource;
|
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.image.CloseableImage;
|
||||||
import com.facebook.imagepipeline.request.ImageRequest;
|
import com.facebook.imagepipeline.request.ImageRequest;
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||||
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
import fr.free.nrw.commons.media.MediaClient;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||||
|
|
@ -41,17 +38,28 @@ import static android.content.Intent.ACTION_VIEW;
|
||||||
*/
|
*/
|
||||||
public class PicOfDayAppWidget extends AppWidgetProvider {
|
public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
MediaClient mediaClient;
|
MediaClient mediaClient;
|
||||||
|
|
||||||
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
|
void updateAppWidget(
|
||||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
|
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
|
// Launch App on Button Click
|
||||||
Intent viewIntent = new Intent(context, MainActivity.class);
|
final Intent viewIntent = new Intent(context, MainActivity.class);
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, PendingIntent.FLAG_IMMUTABLE);
|
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);
|
views.setOnClickPendingIntent(R.id.camera_button, pendingIntent);
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||||
|
|
||||||
|
|
@ -60,15 +68,17 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the picture of the day using media wiki API
|
* Loads the picture of the day using media wiki API
|
||||||
* @param context
|
* @param context The application context.
|
||||||
* @param views
|
* @param views The RemoteViews object used to update the App Widget UI.
|
||||||
* @param appWidgetManager
|
* @param appWidgetManager The AppWidgetManager instance for managing the widget.
|
||||||
* @param appWidgetId
|
* @param appWidgetId he ID of the App Widget to update.
|
||||||
*/
|
*/
|
||||||
private void loadPictureOfTheDay(Context context,
|
private void loadPictureOfTheDay(
|
||||||
RemoteViews views,
|
final Context context,
|
||||||
AppWidgetManager appWidgetManager,
|
final RemoteViews views,
|
||||||
int appWidgetId) {
|
final AppWidgetManager appWidgetManager,
|
||||||
|
final int appWidgetId
|
||||||
|
) {
|
||||||
compositeDisposable.add(mediaClient.getPictureOfTheDay()
|
compositeDisposable.add(mediaClient.getPictureOfTheDay()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
@ -78,13 +88,20 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle());
|
views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle());
|
||||||
|
|
||||||
// View in browser
|
// View in browser
|
||||||
Intent viewIntent = new Intent();
|
final Intent viewIntent = new Intent();
|
||||||
viewIntent.setAction(ACTION_VIEW);
|
viewIntent.setAction(ACTION_VIEW);
|
||||||
viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri()));
|
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")
|
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
|
* Uses Fresco to load an image from Url
|
||||||
* @param imageUrl
|
* @param imageUrl The URL of the image to load.
|
||||||
* @param context
|
* @param context The application context.
|
||||||
* @param views
|
* @param views The RemoteViews object used to update the App Widget UI.
|
||||||
* @param appWidgetManager
|
* @param appWidgetManager The AppWidgetManager instance for managing the widget.
|
||||||
* @param appWidgetId
|
* @param appWidgetId he ID of the App Widget to update.
|
||||||
*/
|
*/
|
||||||
private void loadImageFromUrl(String imageUrl,
|
private void loadImageFromUrl(
|
||||||
Context context,
|
final String imageUrl,
|
||||||
RemoteViews views,
|
final Context context,
|
||||||
AppWidgetManager appWidgetManager,
|
final RemoteViews views,
|
||||||
int appWidgetId) {
|
final AppWidgetManager appWidgetManager,
|
||||||
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build();
|
final int appWidgetId
|
||||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
) {
|
||||||
DataSource<CloseableReference<CloseableImage>> dataSource
|
final ImageRequest request = ImageRequestBuilder
|
||||||
= imagePipeline.fetchDecodedImage(request, context);
|
.newBuilderWithSource(Uri.parse(imageUrl)).build();
|
||||||
|
final ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||||
|
final DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline
|
||||||
|
.fetchDecodedImage(request, context);
|
||||||
|
|
||||||
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
||||||
@Override
|
@Override
|
||||||
protected void onNewResultImpl(@Nullable Bitmap tempBitmap) {
|
protected void onNewResultImpl(@Nullable final Bitmap tempBitmap) {
|
||||||
Bitmap bitmap = null;
|
Bitmap bitmap = null;
|
||||||
if (tempBitmap != null) {
|
if (tempBitmap != null) {
|
||||||
bitmap = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
bitmap = Bitmap.createBitmap(
|
||||||
Canvas canvas = new Canvas(bitmap);
|
tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888
|
||||||
|
);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint());
|
canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint());
|
||||||
}
|
}
|
||||||
views.setImageViewBitmap(R.id.appwidget_image, bitmap);
|
views.setImageViewBitmap(R.id.appwidget_image, bitmap);
|
||||||
|
|
@ -122,32 +145,37 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
|
protected void onFailureImpl(
|
||||||
|
final DataSource<CloseableReference<CloseableImage>> dataSource
|
||||||
|
) {
|
||||||
// Ignore failure for now.
|
// Ignore failure for now.
|
||||||
}
|
}
|
||||||
}, CallerThreadExecutor.getInstance());
|
}, CallerThreadExecutor.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
public void onUpdate(
|
||||||
|
final Context context,
|
||||||
|
final AppWidgetManager appWidgetManager,
|
||||||
|
final int[] appWidgetIds
|
||||||
|
) {
|
||||||
ApplicationlessInjection
|
ApplicationlessInjection
|
||||||
.getInstance(context
|
.getInstance(context.getApplicationContext())
|
||||||
.getApplicationContext())
|
|
||||||
.getCommonsApplicationComponent()
|
.getCommonsApplicationComponent()
|
||||||
.inject(this);
|
.inject(this);
|
||||||
// There may be multiple widgets active, so update all of them
|
// There may be multiple widgets active, so update all of them
|
||||||
for (int appWidgetId : appWidgetIds) {
|
for (final int appWidgetId : appWidgetIds) {
|
||||||
updateAppWidget(context, appWidgetManager, appWidgetId);
|
updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnabled(Context context) {
|
public void onEnabled(final Context context) {
|
||||||
// Enter relevant functionality for when the first widget is created
|
// Enter relevant functionality for when the first widget is created
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisabled(Context context) {
|
public void onDisabled(final Context context) {
|
||||||
// Enter relevant functionality for when the last widget is disabled
|
// Enter relevant functionality for when the last widget is disabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<include layout="@layout/custom_selector_toolbar" />
|
<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
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/fragment_container"
|
android:id="@+id/fragment_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom_layout"
|
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" />
|
<include layout="@layout/custom_selector_bottom_layout" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ buildscript {
|
||||||
maven { url "https://plugins.gradle.org/m2/" }
|
maven { url "https://plugins.gradle.org/m2/" }
|
||||||
}
|
}
|
||||||
dependencies {
|
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 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
|
||||||
classpath 'org.codehaus.groovy:groovy-all:2.4.15'
|
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
|
#Sun Apr 23 18:22:54 IST 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
Loading…
Add table
Add a link
Reference in a new issue