diff --git a/jami-android/app/src/main/java/cx/ring/services/CameraService.kt b/jami-android/app/src/main/java/cx/ring/services/CameraService.kt index 33c37582fa2118323f9ab32b53d741a0ef458d49..f61adb1b1e436b4098cde43e4911635fb0a638b3 100644 --- a/jami-android/app/src/main/java/cx/ring/services/CameraService.kt +++ b/jami-android/app/src/main/java/cx/ring/services/CameraService.kt @@ -118,6 +118,9 @@ class CameraService internal constructor(c: Context) { val maxResolutions: Observable<Pair<Int?, Int?>> get() = maxResolutionSubject + val handler: Handler + get() = videoHandler + class VideoDevices { val cameras: MutableList<String> = ArrayList() var currentId: String? = null diff --git a/jami-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt b/jami-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt index df41a47818cbbd9efe974c06cc9ab80d7ebbd602..61bcd4c9e6a918dd231416324b8ca822ddb9026a 100644 --- a/jami-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt +++ b/jami-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.kt @@ -27,6 +27,7 @@ import android.media.AudioManager.OnAudioFocusChangeListener import android.media.MediaRecorder import android.media.projection.MediaProjection import android.os.Build +import android.os.Handler import android.telecom.CallAudioState import android.util.Log import android.util.Size @@ -85,6 +86,8 @@ class HardwareServiceImpl( init { pushLogEnabled = sharedPreferences.getBoolean(LOGGING_ENABLED_KEY, false) } + val handler: Handler + get() = cameraService.handler override fun initVideo(): Completable = cameraService.init() diff --git a/jami-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.kt b/jami-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.kt index 36fb8d012d2fd7e5e529476323e227a149ce8ff9..23813865d3bf8ea8aa7b2d2e2b75134527723aec 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.kt @@ -17,12 +17,19 @@ package cx.ring.tv.camera import android.content.Context +import android.graphics.SurfaceTexture import android.hardware.Camera -import android.view.SurfaceView -import android.view.SurfaceHolder +import android.util.Log +import android.view.TextureView import java.lang.Exception -class CameraPreview(context: Context, private var mCamera: Camera?) : SurfaceView(context), SurfaceHolder.Callback { +class CameraPreview(context: Context, private var mCamera: Camera?) : TextureView(context), TextureView.SurfaceTextureListener { + private var mSurfaceTexture: SurfaceTexture? = null + + init { + surfaceTextureListener = this + } + fun stop() { mCamera?.let { camera -> try { @@ -32,37 +39,47 @@ class CameraPreview(context: Context, private var mCamera: Camera?) : SurfaceVie } camera.release() mCamera = null + mSurfaceTexture?.release() } + surfaceTextureListener = null } - override fun surfaceCreated(surfaceHolder: SurfaceHolder) { - mCamera?.let { camera -> - try { - camera.setPreviewDisplay(surfaceHolder) - camera.startPreview() - } catch (e: Exception) { - // left blank for now - } - } + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + Log.w(TAG, "Surface texture available: $width x $height") + startPreview(surface) + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + Log.w(TAG, "Surface texture changed: $width x $height") + startPreview(surface) } - override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + Log.w(TAG, "Surface texture destroyed") stop() + mSurfaceTexture = null + return true } - override fun surfaceChanged(surfaceHolder: SurfaceHolder, format: Int, width: Int, height: Int) { + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + // intentionally left blank + } + + private fun startPreview(surfaceTexture: SurfaceTexture) { mCamera?.let { camera -> try { - camera.setPreviewDisplay(surfaceHolder) + if (mSurfaceTexture != surfaceTexture) { + camera.setPreviewTexture(surfaceTexture) + mSurfaceTexture = surfaceTexture + } camera.startPreview() } catch (e: Exception) { - // intentionally left blank for a test + Log.w(TAG, "Error starting camera preview: ${e.message}") } } } - // Constructor that obtains context and camera - init { - holder.addCallback(this) + companion object { + private const val TAG = "CameraPreview" } } \ No newline at end of file diff --git a/jami-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.kt b/jami-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.kt index 6a99cdc8d88735b7408d0b61af3bdcfa6c3a2e46..97a1e55ad6d9a488805cdaac2e2bb248e75b7b12 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.kt @@ -34,6 +34,7 @@ import android.media.CamcorderProfile import android.net.Uri import android.os.Build import android.util.Log +import android.view.Surface import android.view.View import cx.ring.databinding.CamerapickerBinding import cx.ring.utils.AndroidFileUtils @@ -244,7 +245,7 @@ class CustomCameraActivity : Activity() { } private fun prepareRecorder() { - recorder!!.setPreviewDisplay(mCameraPreview!!.holder.surface) + recorder!!.setPreviewDisplay(Surface(mCameraPreview!!.surfaceTexture)) try { recorder!!.prepare() recorder!!.start() diff --git a/jami-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.kt b/jami-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.kt index 3d34ef0c0d5069c7c865ef8fc3abcb6296e2e0fb..ad2f6959e7f7ff4b8fb60aaaac09e605ff8919d6 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/contact/TVContactFragment.kt @@ -27,8 +27,8 @@ import cx.ring.fragments.CallFragment import cx.ring.tv.call.TVCallActivity import cx.ring.tv.contact.more.TVContactMoreActivity import cx.ring.tv.contact.more.TVContactMoreFragment -import cx.ring.tv.contactrequest.TVContactRequestDetailPresenter import cx.ring.tv.main.BaseDetailFragment +import cx.ring.tv.main.HomeActivity import cx.ring.utils.ConversationPath import cx.ring.views.AvatarDrawable import dagger.hilt.android.AndroidEntryPoint @@ -100,6 +100,11 @@ class TVContactFragment : BaseDetailFragment<TVContactPresenter>(), TVContactVie } } + override fun onResume() { + super.onResume() + (activity as? HomeActivity)?.enableBlur(false) + } + override fun showContact(account: Account, model: ConversationItemViewModel) { val context = requireContext() val row = DetailsOverviewRow(model) diff --git a/jami-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt b/jami-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt index 88b31a476b79aa9116b94daabe2c4ca5da29f92e..ec3daa47de2627b1da285026c3747f09e2fbc7e1 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/main/HomeActivity.kt @@ -20,7 +20,8 @@ import android.Manifest import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap -import android.graphics.drawable.ColorDrawable +import android.graphics.RenderEffect +import android.graphics.Shader import android.hardware.Camera import android.hardware.Camera.ErrorCallback import android.hardware.camera2.CameraManager @@ -37,7 +38,6 @@ import android.util.Log import android.util.Size import android.view.KeyEvent import android.os.Handler -import android.os.Looper import android.view.View import android.widget.FrameLayout import android.widget.ImageView @@ -64,7 +64,12 @@ import net.jami.services.DeviceRuntimeService import net.jami.services.HardwareService import java.util.Collections import javax.inject.Inject +import androidx.core.view.isGone +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isInvisible +@Suppress("DEPRECATION") @AndroidEntryPoint class HomeActivity : FragmentActivity() { private val mDisposableBag = CompositeDisposable() @@ -90,24 +95,42 @@ class HomeActivity : FragmentActivity() { private var out: Allocation? = null private var mBlurOut: Allocation? = null private var cameraPermissionIsRefusedFlag = false // to not ask for permission again if refused + private var paused = false + private var renderEffect: RenderEffect? = null private val mErrorCallback = ErrorCallback { error, camera -> + Log.w(TAG, "Camera error: $error") try { mBlurImage.visibility = View.INVISIBLE mBackgroundManager.drawable = - ContextCompat.getDrawable(this@HomeActivity, R.drawable.tv_background) - ?: ColorDrawable(getColor(R.color.colorPrimary)) + ContextCompat.getDrawable(this@HomeActivity, R.drawable.background_welcome_jami) + ?: getColor(R.color.colorPrimary).toDrawable() + mCamera = null + mPreviewView.removeAllViews() + mCameraPreview?.stop() } catch (e: Exception) { Log.e(TAG, "ErrorCallback", e) } } private val mCameraAvailabilityCallback: AvailabilityCallback = object : AvailabilityCallback() { override fun onCameraAvailable(cameraId: String) { - if (mBlurImage.visibility == View.INVISIBLE) { + Log.w(TAG, "onCameraAvailable $cameraId paused:$paused") + if (!paused && mBlurImage.isInvisible) { checkCameraAvailability() } } } + + fun enableBlur(enable: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (enable) { + mPreviewView.setRenderEffect(renderEffect) + } else { + mPreviewView.setRenderEffect(null) + } + } + } + private val mPreviewCallback = Camera.PreviewCallback { data, camera -> if (supportFragmentManager.findFragmentByTag(TVContactFragment.TAG) != null) { mBlurImage.visibility = View.GONE @@ -127,7 +150,7 @@ class HomeActivity : FragmentActivity() { setRadius(BLUR_RADIUS * size.width / 1080) setInput(out) } - mBlurOutputBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888) + mBlurOutputBitmap = createBitmap(size.width, size.height) mBlurOut = Allocation.createFromBitmap(rs, mBlurOutputBitmap) } input!!.copyFrom(data) @@ -135,7 +158,7 @@ class HomeActivity : FragmentActivity() { blurIntrinsic!!.forEach(mBlurOut) mBlurOut!!.copyTo(mBlurOutputBitmap) mBlurImage.setImageBitmap(mBlurOutputBitmap) - if (mBlurImage.visibility == View.GONE) { + if (mBlurImage.isGone) { mPreviewView.visibility = View.INVISIBLE mBlurImage.visibility = View.VISIBLE mFadeView.visibility = View.VISIBLE @@ -152,6 +175,13 @@ class HomeActivity : FragmentActivity() { mPreviewView = findViewById(R.id.previewView) mBlurImage = findViewById(R.id.blur) mFadeView = findViewById(R.id.fade) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val radius = BLUR_RADIUS * TARGET_SIZE.width / 1080 * resources.displayMetrics.density + renderEffect = RenderEffect.createBlurEffect( + radius, radius, + Shader.TileMode.MIRROR + ) + } } override fun onBackPressed() { @@ -189,16 +219,23 @@ class HomeActivity : FragmentActivity() { override fun onPostResume() { super.onPostResume() + paused = false checkCameraAvailability() } override fun onPause() { super.onPause() + paused = true mCameraPreview?.let { preview -> - mCamera?.setPreviewCallback(null) + mCamera?.let { camera -> + camera.release(); + mCamera = null + } preview.stop() mCameraPreview = null + mPreviewView.removeAllViews() } + mDisposableBag.clear() } override fun onDestroy() { @@ -251,7 +288,7 @@ class HomeActivity : FragmentActivity() { val w = target.width val h = target.height for (option in choices) { - Log.w(TAG, "supportedSize: $option") + //Log.w(TAG, "supportedSize: $option") if (option.width <= maxWidth && option.height <= maxHeight && option.height == option.width * h / w) { if (option.width >= minWidth && option.height >= minHeight) { bigEnough.add(option) @@ -264,8 +301,8 @@ class HomeActivity : FragmentActivity() { // Pick the smallest of those big enough. If there is no one big enough, pick the // largest of those not big enough. return when { - bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea()) - notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea()) + bigEnough.isNotEmpty() -> Collections.min(bigEnough, CompareSizesByArea()) + notBigEnough.isNotEmpty() -> Collections.max(notBigEnough, CompareSizesByArea()) else -> { Log.e(TAG, "Couldn't find any suitable preview size") choices[0] @@ -274,39 +311,70 @@ class HomeActivity : FragmentActivity() { } private fun setUpCamera() { + Log.w(TAG, "setUpCamera()") + if (mCamera != null) { + Log.w(TAG, "setUpCamera() camera already set up") + return + } mDisposableBag.add(Single.fromCallable { val currentCamera = 0 - Camera.open(currentCamera)!!.apply { mCamera = this } + if (mCameraManager == null) { + mCameraManager = (getSystemService(CAMERA_SERVICE) as CameraManager).apply { + registerAvailabilityCallback((mCameraAvailabilityCallback), Handler(mainLooper)) + } + } + mCameraPreview?.stop() + + val camera = try { + Camera.open(currentCamera) + } catch (e: Exception) { + Log.e(TAG, "setUpCamera() failed to open camera", e) + throw RuntimeException("Camera unavailable", e) + } + + camera.apply { + val params = parameters + Log.w(TAG, "setUpCamera() supportedPictureSizes: ${params.previewSize.width}, x: ${params.previewSize.height}") + val selectSize = chooseOptimalSize(params.supportedPictureSizes, 1280, 720, 1920, 1080, TARGET_SIZE) + Log.w(TAG, "setUpCamera() selectSize: ${selectSize.width}, x: ${selectSize.height}") + params.setPictureSize(selectSize.width, selectSize.height) + params.setPreviewSize(selectSize.width, selectSize.height) + setErrorCallback(mErrorCallback) + try { + parameters = params + } catch (e: Exception) { + Log.e(TAG, "setParameters() error", e) + } + mCamera = this + } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ camera: Camera -> + .subscribe({ camera -> try { - Log.w(TAG, "setUpCamera()") - val params = camera.parameters - val selectSize = chooseOptimalSize(params.supportedPictureSizes, 1280, 720, 1920, 1080, Size(1280, 720)) - Log.w(TAG, "setUpCamera() selectSize " + selectSize.width + "x" + selectSize.height) - params.setPictureSize(selectSize.width, selectSize.height) - params.setPreviewSize(selectSize.width, selectSize.height) - camera.parameters = params - mBlurImage.visibility = View.VISIBLE + Log.w(TAG, "setUpCamera() $camera") mFadeView.visibility = View.VISIBLE - if (mCameraManager == null) { - mCameraManager = (getSystemService(CAMERA_SERVICE) as CameraManager).apply { - registerAvailabilityCallback((mCameraAvailabilityCallback), null) - } - } - mCameraPreview = CameraPreview(this, camera) + val cameraPreview = CameraPreview(this, camera) mPreviewView.removeAllViews() - mPreviewView.addView(mCameraPreview, 0) - camera.setErrorCallback(mErrorCallback) - camera.setPreviewCallback(mPreviewCallback) + mPreviewView.addView(cameraPreview, 0) + mCameraPreview = cameraPreview + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + mBlurImage.visibility = View.INVISIBLE + val radiusDp = BLUR_RADIUS * TARGET_SIZE.width / 1080 + val radius = radiusDp * resources.displayMetrics.density + Log.w(TAG, "setUpCamera() blur radius $radius") + mPreviewView.setRenderEffect(renderEffect) + } else { + mBlurImage.visibility = View.VISIBLE + camera.setPreviewCallback(mPreviewCallback) + } } catch (e: Exception) { - Log.e(TAG, "setUpCamera() error", e) - mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.tv_background) + Log.e(TAG, "setUpCamera() display error", e) + mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.background_welcome_jami) } - }) { - mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.tv_background) + }) { e -> + Log.e(TAG, "setUpCamera() error", e) + mBackgroundManager.drawable = ContextCompat.getDrawable(this@HomeActivity, R.drawable.background_welcome_jami) }) } @@ -345,6 +413,7 @@ class HomeActivity : FragmentActivity() { companion object { private val TAG = HomeActivity::class.simpleName!! + private val TARGET_SIZE = Size(1280, 720) private const val BLUR_RADIUS = 7.5f } } \ No newline at end of file diff --git a/jami-android/app/src/main/java/cx/ring/tv/main/MainFragment.kt b/jami-android/app/src/main/java/cx/ring/tv/main/MainFragment.kt index 2b9240a8756090f0c921115fd10057e861afcf7d..feb4ff5808e4b705162f3ba799592e7793f0d450 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/main/MainFragment.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/main/MainFragment.kt @@ -106,6 +106,13 @@ class MainFragment : BaseBrowseFragment<MainPresenter>(), MainView { super.onViewCreated(view, savedInstanceState) } + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + (activity as? HomeActivity)?.enableBlur(true) + } + } + override fun onDestroyView() { super.onDestroyView() mDisposable.clear() diff --git a/jami-android/app/src/main/res/layout/tv_activity_home.xml b/jami-android/app/src/main/res/layout/tv_activity_home.xml index 98b8881ca44eb9f2f9badcd7926e0793cff6e6f1..3d64a8ba6ed448a17ffa3c4089240ea2a5a93767 100644 --- a/jami-android/app/src/main/res/layout/tv_activity_home.xml +++ b/jami-android/app/src/main/res/layout/tv_activity_home.xml @@ -8,7 +8,7 @@ android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="invisible"/> + android:visibility="visible"/> <ImageView android:id="@+id/blur"