diff --git a/app/build.gradle b/app/build.gradle index 244b59d..2ec9a4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,15 +63,15 @@ dependencies { implementation project(':utils') // Kotlin lang - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation "androidx.viewpager2:viewpager2:1.0.0" - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Navigation library def nav_version = '2.2.2' @@ -79,7 +79,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // EXIF Interface - implementation 'androidx.exifinterface:exifinterface:1.2.0' + implementation 'androidx.exifinterface:exifinterface:1.3.6' // Glide implementation 'com.github.bumptech.glide:glide:4.11.0' diff --git a/app/src/main/java/com/samsung/android/scan3d/CameraActivity.kt b/app/src/main/java/com/samsung/android/scan3d/CameraActivity.kt index ae7e22a..baaf7c8 100644 --- a/app/src/main/java/com/samsung/android/scan3d/CameraActivity.kt +++ b/app/src/main/java/com/samsung/android/scan3d/CameraActivity.kt @@ -20,21 +20,23 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build import android.os.Bundle import android.util.Log -import android.view.View import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import com.samsung.android.scan3d.databinding.ActivityCameraBinding -import com.samsung.android.scan3d.http.HttpService import com.samsung.android.scan3d.serv.Cam -import kotlinx.coroutines.channels.Channel +import com.samsung.android.scan3d.serv.CameraActionState +import com.samsung.android.scan3d.serv.CameraActionState.ON_PAUSE +import com.samsung.android.scan3d.serv.CameraActionState.ON_RESUME +import com.samsung.android.scan3d.serv.CameraActionState.START +import com.samsung.android.scan3d.serv.CameraActionState.STOP +const val KILL_THE_APP = "KILL" class CameraActivity : AppCompatActivity() { - private lateinit var activityCameraBinding: ActivityCameraBinding + private var _binding: ActivityCameraBinding? = null + private val binding get() = _binding!! private val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -45,44 +47,38 @@ class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.i("CAMERAACTIVITY", "CAMERAACTIVITY onCreate") - activityCameraBinding = ActivityCameraBinding.inflate(layoutInflater) - setContentView(activityCameraBinding.root) - sendCam { - it.action = "start" - } - registerReceiver(receiver, IntentFilter("KILL")) + _binding = ActivityCameraBinding.inflate(layoutInflater) + setContentView(binding.root) + + setCameraForegroundServiceState(START) + registerReceiver(receiver, IntentFilter(KILL_THE_APP)) } override fun onPause() { super.onPause() - sendCam { - it.action = "onPause" - } - } - - fun sendCam(extra: (Intent) -> Unit) { - var intent = Intent(this, Cam::class.java) - extra(intent) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } + setCameraForegroundServiceState(ON_PAUSE) } override fun onDestroy() { + _binding = null super.onDestroy() - sendCam { - it.action = "stop" - } + setCameraForegroundServiceState(STOP) unregisterReceiver(receiver) } override fun onResume() { super.onResume() - sendCam { - it.action = "onResume" + setCameraForegroundServiceState(ON_RESUME) + } + + fun setCameraForegroundServiceState(action: CameraActionState, extra: ((Intent) -> Unit)? = null) { + try { + val intent = Intent(this, Cam::class.java).also { it.action = action.name } + if (extra != null) extra(intent) + + startForegroundService(intent) + } catch (exc: Throwable) { + Log.e("CameraFragment.TAG", "Error closing camera", exc) } } } diff --git a/app/src/main/java/com/samsung/android/scan3d/fragments/CameraFragment.kt b/app/src/main/java/com/samsung/android/scan3d/fragments/CameraFragment.kt index 8e47d0e..81ca3d3 100644 --- a/app/src/main/java/com/samsung/android/scan3d/fragments/CameraFragment.kt +++ b/app/src/main/java/com/samsung/android/scan3d/fragments/CameraFragment.kt @@ -16,40 +16,50 @@ package com.samsung.android.scan3d.fragments +import android.Manifest.permission.* import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle -import android.os.Parcelable import android.util.Log +import android.util.Size import android.view.LayoutInflater import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter -import android.widget.CompoundButton import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.Navigation import com.example.android.camera.utils.OrientationLiveData import com.samsung.android.scan3d.CameraActivity +import com.samsung.android.scan3d.KILL_THE_APP import com.samsung.android.scan3d.R import com.samsung.android.scan3d.databinding.FragmentCameraBinding import com.samsung.android.scan3d.serv.CamEngine +import com.samsung.android.scan3d.serv.CameraActionState +import com.samsung.android.scan3d.serv.CameraActionState.NEW_VIEW_STATE import com.samsung.android.scan3d.util.ClipboardUtil import com.samsung.android.scan3d.util.IpUtil -import kotlinx.parcelize.Parcelize +import com.samsung.android.scan3d.util.Selector +import com.samsung.android.scan3d.util.isAllPermissionsGranted +import com.samsung.android.scan3d.util.requestPermissionList +import kotlinx.coroutines.launch class CameraFragment : Fragment() { - /** Android ViewBinding */ - private var _fragmentCameraBinding: FragmentCameraBinding? = null - - private val fragmentCameraBinding get() = _fragmentCameraBinding!! + private var _binding: FragmentCameraBinding? = null + private val binding get() = _binding!! + private val viewModel: CameraViewModel by viewModels() /** Host's navigation controller */ private val navController: NavController by lazy { @@ -59,255 +69,230 @@ class CameraFragment : Fragment() { /** AndroidX navigation arguments */ // private val args: CameraFragmentArgs by navArgs() - var resW = 1280 - var resH = 720 - - var viewState = - ViewState(true, stream = false, cameraId = "0", quality = 80, resolutionIndex = null) + private var resolutionWidth = DEFAULT_WIDTH + private var resolutionHeight = DEFAULT_HEIGHT - lateinit var Cac: CameraActivity + private lateinit var cameraActivity: CameraActivity /** Live data listener for changes in the device orientation relative to the camera */ private lateinit var relativeOrientation: OrientationLiveData override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - _fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false) - - // Get the local ip address - val localIp = IpUtil.getLocalIpAddress() - _fragmentCameraBinding!!.textView6.text = "$localIp:8080/cam.mjpeg" - _fragmentCameraBinding!!.textView6.setOnClickListener { - // Copy the ip address to the clipboard - ClipboardUtil.copyToClipboard(context, "ip", _fragmentCameraBinding!!.textView6.text.toString()) - // Toast to notify the user - Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() - } - - Cac = (activity as CameraActivity?)!! - return fragmentCameraBinding.root + _binding = FragmentCameraBinding.inflate(inflater, container, false) + return binding.root } - private val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - - - intent.extras?.getParcelable("dataQuick")?.apply { - activity?.runOnUiThread(Runnable { - // Stuff that updates the UI - fragmentCameraBinding.qualFeedback?.text = - " " + this.rateKbs + "kB/sec" - fragmentCameraBinding.ftFeedback?.text = - " " + this.ms + "ms" - }) - + intent.extras?.getParcelable("dataQuick")?.let { + binding.qualFeedback.text = " " + it.rateKbs + "kB/sec" + binding.ftFeedback.text = " " + it.ms + "ms" } + intent.extras?.getParcelable("data")?.let { + setViews(it) + } ?: run { return } + } - val data = intent.extras?.getParcelable("data") ?: return - - - val re = data.resolutions[data.resolutionSelected] - resW = re.width - resH = re.height - - activity?.runOnUiThread(Runnable { - - fragmentCameraBinding.viewFinder.setAspectRatio(resW, resH) - }) - + private fun setViews(data: CamEngine.Companion.Data) { + val resolution = data.resolutions[data.resolutionSelected] + resolutionWidth = resolution.width + resolutionHeight = resolution.height + binding.viewFinder.setAspectRatio(resolutionWidth, resolutionHeight) + setSwitchListeners() + setSpinnerCam(data) + setSpinnerQua() + setSpinnerRes(data) + } - fragmentCameraBinding.switch1?.setOnCheckedChangeListener(object : - CompoundButton.OnCheckedChangeListener { - override fun onCheckedChanged(p0: CompoundButton?, p1: Boolean) { - viewState.preview = p1 - sendViewState() - } - }) - fragmentCameraBinding.switch2?.setOnCheckedChangeListener(object : - CompoundButton.OnCheckedChangeListener { - override fun onCheckedChanged(p0: CompoundButton?, p1: Boolean) { - viewState.stream = p1 - sendViewState() - } - }) - - run { - val spinner = fragmentCameraBinding.spinnerCam - val spinnerDataList = ArrayList() - data.sensors.forEach { spinnerDataList.add(it.title) } - val spinnerAdapter = - ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - spinnerDataList - ) - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - spinner!!.adapter = spinnerAdapter - spinner.setSelection(data.sensors.indexOf(data.sensorSelected)) - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + private fun setSwitchListeners() { + binding.switch1.setOnCheckedChangeListener { _, prev -> + viewModel.uiState.value.preview = prev + sendViewState() + } + binding.switch2.setOnCheckedChangeListener { _, prev -> + viewModel.uiState.value.stream = prev + sendViewState() + } + } - if (viewState.cameraId != data.sensors[p2].cameraId) { - viewState.resolutionIndex = null + private fun setSpinnerRes(data: CamEngine.Companion.Data) { + val outputFormats = data.resolutions + val spinnerDataList = outputFormats.map(Size::toString) + val spinnerAdapter = ArrayAdapter( + requireContext(), android.R.layout.simple_spinner_item, spinnerDataList + ).also { it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } + + with(binding.spinnerRes) { + adapter = spinnerAdapter + viewModel.uiState.value.resolutionIndex?.let { + setSelection(viewModel.uiState.value.resolutionIndex!!) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + resolutionWidth = outputFormats[p2].width + resolutionHeight = outputFormats[p2].height + binding.viewFinder.setAspectRatio(resolutionWidth, resolutionHeight) + if (p2 != viewModel.uiState.value.resolutionIndex) { + viewModel.uiState.value.resolutionIndex = p2 + sendViewState() + } } - viewState.cameraId = data.sensors[p2].cameraId - - - sendViewState() - } - - override fun onNothingSelected(p0: AdapterView<*>?) { + override fun onNothingSelected(p0: AdapterView<*>?) {} } + } ?: run { + Log.i("DEUIBGGGGGG", "NO PRIOR R, " + data.resolutionSelected) + viewModel.uiState.value.resolutionIndex = data.resolutionSelected } } + } - run { - val spinner = fragmentCameraBinding.spinnerQua - val spinnerDataList = ArrayList() - val quals = arrayOf(1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) - quals.forEach { spinnerDataList.add(it.toString()) } - // Initialize the ArrayAdapter - val spinnerAdapter = - ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - spinnerDataList - ) - // Set the dropdown layout style - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - // Set the adapter for the Spinner - spinner!!.adapter = spinnerAdapter - spinner.setSelection(quals.indexOfFirst { it == viewState.quality }) - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + private fun setSpinnerQua() { + val spinnerDataList = arrayOf("1", "10", "20", "30", "40", "50", "60", "70", "80", "90", "100") + val spinnerAdapter = ArrayAdapter( + requireContext(), android.R.layout.simple_spinner_item, spinnerDataList + ).also { it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } + + with(binding.spinnerQua) { + adapter = spinnerAdapter + setSelection(spinnerDataList.indexOfFirst { it.toInt() == viewModel.uiState.value.quality }) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { - viewState.quality = quals[p2] + viewModel.uiState.value.quality = spinnerDataList[p2].toInt() sendViewState() } - override fun onNothingSelected(p0: AdapterView<*>?) { - } + override fun onNothingSelected(p0: AdapterView<*>?) {} } } + } - run { - - val outputFormats = data.resolutions - - val spinner = fragmentCameraBinding.spinnerRes - val spinnerDataList = ArrayList() - outputFormats.forEach { spinnerDataList.add(it.toString()) } - // Initialize the ArrayAdapter - val spinnerAdapter = - ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - spinnerDataList - ) - // Set the dropdown layout style - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - // Set the adapter for the Spinner - spinner!!.adapter = spinnerAdapter - - - if (viewState.resolutionIndex == null) { - Log.i("DEUIBGGGGGG", "NO PRIOR R, " + data.resolutionSelected) - viewState.resolutionIndex = data.resolutionSelected - } + private fun setSpinnerCam(data: CamEngine.Companion.Data) { + val spinnerDataList = data.sensors.map(Selector.SensorDesc::title) + val spinnerAdapter = ArrayAdapter( + requireContext(), android.R.layout.simple_spinner_item, spinnerDataList + ).also { it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } - spinner.setSelection(viewState.resolutionIndex!!) - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + with(binding.spinnerCam) { + adapter = spinnerAdapter + setSelection(data.sensors.indexOf(data.sensorSelected)) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { - resW = outputFormats[p2].width - resH = outputFormats[p2].height - activity?.runOnUiThread(Runnable { - - fragmentCameraBinding.viewFinder.setAspectRatio(resW, resH) - }) - if (p2 != viewState.resolutionIndex) { - viewState.resolutionIndex = p2 - sendViewState() + if (viewModel.uiState.value.cameraId != data.sensors[p2].cameraId) { + viewModel.uiState.value.resolutionIndex = null } - + viewModel.uiState.value.cameraId = data.sensors[p2].cameraId + sendViewState() } - override fun onNothingSelected(p0: AdapterView<*>?) { - } + override fun onNothingSelected(p0: AdapterView<*>?) {} } } - } } - fun sendViewState() { - Cac.sendCam { - it.action = "new_view_state" - - it.putExtra("data", viewState) + cameraActivity.setCameraForegroundServiceState(NEW_VIEW_STATE) { + it.putExtra("data", viewModel.uiState.value) } } override fun onPause() { super.onPause() Log.i("onPause", "onPause") - activity?.unregisterReceiver(receiver) + requireActivity().unregisterReceiver(receiver) } - @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onResume() { super.onResume() Log.i("onResume", "onResume") - activity?.registerReceiver(receiver, IntentFilter("UpdateFromCameraEngine")) + requireActivity().registerReceiver(receiver, IntentFilter("UpdateFromCameraEngine")) } - @SuppressLint("MissingPermission") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.i("onViewCreated", "onViewCreated") + requestPermissionList(getPermissionsRequest(), REQUIRED_PERMISSIONS) + cameraActivity = requireActivity() as CameraActivity - Cac.sendCam { - it.action = "start_camera_engine" - } - // engine.start(requireContext()) + initViews() + } - Log.i("CAMMM", "fragmentCameraBinding.buttonKill " + fragmentCameraBinding.buttonKill) - fragmentCameraBinding.buttonKill.setOnClickListener { - Log.i("CameraFrag", "KILL") - val intent = Intent("KILL") //FILTER is a string to identify this intent - context?.sendBroadcast(intent) - } + private fun initViews() { + setViews() + setListeners() + setObservers() + } - fragmentCameraBinding.viewFinder.holder.addCallback(object : SurfaceHolder.Callback { + private fun setViews() = with(binding) { + textView6.text = "${IpUtil.getLocalIpAddress()}:8080/cam.mjpeg" + viewFinder.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceDestroyed(holder: SurfaceHolder) = Unit - - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int - ) = Unit - + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) = Unit override fun surfaceCreated(holder: SurfaceHolder) { - fragmentCameraBinding.viewFinder.setAspectRatio( - resW, resH - ) - Cac.sendCam { - it.action = "new_preview_surface" - it.putExtra("surface", fragmentCameraBinding.viewFinder.holder.surface) + viewFinder.setAspectRatio(resolutionWidth, resolutionHeight) + if (viewModel.isPermissionsGranted.value == true) { + cameraActivity.setCameraForegroundServiceState(CameraActionState.NEW_PREVIEW_SURFACE) { + it.putExtra("surface", viewFinder.holder.surface) + } } } }) } + private fun setListeners() = with(binding) { + textView6.setOnClickListener { + ClipboardUtil.copyToClipboard(context, "ip", textView6.text.toString()) + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() + } + buttonKill.setOnClickListener { + val intent = Intent(KILL_THE_APP) + requireContext().sendBroadcast(intent) + } + } + + private fun setObservers() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isPermissionsGranted.collect { + it?.let { isGranted -> + if (isGranted) { + cameraActivity.setCameraForegroundServiceState(CameraActionState.START_ENGINE) + cameraActivity.setCameraForegroundServiceState(CameraActionState.NEW_PREVIEW_SURFACE) { ca -> + ca.putExtra("surface", binding.viewFinder.holder.surface) + } + // engine.start(requireContext()) + } else { + Toast.makeText( + context, + "Permission should be accepted to delete download videos", + Toast.LENGTH_LONG + ).show() + } + } + } + } + } + } + + private fun getPermissionsRequest() = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + if (isAllPermissionsGranted(REQUIRED_PERMISSIONS)) { + viewModel.isPermissionsGranted.emit(true) + } else { + viewModel.isPermissionsGranted.emit(false) + } + } + } + } + override fun onStop() { super.onStop() try { @@ -317,27 +302,16 @@ class CameraFragment : Fragment() { } } - override fun onDestroy() { - super.onDestroy() - - } - override fun onDestroyView() { - _fragmentCameraBinding = null + _binding = null super.onDestroyView() } companion object { - private val TAG = CameraFragment::class.java.simpleName - - @Parcelize - data class ViewState( - var preview: Boolean, - var stream: Boolean, - var cameraId: String, - var resolutionIndex: Int?, - var quality: Int - ) : Parcelable + private val TAG = CameraFragment::class.java.simpleName + private val REQUIRED_PERMISSIONS = arrayOf(CAMERA, INTERNET, FOREGROUND_SERVICE, POST_NOTIFICATIONS) + private const val DEFAULT_WIDTH = 1280 + private const val DEFAULT_HEIGHT = 720 } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/fragments/CameraViewModel.kt b/app/src/main/java/com/samsung/android/scan3d/fragments/CameraViewModel.kt new file mode 100644 index 0000000..3c6e869 --- /dev/null +++ b/app/src/main/java/com/samsung/android/scan3d/fragments/CameraViewModel.kt @@ -0,0 +1,10 @@ +package com.samsung.android.scan3d.fragments + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow + +class CameraViewModel : ViewModel() { + + var isPermissionsGranted = MutableStateFlow(null) + var uiState = MutableStateFlow(ViewState()) +} \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/fragments/PermissionsFragment.kt b/app/src/main/java/com/samsung/android/scan3d/fragments/PermissionsFragment.kt deleted file mode 100644 index 9fe5ca6..0000000 --- a/app/src/main/java/com/samsung/android/scan3d/fragments/PermissionsFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.samsung.android.scan3d.fragments - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.os.Bundle -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.lifecycle.lifecycleScope -import com.samsung.android.scan3d.R - -private const val PERMISSIONS_REQUEST_CODE = 10 -private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA, Manifest.permission.INTERNET, Manifest.permission.FOREGROUND_SERVICE,Manifest.permission.POST_NOTIFICATIONS) - -/** - * This [Fragment] requests permissions and, once granted, it will navigate to the next fragment - */ -class PermissionsFragment : Fragment() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (hasPermissions(requireContext())) { - // If permissions have already been granted, proceed - nativateToCamera(); - } else { - // Request camera-related permissions - requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == PERMISSIONS_REQUEST_CODE) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Takes the user to the success fragment when permission is granted - nativateToCamera(); - } else { - Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show() - } - } - } - - private fun nativateToCamera() - { - lifecycleScope.launchWhenStarted { - Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate( - PermissionsFragmentDirections.actionPermissionsToSelector()) - } - } - - companion object { - - /** Convenience method used to check if all permissions required by this app are granted */ - fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all { - ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } - } -} diff --git a/app/src/main/java/com/samsung/android/scan3d/fragments/ViewState.kt b/app/src/main/java/com/samsung/android/scan3d/fragments/ViewState.kt new file mode 100644 index 0000000..d642219 --- /dev/null +++ b/app/src/main/java/com/samsung/android/scan3d/fragments/ViewState.kt @@ -0,0 +1,13 @@ +package com.samsung.android.scan3d.fragments + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ViewState( + var preview: Boolean? = true, + var stream: Boolean? = false, + var cameraId: String = "0", + var resolutionIndex: Int? = null, + var quality: Int? = 80 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/http/HttpService.kt b/app/src/main/java/com/samsung/android/scan3d/http/HttpService.kt index 0e05f6f..c2e4dfa 100644 --- a/app/src/main/java/com/samsung/android/scan3d/http/HttpService.kt +++ b/app/src/main/java/com/samsung/android/scan3d/http/HttpService.kt @@ -2,16 +2,20 @@ package com.samsung.android.scan3d.http import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* +import io.ktor.server.application.call +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine +import io.ktor.server.response.respondOutputStream +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.routing import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach import java.io.OutputStream class HttpService { + lateinit var engine: NettyApplicationEngine var channel = Channel(2) fun producer(): suspend OutputStream.() -> Unit = { @@ -23,7 +27,8 @@ class HttpService { o.flush() } } - public fun main() { + + fun main() { engine = embeddedServer(Netty, port = 8080) { routing { get("/cam") { @@ -32,12 +37,12 @@ class HttpService { get("/cam.mjpeg") { call.respondOutputStream( ContentType.parse("multipart/x-mixed-replace;boundary=FRAME"), - HttpStatusCode.OK, producer() + HttpStatusCode.OK, + producer() ) } } } engine.start(wait = false) } - } \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/serv/Cam.kt b/app/src/main/java/com/samsung/android/scan3d/serv/Cam.kt index d2357f0..f8cc98e 100644 --- a/app/src/main/java/com/samsung/android/scan3d/serv/Cam.kt +++ b/app/src/main/java/com/samsung/android/scan3d/serv/Cam.kt @@ -14,126 +14,121 @@ import android.util.Log import android.view.Surface import androidx.core.app.NotificationCompat import com.samsung.android.scan3d.CameraActivity +import com.samsung.android.scan3d.KILL_THE_APP import com.samsung.android.scan3d.R -import com.samsung.android.scan3d.fragments.CameraFragment +import com.samsung.android.scan3d.fragments.ViewState import com.samsung.android.scan3d.http.HttpService +import com.samsung.android.scan3d.serv.CameraActionState.NEW_PREVIEW_SURFACE +import com.samsung.android.scan3d.serv.CameraActionState.NEW_VIEW_STATE +import com.samsung.android.scan3d.serv.CameraActionState.ON_PAUSE +import com.samsung.android.scan3d.serv.CameraActionState.ON_RESUME +import com.samsung.android.scan3d.serv.CameraActionState.START +import com.samsung.android.scan3d.serv.CameraActionState.START_ENGINE import kotlinx.coroutines.runBlocking - class Cam : Service() { + var engine: CamEngine? = null var http: HttpService? = null val CHANNEL_ID = "REMOTE_CAM" - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.i("CAM", "onStartCommand " + intent?.action) if (intent == null) return START_STICKY when (intent.action) { - "start" -> { - val channel = NotificationChannel( - CHANNEL_ID, - CHANNEL_ID, - NotificationManager.IMPORTANCE_DEFAULT - ) - channel.description = "RemoteCam run" - val notificationManager = getSystemService(NotificationManager::class.java) - notificationManager.createNotificationChannel(channel) - - // Create a notification for the foreground service - val notificationIntent = Intent(this, CameraActivity::class.java) - val pendingIntent = PendingIntent.getActivity( - this, - System.currentTimeMillis().toInt(), - notificationIntent, - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - - val intentKill = Intent("KILL") - val pendingIntentKill = PendingIntent.getBroadcast( - this, - System.currentTimeMillis().toInt(), - intentKill, - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - - - var builer = - NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("RemoteCam (active)") - .setContentText("Click to open").setOngoing(true) - .setSmallIcon(R.drawable.ic_linked_camera).addAction(R.drawable.ic_close, "Kill",pendingIntentKill) - .setContentIntent(pendingIntent) - - - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // builer?.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) - } - val notification: Notification = builer.build() - startForeground(123, notification) // Start the foreground service - - http = HttpService() - http?.main() - + START.name -> { + startNotificationService() + startHttpService() } - "onPause" -> { + ON_PAUSE.name -> { engine?.insidePause = true if (engine?.isShowingPreview == true) { engine?.restart() } - } - "onResume" -> { + ON_RESUME.name -> { engine?.insidePause = false; } - "start_camera_engine" -> { + START_ENGINE.name -> { engine = CamEngine(this) engine?.http = http runBlocking { engine?.initializeCamera() } } - "new_view_state" -> { - + NEW_VIEW_STATE.name -> { val old = engine?.viewState!! - val new : CameraFragment.Companion.ViewState = intent.extras?.getParcelable("data")!! - Log.i("CAM", "new_view_state: " + new) - Log.i("CAM", "from: " + old) - engine?.viewState = new + val new: ViewState = intent.extras?.getParcelable("data")!! + engine?.viewState = new if (old != new) { - Log.i("CAM", "diff") engine?.restart() } } - "new_preview_surface" -> { + NEW_PREVIEW_SURFACE.name -> { val surface: Surface? = intent.extras?.getParcelable("surface") - // Toast.makeText(this, "SURFACE", Toast.LENGTH_SHORT).show() engine?.previewSurface = surface if (engine?.viewState?.preview == true) { runBlocking { engine?.initializeCamera() } } } - else -> { - kill() - } - + else -> kill() } return START_STICKY } - fun kill(){ + private fun startNotificationService() { + val channel = NotificationChannel( + CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT + ).also { it.description = "RemoteCam run" } + + val notificationManager = getSystemService(NotificationManager::class.java).also { + it.createNotificationChannel(channel) + } + + // Create a notification for the foreground service + val notificationIntent = Intent(this, CameraActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, + System.currentTimeMillis().toInt(), + notificationIntent, + FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + + val intentKill = Intent(KILL_THE_APP) + val pendingIntentKill = PendingIntent.getBroadcast( + this, System.currentTimeMillis().toInt(), intentKill, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + ) + + val builder = NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("RemoteCam (active)") + .setContentText("Click to open").setOngoing(true).setSmallIcon(R.drawable.ic_linked_camera) + .addAction(R.drawable.ic_close, "Kill", pendingIntentKill).setContentIntent(pendingIntent) + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // builder?.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) + } + val notification: Notification = builder.build() + startForeground(123, notification) // Start the foreground service + } + + private fun startHttpService() { + http = HttpService() + http?.main() + } + + private fun kill() { engine?.destroy() - http?.engine?.stop(500,500) + http?.engine?.stop(500, 500) stopForeground(STOP_FOREGROUND_REMOVE) } + override fun onBind(intent: Intent?): IBinder? { return null } @@ -144,10 +139,8 @@ class Cam : Service() { kill() } - companion object { - sealed class ToCam() + companion object { sealed class ToCam() class Start() : ToCam() class NewSurface(surface: Surface) : ToCam() - } } \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt b/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt index 39a7326..dcb827c 100644 --- a/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt +++ b/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt @@ -21,17 +21,18 @@ import android.os.Parcelable import android.util.Log import android.util.Size import android.view.Surface -import com.samsung.android.scan3d.fragments.CameraFragment +import com.samsung.android.scan3d.fragments.ViewState import com.samsung.android.scan3d.http.HttpService import com.samsung.android.scan3d.util.Selector import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.parcelize.Parcelize +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger + class CamEngine(val context: Context) { var http: HttpService? = null @@ -44,8 +45,7 @@ class CamEngine(val context: Context) { private var cameraManager: CameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - private var cameraList: List = - Selector.enumerateCameras(cameraManager) + private var cameraList: List = Selector.enumerateCameras(cameraManager) val camOutPutFormat = ImageFormat.JPEG // ImageFormat.YUV_420_888// ImageFormat.JPEG @@ -57,8 +57,7 @@ class CamEngine(val context: Context) { list.forEach { if (it.isEncoder) { Log.i( - "CODECS", - "We got type " + it.name + " " + it.supportedTypes.contentToString() + "CODECS", "We got type " + it.name + " " + it.supportedTypes.contentToString() ) if (it.supportedTypes.any { e -> e.equals(mimeType, ignoreCase = true) }) { return it @@ -82,27 +81,19 @@ class CamEngine(val context: Context) { return encoder } - - var viewState: CameraFragment.Companion.ViewState = CameraFragment.Companion.ViewState( - true, - stream = false, - cameraId = "0", - quality = 80, - resolutionIndex = null + var viewState: ViewState = ViewState( + true, stream = false, cameraId = "0", quality = 80, resolutionIndex = null ) /** [CameraCharacteristics] corresponding to the provided Camera ID */ - var characteristics: CameraCharacteristics = - cameraManager.getCameraCharacteristics(viewState.cameraId) + var characteristics: CameraCharacteristics = cameraManager.getCameraCharacteristics(viewState.cameraId) var sizes = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP )!!.getOutputSizes(camOutPutFormat).reversed() - private lateinit var imageReader: ImageReader - private val cameraThread = HandlerThread("CameraThread").apply { start() } private val cameraHandler = Handler(cameraThread.looper) @@ -126,21 +117,17 @@ class CamEngine(val context: Context) { fun restart() { stopRunning() runBlocking { initializeCamera() } - } @SuppressLint("MissingPermission") private suspend fun openCamera( - manager: CameraManager, - cameraId: String, - handler: Handler? = null + manager: CameraManager, cameraId: String, handler: Handler? = null ): CameraDevice = suspendCancellableCoroutine { cont -> manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) = cont.resume(device) override fun onDisconnected(device: CameraDevice) { Log.w("CamEngine", "Camera $cameraId has been disconnected") - } override fun onError(device: CameraDevice, error: Int) { @@ -164,9 +151,7 @@ class CamEngine(val context: Context) { * suspend coroutine */ private suspend fun createCaptureSession( - device: CameraDevice, - targets: List, - handler: Handler? = null + device: CameraDevice, targets: List, handler: Handler? = null ): CameraCaptureSession = suspendCoroutine { cont -> // Create a capture session using the predefined targets; this also involves defining the @@ -186,8 +171,7 @@ class CamEngine(val context: Context) { suspend fun initializeCamera() { Log.i("CAMERA", "initializeCamera") - - val showLiveSurface = viewState.preview && !insidePause && previewSurface != null + val showLiveSurface = viewState.preview == true && !insidePause && previewSurface != null isShowingPreview = showLiveSurface stopRunning() @@ -220,7 +204,6 @@ class CamEngine(val context: Context) { var targets = listOf(imageReader.surface) if (showLiveSurface) { - targets = targets.plus(previewSurface!!) } session = createCaptureSession(camera, targets, cameraHandler) @@ -231,24 +214,19 @@ class CamEngine(val context: Context) { captureRequest.addTarget(previewSurface!!) } captureRequest.addTarget(imageReader.surface) - captureRequest.set(CaptureRequest.JPEG_QUALITY, viewState.quality.toByte()) + captureRequest.set(CaptureRequest.JPEG_QUALITY, viewState.quality?.toByte()) var lastTime = System.currentTimeMillis() - var kodd = 0 var aquired = AtomicInteger(0) session!!.setRepeatingRequest( - captureRequest.build(), - object : CameraCaptureSession.CaptureCallback() { + captureRequest.build(), object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( - session: CameraCaptureSession, - request: CaptureRequest, - result: TotalCaptureResult + session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) - var lastImg = imageReader.acquireNextImage() if (aquired.get() > 1 && lastImg != null) { @@ -272,26 +250,21 @@ class CamEngine(val context: Context) { if (kodd % 10 == 0) { updateViewQuick( DataQuick( - delta.toInt(), - (30 * bytes.size / 1000) + delta.toInt(), (30 * bytes.size / 1000) ) ) } img.close() aquired.decrementAndGet() - if (viewState.stream) { - + if (viewState.stream == true) { http?.channel?.trySend( bytes ) - } - } } - }, - cameraHandler + }, cameraHandler ) updateView() } @@ -304,8 +277,7 @@ class CamEngine(val context: Context) { fun updateView() { val intent = Intent("UpdateFromCameraEngine") //FILTER is a string to identify this intent intent.putExtra( - "data", - Data( + "data", Data( cameraList, cameraList.find { it.cameraId == viewState.cameraId }!!, resolutions = sizes, @@ -323,21 +295,17 @@ class CamEngine(val context: Context) { context.sendBroadcast(intent) } - companion object { - @Parcelize - data class Data( - val sensors: List, - val sensorSelected: Selector.SensorDesc, - val resolutions: List, - val resolutionSelected: Int, - ) : Parcelable + companion object { @Parcelize + data class Data( + val sensors: List, + val sensorSelected: Selector.SensorDesc, + val resolutions: List, + val resolutionSelected: Int, + ) : Parcelable @Parcelize data class DataQuick( - val ms: Int, - val rateKbs: Int + val ms: Int, val rateKbs: Int ) : Parcelable } - - } \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/serv/CameraActionState.kt b/app/src/main/java/com/samsung/android/scan3d/serv/CameraActionState.kt new file mode 100644 index 0000000..1b6538e --- /dev/null +++ b/app/src/main/java/com/samsung/android/scan3d/serv/CameraActionState.kt @@ -0,0 +1,3 @@ +package com.samsung.android.scan3d.serv + +enum class CameraActionState { START, ON_PAUSE, ON_RESUME, STOP, START_ENGINE, NEW_VIEW_STATE, NEW_PREVIEW_SURFACE } \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt b/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt index 1ebf850..b75e504 100644 --- a/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt +++ b/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt @@ -6,7 +6,8 @@ import android.content.Context object ClipboardUtil { fun copyToClipboard(context: Context?, label: String, text: String) { - val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clipboard = + context?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager val clip = ClipData.newPlainText(label, text) clipboard.setPrimaryClip(clip) } diff --git a/app/src/main/java/com/samsung/android/scan3d/util/FragmentExt.kt b/app/src/main/java/com/samsung/android/scan3d/util/FragmentExt.kt new file mode 100644 index 0000000..12ffba9 --- /dev/null +++ b/app/src/main/java/com/samsung/android/scan3d/util/FragmentExt.kt @@ -0,0 +1,14 @@ +package com.samsung.android.scan3d.util + +import android.content.pm.PackageManager +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment + +fun Fragment.requestPermissionList( + request: ActivityResultLauncher>, permissions: Array +) = request.launch(permissions) + +fun Fragment.isAllPermissionsGranted(permissions: Array): Boolean = permissions.all { + ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED +} \ No newline at end of file diff --git a/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt b/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt index 35e3b7f..9e87f9e 100644 --- a/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt +++ b/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt @@ -8,14 +8,12 @@ import android.hardware.camera2.CameraMetadata import android.os.Parcelable import android.util.Log import kotlinx.parcelize.Parcelize -import java.lang.Exception import kotlin.math.atan import kotlin.math.roundToInt -object Selector { - /** Helper class used as a data holder for each selectable camera format item */ - @Parcelize - data class SensorDesc(val title: String, val cameraId: String, val format: Int) : Parcelable +object Selector { /** Helper class used as a data holder for each selectable camera format item */ +@Parcelize +data class SensorDesc(val title: String, val cameraId: String, val format: Int) : Parcelable /** Helper function used to convert a lens orientation enum into a human-readable string */ private fun lensOrientationString(value: Int) = when (value) { @@ -90,26 +88,20 @@ object Selector { ) ) { false - } else if (capabilities.contains( + } else { + capabilities.contains( CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE ) - ) { - true - } else { - false } - - } catch (e: Exception) { false } }.forEach { cameraIds2.add(it) } - // Iterate over the list of cameras and return all the compatible ones cameraIds2.forEach { id -> - Log.i("SELECTOR", "id: " + id) + Log.i("SELECTOR", "id: $id") val characteristics = cameraManager.getCameraCharacteristics(id) val orientation = lensOrientationString( characteristics.get(CameraCharacteristics.LENS_FACING)!! @@ -122,7 +114,6 @@ object Selector { capabilities.forEach { Log.i("CAP", "" + getCapStringAtIndex(it)) } - val outputFormats = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP )!!.outputFormats @@ -131,7 +122,6 @@ object Selector { CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP )!!.getOutputSizes(ImageFormat.JPEG) - val foaclmm = characteristics.get( CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS )!![0] @@ -144,12 +134,13 @@ object Selector { CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE )!! - val vfov =(( 2.0*(180.0 / 3.141592) * atan(sensorSize.height / (2.0 * foaclmm))).roundToInt().toString()+"°").padEnd(4,' ') + val vfov = ((2.0 * (180.0 / 3.141592) * atan(sensorSize.height / (2.0 * foaclmm))).roundToInt() + .toString() + "°").padEnd(4, ' ') // All cameras *must* support JPEG output so we don't need to check characteristics - val title= "vfov:$vfov $foc $ape $orientation" - if(!availableCameras.any {it-> it.title==title } ){ + val title = "vfov:$vfov $foc $ape $orientation" + if (!availableCameras.any { it -> it.title == title }) { availableCameras.add( SensorDesc( title, id, ImageFormat.JPEG @@ -157,8 +148,6 @@ object Selector { ) } - - } return availableCameras diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 5b9f6ec..300d36d 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1,5 +1,4 @@ - - - - - - - - - + app:startDestination="@id/camera_fragment"> - - - - + android:label="Camera" + tools:layout="@layout/fragment_camera" /> \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt index 596897f..018db98 100644 --- a/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt +++ b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt @@ -20,16 +20,15 @@ import android.content.Context import android.util.AttributeSet import android.util.Log import android.view.SurfaceView -import kotlin.math.roundToInt /** * A [SurfaceView] that can be adjusted to a specified aspect ratio and * performs center-crop transformation of input frames. */ class AutoFitSurfaceView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 ) : SurfaceView(context, attrs, defStyle) { private var aspectRatio = 0f @@ -51,7 +50,6 @@ class AutoFitSurfaceView @JvmOverloads constructor( override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) Log.i("AUTOFIT", "Measured dimensions set: $width x $height") @@ -59,31 +57,29 @@ class AutoFitSurfaceView @JvmOverloads constructor( setMeasuredDimension(width, height) } else { - - val currentRatio = width.toFloat()/height.toFloat() - val inv = 1.0/aspectRatio + val currentRatio = width.toFloat() / height.toFloat() + val inv = 1.0 / aspectRatio val newWidth: Int val newHeight: Int - if(currentRatio > inv){ + if (currentRatio > inv) { //The screen is larger than the picture //We need black bars on the side - newWidth = (height*inv).toInt() - newHeight= height - - }else{ - newWidth = width - newHeight= (width/inv).toInt() + newWidth = (height * inv).toInt() + newHeight = height + } else { + newWidth = width + newHeight = (width / inv).toInt() } // Performs center-crop transformation of the camera frames - Log.i(TAG, "Measured dimensions set: $newWidth x $newHeight") setMeasuredDimension(newWidth, newHeight) } } companion object { + private val TAG = AutoFitSurfaceView::class.java.simpleName } } diff --git a/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt index 6db01d3..64cbb6d 100644 --- a/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt +++ b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt @@ -26,6 +26,7 @@ import kotlin.math.min /** Helper class used to pre-compute shortest and longest sides of a [Size] */ class SmartSize(width: Int, height: Int) { + var size = Size(width, height) var long = max(size.width, size.height) var short = min(size.width, size.height) @@ -47,11 +48,11 @@ fun getDisplaySmartSize(display: Display): SmartSize { * https://d.android.com/reference/android/hardware/camera2/CameraDevice and * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap */ -fun getPreviewOutputSize( - display: Display, - characteristics: CameraCharacteristics, - targetClass: Class, - format: Int? = null +fun getPreviewOutputSize( + display: Display, + characteristics: CameraCharacteristics, + targetClass: Class, + format: Int? = null ): Size { // Find which is smaller: screen or 1080p @@ -61,7 +62,8 @@ fun getPreviewOutputSize( // If image format is provided, use it to determine supported sizes; else use target class val config = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP + )!! if (format == null) assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) else @@ -71,8 +73,8 @@ fun getPreviewOutputSize( // Get available sizes and sort them by area from largest to smallest val validSizes = allSizes - .sortedWith(compareBy { it.height * it.width }) - .map { SmartSize(it.width, it.height) }.reversed() + .sortedWith(compareBy { it.height * it.width }) + .map { SmartSize(it.width, it.height) }.reversed() // Then, get the largest output size that is smaller or equal than our max size return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size diff --git a/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt index 561c14b..83051f3 100644 --- a/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt +++ b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt @@ -59,6 +59,7 @@ fun decodeExifOrientation(exifOrientation: Int): Matrix { matrix.postScale(-1F, 1F) matrix.postRotate(270F) } + ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.postScale(-1F, 1F) matrix.postRotate(90F) diff --git a/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt index a55af27..873d04d 100644 --- a/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt +++ b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt @@ -26,25 +26,29 @@ typealias BindCallback = (view: View, data: T, position: Int) -> Unit /** List adapter for generic types, intended used for small-medium lists of data */ class GenericListAdapter( - private val dataset: List, - private val itemLayoutId: Int? = null, - private val itemViewFactory: (() -> View)? = null, - private val onBind: BindCallback + private val dataset: List, + private val itemLayoutId: Int? = null, + private val itemViewFactory: (() -> View)? = null, + private val onBind: BindCallback ) : RecyclerView.Adapter() { class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder(when { - itemViewFactory != null -> itemViewFactory.invoke() - itemLayoutId != null -> { - LayoutInflater.from(parent.context) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder( + when { + itemViewFactory != null -> itemViewFactory.invoke() + itemLayoutId != null -> { + LayoutInflater.from(parent.context) .inflate(itemLayoutId, parent, false) + } + + else -> { + throw IllegalStateException( + "Either the layout ID or the view factory need to be non-null" + ) + } } - else -> { - throw IllegalStateException( - "Either the layout ID or the view factory need to be non-null") - } - }) + ) override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { if (position < 0 || position > dataset.size) return diff --git a/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt index f9d9a47..2659197 100644 --- a/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt +++ b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt @@ -22,16 +22,15 @@ import android.view.OrientationEventListener import android.view.Surface import androidx.lifecycle.LiveData - /** * Calculates closest 90-degree orientation to compensate for the device * rotation relative to sensor orientation, i.e., allows user to see camera * frames with the expected orientation. */ class OrientationLiveData( - context: Context, - characteristics: CameraCharacteristics -): LiveData() { + context: Context, + characteristics: CameraCharacteristics +) : LiveData() { private val listener = object : OrientationEventListener(context.applicationContext) { override fun onOrientationChanged(orientation: Int) { @@ -69,11 +68,11 @@ class OrientationLiveData( */ @JvmStatic private fun computeRelativeRotation( - characteristics: CameraCharacteristics, - surfaceRotation: Int + characteristics: CameraCharacteristics, + surfaceRotation: Int ): Int { val sensorOrientationDegrees = - characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! + characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! val deviceOrientationDegrees = when (surfaceRotation) { Surface.ROTATION_0 -> 0 @@ -85,7 +84,8 @@ class OrientationLiveData( // Reverse device orientation for front-facing cameras val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == - CameraCharacteristics.LENS_FACING_FRONT) 1 else -1 + CameraCharacteristics.LENS_FACING_FRONT + ) 1 else -1 // Calculate desired JPEG orientation relative to camera orientation to make // the image upright relative to the device orientation diff --git a/utils/src/main/java/com/example/android/camera/utils/Yuv.kt b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt index c476ad0..4ab64fd 100644 --- a/utils/src/main/java/com/example/android/camera/utils/Yuv.kt +++ b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt @@ -45,6 +45,7 @@ More about each format: https://www.fourcc.org/yuv.php annotation class YuvType class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { + @YuvType val type: Int val buffer: ByteBuffer @@ -62,8 +63,8 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { dstBuffer == null || dstBuffer.capacity() < size || dstBuffer.isReadOnly || !dstBuffer.isDirect ) { - ByteBuffer.allocateDirect(size) } - else { + ByteBuffer.allocateDirect(size) + } else { dstBuffer } buffer.rewind() @@ -157,8 +158,9 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { return duplicate.slice() } - private class ImageWrapper(image:Image) { - val width= image.width + private class ImageWrapper(image: Image) { + + val width = image.width val height = image.height val y = PlaneWrapper(width, height, image.planes[0]) val u = PlaneWrapper(width / 2, height / 2, image.planes[1]) @@ -172,8 +174,8 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { } require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) { "U and V planes must have the same pixel and row strides " + - "but got pixel=${u.pixelStride} row=${u.rowStride} for U " + - "and pixel=${v.pixelStride} and row=${v.rowStride} for V" + "but got pixel=${u.pixelStride} row=${u.rowStride} for U " + + "and pixel=${v.pixelStride} and row=${v.rowStride} for V" } require(u.pixelStride == 1 || u.pixelStride == 2) { "Supported" + " pixel strides for U and V planes are 1 and 2" @@ -182,6 +184,7 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { } private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) { + val width = width val height = height val buffer: ByteBuffer = plane.buffer diff --git a/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt index 8dcd559..2944ce8 100644 --- a/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt +++ b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt @@ -42,6 +42,7 @@ import java.nio.ByteBuffer * might have better performance. */ class YuvToRgbConverter(context: Context) { + private val rs = RenderScript.create(context) private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) @@ -91,9 +92,9 @@ class YuvToRgbConverter(context: Context) { private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean { return (inputAllocation == null || // the very 1st call - inputAllocation!!.type.x != image.width || // image size changed - inputAllocation!!.type.y != image.height || - inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed - bytes.size == yuvBuffer.buffer.capacity()) + inputAllocation!!.type.x != image.width || // image size changed + inputAllocation!!.type.y != image.height || + inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed + bytes.size == yuvBuffer.buffer.capacity()) } }