1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-25 01:24:24 +00:00

Modify bitmaps before using them to create a room

This commit is contained in:
Steffo 2023-12-07 01:31:47 +01:00
parent 5ca508f645
commit 63bf438b1b
Signed by: steffo
GPG key ID: 2A24051445686895
5 changed files with 157 additions and 75 deletions

View file

@ -0,0 +1,54 @@
package eu.steffo.twom.create
import android.graphics.Bitmap
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.avatar.AvatarFromBitmap
@Composable
@Preview
fun AvatarSelector(
modifier: Modifier = Modifier,
onSelectAvatar: (bitmap: Bitmap) -> Unit = {},
) {
val context = LocalContext.current
val resolver = context.contentResolver
var selection by rememberSaveable { mutableStateOf<Bitmap?>(null) }
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) ImageSelect@{
it ?: return@ImageSelect
val rawBitmap = ImageHandler.getRawBitmap(resolver, it) ?: return@ImageSelect
val orientation = ImageHandler.getOrientation(resolver, it) ?: return@ImageSelect
val correctedBitmap = ImageHandler.squareAndOrient(rawBitmap, orientation)
selection = correctedBitmap
onSelectAvatar(correctedBitmap)
}
Box(
modifier = modifier
.clickable {
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
) {
AvatarFromBitmap(
bitmap = selection?.asImageBitmap(),
)
}
}

View file

@ -26,11 +26,10 @@ class CreateActivity : ComponentActivity() {
val resultIntent = Intent()
resultIntent.putExtra(NAME_EXTRA, name)
resultIntent.putExtra(DESCRIPTION_EXTRA, description)
// Kotlin cannot use nullable types in Java interop generics
if (avatarUri != null) {
// Kotlin cannot use nullable types in Java interop generics
resultIntent.putExtra(AVATAR_EXTRA, avatarUri)
}
setResult(RESULT_OK, resultIntent)
finish()
},

View file

