diff --git a/app/build.gradle b/app/build.gradle index eb89304d..feb8ce92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ repositories { android { - compileSdkVersion 32 - buildToolsVersion "30.0.2" + compileSdkVersion 33 + buildToolsVersion "30.0.3" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -36,7 +36,7 @@ android { defaultConfig { applicationId "net.pokeranalytics.android" minSdkVersion 23 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 151 versionName "6.0.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -141,6 +141,16 @@ dependencies { // ffmpeg for encoding video (HH export) implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS' + // Camera + def camerax_version = "1.1.0-beta01" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" + + implementation "androidx.camera:camera-view:${camerax_version}" + implementation "androidx.camera:camera-extensions:${camerax_version}" + // Instrumented Tests androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 714f4e26..e6fedda6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,13 +3,18 @@ xmlns:tools="http://schemas.android.com/tools" package="net.pokeranalytics.android"> + + + + - + + + + = RealmList() + @Ignore + var pictureUri: Uri? = null + @Ignore override val realmObjectClass: Class = Player::class.java @@ -63,7 +67,7 @@ open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepres when (row) { PlayerPropertiesRow.NAME -> this.name = value as String? ?: "" PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: "" - PlayerPropertiesRow.IMAGE -> this.picture = value as String? ?: "" + PlayerPropertiesRow.IMAGE -> this.pictureUri = value as? Uri } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt new file mode 100644 index 00000000..29e7d0eb --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/CameraActivity.kt @@ -0,0 +1,188 @@ +package net.pokeranalytics.android.ui.activity.components + +import android.Manifest +import android.app.Activity +import android.content.ContentValues +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.widget.Toast +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import net.pokeranalytics.android.databinding.ActivityCameraBinding +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class CameraActivity : BaseActivity() { + + companion object { + + const val IMAGE_URI = "image_uri" + + fun newInstanceForResult(fragment: Fragment, code: RequestCode) { + val intent = Intent(fragment.requireContext(), CameraActivity::class.java) + fragment.startActivityForResult(intent, code.value) + } + + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" + private const val REQUEST_CODE_PERMISSIONS = 10 + private val REQUIRED_PERMISSIONS = + mutableListOf ( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO + ).apply { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + }.toTypedArray() + + } + + private lateinit var viewBinding: ActivityCameraBinding + + private lateinit var cameraExecutor: ExecutorService + private var imageCapture: ImageCapture? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + this.viewBinding = ActivityCameraBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + + if (allPermissionsGranted()) { + cameraExecutor = Executors.newSingleThreadExecutor() + startCamera() + } else { + ActivityCompat.requestPermissions( + this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) + } + + viewBinding.imageCaptureButton.setOnClickListener { takePhoto() } + cameraExecutor = Executors.newSingleThreadExecutor() + + } + + override fun onDestroy() { + super.onDestroy() + this.cameraExecutor.shutdown() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == REQUEST_CODE_PERMISSIONS) { + if (allPermissionsGranted()) { + startCamera() + } else { + Toast.makeText(this, + "Permissions not granted by the user.", + Toast.LENGTH_SHORT).show() + finish() + } + } + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + baseContext, it) == PackageManager.PERMISSION_GRANTED + } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(this) + + cameraProviderFuture.addListener({ + // Used to bind the lifecycle of cameras to the lifecycle owner + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + // Preview + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(this.viewBinding.viewFinder.surfaceProvider) + } + + imageCapture = ImageCapture.Builder().build() + + // Select back camera as a default + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture) + + } catch(exc: Exception) { + Timber.e(exc) + } + + }, ContextCompat.getMainExecutor(this)) + } + + + private fun takePhoto() { + // Get a stable reference of the modifiable image capture use case + val imageCapture = imageCapture ?: return + + // Create time stamped name and MediaStore entry. + val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) + .format(System.currentTimeMillis()) + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, name) + put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") + if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") + } + } + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(contentResolver, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + contentValues) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(this), + object : ImageCapture.OnImageSavedCallback { + override fun onError(e: ImageCaptureException) { + Timber.e(e) + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + + val msg = "Photo capture succeeded: ${output.savedUri}" + Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() + Timber.d(msg) + + val intent = Intent() + intent.putExtra(IMAGE_URI, output.savedUri.toString()) + setResult(Activity.RESULT_OK, intent) + + finish() + } + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt index e91359dd..0b6e44cf 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/components/Codes.kt @@ -16,7 +16,8 @@ enum class RequestCode(var value: Int) { IMPORT(900), SUBSCRIPTION(901), CURRENCY(902), - PERMISSION_WRITE_EXTERNAL_STORAGE(1000) + PERMISSION_WRITE_EXTERNAL_STORAGE(1000), + CAMERA(1001) } enum class ResultCode(var value: Int) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt index cab56f4b..97d40a70 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt @@ -20,7 +20,9 @@ import net.pokeranalytics.android.model.realm.Comment import net.pokeranalytics.android.model.realm.Player import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.ui.activity.ColorPickerActivity +import net.pokeranalytics.android.ui.activity.components.CameraActivity import net.pokeranalytics.android.ui.activity.components.MediaActivity +import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.extensions.showAlertDialog @@ -72,6 +74,13 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou player.color = if (color != Color.TRANSPARENT) color else null rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) } + + if (requestCode == RequestCode.CAMERA.value) { + val uri = data?.getStringExtra(CameraActivity.IMAGE_URI) + this.player.picture = uri + rowRepresentableAdapter.refreshRow(PlayerPropertiesRow.IMAGE) + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -218,7 +227,11 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou builder.setItems(placesArray.toTypedArray()) { _, which -> when (placesArray[which]) { - getString(R.string.take_a_picture) -> mediaActivity?.openImageCaptureIntent(false) + getString(R.string.take_a_picture) -> { + CameraActivity.newInstanceForResult(this, RequestCode.CAMERA) + +// mediaActivity?.takePicture() + } // mediaActivity?.openImageCaptureIntent(false) getString(R.string.library) -> mediaActivity?.openImageGalleryIntent(false) getString(R.string.select_a_color) -> { ColorPickerActivity.newInstanceForResult(this, REQUEST_CODE_PICK_COLOR) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt index c5707d4e..dd0630d2 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/NewDataMenuActivity.kt @@ -149,11 +149,17 @@ class NewDataMenuActivity : BaseActivity() { anim.duration = 150 anim.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) menuContainer.visibility = View.INVISIBLE finish() } + +// override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) { +// super.onAnimationEnd(animation, isReverse) +// menuContainer.visibility = View.INVISIBLE +// finish() +// } }) anim.start() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt index 927767df..d8981d8c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/PlayerImageView.kt @@ -2,7 +2,10 @@ package net.pokeranalytics.android.ui.view import android.content.Context import android.graphics.Color +import android.graphics.ImageDecoder import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.provider.MediaStore import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater @@ -14,9 +17,11 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat.getColor import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.core.net.toUri import net.pokeranalytics.android.R import net.pokeranalytics.android.model.realm.Player import net.pokeranalytics.android.ui.extensions.px +import timber.log.Timber /** @@ -91,9 +96,33 @@ class PlayerImageView : FrameLayout { // Picture player.picture?.let { picture -> - val rDrawable = RoundedBitmapDrawableFactory.create(resources, picture) - rDrawable.isCircular = true - this.playerImage.setImageDrawable(rDrawable) + + Timber.d("picture = $picture") + + val uri = picture.toUri() + + context?.contentResolver?.let { resolver -> + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(resolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(resolver, uri) + } + + val rDrawable = RoundedBitmapDrawableFactory.create(resources, bitmap) + rDrawable.isCircular = true + this.playerImage.setImageDrawable(rDrawable) + + } + +// player.picture?.let { pic -> +// pic.toUri().path?.let { +// val rDrawable = RoundedBitmapDrawableFactory.create(resources, it) +// rDrawable.isCircular = true +// this.playerImage.setImageDrawable(rDrawable) +// } +// +// this.playerImage.setImageURI(uri) +// } } ?: run { diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml new file mode 100644 index 00000000..71504c4b --- /dev/null +++ b/app/src/main/res/layout/activity_camera.xml @@ -0,0 +1,31 @@ + + + + + +