Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VOC-401 More intuatively asks for Camera permissions. #443

Merged
merged 7 commits into from
Nov 22, 2023
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
// Provides ARCore Session and related resources.
Expand Down Expand Up @@ -105,6 +105,7 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.1"
implementation "androidx.navigation:navigation-ui-ktx:2.4.1"

implementation 'com.vmadalin:easypermissions-ktx:1.0.0'

implementation 'com.jakewharton.timber:timber:5.0.1'
implementation 'com.google.firebase:firebase-analytics:20.1.2'
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/com/willowtree/vocable/AppKoinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.willowtree.vocable

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.willowtree.vocable.facetracking.FaceTrackingViewModel
import com.willowtree.vocable.presets.IPresetsRepository
import com.willowtree.vocable.presets.PresetCategoriesRepository
import com.willowtree.vocable.presets.PresetsRepository
Expand All @@ -12,23 +13,25 @@ import com.willowtree.vocable.room.StoredCategoriesRepository
import com.willowtree.vocable.settings.AddUpdateCategoryViewModel
import com.willowtree.vocable.settings.EditCategoriesViewModel
import com.willowtree.vocable.settings.EditCategoryMenuViewModel
import com.willowtree.vocable.settings.selectionmode.SelectionModeViewModel
import com.willowtree.vocable.utils.DateProvider
import com.willowtree.vocable.utils.ILocalizedResourceUtility
import com.willowtree.vocable.utils.IVocableSharedPreferences
import com.willowtree.vocable.utils.JavaDateProvider
import com.willowtree.vocable.utils.locale.JavaLocaleProvider
import com.willowtree.vocable.utils.locale.LocaleProvider
import com.willowtree.vocable.utils.locale.LocalizedResourceUtility
import com.willowtree.vocable.utils.RandomUUIDProvider
import com.willowtree.vocable.utils.UUIDProvider
import com.willowtree.vocable.utils.VocableSharedPreferences
import com.willowtree.vocable.utils.locale.JavaLocaleProvider
import com.willowtree.vocable.utils.locale.LocaleProvider
import com.willowtree.vocable.utils.locale.LocalizedResourceUtility
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module