@ -1,11 +1,6 @@
package eu.steffo.twom.create
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -23,16 +18,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.steffo.twom.R
import eu.steffo.twom.matrix.avatar.AvatarFromBitmap
import eu.steffo.twom.theme.TwoMPadding
@Composable
@ -41,40 +32,28 @@ fun CreateActivityContent(
modifier: Modifier = Modifier,
onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
) {
val context = LocalContext.current
var name by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") }
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
var avatarBitmap by rememberSaveable { mutableStateOf<ImageBitmap?>(null) }
// val avatarBitmap = if(avatarUri != null) BitmapFactory.decodeFile(avatarUri.toString()).asImageBitmap() else null
val avatarSelectLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) {
avatarUri = it
avatarBitmap = if (it != null) ImageHandler.uriToBitmap(context.contentResolver, it)
?.asImageBitmap() else null
}
Column(modifier) {
Row(TwoMPadding.base) {
val avatarContentDescription = stringResource(R.string.create_avatar_label)
Box(
AvatarSelector(
modifier = Modifier
.size(60.dp)
.clip(MaterialTheme.shapes.medium)
.clickable {
avatarSelectLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
.semantics {
this.contentDescription = avatarContentDescription
}
) {
AvatarFromBitmap(
bitmap = avatarBitmap,
)
}
},
onSelectAvatar = SelectAvatar@{
val cache = ImageHandler.bitmapToCache("createAvatar", it)
avatarUri = Uri.fromFile(cache)
},
)
TextField(
modifier = Modifier
.height(60.dp)

View file

@ -6,73 +6,84 @@ import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import java.io.File
class ImageHandler {
companion object {
fun uriToBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? {
// Open two streams...
// One to read the EXIF metadata from:
val exifStream = contentResolver.openInputStream(uri)
// One to read the image data itself from:
val bitmapStream = contentResolver.openInputStream(uri)
if (exifStream == null || bitmapStream == null) {
return null
fun getOrientation(contentResolver: ContentResolver, uri: Uri): Int? {
contentResolver.openInputStream(uri).use {
if (it == null) {
return null
} else {
return ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
}
}
}
// Use the EXIF metadata to determine the orientation of the image
val exifInterface = ExifInterface(exifStream)
val orientation =
exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
exifStream.close()
// Parse the image data as-is
val originalBitmap = BitmapFactory.decodeStream(bitmapStream)
bitmapStream.close()
fun getRawBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? {
contentResolver.openInputStream(uri).use {
if (it == null) {
return null
} else {
return BitmapFactory.decodeStream(it)
}
}
}
fun squareAndOrient(
bitmap: Bitmap,
orientation: Int = ExifInterface.ORIENTATION_NORMAL
): Bitmap {
// Determine the starting points and the size to crop the image to a 1:1 square
val xStart: Int
val yStart: Int
val size: Int
if (originalBitmap.width > originalBitmap.height) {
if (bitmap.width > bitmap.height) {
yStart = 0
xStart = (originalBitmap.width - originalBitmap.height) / 2
size = originalBitmap.height
xStart = (bitmap.width - bitmap.height) / 2
size = bitmap.height
} else {
xStart = 0
yStart = (originalBitmap.height - originalBitmap.width) / 2
size = originalBitmap.width
yStart = (bitmap.height - bitmap.width) / 2
size = bitmap.width
}
// Create a transformation matrix to rotate the bitmap based on the orientation
val transformationMatrix = Matrix()
// TODO: Make sure these transformations are valid
// TODO: Make sure all these transformations are valid
when (orientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> transformationMatrix.postScale(
-1f,
1f
)
ExifInterface.ORIENTATION_ROTATE_180 -> transformationMatrix.postRotate(180f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> transformationMatrix.postScale(
1f,
-1f
)
ExifInterface.ORIENTATION_TRANSPOSE -> {/* TODO: Transpose the image Matrix */
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
transformationMatrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_ROTATE_90 -> transformationMatrix.postRotate(90f)
ExifInterface.ORIENTATION_TRANSVERSE -> {/* TODO: Flip horizontally the image Matrix, then transpose it */
ExifInterface.ORIENTATION_ROTATE_180 -> {
transformationMatrix.postRotate(180f)
}
ExifInterface.ORIENTATION_ROTATE_270 -> transformationMatrix.postRotate(270f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
transformationMatrix.postScale(1f, -1f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
/* TODO: Transpose the image Matrix */
}
ExifInterface.ORIENTATION_ROTATE_90 -> {
transformationMatrix.postRotate(90f)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
/* TODO: Flip horizontally the image Matrix, then transpose it */
}
ExifInterface.ORIENTATION_ROTATE_270 -> {
transformationMatrix.postRotate(270f)
}
}
// Crop the bitmap
val croppedBitmap = Bitmap.createBitmap(
originalBitmap,
return Bitmap.createBitmap(
bitmap,
xStart,
yStart,
size,
@ -80,8 +91,21 @@ class ImageHandler {
transformationMatrix,
true
)
}
return croppedBitmap
fun bitmapToCache(id: String, bitmap: Bitmap): File {
val file = File.createTempFile("bitmap_$id", ".jpg")
file.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it)
it.flush()
}
return file
}
fun bitmapFromCache(file: File): Bitmap {
file.inputStream().use {
return BitmapFactory.decodeStream(it)
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
import eu.steffo.twom.create.CreateActivity
import eu.steffo.twom.login.LoginActivity
@ -160,17 +161,42 @@ class MainActivity : ComponentActivity() {
val currentSession = session
val createRoomParams = CreateRoomParams()
createRoomParams.name = name
createRoomParams.topic = description
createRoomParams.avatarUri = avatarUri
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE
when (avatarUri?.toFile()?.isFile) {
false -> {
Log.e(
"Main",
"Avatar has been deleted from cache before room could possibly be created, ignoring..."
)
}
true -> {
Log.d(
"Main",
"Avatar seems to exist at: $avatarUri"
)
createRoomParams.avatarUri = avatarUri
}
null -> {
Log.d(
"Main",
"Avatar was not set, ignoring..."
)
}
}
Log.d(
"Main",
"Creating room '$name' with description '$description' and avatar '$avatarUri'..."
)
val roomId = currentSession!!.roomService().createRoom(createRoomParams)
Log.d(
"Main",
"Created room '$name' with description '$description' and avatar '$avatarUri': $roomId"