diff --git a/app/build.gradle b/app/build.gradle index 9e257f56..a1451308 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,12 @@ plugins { } android { - compileSdkVersion 33 - buildToolsVersion "31.0.0" - + compileSdk 34 +namespace 'io.ak1.pixsample' defaultConfig { applicationId "io.ak1.pixsample" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -24,11 +23,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '17' } buildFeatures { viewBinding true diff --git a/app/src/main/java/io/ak1/pixsample/custom/CustomViews.kt b/app/src/main/java/io/ak1/pixsample/custom/CustomViews.kt index a5dd8d4f..30f648a5 100644 --- a/app/src/main/java/io/ak1/pixsample/custom/CustomViews.kt +++ b/app/src/main/java/io/ak1/pixsample/custom/CustomViews.kt @@ -48,7 +48,7 @@ fun fragmentBody( this.gravity = Gravity.RIGHT or Gravity.BOTTOM } imageTintList = ColorStateList.valueOf(Color.WHITE) - setImageResource(R.drawable.ic_photo_camera) + setImageResource(io.ak1.pix.R.drawable.ic_photo_camera) setOnClickListener(clickCallback) }) } diff --git a/build.gradle b/build.gradle index 373bf223..04f54acc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.7.20" + ext.kotlin_version = "1.9.22" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:8.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.vanniktech:gradle-maven-publish-plugin:0.24.0' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a197eb2e..8770b210 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Aug 30 18:27:41 CDT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/pix/build.gradle b/pix/build.gradle index 89d7a3a9..3b607c0e 100644 --- a/pix/build.gradle +++ b/pix/build.gradle @@ -8,14 +8,11 @@ plugins { } android { - compileSdkVersion 33 - buildToolsVersion "31.0.0" - +namespace 'io.ak1.pix' + compileSdk 34 defaultConfig { minSdkVersion 21 - targetSdkVersion 33 - versionCode 6 - versionName "1.5.2" + targetSdkVersion 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } @@ -27,15 +24,16 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '17' } buildFeatures { viewBinding true + dataBinding true } } @@ -46,20 +44,20 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'com.google.android.material:material:1.3.0' - implementation 'androidx.fragment:fragment-ktx:1.3.4' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.fragment:fragment-ktx:1.6.2' - def camerax_version = '1.2.3' + def camerax_version = '1.3.2' // CameraX core library using camera2 implementation implementation "androidx.camera:camera-camera2:$camerax_version" // CameraX Lifecycle Library implementation "androidx.camera:camera-lifecycle:$camerax_version" // CameraX View class - implementation 'androidx.camera:camera-view:1.2.3' + implementation 'androidx.camera:camera-view:1.3.2' // If you want to additionally use the CameraX Extensions library - implementation 'androidx.camera:camera-extensions:1.2.3' + implementation 'androidx.camera:camera-extensions:1.3.2' //Glide implementation 'com.github.bumptech.glide:glide:4.12.0' @@ -71,8 +69,8 @@ dependencies { } //Coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-native-mt' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - androidTestImplementation 'androidx.test.ext:junit:1.1.4' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } \ No newline at end of file diff --git a/pix/src/main/java/io/ak1/pix/PixFragment.kt b/pix/src/main/java/io/ak1/pix/PixFragment.kt index ea62cec9..8b1955cc 100644 --- a/pix/src/main/java/io/ak1/pix/PixFragment.kt +++ b/pix/src/main/java/io/ak1/pix/PixFragment.kt @@ -209,7 +209,9 @@ class PixFragment(private val resultCallback: ((PixEventCallback.Results) -> Uni model.longSelection.observe(requireActivity()) { //Log.e(TAG, "longSelection is now changed to $it") binding.longSelectionStatus(it) - if (mBottomSheetBehavior?.state ?: BottomSheetBehavior.STATE_COLLAPSED == BottomSheetBehavior.STATE_COLLAPSED) { + if ((mBottomSheetBehavior?.state + ?: BottomSheetBehavior.STATE_COLLAPSED) == BottomSheetBehavior.STATE_COLLAPSED + ) { binding.gridLayout.sendButtonStateAnimation(it) } } @@ -270,14 +272,14 @@ class PixFragment(private val resultCallback: ((PixEventCallback.Results) -> Uni 0 -> model.returnObjects() 1 -> mBottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED 2 -> model.longSelection.postValue(true) - 3 -> requireActivity().scanPhoto(uri.toFile()) { it -> + 3 -> { if (model.selectionList.value.isNullOrEmpty()) { - model.selectionList.value?.add(Img(contentUrl = it)) + model.selectionList.value?.add(Img(contentUrl = uri)) scope.cancel(CancellationException("canceled intentionally")) model.returnObjects() - return@scanPhoto + return@setupClickControls } - model.selectionList.value?.add(Img(contentUrl = it)) + model.selectionList.value?.add(Img(contentUrl = uri)) Handler(Looper.getMainLooper()).post { binding.setSelectionText( requireActivity(), diff --git a/pix/src/main/java/io/ak1/pix/helpers/CameraXManager.kt b/pix/src/main/java/io/ak1/pix/helpers/CameraXManager.kt index 7019a9f8..4e32d7c3 100644 --- a/pix/src/main/java/io/ak1/pix/helpers/CameraXManager.kt +++ b/pix/src/main/java/io/ak1/pix/helpers/CameraXManager.kt @@ -1,16 +1,24 @@ package io.ak1.pix.helpers import android.annotation.SuppressLint +import android.content.ContentValues import android.net.Uri +import android.provider.MediaStore import android.util.DisplayMetrics import android.util.Log -import android.widget.Toast import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.video.FallbackStrategy +import androidx.camera.video.MediaStoreOutputOptions +import androidx.camera.video.Quality +import androidx.camera.video.QualitySelector +import androidx.camera.video.Recorder +import androidx.camera.video.Recording +import androidx.camera.video.VideoCapture +import androidx.camera.video.VideoRecordEvent import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import io.ak1.pix.databinding.FragmentPixBinding import io.ak1.pix.models.Flash import io.ak1.pix.models.Mode import io.ak1.pix.models.Options @@ -34,10 +42,11 @@ class CameraXManager( private val requireActivity: FragmentActivity, private val options: Options, ) { + var recording: Recording? = null private val executor = ContextCompat.getMainExecutor(requireActivity) private var lensFacing: Int = CameraSelector.LENS_FACING_BACK var imageCapture: ImageCapture? = null - var videoCapture: VideoCapture? = null + var videoCapture: VideoCapture? = null private var useCases = ArrayList() private var preview: Preview? = null private var cameraProvider: ProcessCameraProvider? = null @@ -187,92 +196,29 @@ class CameraXManager( } + @SuppressLint("RestrictedApi") private fun createVideoCaptureUseCase( screenAspectRatio: Int, videoBitrate: Int?, audioBitrate: Int?, videoFrameRate: Int? - ): VideoCapture { - val builder = VideoCapture.Builder().apply { - //setTargetRotation(previewView.display.rotation) - // setAudioRecordSource() - //setAudioSource(MediaRecorder.AudioSource - videoBitrate?.let { setBitRate(it) } - audioBitrate?.let { setAudioBitRate(it) } - videoFrameRate?.let { setVideoFrameRate(it) } - }.setTargetAspectRatio(screenAspectRatio) - return builder.build() - } - - - /*private fun createImageAnalyserUseCase(): ImageAnalysis { - val imageAnalysis = ImageAnalysis.Builder() - .setTargetResolution(Size(1280, 720)) - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() + ): VideoCapture { - imageAnalysis.setAnalyzer(executor, { - //image as a param - //val rotationDegrees = image.imageInfo.rotationDegrees - // insert your code here. - }) - return imageAnalysis - } - - private fun createPreviewUseCase(): Preview { - return Preview.Builder().apply { - } - .build() - .also { - it.setSurfaceProvider(previewView.surfaceProvider) + val qualitySelector = QualitySelector.fromOrderedList( + listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD), + FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)) + val recorder = Recorder.Builder() + .setExecutor(executor) + .setQualitySelector(qualitySelector) + .setAspectRatio(screenAspectRatio).apply { + videoBitrate?.let { this.setTargetVideoEncodingBitRate(it) } } - } - - private fun createImageCaptureUseCase(options: Options): ImageCapture { - //reference - //https://developer.android.com/reference/androidx/camera/extensions/AutoImageCaptureExtender - - val builder = ImageCapture.Builder().apply { - // setTargetRotation(previewView.display.rotation) - Log.e("flash mode", "-> ${options.flash.name}") - setFlashMode( - when (options.flash) { - Flash.Auto -> ImageCapture.FLASH_MODE_AUTO - Flash.Off -> ImageCapture.FLASH_MODE_OFF - Flash.On -> ImageCapture.FLASH_MODE_ON - else -> ImageCapture.FLASH_MODE_AUTO - } - ) - - } - /** - * - // Create an Extender object which can be used to apply extension - // configurations. - val bokehImageCapture = BokehImageCaptureExtender.create(builder) - if (bokehImageCapture.isExtensionAvailable(cameraSelector)) { - // Enable the extension if available. - bokehImageCapture.enableExtension(cameraSelector) - } - val hdrImageCaptureExtender = HdrImageCaptureExtender.create(builder) - if (hdrImageCaptureExtender.isExtensionAvailable(cameraSelector)) { - // Enable the extension if available. - hdrImageCaptureExtender.enableExtension(cameraSelector) - } - val nightImageCaptureExtender = NightImageCaptureExtender.create(builder) - if (nightImageCaptureExtender.isExtensionAvailable(cameraSelector)) { - // Enable the extension if available. - nightImageCaptureExtender.enableExtension(cameraSelector) - } - - */ - - return builder - // .setTargetRotation(view.display.rotation) .build() - }*/ + val videoCapture = VideoCapture.withOutput(recorder) + return videoCapture + } fun takePhoto(callback: (Uri, String?) -> Unit) { // Get a stable reference of the modifiable image capture use case @@ -305,37 +251,49 @@ class CameraXManager( val msg = "Photo capture succeeded: $savedUri" //Toast.makeText(requireActivity, msg, Toast.LENGTH_SHORT).show() Log.d("TAG", msg) - callback(savedUri, null) + requireActivity.scanPhoto(photoFile){ + output.savedUri?.let { + callback(it, null) + } + } } }) } @SuppressLint("RestrictedApi", "MissingPermission") fun takeVideo(callback: (Uri, String?) -> Unit) { - val videoFile = File( - getOutputDirectory(), - SimpleDateFormat( - FILENAME_FORMAT, Locale.US - ).format(System.currentTimeMillis()) + ".mp4" - ) - videoCapture?.startRecording( - VideoCapture.OutputFileOptions.Builder(videoFile).build(), - executor, - object : VideoCapture.OnVideoSavedCallback { - override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) { - val savedUri = Uri.fromFile(videoFile) - val msg = "Photo capture succeeded: $savedUri" - Log.e(TAG, msg) - outputFileResults.savedUri - callback(outputFileResults.savedUri ?: Uri.EMPTY, null) - } + // Create MediaStoreOutputOptions for our recorder + val name = "Pix-recording-" + + SimpleDateFormat(FILENAME_FORMAT, Locale.US) + .format(System.currentTimeMillis()) + ".mp4" + val contentValues = ContentValues().apply { + put(MediaStore.Video.Media.DISPLAY_NAME, name) + } + val mediaStoreOutput = MediaStoreOutputOptions.Builder(requireActivity.contentResolver, + MediaStore.Video.Media.EXTERNAL_CONTENT_URI) + .setContentValues(contentValues) + .build() - override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { - Log.e(TAG, "video Capture failed: $message", cause) - callback(Uri.EMPTY, message) +if (videoCapture==null) return + recording = null + recording = videoCapture!!.output + .prepareRecording(requireActivity, mediaStoreOutput) + .withAudioEnabled() + .start(executor + ) {vre -> + when (vre) { + is VideoRecordEvent.Start -> { + Log.d(TAG, "Recording started") } - }) - //videoCapture?.stopRecording() + is VideoRecordEvent.Pause -> { + Log.d(TAG, "Recording stopped") + } + is VideoRecordEvent.Resume -> { + } + is VideoRecordEvent.Finalize ->{ + callback(vre.outputResults.outputUri, null) + } + }} } private fun getOutputDirectory(): File { diff --git a/pix/src/main/java/io/ak1/pix/helpers/ControlsHelper.kt b/pix/src/main/java/io/ak1/pix/helpers/ControlsHelper.kt index 771350c2..fb564544 100644 --- a/pix/src/main/java/io/ak1/pix/helpers/ControlsHelper.kt +++ b/pix/src/main/java/io/ak1/pix/helpers/ControlsHelper.kt @@ -16,7 +16,6 @@ import androidx.camera.core.ImageCapture import androidx.core.graphics.drawable.DrawableCompat import androidx.fragment.app.FragmentActivity import io.ak1.pix.R -import io.ak1.pix.databinding.FragmentPixBinding import io.ak1.pix.models.Flash import io.ak1.pix.models.Mode import io.ak1.pix.models.Options @@ -143,7 +142,7 @@ internal fun PixBindings.setupClickControls( videoCounterLayout.videoCounterLayout.hide() videoCounterHandler.removeCallbacks(videoCounterRunnable) videoRecordingEndAnim() - cameraXManager?.videoCapture?.stopRecording() + cameraXManager?.recording?.stop() } else { videoCounterHandler.postDelayed(this, 1000) } @@ -197,7 +196,7 @@ internal fun PixBindings.setupClickControls( videoCounterLayout.videoCounterLayout.hide() videoCounterHandler.removeCallbacks(videoCounterRunnable) videoRecordingEndAnim() - cameraXManager?.videoCapture?.stopRecording() + cameraXManager?.recording?.stop() } false } diff --git a/pix/src/main/java/io/ak1/pix/helpers/UtilityHelper.kt b/pix/src/main/java/io/ak1/pix/helpers/UtilityHelper.kt index cd4b9542..66229345 100644 --- a/pix/src/main/java/io/ak1/pix/helpers/UtilityHelper.kt +++ b/pix/src/main/java/io/ak1/pix/helpers/UtilityHelper.kt @@ -17,8 +17,6 @@ import androidx.recyclerview.widget.RecyclerView import io.ak1.pix.R import io.ak1.pix.adapters.MainImageAdapter -import io.ak1.pix.databinding.FragmentPixBinding -import io.ak1.pix.databinding.GridLayoutBinding import io.ak1.pix.utility.HeaderItemDecoration import io.ak1.pix.utility.IMAGE_VIDEO_URI import io.ak1.pix.utility.PixBindings @@ -63,7 +61,7 @@ val Int.counterText: String return "$min:$sec" } -fun Context.scanPhoto(file: File, callback: ((Uri) -> Unit)? = null) = +fun Context.scanPhoto(file: File, callback: ((Uri) -> Unit)? = null){ MediaScannerConnection.scanFile( this, arrayOf(file.toString()), @@ -74,7 +72,7 @@ fun Context.scanPhoto(file: File, callback: ((Uri) -> Unit)? = null) = uri.lastPathSegment ) callback?.invoke(mainUri) - } + }} fun FragmentActivity.setUpMargins(binding: PixBindings) { val height =