object AppKoinModule {

fun getModule() = module {
single { VocableSharedPreferences() }
single { VocableSharedPreferences() } bind IVocableSharedPreferences::class
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

single { PresetsRepository(get()) } bind IPresetsRepository::class
single { Moshi.Builder().add(KotlinJsonAdapterFactory()).build() }
single { LocalizedResourceUtility() } bind ILocalizedResourceUtility::class
Expand All @@ -43,5 +46,7 @@ object AppKoinModule {
viewModel { EditCategoriesViewModel(get(), get(), get()) }
viewModel { AddUpdateCategoryViewModel(get(), get(), get()) }
viewModel { EditCategoryMenuViewModel(get(), get()) }
viewModel { SelectionModeViewModel(get()) }
viewModel { FaceTrackingViewModel() }
}
}
259 changes: 157 additions & 102 deletions app/src/main/java/com/willowtree/vocable/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.willowtree.vocable

import android.Manifest
import android.app.ActivityManager
import android.content.Context
import android.graphics.Rect
Expand All @@ -10,129 +11,175 @@ import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.ar.core.ArCoreApk
import com.vmadalin.easypermissions.EasyPermissions
import com.vmadalin.easypermissions.dialogs.SettingsDialog
import com.willowtree.vocable.customviews.PointerListener
import com.willowtree.vocable.customviews.PointerView
import com.willowtree.vocable.databinding.ActivityMainBinding
import com.willowtree.vocable.facetracking.FaceTrackFragment
import com.willowtree.vocable.facetracking.FaceTrackingViewModel
import com.willowtree.vocable.settings.SettingsViewModel
import com.willowtree.vocable.settings.selectionmode.HeadTrackingPermissionState
import com.willowtree.vocable.settings.selectionmode.SelectionModeViewModel
import com.willowtree.vocable.utils.VocableSharedPreferences
import com.willowtree.vocable.utils.VocableTextToSpeech
import io.github.inflationx.viewpump.ViewPumpContextWrapper
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ViewModelOwner
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber

class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(),
EasyPermissions.PermissionCallbacks {

private val minOpenGlVersion = 3.0
private val displayMetrics = DisplayMetrics()
private var currentView: View? = null
private lateinit var viewModel: FaceTrackingViewModel
private var paused = false
private lateinit var binding: ActivityMainBinding
private val sharedPrefs: VocableSharedPreferences by inject()
private var hasSetupAr: Boolean = false
private val allViews = mutableListOf<View>()
private val settingsViewModel: SettingsViewModel by viewModels()

private val selectionModeViewModel: SelectionModeViewModel by viewModel(owner = {
ViewModelOwner.from(this)
})
private val faceTrackingViewModel: FaceTrackingViewModel by viewModel(owner = {
ViewModelOwner.from(this)
})

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val shouldForceDisableHeadTracking = !BuildConfig.USE_HEAD_TRACKING
val isNotSupportedDevice = !checkIsSupportedDeviceOrFinish()
binding = ActivityMainBinding.inflate(layoutInflater)
binding.pointerView.isVisible = false
setContentView(binding.root)

val canUseHeadTracking = BuildConfig.USE_HEAD_TRACKING
val isSupportedDevice = checkIsSupportedDeviceOrFinish()

if (canUseHeadTracking && isSupportedDevice) {
selectionModeViewModel.headTrackingPermissionState.observe(this) { headTrackingState ->
when (headTrackingState) {
HeadTrackingPermissionState.PermissionRequested -> requestPermissions()
HeadTrackingPermissionState.Enabled -> {
togglePointerVisible(true)
setupArTracking()
}

if (shouldForceDisableHeadTracking || isNotSupportedDevice) {
return
HeadTrackingPermissionState.Disabled -> {
togglePointerVisible(false)
}
}
}
}

if (supportFragmentManager.findFragmentById(R.id.face_fragment) == null && BuildConfig.USE_HEAD_TRACKING) {
supportFragmentManager
.beginTransaction()
.replace(R.id.face_fragment, FaceTrackFragment())
.commitAllowingStateLoss()
} else {
window
.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
.or(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
.or(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
.or(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
.or(View.SYSTEM_UI_FLAG_FULLSCREEN)
.or(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
IanCrossCD marked this conversation as resolved.
Show resolved Hide resolved

faceTrackingViewModel.showError.observe(this) { showError ->
if (!sharedPrefs.getHeadTrackingEnabled()) {
getPointerView().isVisible = false
getErrorView().isVisible = false
return@observe
}
if (showError) {
(currentView as? PointerListener)?.onPointerExit()
}
getErrorView().isVisible = showError
getPointerView().isVisible = !showError
}

windowManager.defaultDisplay.getMetrics(displayMetrics)
viewModel = ViewModelProviders.of(this).get(FaceTrackingViewModel::class.java)
subscribeToViewModel()
supportActionBar?.hide()
VocableTextToSpeech.initialize(this)

val displayListener = object : DisplayManager.DisplayListener {
binding.mainNavHostFragment.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
allViews.clear()
}
}

private fun togglePointerVisible(visible: Boolean) {
binding.pointerView.isVisible = if (!BuildConfig.USE_HEAD_TRACKING) false else visible
}

private fun setupArTracking() {

if (!hasSetupAr) {

hasSetupAr = true

if (supportFragmentManager.findFragmentById(R.id.face_fragment) == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.face_fragment, FaceTrackFragment())
.commitAllowingStateLoss()
} else {
window
.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
.or(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
.or(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
.or(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
.or(View.SYSTEM_UI_FLAG_FULLSCREEN)
.or(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}

faceTrackingViewModel.pointerLocation.observe(this) {
updatePointer(it.x, it.y)
}

private var orientation = windowManager.defaultDisplay.rotation
windowManager.defaultDisplay.getMetrics(displayMetrics)

override fun onDisplayChanged(displayId: Int) {
val newOrientation = windowManager.defaultDisplay.rotation
// Only reset FaceTrackFragment if device is rotated 180 degrees
when (orientation) {
Surface.ROTATION_0 -> {
if (newOrientation == Surface.ROTATION_180) {
resetFaceTrackFragment("${Surface.ROTATION_180}")
val displayListener = object : DisplayManager.DisplayListener {

private var orientation = windowManager.defaultDisplay.rotation

override fun onDisplayChanged(displayId: Int) {
val newOrientation = windowManager.defaultDisplay.rotation
// Only reset FaceTrackFragment if device is rotated 180 degrees
when (orientation) {
Surface.ROTATION_0 -> {
if (newOrientation == Surface.ROTATION_180) {
resetFaceTrackFragment("${Surface.ROTATION_180}")
}
}
}
Surface.ROTATION_90 -> {
if (newOrientation == Surface.ROTATION_270) {
resetFaceTrackFragment("${Surface.ROTATION_270}")

Surface.ROTATION_90 -> {
if (newOrientation == Surface.ROTATION_270) {
resetFaceTrackFragment("${Surface.ROTATION_270}")
}
}
}
Surface.ROTATION_180 -> {
if (newOrientation == Surface.ROTATION_0) {
resetFaceTrackFragment("${Surface.ROTATION_0}")

Surface.ROTATION_180 -> {
if (newOrientation == Surface.ROTATION_0) {
resetFaceTrackFragment("${Surface.ROTATION_0}")
}
}
}
Surface.ROTATION_270 -> {
if (newOrientation == Surface.ROTATION_90) {
resetFaceTrackFragment("${Surface.ROTATION_90}")

Surface.ROTATION_270 -> {
if (newOrientation == Surface.ROTATION_90) {
resetFaceTrackFragment("${Surface.ROTATION_90}")
}
}
}
orientation = newOrientation
}
orientation = newOrientation
}

override fun onDisplayAdded(displayId: Int) = Unit

override fun onDisplayRemoved(displayId: Int) = Unit
}
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
override fun onDisplayAdded(displayId: Int) = Unit

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
override fun onDisplayRemoved(displayId: Int) = Unit
}

supportActionBar?.hide()
VocableTextToSpeech.initialize(this)
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)

binding.mainNavHostFragment.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
allViews.clear()
IanCrossCD marked this conversation as resolved.
Show resolved Hide resolved
}
}

override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
}

override fun onResume() {
super.onResume()
if (!BuildConfig.USE_HEAD_TRACKING) {
binding.pointerView.isVisible = false
} else {
binding.pointerView.isVisible = sharedPrefs.getHeadTrackingEnabled()
}
}

override fun onDestroy() {
super.onDestroy()
VocableTextToSpeech.shutdown()
Expand All @@ -154,37 +201,6 @@ class MainActivity : AppCompatActivity() {
allViews.clear()
}

fun subscribeToViewModel() {
viewModel.showError.observe(this, Observer {
if (!sharedPrefs.getHeadTrackingEnabled()) {
getPointerView().isVisible = false
getErrorView().isVisible = false
return@Observer
}
it?.let {
if (it) {
(currentView as? PointerListener)?.onPointerExit()
}
getErrorView().isVisible = it
getPointerView().isVisible = !it
}
})
viewModel.pointerLocation.observe(this) {
it?.let {
updatePointer(it.x, it.y)
}
}

settingsViewModel.headTrackingEnabled.observe(this) {
it?.let {
val faceFragment = supportFragmentManager.findFragmentById(R.id.face_fragment)
if (faceFragment is FaceTrackFragment) {
faceFragment.enableFaceTracking(it)
}
}
}
}

private fun getAllChildViews(viewGroup: ViewGroup) {
viewGroup.children.forEach {
if (it is PointerListener) {
Expand Down Expand Up @@ -312,4 +328,43 @@ class MainActivity : AppCompatActivity() {
}
return true
}

/**
* PERMISSIONS
*/

private fun requestPermissions() {
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) {
selectionModeViewModel.enableHeadTracking()
} else {
// Do not have permissions, request them now
EasyPermissions.requestPermissions(
host = this,
rationale = "Allow camera permissions to enable Head Tracking.",
requestCode = REQUEST_CAMERA_PERMISSION_CODE,
perms = arrayOf(Manifest.permission.CAMERA)
)
}
IanCrossCD marked this conversation as resolved.
Show resolved Hide resolved
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// EasyPermissions handles the request result.
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}

override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
if (EasyPermissions.somePermissionPermanentlyDenied(this@MainActivity, perms)) {
SettingsDialog.Builder(this@MainActivity).build().show()
} else {
selectionModeViewModel.disableHeadTracking()
}
}

override fun onPermissionsGranted(requestCode: Int, perms: List<String>) {
requestPermissions()
}

}

const val REQUEST_CAMERA_PERMISSION_CODE = 5504
Loading