diff --git a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt index 034cf9bf3..f80788bdf 100644 --- a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt +++ b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt @@ -20,13 +20,17 @@ package org.linphone.ui.assistant import android.os.Bundle +import android.view.ViewGroup +import androidx.annotation.DrawableRes import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import androidx.databinding.DataBindingUtil +import androidx.lifecycle.lifecycleScope import org.linphone.LinphoneApplication import org.linphone.R import org.linphone.databinding.AssistantActivityBinding +import org.linphone.utils.slideInToastFromTopForDuration @UiThread class AssistantActivity : AppCompatActivity() { @@ -43,4 +47,20 @@ class AssistantActivity : AppCompatActivity() { binding = DataBindingUtil.setContentView(this, R.layout.assistant_activity) binding.lifecycleOwner = this } + + fun showGreenToast(message: String, @DrawableRes icon: Int) { + binding.greenToast.message = message + binding.greenToast.icon = icon + + val target = binding.greenToast.root + target.slideInToastFromTopForDuration(binding.root as ViewGroup, lifecycleScope) + } + + fun showRedToast(message: String, @DrawableRes icon: Int) { + binding.redToast.message = message + binding.redToast.icon = icon + + val target = binding.redToast.root + target.slideInToastFromTopForDuration(binding.root as ViewGroup, lifecycleScope) + } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt index 391498a86..4bd3840bf 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt @@ -24,6 +24,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.core.content.ContextCompat import androidx.navigation.fragment.findNavController import androidx.navigation.navGraphViewModels import org.linphone.R @@ -67,10 +68,26 @@ class LoginFragment : GenericFragment() { findNavController().navigate(action) } + binding.setQrCodeClickListener { + val action = LoginFragmentDirections.actionLoginFragmentToQrCodeScannerFragment() + findNavController().navigate(action) + } + viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) { it.consume { goBack() } } } + + override fun onResume() { + super.onResume() + + // In case we come back from QrCodeScannerFragment + val white = ContextCompat.getColor( + requireContext(), + R.color.white + ) + requireActivity().window.navigationBarColor = white + } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt new file mode 100644 index 000000000..c5f53aa9c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.assistant.fragment + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.UiThread +import androidx.core.content.ContextCompat +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.databinding.AssistantQrCodeScannerFragmentBinding +import org.linphone.ui.assistant.AssistantActivity +import org.linphone.ui.assistant.viewmodel.QrCodeViewModel +import org.linphone.ui.main.fragment.GenericFragment + +@UiThread +class QrCodeScannerFragment : GenericFragment() { + companion object { + private const val TAG = "[Qr Code Scanner Fragment]" + } + + private lateinit var binding: AssistantQrCodeScannerFragmentBinding + + private val viewModel: QrCodeViewModel by navGraphViewModels( + R.id.qrCodeScannerFragment + ) + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + Log.i("$TAG Camera permission has been granted") + enableQrCodeVideoScanner() + } else { + Log.e("$TAG Camera permission has been denied, leaving this fragment") + goBack() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantQrCodeScannerFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun goBack() { + findNavController().popBackStack() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + + binding.setBackClickListener { + goBack() + } + + viewModel.qrCodeFoundEvent.observe(viewLifecycleOwner) { + it.consume { isValid -> + if (isValid) { + (requireActivity() as AssistantActivity).showGreenToast( + "QR code validated!", + R.drawable.check_fat_fill + ) + } else { + (requireActivity() as AssistantActivity).showRedToast( + "Invalid QR code!", + R.drawable.warning_circle + ) + } + } + } + + if (!isCameraPermissionGranted()) { + Log.w("$TAG Camera permission wasn't granted yet, asking for it now") + requestPermissionLauncher.launch(Manifest.permission.CAMERA) + } + + // Better to have black background in navigation bar when doing full screen video + val black = ContextCompat.getColor( + requireContext(), + R.color.black + ) + requireActivity().window.navigationBarColor = black + } + + override fun onResume() { + super.onResume() + + if (isCameraPermissionGranted()) { + viewModel.setBackCamera() + enableQrCodeVideoScanner() + } + } + + override fun onPause() { + coreContext.postOnCoreThread { core -> + core.nativePreviewWindowId = null + core.isVideoPreviewEnabled = false + core.isQrcodeVideoPreviewEnabled = false + } + + super.onPause() + } + + private fun isCameraPermissionGranted(): Boolean { + val granted = ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + Log.i("$TAG Camera permission is ${if (granted) "granted" else "denied"}") + return granted + } + + private fun enableQrCodeVideoScanner() { + coreContext.postOnCoreThread { core -> + core.nativePreviewWindowId = binding.qrCodePreview + core.isQrcodeVideoPreviewEnabled = true + core.isVideoPreviewEnabled = true + } + } +} diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt new file mode 100644 index 000000000..c46dd3fc6 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.assistant.viewmodel + +import android.util.Patterns +import androidx.annotation.UiThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class QrCodeViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Qr Code Scanner ViewModel]" + } + + val qrCodeFoundEvent = MutableLiveData>() + + private val coreListener = object : CoreListenerStub() { + @WorkerThread + override fun onQrcodeFound(core: Core, result: String?) { + Log.i("$TAG QR Code found: [$result]") + if (result == null) { + qrCodeFoundEvent.postValue(Event(false)) + } else { + val isValidUrl = Patterns.WEB_URL.matcher(result).matches() + if (!isValidUrl) { + Log.e("$TAG The content of the QR Code doesn't seem to be a valid web URL") + } + qrCodeFoundEvent.postValue(Event(isValidUrl)) + } + } + } + + init { + coreContext.postOnCoreThread { core -> + core.addListener(coreListener) + } + } + + @UiThread + override fun onCleared() { + coreContext.postOnCoreThread { core -> + core.removeListener(coreListener) + } + super.onCleared() + } + + @UiThread + fun setBackCamera() { + coreContext.postOnCoreThread { core -> + for (camera in core.videoDevicesList) { + if (camera.contains("Back")) { + Log.i("$TAG Found back facing camera [$camera], using it") + coreContext.core.videoDevice = camera + return@postOnCoreThread + } + } + + val first = core.videoDevicesList.firstOrNull() + if (first != null) { + Log.w("$TAG No back facing camera found, using first one available [$first]") + coreContext.core.videoDevice = first + } + } + } +} diff --git a/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt b/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt index 82d118275..a053545fd 100644 --- a/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt +++ b/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt @@ -26,6 +26,7 @@ import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import androidx.annotation.UiThread +import java.lang.NumberFormatException import org.linphone.R import org.linphone.mediastream.video.capture.CaptureTextureView @@ -71,12 +72,14 @@ class RoundCornersTextureView : CaptureTextureView { 2 -> DisplayMode.HYBRID else -> DisplayMode.BLACK_BARS } - mRadius = getFloat( - R.styleable.RoundCornersTextureView_radius, - context.resources.getDimension( - R.dimen.in_call_round_corners_texture_view_radius + mRadius = try { + getFloat( + R.styleable.RoundCornersTextureView_radius, + context.resources.getDimension( + R.dimen.in_call_round_corners_texture_view_radius + ) ) - ) + } catch (nfe: NumberFormatException) { 0f } } finally { recycle() } diff --git a/app/src/main/java/org/linphone/ui/welcome/WelcomeActivity.kt b/app/src/main/java/org/linphone/ui/welcome/WelcomeActivity.kt index 7b1503230..3fe19d7fc 100644 --- a/app/src/main/java/org/linphone/ui/welcome/WelcomeActivity.kt +++ b/app/src/main/java/org/linphone/ui/welcome/WelcomeActivity.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.welcome +import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity @@ -32,6 +33,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.WelcomeActivityBinding +import org.linphone.ui.assistant.AssistantActivity import org.linphone.ui.welcome.fragment.WelcomePage1Fragment import org.linphone.ui.welcome.fragment.WelcomePage2Fragment import org.linphone.ui.welcome.fragment.WelcomePage3Fragment @@ -80,8 +82,12 @@ class WelcomeActivity : AppCompatActivity() { binding.setNextClickListener { if (viewPager.currentItem == PAGES - 1) { - Log.i("$TAG User clicked on 'start' button, leaving activity") + Log.i( + "$TAG User clicked on 'start' button, leaving activity and going into Assistant" + ) finish() + val intent = Intent(this, AssistantActivity::class.java) + startActivity(intent) } else { viewPager.currentItem = viewPager.currentItem + 1 } diff --git a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt b/app/src/main/java/org/linphone/utils/AnimationsUtils.kt index daeee5d20..329c58d10 100644 --- a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt +++ b/app/src/main/java/org/linphone/utils/AnimationsUtils.kt @@ -58,6 +58,10 @@ fun View.slideInToastFromTopForDuration( duration: Long = 4000 ) { val view = this + if (view.visibility == View.VISIBLE) { + // Toast is already visible, hide existing one first + view.visibility = View.GONE + } val transition: Transition = Slide(Gravity.TOP) transition.duration = 600 diff --git a/app/src/main/res/layout/assistant_activity.xml b/app/src/main/res/layout/assistant_activity.xml index 11a9dead4..4db7a39ad 100644 --- a/app/src/main/res/layout/assistant_activity.xml +++ b/app/src/main/res/layout/assistant_activity.xml @@ -11,13 +11,51 @@ android:layout_height="match_parent" tools:context=".ui.assistant.AssistantActivity"> - + android:layout_height="match_parent"> + + + + + + + + diff --git a/app/src/main/res/layout/assistant_login_fragment.xml b/app/src/main/res/layout/assistant_login_fragment.xml index eec9635e4..2e78baac6 100644 --- a/app/src/main/res/layout/assistant_login_fragment.xml +++ b/app/src/main/res/layout/assistant_login_fragment.xml @@ -12,6 +12,9 @@ + @@ -203,6 +206,7 @@ app:layout_constraintBottom_toBottomOf="@id/or"/> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/assistant_nav_graph.xml b/app/src/main/res/navigation/assistant_nav_graph.xml index 41afb403b..8be29f525 100644 --- a/app/src/main/res/navigation/assistant_nav_graph.xml +++ b/app/src/main/res/navigation/assistant_nav_graph.xml @@ -16,7 +16,16 @@ app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" - app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" + app:launchSingleTop="true" /> + + + \ No newline at end of file