mirror of
https://github.com/Steffo99/twom.git
synced 2024-11-25 17:44:24 +00:00
Modify bitmaps before using them to create a room
This commit is contained in:
parent
5ca508f645
commit
63bf438b1b
5 changed files with 157 additions and 75 deletions
54
app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt
Normal file
54
app/src/main/java/eu/steffo/twom/create/AvatarSelector.kt
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,11 +26,10 @@ class CreateActivity : ComponentActivity() {
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
resultIntent.putExtra(NAME_EXTRA, name)
|
resultIntent.putExtra(NAME_EXTRA, name)
|
||||||
resultIntent.putExtra(DESCRIPTION_EXTRA, description)
|
resultIntent.putExtra(DESCRIPTION_EXTRA, description)
|
||||||
|
// Kotlin cannot use nullable types in Java interop generics
|
||||||
if (avatarUri != null) {
|
if (avatarUri != null) {
|
||||||
// Kotlin cannot use nullable types in Java interop generics
|
|
||||||
resultIntent.putExtra(AVATAR_EXTRA, avatarUri)
|
resultIntent.putExtra(AVATAR_EXTRA, avatarUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult(RESULT_OK, resultIntent)
|
setResult(RESULT_OK, resultIntent)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
package eu.steffo.twom.create
|
package eu.steffo.twom.create
|
||||||
|
|
||||||
import android.net.Uri
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -23,16 +18,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.steffo.twom.R
|
import eu.steffo.twom.R
|
||||||
import eu.steffo.twom.matrix.avatar.AvatarFromBitmap
|
|
||||||
import eu.steffo.twom.theme.TwoMPadding
|
import eu.steffo.twom.theme.TwoMPadding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -41,40 +32,28 @@ fun CreateActivityContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
|
onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var name by rememberSaveable { mutableStateOf("") }
|
var name by rememberSaveable { mutableStateOf("") }
|
||||||
var description by rememberSaveable { mutableStateOf("") }
|
var description by rememberSaveable { mutableStateOf("") }
|
||||||
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
|
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 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) {
|
Column(modifier) {
|
||||||
Row(TwoMPadding.base) {
|
Row(TwoMPadding.base) {
|
||||||
val avatarContentDescription = stringResource(R.string.create_avatar_label)
|
val avatarContentDescription = stringResource(R.string.create_avatar_label)
|
||||||
Box(
|
AvatarSelector(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(60.dp)
|
.size(60.dp)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.clickable {
|
|
||||||
avatarSelectLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
|
||||||
}
|
|
||||||
.semantics {
|
.semantics {
|
||||||
this.contentDescription = avatarContentDescription
|
this.contentDescription = avatarContentDescription
|
||||||
}
|
},
|
||||||
) {
|
onSelectAvatar = SelectAvatar@{
|
||||||
AvatarFromBitmap(
|
val cache = ImageHandler.bitmapToCache("createAvatar", it)
|
||||||
bitmap = avatarBitmap,
|
avatarUri = Uri.fromFile(cache)
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
|
|
|
@ -6,73 +6,84 @@ import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class ImageHandler {
|
class ImageHandler {
|
||||||
companion object {
|
companion object {
|
||||||
fun uriToBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? {
|
fun getOrientation(contentResolver: ContentResolver, uri: Uri): Int? {
|
||||||
// Open two streams...
|
contentResolver.openInputStream(uri).use {
|
||||||
// One to read the EXIF metadata from:
|
if (it == null) {
|
||||||
val exifStream = contentResolver.openInputStream(uri)
|
return null
|
||||||
// One to read the image data itself from:
|
} else {
|
||||||
val bitmapStream = contentResolver.openInputStream(uri)
|
return ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
|
||||||
|
}
|
||||||
if (exifStream == null || bitmapStream == null) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use the EXIF metadata to determine the orientation of the image
|
fun getRawBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? {
|
||||||
val exifInterface = ExifInterface(exifStream)
|
contentResolver.openInputStream(uri).use {
|
||||||
val orientation =
|
if (it == null) {
|
||||||
exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
|
return null
|
||||||
exifStream.close()
|
} else {
|
||||||
|
return BitmapFactory.decodeStream(it)
|
||||||
// Parse the image data as-is
|
}
|
||||||
val originalBitmap = BitmapFactory.decodeStream(bitmapStream)
|
}
|
||||||
bitmapStream.close()
|
}
|
||||||
|
|
||||||
|
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
|
// Determine the starting points and the size to crop the image to a 1:1 square
|
||||||
val xStart: Int
|
val xStart: Int
|
||||||
val yStart: Int
|
val yStart: Int
|
||||||
val size: Int
|
val size: Int
|
||||||
if (originalBitmap.width > originalBitmap.height) {
|
if (bitmap.width > bitmap.height) {
|
||||||
yStart = 0
|
yStart = 0
|
||||||
xStart = (originalBitmap.width - originalBitmap.height) / 2
|
xStart = (bitmap.width - bitmap.height) / 2
|
||||||
size = originalBitmap.height
|
size = bitmap.height
|
||||||
} else {
|
} else {
|
||||||
xStart = 0
|
xStart = 0
|
||||||
yStart = (originalBitmap.height - originalBitmap.width) / 2
|
yStart = (bitmap.height - bitmap.width) / 2
|
||||||
size = originalBitmap.width
|
size = bitmap.width
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a transformation matrix to rotate the bitmap based on the orientation
|
// Create a transformation matrix to rotate the bitmap based on the orientation
|
||||||
val transformationMatrix = Matrix()
|
val transformationMatrix = Matrix()
|
||||||
|
|
||||||
// TODO: Make sure these transformations are valid
|
// TODO: Make sure all these transformations are valid
|
||||||
when (orientation) {
|
when (orientation) {
|
||||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> transformationMatrix.postScale(
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
|
||||||
-1f,
|
transformationMatrix.postScale(-1f, 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_ROTATE_90 -> transformationMatrix.postRotate(90f)
|
ExifInterface.ORIENTATION_ROTATE_180 -> {
|
||||||
ExifInterface.ORIENTATION_TRANSVERSE -> {/* TODO: Flip horizontally the image Matrix, then transpose it */
|
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
|
return Bitmap.createBitmap(
|
||||||
val croppedBitmap = Bitmap.createBitmap(
|
bitmap,
|
||||||
originalBitmap,
|
|
||||||
xStart,
|
xStart,
|
||||||
yStart,
|
yStart,
|
||||||
size,
|
size,
|
||||||
|
@ -80,8 +91,21 @@ class ImageHandler {
|
||||||
transformationMatrix,
|
transformationMatrix,
|
||||||
true
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.steffo.twom.create.CreateActivity
|
import eu.steffo.twom.create.CreateActivity
|
||||||
import eu.steffo.twom.login.LoginActivity
|
import eu.steffo.twom.login.LoginActivity
|
||||||
|
@ -160,17 +161,42 @@ class MainActivity : ComponentActivity() {
|
||||||
val currentSession = session
|
val currentSession = session
|
||||||
|
|
||||||
val createRoomParams = CreateRoomParams()
|
val createRoomParams = CreateRoomParams()
|
||||||
|
|
||||||
createRoomParams.name = name
|
createRoomParams.name = name
|
||||||
createRoomParams.topic = description
|
createRoomParams.topic = description
|
||||||
createRoomParams.avatarUri = avatarUri
|
|
||||||
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
|
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
|
||||||
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE
|
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(
|
Log.d(
|
||||||
"Main",
|
"Main",
|
||||||
"Creating room '$name' with description '$description' and avatar '$avatarUri'..."
|
"Creating room '$name' with description '$description' and avatar '$avatarUri'..."
|
||||||
)
|
)
|
||||||
val roomId = currentSession!!.roomService().createRoom(createRoomParams)
|
val roomId = currentSession!!.roomService().createRoom(createRoomParams)
|
||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"Main",
|
"Main",
|
||||||
"Created room '$name' with description '$description' and avatar '$avatarUri': $roomId"
|
"Created room '$name' with description '$description' and avatar '$avatarUri': $roomId"
|
||||||
|
|
Loading…
Reference in a new issue