From 4aa743151be9de6c696f23a67e3d56bc60f83d15 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Thu, 22 May 2025 22:48:30 +0530 Subject: [PATCH 1/8] Add extension method to transfer data from InputStream to OutputStream --- .../app/grapheneos/camera/ktx/InputStream.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/src/main/java/app/grapheneos/camera/ktx/InputStream.kt diff --git a/app/src/main/java/app/grapheneos/camera/ktx/InputStream.kt b/app/src/main/java/app/grapheneos/camera/ktx/InputStream.kt new file mode 100644 index 00000000..95582246 --- /dev/null +++ b/app/src/main/java/app/grapheneos/camera/ktx/InputStream.kt @@ -0,0 +1,20 @@ +package app.grapheneos.camera.ktx + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +private const val DEFAULT_BUFFER_SIZE = 0x2000 + +// TODO: Replace calls with transferTo when minSdk becomes 33 or above +@Throws(IOException::class) +fun InputStream.transfer(out: OutputStream): Long { + var transferred: Long = 0 + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + while ((this.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it }) >= 0) { + out.write(buffer, 0, read) + transferred += read.toLong() + } + return transferred +} \ No newline at end of file From 7c60f4910e950146ddb8133f7eded214f0205347 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Thu, 22 May 2025 22:51:23 +0530 Subject: [PATCH 2/8] Add icon for image picker (as image resource) --- app/src/main/res/drawable/image_search.xml | 5 +++++ app/src/main/res/layout/activity_main.xml | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 app/src/main/res/drawable/image_search.xml diff --git a/app/src/main/res/drawable/image_search.xml b/app/src/main/res/drawable/image_search.xml new file mode 100644 index 00000000..382508bb --- /dev/null +++ b/app/src/main/res/drawable/image_search.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index bb02b81c..1175054b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -365,6 +365,15 @@ android:layout_gravity="center" android:contentDescription="@string/play_preview"/> + + From 02e12ff78b3229ec8c4de7d098c21fc47974cbdd Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:33:25 +0530 Subject: [PATCH 3/8] Add string resources (for image and video picker feature) --- app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/values/strings.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1175054b..5c14acb7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -371,7 +371,7 @@ android:layout_height="32dp" android:src="@drawable/image_search" android:layout_gravity="center" - android:contentDescription="@string/select_image_from_picker" + android:contentDescription="@string/select_media_from_picker" android:visibility="gone"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9aa6ae1e..14221c83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,9 @@ The video\'s audio recording has been muted The video\'s audio recording has been unmuted + + Select media from picker + No image selected from picker + No video selected from picker + An unexpected error occurred From d9a9f8e008ece9ab49d16190d3c89aa8ab5d1b68 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:36:55 +0530 Subject: [PATCH 4/8] Refactor code for resize image function --- .../grapheneos/camera/ui/activities/CaptureActivity.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt index 00a2d768..ad3d1a4c 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt @@ -234,16 +234,17 @@ open class CaptureActivity : MainActivity() { private fun resizeImage(image: Bitmap): Bitmap { + // If within supported 1 megabyte size + if (image.byteCount <= 0x100000) + return image + val width = image.width val height = image.height val scaleWidth = width / 10 val scaleHeight = height / 10 - if (image.byteCount <= 1000000) - return image - - return Bitmap.createScaledBitmap(image, scaleWidth, scaleHeight, false) + return image.scale(scaleWidth, scaleHeight, false) } private fun Bitmap.rotate(degrees: Float): Bitmap { From d9816859b26846b73caee3ebbeedc6849f660768 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:38:18 +0530 Subject: [PATCH 5/8] Add flag to know if preview is visible or not --- .../grapheneos/camera/ui/activities/CaptureActivity.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt index ad3d1a4c..2b8a07b0 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt @@ -36,6 +36,9 @@ open class CaptureActivity : MainActivity() { private lateinit var retakeIcon: ImageView private lateinit var flipCameraContent: ImageView + + protected var isPreviewShown = false + lateinit var confirmButton: ImageButton fun isOutputUriAvailable(): Boolean { @@ -155,6 +158,8 @@ open class CaptureActivity : MainActivity() { open fun showPreview() { + isPreviewShown = true + camConfig.cameraProvider?.unbindAll() mainOverlay.setImageBitmap(bitmap) @@ -172,6 +177,9 @@ open class CaptureActivity : MainActivity() { } open fun hidePreview() { + + isPreviewShown = false + camConfig.startCamera(true) settingsIcon.visibility = View.VISIBLE From 65d5efc7f885e3125f28f9d8ce73d0e98c8cc731 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:40:16 +0530 Subject: [PATCH 6/8] Add code for image picker functionality --- .../camera/ui/activities/CaptureActivity.kt | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt index 2b8a07b0..e364a169 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt @@ -14,6 +14,8 @@ import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.widget.ImageButton import android.widget.ImageView +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageProxy @@ -41,6 +43,14 @@ open class CaptureActivity : MainActivity() { lateinit var confirmButton: ImageButton + private val imagePicker = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri != null) { + confirmPickedImage(uri) + } else { + showMessage(R.string.no_image_selected) + } + } + fun isOutputUriAvailable(): Boolean { return ::outputUri.isInitialized } @@ -116,7 +126,7 @@ open class CaptureActivity : MainActivity() { } confirmButton.setOnClickListener { - confirmImage() + confirmCapturedImage() } // Display the activity @@ -193,7 +203,51 @@ open class CaptureActivity : MainActivity() { previewView.visibility = View.VISIBLE } - private fun confirmImage() { + private fun confirmPickedImage(uri: Uri) { + + val resultIntent = Intent("inline-data") + + if (::outputUri.isInitialized) { + + try { + val fis = contentResolver.openInputStream(uri) + if (fis != null) { + fis.use { + val fos = contentResolver.openOutputStream(outputUri) + if (fos != null) { + fos.use { + fis.transfer(fos) + setResult(RESULT_OK) + finish() + } + } else { + showMessage(R.string.unexpected_error_occurred) + Log.e(TAG, "Output URI's output stream found null") + } + } + } else { + showMessage(R.string.unexpected_error_occurred) + Log.e(TAG, "Chosen Image URI's input stream found null") + } + } catch (e: Exception) { + showMessage(R.string.unexpected_error_occurred) + Log.e(TAG, "Error occurred while writing back image to output URI", e) + } + } else { + try { + bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, uri)) + bitmap = resizeImage(bitmap) + resultIntent.putExtra("data", bitmap) + setResult(RESULT_OK, resultIntent) + finish() + } catch (e: Exception) { + showMessage(R.string.unexpected_error_occurred) + Log.e(TAG, "Error while sending bitmap to caller activity", e) + } + } + } + + private fun confirmCapturedImage() { val resultIntent = Intent("inline-data") From 5f8d3769b463d904c8061018f9f7909621c3b577 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:41:52 +0530 Subject: [PATCH 7/8] Add code for video picker functionality --- .../camera/ui/activities/CaptureActivity.kt | 72 +++++++++++++++++-- .../camera/ui/activities/MainActivity.kt | 5 +- .../ui/activities/VideoCaptureActivity.kt | 23 +++++- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt index e364a169..9d6e6245 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt @@ -4,14 +4,19 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.Bitmap.CompressFormat import android.graphics.BitmapFactory +import android.graphics.ImageDecoder import android.graphics.Matrix import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.MediaStore +import android.util.Log import android.view.View import android.view.ViewGroup.MarginLayoutParams +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation import android.widget.ImageButton import android.widget.ImageView import androidx.activity.result.PickVisualMediaRequest @@ -23,12 +28,15 @@ import androidx.core.content.ContextCompat import app.grapheneos.camera.R import app.grapheneos.camera.util.getParcelableExtra import java.io.ByteArrayOutputStream -import java.lang.Exception import java.nio.ByteBuffer +import androidx.core.graphics.scale +import app.grapheneos.camera.ktx.transfer +import kotlin.Exception open class CaptureActivity : MainActivity() { companion object { + private const val TAG = "CaptureActivity" private const val CAPTURE_BUTTON_APPEARANCE_DELAY = 1000L } @@ -36,6 +44,8 @@ open class CaptureActivity : MainActivity() { lateinit var bitmap: Bitmap private lateinit var retakeIcon: ImageView + private lateinit var whiteOptionCircle: ImageView + protected lateinit var selectImageIcon: ImageView private lateinit var flipCameraContent: ImageView @@ -59,6 +69,8 @@ open class CaptureActivity : MainActivity() { super.onCreate(savedInstanceState) retakeIcon = findViewById(R.id.retake_icon) + selectImageIcon = findViewById(R.id.select_image_icon) + whiteOptionCircle = findViewById(R.id.white_option_circle) flipCameraContent = findViewById(R.id.flip_camera_icon_content) confirmButton = findViewById(R.id.confirm_button) @@ -67,10 +79,22 @@ open class CaptureActivity : MainActivity() { outputUri = it } + imagePreview.visibility = View.GONE + whiteOptionCircle.visibility = View.GONE + selectImageIcon.visibility = View.VISIBLE + + thirdCircle.setOnClickListener { + imagePicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + // Disable capture button for a while (to avoid picture capture) captureButton.isEnabled = false captureButton.alpha = 0f + // Disable capture button for a while (to avoid picture capture) + thirdCircle.isEnabled = false + thirdOption.alpha = 0f + // Enable the capture button after a while Handler(Looper.getMainLooper()).postDelayed({ @@ -81,6 +105,13 @@ open class CaptureActivity : MainActivity() { captureButton.isEnabled = true } + thirdOption.animate() + .alpha(1f) + .setDuration(300) + .withEndAction { + thirdCircle.isEnabled = true + } + }, CAPTURE_BUTTON_APPEARANCE_DELAY) // Remove the modes tab layout as we do not want the user to be able to switch to @@ -106,9 +137,6 @@ open class CaptureActivity : MainActivity() { finish() } - // Remove the third option/circle from the UI - thirdOption.visibility = View.INVISIBLE - captureButton.setOnClickListener { if (timerDuration == 0) { takePicture() @@ -121,8 +149,36 @@ open class CaptureActivity : MainActivity() { } } - retakeIcon.setOnClickListener { - hidePreview() + flipCameraCircle.setOnClickListener { + if (isPreviewShown) { + hidePreview() + return@setOnClickListener + } + + if (videoCapturer.isRecording) { + videoCapturer.isPaused = !videoCapturer.isPaused + return@setOnClickListener + } + + val rotation: Float = if (flipCameraIcon.rotation < 180) { + 180f + } else { + 360f + } + + val rotate = RotateAnimation( + 0F, + rotation, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.5f + ) + rotate.duration = 400 + rotate.interpolator = LinearInterpolator() + + it.startAnimation(rotate) + camConfig.toggleCameraSelector() } confirmButton.setOnClickListener { @@ -184,6 +240,8 @@ open class CaptureActivity : MainActivity() { confirmButton.visibility = View.VISIBLE previewView.visibility = View.INVISIBLE + + thirdOption.visibility = View.INVISIBLE } open fun hidePreview() { @@ -201,6 +259,8 @@ open class CaptureActivity : MainActivity() { confirmButton.visibility = View.INVISIBLE previewView.visibility = View.VISIBLE + + thirdOption.visibility = View.VISIBLE } private fun confirmPickedImage(uri: Uri) { diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt index 6895accc..644a63f2 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt @@ -193,6 +193,8 @@ open class MainActivity : AppCompatActivity(), lateinit var gCircleFrame: FrameLayout + lateinit var flipCameraIcon: ImageView + private lateinit var gAngleTextView: TextView private lateinit var gCircle: LinearLayout @@ -712,7 +714,6 @@ open class MainActivity : AppCompatActivity(), return@setOnClickListener } - val flipCameraIcon: ImageView = binding.flipCameraIcon val rotation: Float = if (flipCameraIcon.rotation < 180) { 180f } else { @@ -786,6 +787,8 @@ open class MainActivity : AppCompatActivity(), } } + flipCameraIcon = binding.flipCameraIcon + cancelButtonView = binding.cancelButton // cancelButtonView.setOnClickListener(object : View.OnClickListener { // diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt index da57086c..f864c8b5 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt @@ -6,6 +6,8 @@ import android.os.Bundle import android.provider.MediaStore.EXTRA_OUTPUT import android.view.View import android.widget.ImageView +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import app.grapheneos.camera.R class VideoCaptureActivity : CaptureActivity() { @@ -15,6 +17,14 @@ class VideoCaptureActivity : CaptureActivity() { private var savedUri: Uri? = null + private val videoPicker = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri != null) { + confirmSelectedVideo(uri) + } else { + showMessage(R.string.no_video_selected) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,7 +55,7 @@ class VideoCaptureActivity : CaptureActivity() { playPreview.visibility = View.VISIBLE confirmButton.setOnClickListener { - confirmVideo() + confirmCapturedVideo() } } @@ -66,7 +76,7 @@ class VideoCaptureActivity : CaptureActivity() { thirdOption.visibility = View.VISIBLE } - private fun confirmVideo() { + private fun confirmCapturedVideo() { if (savedUri == null) { setResult(RESULT_CANCELED) } else { @@ -82,6 +92,15 @@ class VideoCaptureActivity : CaptureActivity() { finish() } + private fun confirmSelectedVideo(uri: Uri) { + val resultIntent = Intent() + resultIntent.data = uri + resultIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + resultIntent.putExtra(EXTRA_OUTPUT, uri) + setResult(RESULT_OK, resultIntent) + finish() + } + override fun hidePreview() { super.hidePreview() thirdOption.visibility = View.INVISIBLE From 627324b00ece3ece30962ebe61c27015c4986451 Mon Sep 17 00:00:00 2001 From: MHShetty Date: Fri, 23 May 2025 22:42:46 +0530 Subject: [PATCH 8/8] Make video picker UI functional --- .../ui/activities/VideoCaptureActivity.kt | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt index f864c8b5..2186d346 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt @@ -33,6 +33,19 @@ class VideoCaptureActivity : CaptureActivity() { captureButton.setImageResource(R.drawable.recording) + thirdCircle.setOnClickListener { + if (isPreviewShown) { + val i = Intent( + this@VideoCaptureActivity, + VideoPlayer::class.java + ) + i.putExtra("videoUri", savedUri) + startActivity(i) + } else { + videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) + } + } + captureButton.setOnClickListener OnClickListener@{ if (videoCapturer.isRecording) { videoCapturer.stopRecording() @@ -41,18 +54,9 @@ class VideoCaptureActivity : CaptureActivity() { } } - playPreview.setOnClickListener { - val i = Intent( - this@VideoCaptureActivity, - VideoPlayer::class.java - ) - i.putExtra("videoUri", savedUri) - startActivity(i) - } - imagePreview.visibility = View.GONE whiteOptionCircle.visibility = View.GONE - playPreview.visibility = View.VISIBLE + // playPreview.visibility = View.VISIBLE confirmButton.setOnClickListener { confirmCapturedVideo() @@ -74,6 +78,9 @@ class VideoCaptureActivity : CaptureActivity() { override fun showPreview() { super.showPreview() thirdOption.visibility = View.VISIBLE + selectImageIcon.visibility = View.GONE + playPreview.visibility = View.VISIBLE + thirdCircle.setImageResource(R.drawable.option_circle) } private fun confirmCapturedVideo() { @@ -103,6 +110,7 @@ class VideoCaptureActivity : CaptureActivity() { override fun hidePreview() { super.hidePreview() - thirdOption.visibility = View.INVISIBLE + selectImageIcon.visibility = View.VISIBLE + playPreview.visibility = View.GONE } } \ No newline at end of file