From eefca5ee0a69697fbfc1da08f503748ba90dccd4 Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Sat, 19 Dec 2020 19:55:54 +0100 Subject: [PATCH] Switching to CameraX --- app/build.gradle | 10 +- app/proguard-rules.pro | 3 +- app/src/main/AndroidManifest.xml | 1 + .../sushi/hardcore/droidfs/CameraActivity.kt | 246 +++++++++++++----- .../droidfs/explorers/BaseExplorerActivity.kt | 2 +- ...icon_grid_on.xml => icon_aspect_ratio.xml} | 2 +- app/src/main/res/drawable/icon_grid_off.xml | 5 - app/src/main/res/drawable/icon_hdr_off.xml | 5 - app/src/main/res/drawable/icon_hdr_on.xml | 5 - app/src/main/res/layout/activity_camera.xml | 39 +-- app/src/main/res/values/strings.xml | 6 +- build.gradle | 2 +- 12 files changed, 209 insertions(+), 117 deletions(-) rename app/src/main/res/drawable/{icon_grid_on.xml => icon_aspect_ratio.xml} (50%) delete mode 100644 app/src/main/res/drawable/icon_grid_off.xml delete mode 100644 app/src/main/res/drawable/icon_hdr_off.xml delete mode 100644 app/src/main/res/drawable/icon_hdr_on.xml diff --git a/app/build.gradle b/app/build.gradle index 86b956d..b80e54f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,7 +47,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.appcompat:appcompat:1.2.0" - implementation "androidx.constraintlayout:constraintlayout:2.0.3" + implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.sqlite:sqlite-ktx:2.1.0" implementation "androidx.preference:preference-ktx:1.1.1" @@ -56,6 +56,12 @@ dependencies { implementation "com.github.bumptech.glide:glide:4.11.0" implementation "com.google.android.exoplayer:exoplayer-core:2.11.7" implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7" - implementation "com.otaliastudios:cameraview:2.6.4" implementation "androidx.biometric:biometric:1.0.1" + + def camerax_version = "1.0.0-rc01" + implementation "androidx.camera:camera-camera2:$camerax_version" + implementation "androidx.camera:camera-lifecycle:$camerax_version" + implementation "androidx.camera:camera-view:1.0.0-alpha20" + implementation "androidx.camera:camera-extensions:1.0.0-alpha20" + } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 01533fb..af15bf1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,5 +24,4 @@ -keep class sushi.hardcore.droidfs.SettingsActivity$** { *; } --keep class sushi.hardcore.droidfs.explorers.ExplorerElement --keep class com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker \ No newline at end of file +-keep class sushi.hardcore.droidfs.explorers.ExplorerElement \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8d1dd2..78e6687 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + ? = null + private var currentResolutionIndex: Int = 0 + private var isBackCamera = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) gocryptfsVolume = GocryptfsVolume(intent.getIntExtra("sessionID", -1)) outputDirectory = intent.getStringExtra("path")!! - context = this - camera.setLifecycleOwner(this) - camera.addCameraListener(object: CameraListener(){ - override fun onPictureTaken(result: PictureResult) { - take_photo_button.onPhotoTaken() - val inputStream = ByteArrayInputStream(result.data) - if (gocryptfsVolume.importFile(inputStream, PathUtils.pathJoin(outputDirectory, fileName))){ - Toast.makeText(context, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){ + setupCamera() + } else { + requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE) + } + } else { + setupCamera() + } + + cameraExecutor = Executors.newSingleThreadExecutor() + + take_photo_button.onClick = ::onClickTakePhoto + orientedIcons = listOf(image_ratio, image_timer, image_close, image_flash, image_camera_switch) + sensorOrientationListener = SensorOrientationListener(this) + + val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){ + override fun onScale(detector: ScaleGestureDetector): Boolean { + val currentZoomRatio = imageCapture?.camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 0F + imageCapture?.camera?.cameraControl?.setZoomRatio(currentZoomRatio*detector.scaleFactor) + return true + } + }) + camera_preview.setOnTouchListener { _, motionEvent: MotionEvent -> + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> true + MotionEvent.ACTION_UP -> { + val factory = camera_preview.meteringPointFactory + val point = factory.createPoint(motionEvent.x, motionEvent.y) + val action = FocusMeteringAction.Builder(point).build() + imageCapture?.camera?.cameraControl?.startFocusAndMetering(action) + true + } + MotionEvent.ACTION_MOVE -> scaleGestureDetector.onTouchEvent(motionEvent) + else -> false + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults.size == 1) { + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(R.string.camera_perm_needed) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + isFinishingIntentionally = true + finish() + }.show() } else { - ColoredAlertDialogBuilder(context) + setupCamera() + } + } + } + } + + private fun adaptPreviewSize(resolution: Size){ + val metrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(metrics) + //resolution.width and resolution.height seem to be inverted + val width = resolution.height + val height = resolution.width + camera_preview.layoutParams = if (metrics.widthPixels < width){ + RelativeLayout.LayoutParams( + metrics.widthPixels, + (height * (metrics.widthPixels.toFloat() / width)).toInt() + ) + } else { + RelativeLayout.LayoutParams(width, height) + } + (camera_preview.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.CENTER_IN_PARENT) + } + + private fun setupCamera(resolution: Size? = null){ + val cameraProviderFuture = ProcessCameraProvider.getInstance(this) + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(camera_preview.surfaceProvider) + } + val builder = ImageCapture.Builder() + .setFlashMode(ImageCapture.FLASH_MODE_AUTO) + resolution?.let { + builder.setTargetResolution(it) + } + val hdrImageCapture = HdrImageCaptureExtender.create(builder) + val cameraSelector = if (isBackCamera){ CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA } + + if (hdrImageCapture.isExtensionAvailable(cameraSelector)){ + hdrImageCapture.enableExtension(cameraSelector) + } + + imageCapture = builder.build() + + cameraProvider.unbindAll() + val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) + + adaptPreviewSize(imageCapture!!.attachedSurfaceResolution!!) + + 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) + } + }, ContextCompat.getMainExecutor(this)) + } + + private fun takePhoto() { + val imageCapture = imageCapture ?: return + val outputBuff = ByteArrayOutputStream() + val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build() + imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + take_photo_button.onPhotoTaken() + if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), PathUtils.pathJoin(outputDirectory, fileName))){ + Toast.makeText(applicationContext, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show() + } else { + ColoredAlertDialogBuilder(applicationContext) .setTitle(R.string.error) .setMessage(R.string.picture_save_failed) .setCancelable(false) @@ -83,10 +207,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { .show() } } + override fun onError(exception: ImageCaptureException) { + take_photo_button.onPhotoTaken() + Toast.makeText(applicationContext, exception.message, Toast.LENGTH_SHORT).show() + } }) - take_photo_button.onClick = ::onClickTakePhoto - orientedIcons = listOf(image_hdr, image_timer, image_grid, image_close, image_flash, image_camera_switch) - sensorOrientationListener = SensorOrientationListener(this) } private fun onClickTakePhoto() { @@ -102,49 +227,41 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { Thread.sleep(1000) } runOnUiThread { - camera.takePicture() + takePhoto() text_timer.visibility = View.GONE } }.start() } else { - camera.takePicture() + takePhoto() } } fun onClickFlash(view: View) { - currentFlashModeIndex = MiscUtils.incrementIndex(currentFlashModeIndex, flashModes) - camera.flash = flashModes[currentFlashModeIndex] - image_flash.setImageResource(when (camera.flash) { - Flash.AUTO -> R.drawable.icon_flash_auto - Flash.ON -> R.drawable.icon_flash_on - else -> R.drawable.icon_flash_off + image_flash.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 + } + else -> { + imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO + R.drawable.icon_flash_auto + } }) } fun onClickCameraSwitch(view: View) { - camera.toggleFacing() - if (camera.facing == Facing.FRONT){ - image_camera_switch.setImageResource(R.drawable.icon_camera_back) - } else { + isBackCamera = if (isBackCamera) { image_camera_switch.setImageResource(R.drawable.icon_camera_front) - Thread { - Thread.sleep(25) - camera.flash = flashModes[currentFlashModeIndex] //refresh flash mode after switching camera - }.start() - } - } - - fun onClickHDR(view: View) { - camera.hdr = when (camera.hdr){ - Hdr.ON -> { - image_hdr.setImageResource(R.drawable.icon_hdr_off) - Hdr.OFF - } - Hdr.OFF -> { - image_hdr.setImageResource(R.drawable.icon_hdr_on) - Hdr.ON - } + false + } else { + image_camera_switch.setImageResource(R.drawable.icon_camera_back) + true } + setupCamera() } fun onClickTimer(view: View) { @@ -173,16 +290,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { dialog.show() } - fun onClickGrid(view: View) { - ColoredAlertDialogBuilder(this) - .setTitle(getString(R.string.choose_grid)) - .setSingleChoiceItems(gridTitles.map { getString(it) }.toTypedArray(), gridValues.indexOf(camera.grid)){ dialog, which -> - camera.grid = gridValues[which] - image_grid.setImageResource(if (camera.grid == Grid.OFF){ R.drawable.icon_grid_off } else { R.drawable.icon_grid_on }) - dialog.dismiss() - } - .setNegativeButton(R.string.cancel, null) - .show() + fun onClickRatio(view: View) { + resolutions?.let { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.choose_resolution) + .setSingleChoiceItems(DialogSingleChoiceAdapter(this, it.map { size -> size.toString() }.toTypedArray()), currentResolutionIndex) { dialog, which -> + setupCamera(resolutions!![which]) + dialog.dismiss() + currentResolutionIndex = which + } + .setNegativeButton(R.string.cancel, null) + .show() + } } fun onClickClose(view: View) { @@ -192,6 +311,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener { override fun onDestroy() { super.onDestroy() + cameraExecutor.shutdown() if (!isFinishingIntentionally) { gocryptfsVolume.close() RestrictedFileProvider.wipeAll(this) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index 1c51d84..9cf57c3 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -419,7 +419,7 @@ open class BaseExplorerActivity : BaseActivity() { setCurrentPath(currentDirectoryPath) dialog.dismiss() } - .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } + .setNegativeButton(R.string.cancel, null) .show() true } diff --git a/app/src/main/res/drawable/icon_grid_on.xml b/app/src/main/res/drawable/icon_aspect_ratio.xml similarity index 50% rename from app/src/main/res/drawable/icon_grid_on.xml rename to app/src/main/res/drawable/icon_aspect_ratio.xml index 7bfc825..c7b8b5f 100644 --- a/app/src/main/res/drawable/icon_grid_on.xml +++ b/app/src/main/res/drawable/icon_aspect_ratio.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/icon_grid_off.xml b/app/src/main/res/drawable/icon_grid_off.xml deleted file mode 100644 index e247223..0000000 --- a/app/src/main/res/drawable/icon_grid_off.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_hdr_off.xml b/app/src/main/res/drawable/icon_hdr_off.xml deleted file mode 100644 index 550f633..0000000 --- a/app/src/main/res/drawable/icon_hdr_off.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_hdr_on.xml b/app/src/main/res/drawable/icon_hdr_on.xml deleted file mode 100644 index 4e10da5..0000000 --- a/app/src/main/res/drawable/icon_hdr_on.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml index 17a4491..dc63ec0 100644 --- a/app/src/main/res/layout/activity_camera.xml +++ b/app/src/main/res/layout/activity_camera.xml @@ -6,17 +6,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".CameraActivity"> - + - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe606da..8ffe31e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,10 +162,6 @@ The selected items have been successfully moved. Move successful ! Enter the timer duration (in s) - None - 3x3 - 4x4 - Choose grid Please enter a numeric value Failed to retrieve the selected path. DroidFS doesn\'t have write access to this path. Please try another location. @@ -193,4 +189,6 @@ Hidden Volume Volume name cannot contain slashes Hidden volumes are stored in the app\'s internal storage. Other apps can\'t see these volumes without root access. However, if you uninstall DroidFS or clear data of the app, all your hidden volumes will be LOST. Be sure to make backups ! + Camera permission is needed to take photo. + Choose a resolution diff --git a/build.gradle b/build.gradle index d5f6eb6..1590fed 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } }