Skip to content

Commit

Permalink
Migrate to android native photo picker approach. Deprecate usage of R…
Browse files Browse the repository at this point in the history
…EAD_MEDIA_IMAGES permission. (#353)
  • Loading branch information
ShiftHackZ authored Nov 8, 2024
1 parent 1113109 commit f898885
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 30 deletions.
1 change: 0 additions & 1 deletion app/src/foss/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
Expand Down
1 change: 0 additions & 1 deletion app/src/full/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,15 @@
android:name="autoStoreLocales"
android:value="true" />
</service>

<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
</application>
</manifest>
2 changes: 2 additions & 0 deletions app/src/main/res/xml/file_provider_paths.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
name="folder_work"
path="work/" />

<cache-path name="cache" path="." />

<external-path name="media" path="." />
<external-path name="external_files" path="."/>
<external-path name="external" path="."/>
Expand Down
1 change: 0 additions & 1 deletion app/src/playstore/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
Expand Down
2 changes: 0 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ crop = "0.1.1"
mvi = "1.0.2"
preferences = "1.0.1"
dayNightSwitch = "1.0.0"
imagepicker = "v2.0.3"
catppuccin = "0.1.2"
turbine = "1.2.0"
roboelectric = "4.13"
Expand Down Expand Up @@ -108,7 +107,6 @@ compose-gestures = { group = "com.github.SmartToolFactory", name = "Compose-Exte
compose-crop = { group = "io.github.mr0xf00", name = "easycrop", version.ref = "crop" }
shifthackz-mvi = { group = "com.github.ShiftHackZ", name = "AndroidCoreMVI", version.ref = "mvi" }
shifthackz-preferences = { group = "com.github.ShiftHackZ", name = "AndroidPreferences", version.ref = "preferences" }
shifthackz-imagepicker = { group = "com.github.ShiftHackZ", name = "ImagePicker", version.ref = "imagepicker" }
shifthackz-daynightswitch = { group = "com.github.ShiftHackZ", name = "DayNightSwitch", version.ref = "dayNightSwitch" }
shifthackz-catppuccin-legacy = { group = "com.github.ShiftHackZ.Catppuccin-Android-Library", name = "palette-legacy", version.ref = "catppuccin" }
shifthackz-catppuccin-compose = { group = "com.github.ShiftHackZ.Catppuccin-Android-Library", name = "compose", version.ref = "catppuccin" }
Expand Down
1 change: 0 additions & 1 deletion presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ dependencies {
implementation(libs.rx.kotlin)
implementation(libs.rx.android)

implementation(libs.shifthackz.imagepicker)
implementation(libs.shifthackz.daynightswitch)
implementation(libs.shifthackz.catppuccin.compose)
implementation(libs.shifthackz.catppuccin.splash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.shifthackz.aisdv1.domain.entity.StabilityAiStylePreset
import com.shifthackz.aisdv1.presentation.model.Modal
import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent
import com.shifthackz.android.core.mvi.MviIntent
import com.shz.imagepicker.imagepicker.model.PickedResult

sealed interface GenerationMviIntent : MviIntent {

Expand Down Expand Up @@ -109,7 +108,7 @@ sealed interface ImageToImageIntent : GenerationMviIntent {

data class UpdateImage(val bitmap: Bitmap) : ImageToImageIntent

data class CropImage(val result: PickedResult) : ImageToImageIntent
data class CropImage(val bitmap: Bitmap) : ImageToImageIntent

enum class Pick : ImageToImageIntent {
Camera, Gallery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

package com.shifthackz.aisdv1.presentation.screen.img2img

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -53,6 +56,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import com.shifthackz.aisdv1.core.common.file.FileProviderDescriptor
import com.shifthackz.aisdv1.core.common.math.roundTo
import com.shifthackz.aisdv1.core.model.UiText
Expand All @@ -70,33 +74,73 @@ import com.shifthackz.aisdv1.presentation.screen.inpaint.components.InPaintCompo
import com.shifthackz.aisdv1.presentation.theme.sliderColors
import com.shifthackz.aisdv1.presentation.utils.Constants.DENOISING_STRENGTH_MAX
import com.shifthackz.aisdv1.presentation.utils.Constants.DENOISING_STRENGTH_MIN
import com.shifthackz.aisdv1.presentation.utils.PermissionUtil
import com.shifthackz.aisdv1.presentation.utils.uriToBitmap
import com.shifthackz.aisdv1.presentation.widget.input.GenerationInputForm
import com.shifthackz.aisdv1.presentation.widget.toolbar.GenerationBottomToolbar
import com.shifthackz.aisdv1.presentation.widget.work.BackgroundWorkWidget
import com.shz.imagepicker.imagepicker.ImagePicker
import com.shz.imagepicker.imagepicker.model.GalleryPicker
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import java.io.File
import com.shifthackz.aisdv1.core.localization.R as LocalizationR

@Composable
fun ImageToImageScreen() {
val context = LocalContext.current
val viewModel = koinViewModel<ImageToImageViewModel>()
val fileProviderDescriptor: FileProviderDescriptor = koinInject()

val cameraFile = File(context.cacheDir, "camera.jpg").apply {
createNewFile()
deleteOnExit()
}

val cameraUri = FileProvider.getUriForFile(
context,
fileProviderDescriptor.providerPath,
cameraFile,
)

val cameraPicker = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture(),
) { success ->
if (!success) return@rememberLauncherForActivityResult
val bitmap = uriToBitmap(context, cameraUri) ?: return@rememberLauncherForActivityResult
viewModel.processIntent(ImageToImageIntent.CropImage(bitmap))
}

val cameraPermission = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) return@rememberLauncherForActivityResult
cameraPicker.launch(cameraUri)
}

val mediaPicker = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia(),
) { uri ->
val bitmap =
uri?.let { uriToBitmap(context, it) } ?: return@rememberLauncherForActivityResult
viewModel.processIntent(ImageToImageIntent.CropImage(bitmap))
}

MviComponent(
viewModel = viewModel,
processEffect = { effect ->
ImagePicker.Builder(fileProviderDescriptor.providerPath) { result ->
viewModel.processIntent(ImageToImageIntent.CropImage(result))
when (effect) {
ImageToImageEffect.GalleryPicker -> {
val request = PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly,
)
mediaPicker.launch(request)
}

ImageToImageEffect.CameraPicker -> {
if (PermissionUtil.checkCameraPermission(context, cameraPermission::launch)) {
cameraPicker.launch(cameraUri)
}
}
}
.useGallery(effect == ImageToImageEffect.GalleryPicker)
.useCamera(effect == ImageToImageEffect.CameraPicker)
.autoRotate(effect == ImageToImageEffect.GalleryPicker)
.multipleSelection(false)
.galleryPicker(GalleryPicker.NATIVE)
.build()
.launch(context)
},
) { state, intentHandler ->
ScreenContent(
Expand All @@ -111,7 +155,7 @@ fun ImageToImageScreen() {
private fun ScreenContent(
modifier: Modifier = Modifier,
state: ImageToImageState,
processIntent: (GenerationMviIntent) -> Unit = {}
processIntent: (GenerationMviIntent) -> Unit = {},
) {
val promptChipTextFieldState = remember { mutableStateOf(TextFieldValue()) }
val negativePromptChipTextFieldState = remember { mutableStateOf(TextFieldValue()) }
Expand Down Expand Up @@ -340,7 +384,7 @@ private fun ScreenContent(
ServerSource.OPEN_AI -> LocalizationR.string.action_change_configuration

else -> LocalizationR.string.action_generate
}
},
),
color = LocalContentColor.current,
)
Expand Down Expand Up @@ -499,7 +543,7 @@ private fun ImagePickButtonBox(
id = when (buttonType) {
ImagePickButton.PHOTO -> LocalizationR.string.action_image_picker_gallery
ImagePickButton.CAMERA -> LocalizationR.string.action_image_picker_camera
}
},
),
fontSize = 17.sp,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.shifthackz.aisdv1.presentation.model.Modal
import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter
import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter
import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintStateProducer
import com.shz.imagepicker.imagepicker.model.PickedResult
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
Expand Down Expand Up @@ -141,12 +140,8 @@ class ImageToImageViewModel(

ImageToImageIntent.Pick.Gallery -> emitEffect(ImageToImageEffect.GalleryPicker)

is ImageToImageIntent.CropImage -> when (intent.result) {
is PickedResult.Single -> updateState {
it.copy(screenModal = Modal.Image.Crop(intent.result.image.bitmap))
}

else -> Unit
is ImageToImageIntent.CropImage -> updateState {
it.copy(screenModal = Modal.Image.Crop(intent.bitmap))
}

is ImageToImageIntent.UpdateImage -> updateState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object PermissionUtil {
fun checkStoragePermission(
context: Context,
onLaunch: (missingPermissions: Array<String>) -> Unit = {},
): Boolean {
): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return false
}
Expand Down Expand Up @@ -43,4 +43,19 @@ object PermissionUtil {
}
return true
}

fun checkCameraPermission(
context: Context,
onLaunch: (missingPermission: String) -> Unit,
): Boolean {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
onLaunch(Manifest.permission.CAMERA)
return false
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.shifthackz.aisdv1.presentation.utils

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import com.shifthackz.aisdv1.core.common.log.errorLog

fun uriToBitmap(context: Context, uri: Uri): Bitmap? = try {
val inputStream = context.contentResolver.openInputStream(uri)
BitmapFactory.decodeStream(inputStream).also { inputStream?.close() }
} catch (e: Exception) {
errorLog("UrlToBitmap", e)
null
}

0 comments on commit f898885

Please sign in to comment.