@ -10,34 +10,33 @@ import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Size
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.WindowManager
import android.view.*
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.EditText
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.*
//import androidx.camera.core.VideoCapture
import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.video_recording.SeekableWriter
import sushi.hardcore.droidfs.video_recording.VideoCapture
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.*
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executor
@ -80,10 +79,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
private var camera : Camera ? = null
private var resolutions : List < Size > ? = null
private var currentResolutionIndex : Int = 0
private var currentResolution : Size ? = null
private var captureMode = ImageCapture . CAPTURE _MODE _MAXIMIZE _QUALITY
private var isBackCamera = true
private var isInVideoMode = false
private var isRecording = false
private var isWaitingForTimer = false
private lateinit var binding : ActivityCameraBinding
override fun onCreate ( savedInstanceState : Bundle ? ) {
@ -91,7 +92,8 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
usf _keep _open = sharedPrefs . getBoolean ( " usf_keep_open " , false )
binding = ActivityCameraBinding . inflate ( layoutInflater )
setContentView ( binding . root )
gocryptfsVolume = GocryptfsVolume ( intent . getIntExtra ( " sessionID " , - 1 ) )
supportActionBar ?. hide ( )
gocryptfsVolume = GocryptfsVolume ( applicationContext , intent . getIntExtra ( " sessionID " , - 1 ) )
outputDirectory = intent . getStringExtra ( " path " ) !!
if ( Build . VERSION . SDK _INT >= Build . VERSION _CODES . M ) {
@ -110,13 +112,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
ProcessCameraProvider . getInstance ( this ) . apply {
addListener ( {
cameraProvider = get ( )
setupCamera ( )
} , executor )
}
ExtensionsManager . getInstance ( this ) . apply {
addListener ( {
extensionsManager = get ( )
setupCamera ( )
ExtensionsManager . getInstanceAsync ( this @CameraActivity , cameraProvider ) . apply {
addListener ( {
extensionsManager = get ( )
setupCamera ( )
} , executor )
}
} , executor )
}
@ -126,9 +127,9 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
} else {
1
}
Colored AlertDialogBuilder ( this )
Custom AlertDialogBuilder ( this , themeValue )
. setTitle ( R . string . camera _optimization )
. setSingleChoiceItems ( DialogSingleChoiceAdapter ( this , arrayOf ( R . string . maximize _quality , R . string . minimize _latency ) . map { getString ( it ) } ) , currentIndex ) { dialog , which ->
. setSingleChoiceItems ( arrayOf ( getString ( R . string . maximize _quality ) , getString ( R . string . minimize _latency ) ) , currentIndex ) { dialog , which ->
val resId : Int
val newCaptureMode = if ( which == 0 ) {
resId = R . drawable . icon _high _quality
@ -140,7 +141,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
if ( newCaptureMode != captureMode ) {
captureMode = newCaptureMode
binding . imageCaptureMode . setImageResource ( resId )
setupCamera ( )
if ( !is InVideoMode ) {
cameraProvider . unbind ( imageCapture )
refreshImageCapture ( )
cameraProvider . bindToLifecycle ( this , cameraSelector , imageCapture )
}
}
dialog . dismiss ( )
}
@ -149,61 +154,65 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
}
binding . imageRatio . setOnClickListener {
resolutions ?. let {
Colored AlertDialogBuilder ( this )
Custom AlertDialogBuilder ( this , themeValue )
. setTitle ( R . string . choose _resolution )
. setSingleChoiceItems ( DialogSingleChoiceAdapter ( this , it . map { size -> size . toString ( ) } ) , currentResolutionIndex ) { dialog , which ->
setupCamera ( resolutions !! [ which ] )
dialog . dismiss ( )
. setSingleChoiceItems ( it . map { size -> size . toString ( ) } . toTypedArray ( ) , currentResolutionIndex ) { dialog , which ->
currentResolution = resolutions !! [ which ]
currentResolutionIndex = which
setupCamera ( )
dialog . dismiss ( )
}
. setNegativeButton ( R . string . cancel , null )
. show ( )
}
}
binding . imageTimer . setOnClickListener {
val dialogEditTextView = layoutInflater . inflate ( R . layout . dialog _edit _text , null )
val dialogEditText = dialogEditTextView . findViewById < EditText > ( R . id . dialog _edit _text )
dialogEditText . inputType = InputType . TYPE _CLASS _NUMBER
val dialog = ColoredAlertDialogBuilder ( this )
. setView ( dialogEditTextView )
. setTitle ( getString ( R . string . enter _timer _duration ) )
. setPositiveButton ( R . string . ok ) { _ , _ ->
val enteredValue = dialogEditText . text . toString ( )
if ( enteredValue . isEmpty ( ) ) {
Toast . makeText ( this , getString ( R . string . timer _empty _error _msg ) , Toast . LENGTH _SHORT ) . show ( )
} else {
timerDuration = enteredValue . toInt ( )
}
with ( EditTextDialog ( this , R . string . enter _timer _duration ) {
try {
timerDuration = it . toInt ( )
} catch ( e : NumberFormatException ) {
Toast . makeText ( this , R . string . invalid _number , Toast . LENGTH _SHORT ) . show ( )
}
. setNegativeButton ( R . string . cancel , null )
. create ( )
dialogEditText . setOnEditorActionListener { _ , _ , _ ->
timerDuration = dialogEditText . text . toString ( ) . toInt ( )
dialog . dismiss ( )
true
} ) {
binding . dialogEditText . inputType = InputType . TYPE _CLASS _NUMBER
show ( )
}
dialog . window ?. setSoftInputMode ( WindowManager . LayoutParams . SOFT _INPUT _STATE _ALWAYS _VISIBLE )
dialog . show ( )
}
binding . imageFlash . setOnClickListener {
binding . imageFlash . setImageResource ( when ( imageCapture ?. flashMode ) {
ImageCapture . FLASH _MODE _AUTO -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _ON
R . drawable . icon _flash _on
}
ImageCapture . FLASH _MODE _ON -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _OFF
R . drawable . icon _flash _off
binding . imageFlash . setImageResource ( if ( isInVideoMode ) {
when ( imageCapture ?. flashMode ) {
ImageCapture . FLASH _MODE _ON -> {
camera ?. cameraControl ?. enableTorch ( false )
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _OFF
R . drawable . icon _flash _off
}
else -> {
camera ?. cameraControl ?. enableTorch ( true )
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _ON
R . drawable . icon _flash _on
}
}
else -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _AUTO
R . drawable . icon _flash _auto
} else {
when ( imageCapture ?. flashMode ) {
ImageCapture . FLASH _MODE _AUTO -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _ON
R . drawable . icon _flash _on
}
ImageCapture . FLASH _MODE _ON -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _OFF
R . drawable . icon _flash _off
}
else -> {
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _AUTO
R . drawable . icon _flash _auto
}
}
} )
}
binding . imageModeSwitch . setOnClickListener {
isInVideoMode = !is InVideoMode
if ( isInVideoMode ) {
setupCamera ( )
binding . imageFlash . setImageResource ( if ( isInVideoMode ) {
binding . recordVideoButton . visibility = View . VISIBLE
binding . takePhotoButton . visibility = View . GONE
if ( Build . VERSION . SDK _INT >= Build . VERSION _CODES . M ) {
@ -212,10 +221,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
requestPermissions ( arrayOf ( Manifest . permission . RECORD _AUDIO ) , AUDIO _PERMISSION _REQUEST _CODE )
}
}
binding . imageModeSwitch . setImageDrawable ( ContextCompat . getDrawable ( this , R . drawable . icon _photo ) ?. mutate ( ) ?. also {
it . setTint ( ContextCompat . getColor ( this , R . color . neutralIconTint ) )
} )
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _OFF
R . drawable . icon _flash _off
} else {
binding . recordVideoButton . visibility = View . GONE
binding . takePhotoButton . visibility = View . VISIBLE
}
binding . imageModeSwitch . setImageResource ( R . drawable . icon _video )
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _AUTO
R . drawable . icon _flash _auto
} )
}
binding . imageCameraSwitch . setOnClickListener {
isBackCamera = if ( isBackCamera ) {
@ -223,8 +240,14 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
false
} else {
binding . imageCameraSwitch . setImageResource ( R . drawable . icon _camera _front )
if ( isInVideoMode ) {
//reset flash state
imageCapture ?. flashMode = ImageCapture . FLASH _MODE _OFF
binding . imageFlash . setImageResource ( R . drawable . icon _flash _off )
}
true
}
resolutions = null
setupCamera ( )
}
binding . takePhotoButton . onClick = :: onClickTakePhoto
@ -266,7 +289,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
permissionsGranted = true
setupCamera ( )
} else {
Colored AlertDialogBuilder ( this )
Custom AlertDialogBuilder ( this , themeValue )
. setTitle ( R . string . error )
. setMessage ( R . string . camera _perm _needed )
. setCancelable ( false )
@ -287,85 +310,72 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
private fun adaptPreviewSize ( resolution : Size ) {
val screenWidth = resources . displayMetrics . widthPixels
binding . cameraPreview . layoutParams = if ( screenWidth < resolution . width ) {
RelativeLayout . LayoutParams (
screenWidth ,
( resolution . height * ( screenWidth . toFloat ( ) / resolution . width ) ) . toInt ( )
)
} else {
RelativeLayout . LayoutParams ( resolution . width , resolution . height )
val screenHeight = resources . displayMetrics . heightPixels
var height = ( resolution . height * ( screenWidth . toFloat ( ) / resolution . width ) ) . toInt ( )
var width = screenWidth
if ( height > screenHeight ) {
width = ( width * ( screenHeight . toFloat ( ) / height ) ) . toInt ( )
height = screenHeight
}
binding . cameraPreview . layoutParams = RelativeLayout . LayoutParams ( width , height ) . apply {
addRule ( RelativeLayout . CENTER _IN _PARENT )
}
( binding . cameraPreview . layoutParams as RelativeLayout . LayoutParams ) . addRule ( RelativeLayout . CENTER _IN _PARENT )
}
@SuppressLint ( " RestrictedApi " )
private fun setupCamera ( resolution : Size ? = null ) {
if ( permissionsGranted && :: extensionsManager . isInitialized && :: cameraProvider . isInitialized ) {
imageCapture = ImageCapture . Builder ( )
. setCaptureMode ( captureMode )
. setFlashMode ( imageCapture ?. flashMode ?: ImageCapture . FLASH _MODE _AUTO )
. apply {
resolution ?. let {
setTargetResolution ( it )
}
}
. build ( )
videoCapture = VideoCapture . Builder ( ) . apply {
resolution ?. let {
private fun refreshImageCapture ( ) {
imageCapture = ImageCapture . Builder ( )
. setCaptureMode ( captureMode )
. setFlashMode ( imageCapture ?. flashMode ?: ImageCapture . FLASH _MODE _AUTO )
. apply {
currentResolution ?. let {
setTargetResolution ( it )
}
} . build ( )
}
. build ( )
}
private fun refreshVideoCapture ( ) {
videoCapture = VideoCapture . Builder ( ) . apply {
currentResolution ?. let {
setTargetResolution ( it )
}
} . build ( )
}
@SuppressLint ( " RestrictedApi " )
private fun setupCamera ( ) {
if ( permissionsGranted && :: extensionsManager . isInitialized && :: cameraProvider . isInitialized ) {
cameraSelector = if ( isBackCamera ) { CameraSelector . DEFAULT _BACK _CAMERA } else { CameraSelector . DEFAULT _FRONT _CAMERA }
if ( extensionsManager . isExtensionAvailable ( cameraProvider , cameraSelector , ExtensionMode . HDR ) ) {
cameraSelector = extensionsManager . getExtensionEnabledCameraSelector ( cameraProvider , cameraSelector , ExtensionMode . HDR )
if ( extensionsManager . isExtensionAvailable ( cameraSelector , ExtensionMode . AUTO ) ) {
cameraSelector = extensionsManager . getExtensionEnabledCameraSelector ( cameraSelector , ExtensionMode . AUTO )
}
cameraProvider . unbindAll ( )
camera = cameraProvider . bindToLifecycle ( this , cameraSelector , cameraPreview , imageCapture , videoCapture )
adaptPreviewSize ( resolution ?: imageCapture !! . attachedSurfaceResolution !! . swap ( ) )
val currentUseCase = ( if ( isInVideoMode ) {
refreshVideoCapture ( )
camera = cameraProvider . bindToLifecycle ( this , cameraSelector , cameraPreview , videoCapture )
videoCapture
} else {
refreshImageCapture ( )
camera = cameraProvider . bindToLifecycle ( this , cameraSelector , cameraPreview , imageCapture )
imageCapture
} ) !!
adaptPreviewSize ( currentResolution ?: currentUseCase . attachedSurfaceResolution !! . swap ( ) )
if ( resolutions == null ) {
val info = Camera2CameraInfo . from ( camera !! . cameraInfo )
val cameraManager = getSystemService ( Context . CAMERA _SERVICE ) as CameraManager
val characteristics = cameraManager . getCameraCharacteristics ( info . cameraId )
characteristics . get ( CameraCharacteristics . SCALER _STREAM _CONFIGURATION _MAP ) ?. let { streamConfigurationMap ->
resolutions = streamConfigurationMap . getOutputSizes ( imageCapture !! . imageFormat ) . map { it . swap ( ) }
resolutions = streamConfigurationMap . getOutputSizes ( currentUseCase . imageFormat ) . map { it . swap ( ) }
}
}
}
}
private fun takePhoto ( outputPath : String ) {
val imageCapture = imageCapture ?: return
val outputBuff = ByteArrayOutputStream ( )
val outputOptions = ImageCapture . OutputFileOptions . Builder ( outputBuff ) . build ( )
imageCapture . takePicture ( outputOptions , executor , object : ImageCapture . OnImageSavedCallback {
override fun onImageSaved ( outputFileResults : ImageCapture . OutputFileResults ) {
binding . takePhotoButton . onPhotoTaken ( )
if ( gocryptfsVolume . importFile ( ByteArrayInputStream ( outputBuff . toByteArray ( ) ) , outputPath ) ) {
Toast . makeText ( applicationContext , getString ( R . string . picture _save _success , outputPath ) , Toast . LENGTH _SHORT ) . show ( )
} else {
ColoredAlertDialogBuilder ( this @CameraActivity )
. setTitle ( R . string . error )
. setMessage ( R . string . picture _save _failed )
. setCancelable ( false )
. setPositiveButton ( R . string . ok ) { _ , _ ->
isFinishingIntentionally = true
finish ( )
}
. show ( )
}
}
override fun onError ( exception : ImageCaptureException ) {
binding . takePhotoButton . onPhotoTaken ( )
Toast . makeText ( applicationContext , exception . message , Toast . LENGTH _SHORT ) . show ( )
}
} )
}
private fun getOutputPath ( isVideo : Boolean ) : String {
val baseName = if ( isVideo ) { " VID " } else { " IMG " } + '_' + dateFormat . format ( Date ( ) ) + '_'
var fileName : String
@ -375,57 +385,94 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
return PathUtils . pathJoin ( outputDirectory , fileName )
}
private fun onClickTakePhoto ( ) {
val path = getOutputPath ( false )
private fun startTimerThen ( action : ( ) -> Unit ) {
if ( timerDuration > 0 ) {
binding . textTimer . visibility = View . VISIBLE
Thread {
isWaitingForTimer = true
lifecycleScope . launch {
for ( i in timerDuration downTo 1 ) {
runOnUiThread { binding . textTimer . text = i . toString ( ) }
Thread . sleep ( 1000 )
binding . textTimer . text = i . toString ( )
delay ( 1000 )
}
runOnUiThread {
takePhoto ( path )
if ( !is Finishing ) {
action ( )
binding . textTimer . visibility = View . GONE
}
} . start ( )
isWaitingForTimer = false
}
} else {
takePhoto ( path )
action ( )
}
}
private fun onClickTakePhoto ( ) {
if ( !is WaitingForTimer ) {
val outputPath = getOutputPath ( false )
startTimerThen {
imageCapture ?. let { imageCapture ->
val outputBuff = ByteArrayOutputStream ( )
val outputOptions = ImageCapture . OutputFileOptions . Builder ( outputBuff ) . build ( )
imageCapture . takePicture ( outputOptions , executor , object : ImageCapture . OnImageSavedCallback {
override fun onImageSaved ( outputFileResults : ImageCapture . OutputFileResults ) {
binding . takePhotoButton . onPhotoTaken ( )
if ( gocryptfsVolume . importFile ( ByteArrayInputStream ( outputBuff . toByteArray ( ) ) , outputPath ) ) {
Toast . makeText ( applicationContext , getString ( R . string . picture _save _success , outputPath ) , Toast . LENGTH _SHORT ) . show ( )
} else {
CustomAlertDialogBuilder ( this @CameraActivity , themeValue )
. setTitle ( R . string . error )
. setMessage ( R . string . picture _save _failed )
. setCancelable ( false )
. setPositiveButton ( R . string . ok ) { _ , _ ->
isFinishingIntentionally = true
finish ( )
}
. show ( )
}
}
override fun onError ( exception : ImageCaptureException ) {
binding . takePhotoButton . onPhotoTaken ( )
Toast . makeText ( applicationContext , exception . message , Toast . LENGTH _SHORT ) . show ( )
}
} )
}
}
}
}
@SuppressLint ( " MissingPermission " )
private fun onClickRecordVideo ( ) {
isRecording = if ( isRecording ) {
if ( isRecording ) {
videoCapture ?. stopRecording ( )
false
} else {
isRecording = false
} else if ( !is WaitingForTimer ) {
val path = getOutputPath ( true )
val handleId = gocryptfsVolume . openWriteMode ( path )
videoCapture ?. startRecording ( VideoCapture . OutputFileOptions ( object : SeekableWriter {
var offset = 0L
override fun write ( byteArray : ByteArray ) {
offset += gocryptfsVolume . writeFile ( handleId , offset , byteArray , byteArray . size )
}
override fun seek ( offset : Long ) {
this . offset = offset
}
override fun close ( ) {
gocryptfsVolume . closeFile ( handleId )
}
} ) , executor , object : VideoCapture . OnVideoSavedCallback {
override fun onVideoSaved ( ) {
Toast . makeText ( applicationContext , getString ( R . string . video _save _success , path ) , Toast . LENGTH _SHORT ) . show ( )
binding . recordVideoButton . setImageResource ( R . drawable . record _video _button )
}
override fun onError ( videoCaptureError : Int , message : String , cause : Throwable ? ) {
Toast . makeText ( applicationContext , message , Toast . LENGTH _SHORT ) . show ( )
cause ?. printStackTrace ( )
binding . recordVideoButton . setImageResource ( R . drawable . record _video _button )
}
} )
binding . recordVideoButton . setImageResource ( R . drawable . stop _recording _video _button )
true
startTimerThen {
val handleId = gocryptfsVolume . openWriteMode ( path )
videoCapture ?. startRecording ( VideoCapture . OutputFileOptions ( object : SeekableWriter {
var offset = 0L
override fun write ( byteArray : ByteArray ) {
offset += gocryptfsVolume . writeFile ( handleId , offset , byteArray , byteArray . size )
}
override fun seek ( offset : Long ) {
this . offset = offset
}
override fun close ( ) {
gocryptfsVolume . closeFile ( handleId )
}
} ) , executor , object : VideoCapture . OnVideoSavedCallback {
override fun onVideoSaved ( ) {
Toast . makeText ( applicationContext , getString ( R . string . video _save _success , path ) , Toast . LENGTH _SHORT ) . show ( )
binding . recordVideoButton . setImageResource ( R . drawable . record _video _button )
}
override fun onError ( videoCaptureError : Int , message : String , cause : Throwable ? ) {
Toast . makeText ( applicationContext , message , Toast . LENGTH _SHORT ) . show ( )
cause ?. printStackTrace ( )
binding . recordVideoButton . setImageResource ( R . drawable .<