diff --git a/app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt b/app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt new file mode 100644 index 0000000..340cbb7 --- /dev/null +++ b/app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt @@ -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(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(), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt b/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt index 53d01f2..6b83e20 100644 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt +++ b/app/src/main/java/eu/steffo/twom/create/CreateActivity.kt @@ -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() }, diff --git a/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt b/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt index a72d7e0..662d73f 100644 --- a/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt +++ b/app/src/main/java/eu/steffo/twom/create/CreateActivityContent.kt @@ -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(null) } - var avatarBitmap by rememberSaveable { mutableStateOf(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) diff --git a/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt b/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt index ab9c77a..585b4fa 100644 --- a/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt +++ b/app/src/main/java/eu/steffo/twom/create/ImageHandler.kt @@ -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) + } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/steffo/twom/main/MainActivity.kt b/app/src/main/java/eu/steffo/twom/main/MainActivity.kt index 7fe0251..0e47720 100644 --- a/app/src/main/java/eu/steffo/twom/main/MainActivity.kt +++ b/app/src/main/java/eu/steffo/twom/main/MainActivity.kt @@ -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"