Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/src/main/java/app/grapheneos/camera/ktx/InputStream.kt
Original file line number Diff line number Diff line change
@@ -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
}
147 changes: 135 additions & 12 deletions app/src/main/java/app/grapheneos/camera/ui/activities/CaptureActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,63 @@ 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
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
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
}

lateinit var outputUri: Uri
lateinit var bitmap: Bitmap

private lateinit var retakeIcon: ImageView
private lateinit var whiteOptionCircle: ImageView
protected lateinit var selectImageIcon: ImageView

private lateinit var flipCameraContent: ImageView

protected var isPreviewShown = false

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
}
Expand All @@ -46,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)
Expand All @@ -54,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({

Expand All @@ -68,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
Expand All @@ -93,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()
Expand All @@ -108,12 +149,40 @@ 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 {
confirmImage()
confirmCapturedImage()
}

// Display the activity
Expand Down Expand Up @@ -155,6 +224,8 @@ open class CaptureActivity : MainActivity() {

open fun showPreview() {

isPreviewShown = true

camConfig.cameraProvider?.unbindAll()

mainOverlay.setImageBitmap(bitmap)
Expand All @@ -169,9 +240,14 @@ open class CaptureActivity : MainActivity() {
confirmButton.visibility = View.VISIBLE

previewView.visibility = View.INVISIBLE

thirdOption.visibility = View.INVISIBLE
}

open fun hidePreview() {

isPreviewShown = false

camConfig.startCamera(true)

settingsIcon.visibility = View.VISIBLE
Expand All @@ -183,9 +259,55 @@ open class CaptureActivity : MainActivity() {
confirmButton.visibility = View.INVISIBLE

previewView.visibility = View.VISIBLE

thirdOption.visibility = View.VISIBLE
}

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 confirmImage() {
private fun confirmCapturedImage() {

val resultIntent = Intent("inline-data")

Expand Down Expand Up @@ -234,16 +356,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -712,7 +714,6 @@ open class MainActivity : AppCompatActivity(),
return@setOnClickListener
}

val flipCameraIcon: ImageView = binding.flipCameraIcon
val rotation: Float = if (flipCameraIcon.rotation < 180) {
180f
} else {
Expand Down Expand Up @@ -786,6 +787,8 @@ open class MainActivity : AppCompatActivity(),
}
}

flipCameraIcon = binding.flipCameraIcon

cancelButtonView = binding.cancelButton
// cancelButtonView.setOnClickListener(object : View.OnClickListener {
//
Expand Down
Loading