Skip to content

Commit db586eb

Browse files
committed
New CameraPermission and ARCore install/update system. Called automatically and handles creation success or possible fallback like 3D only usage
Fix #7
1 parent d5d6893 commit db586eb

File tree

8 files changed

+269
-146
lines changed

8 files changed

+269
-146
lines changed

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,46 @@ dependencies {
3737
## Usage
3838

3939
*res/layout/main_fragment.xml*
40+
- 3D only
4041
```xml
41-
// 3D only
4242
<io.github.sceneview.SceneView
4343
android:id="@+id/sceneView"
4444
android:layout_width="match_parent"
4545
android:layout_height="match_parent" />
4646
```
4747

48-
48+
- 3D and ARCore
4949
```xml
50-
// 3D and ARCore
5150
<io.github.sceneview.ar.ArSceneView
5251
android:id="@+id/sceneView"
5352
android:layout_width="match_parent"
5453
android:layout_height="match_parent" />
5554
```
5655

56+
## Camera Permission and ARCore install/update/unsupported
57+
58+
The `ArSceneView` handles the camera permission ask and the ARCore requirements automatically and will be proceed when your view is attached to the Activity/Fragment
59+
If you need it, you can add a listener on both ARCore success or failed session creation (including
60+
camera permission denied since a session cannot be created without it).
61+
62+
- Camera permission has been granted and latest ARCore Services version are already installed or have been installed during the auto check
63+
```kotlin
64+
//
65+
sceneView.onArSessionCreated = { arSession: ArSession ->
66+
}
67+
```
68+
69+
- Handle a fallback in case of camera permission denied or AR unavailable and possibly move to 3D only usage
70+
71+
The exception contains the failure reason.
72+
*e.g. SecurityException in case of camera permission denied*
73+
```kotlin
74+
sceneView.onArSessionFailed = { exception: Exception ->
75+
// If AR is not available, we add the model directly to the scene for a 3D only usage
76+
sceneView.addChild(modelNode)
77+
}
78+
```
79+
5780
## Why have we included the Kotlin-Math library in SceneView?
5881

5982
Earlier versions of OpenGL had a fixed rendering pipeline and provided an API for setting positions of vertices, transformation and projection matrices, etc. However, with the new rendering pipeline it is required to prepare this data before passing it to GLSL shaders and OpenGL doesn't provide any mathematical functions to do that.
@@ -75,7 +98,7 @@ You will have a little work to do if you are using the `ArFragment` in Sceneform
7598
After the migration you should get cleaner code and all of the benefits described in the [Features](#Features) section :tada:
7699

77100
#### Requesting the camera permission and installing/updating the Google Play Services for AR
78-
This is handled automatically in the `ArSceneView`. You can use the `ArSceneView.onARCoreException` property to register a callback to be invoked when the ARCore Session cannot be initialized because ARCore is not available on the device or the camera permission has been denied.
101+
This is handled automatically in the `ArSceneView`. You can use the `ArSceneView.onArSessionFailed` property to register a callback to be invoked when the ARCore Session cannot be initialized because ARCore is not available on the device or the camera permission has been denied.
79102

80103
#### Instructions for AR
81104
The `InstructionsController` in the `BaseArFragment` has been replaced with the `Instructions` in the `ArSceneView`.
Lines changed: 166 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
package io.github.sceneview.ar
22

3-
import androidx.activity.ComponentActivity
3+
import android.Manifest
4+
import android.app.Activity
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.pm.PackageManager
8+
import android.net.Uri
9+
import android.provider.Settings
10+
import android.widget.Toast
11+
import androidx.activity.result.ActivityResult
412
import androidx.activity.result.ActivityResultLauncher
13+
import androidx.activity.result.contract.ActivityResultContracts
14+
import androidx.core.app.ActivityCompat
15+
import androidx.core.content.ContextCompat
516
import androidx.lifecycle.LifecycleOwner
617
import com.google.ar.core.*
18+
import com.google.ar.core.ArCoreApk.Availability
719
import com.gorisse.thomas.sceneview.*
820
import io.github.sceneview.ar.arcore.*
921

@@ -32,74 +44,118 @@ class ARCore(
3244
val cameraTextureId: Int,
3345
val lifecycle: ArSceneLifecycle,
3446
val features: Set<Session.Feature> = setOf(),
35-
val config: Config.() -> Unit = {},
36-
var onException: ((Exception) -> Unit)? = null
47+
val config: Config.() -> Unit = {}
3748
) : ArSceneLifecycleObserver {
3849

50+
/**
51+
* //TODO: Doc
52+
*/
53+
var checkCameraPermission = true
54+
55+
/**
56+
* // TODO: Doc
57+
*/
58+
var checkAvailability = true
59+
60+
// TODO: See if it could be useful
61+
// /**
62+
// * ### Whether or not your SceneView can be used without ARCore
63+
// *
64+
// * This can be set to true in order to have a fallback usage when the camera is not available or
65+
// * the camera permission result is not granted.
66+
// * If set to false, the ArSceneView won't be visible until the user accept the permission ask or
67+
// * change the auto-displayed app permissions settings screen.
68+
// *
69+
// * **Warning:** You should not use this to limit to 3D only usage. Using SceneView instead of
70+
// * ArSceneView is a better choice in this case.
71+
// */
72+
// var isOptional = false
73+
74+
lateinit var cameraPermissionLauncher: ActivityResultLauncher<String>
75+
private var cameraPermissionRequested = false
76+
lateinit var appSettingsLauncher: ActivityResultLauncher<Intent>
77+
private var appSettingsRequested = false
3978
private var installRequested = false
4079
internal var session: ArSession? = null
4180
private set
4281

43-
companion object {
44-
var cameraPermissionLauncher: ActivityResultLauncher<String>? = null
45-
46-
fun registerForCameraPermissionResult(activity: ComponentActivity) {
47-
cameraPermissionLauncher = activity.registerForCameraPermissionResult()
82+
var onCameraPermissionResult: (isGranted: Boolean) -> Unit = { isGranted ->
83+
if (!isGranted) {
84+
if (!ActivityCompat.shouldShowRequestPermissionRationale(
85+
lifecycle.activity,
86+
Manifest.permission.CAMERA
87+
)
88+
) {
89+
appSettingsRequested = true
90+
showCameraPermissionSettings(lifecycle.activity)
91+
}
4892
}
4993
}
5094

95+
var onAppSettingsResult: (result: ActivityResult) -> Unit = { result ->
96+
appSettingsRequested = false
97+
}
98+
5199
init {
52-
// Must be called before on resume
53-
try {
54-
registerForCameraPermissionResult(lifecycle.activity)
55-
} catch (exception: Exception) {
56-
throw AssertionError(
57-
"\n######################################################################\n" +
58-
"# Camera permission result must be registered before Activity resume #\n" +
59-
"# #\n" +
60-
"# - Add the ArSceneView before onResume() #\n" +
61-
"# #\n" +
62-
"# OR #\n" +
63-
"# #\n" +
64-
"# - Add this call in your Activity onCreate(): #\n" +
65-
"# #\n" +
66-
"# ARCore.registerForCameraPermissionResult(this) #\n" +
67-
"# #\n" +
68-
"######################################################################\n" +
69-
exception
70-
)
71-
}
72100
lifecycle.addObserver(this)
73101
}
74102

103+
override fun onCreate(owner: LifecycleOwner) {
104+
super.onCreate(owner)
105+
106+
// Must be called before on resume
107+
cameraPermissionLauncher = lifecycle.activity.activityResultRegistry.register(
108+
"sceneview_camera_permission",
109+
owner,
110+
ActivityResultContracts.RequestPermission(),
111+
onCameraPermissionResult
112+
)
113+
appSettingsLauncher = lifecycle.activity.activityResultRegistry.register(
114+
"sceneview_app_settings",
115+
owner,
116+
ActivityResultContracts.StartActivityForResult(),
117+
onAppSettingsResult
118+
)
119+
}
120+
75121
override fun onResume(owner: LifecycleOwner) {
76122
if (this.session == null) {
77-
this.session = try {
78-
when {
79-
// Request installation if necessary.
80-
ArCoreApk.getInstance().requestInstall(lifecycle.activity, !installRequested) ==
81-
ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
123+
// Camera Permission
124+
if (checkCameraPermission && !cameraPermissionRequested &&
125+
!checkCameraPermission(lifecycle.activity, cameraPermissionLauncher)
126+
) {
127+
cameraPermissionRequested = true
128+
} else if (!appSettingsRequested) {
129+
// In case of Camera permission previously denied, the allow popup won't show but
130+
// the onResume will be called anyway.
131+
// So if we launch the app settings screen, the onResume will be called twice.
132+
// In order to avoid multiple session creation failing because camera permission is
133+
// still not granted, we check if the app settings screen is displayed.
134+
// In last case, if the camera permission is still not granted a SecurityException
135+
// will be thrown when trying to create session.
136+
try {
137+
// ARCore install and update if camera permission is granted.
138+
// For now, ARCore session will throw an exception if the camera is not
139+
// accessible (ARCore cannot be used without camera.
140+
// Request installation if necessary
141+
if (checkAvailability && !installRequested &&
142+
!checkInstall(lifecycle.activity, installRequested)
143+
) {
144+
// Session will be created if everything is ok on next onResume(), so we
145+
// return for now
82146
installRequested = true
83-
// createSession will be called again, so we return null for now.
84-
null
85-
}
86-
!lifecycle.activity.hasCameraPermission -> {
87-
cameraPermissionLauncher?.requestCameraPermission()
88-
// createSession will be called again, so we return null for now.
89-
null
90-
}
91-
else -> {
92-
// Create a session if Google Play Services for AR is installed and up to date.
93-
ArSession(cameraTextureId, lifecycle, features, config).also {
147+
} else {
148+
// Create a session if Google Play Services for AR is installed and up to
149+
// date.
150+
session = createSession(cameraTextureId, lifecycle, features, config).also {
94151
lifecycle.dispatchEvent<ArSceneLifecycleObserver> {
95152
onArSessionCreated(it)
96153
}
97154
}
98155
}
156+
} catch (e: Exception) {
157+
onException(e)
99158
}
100-
} catch (e: Exception) {
101-
onException?.invoke(e)
102-
null
103159
}
104160
}
105161
}
@@ -113,4 +169,67 @@ class ARCore(
113169
override fun onDestroy(owner: LifecycleOwner) {
114170
session = null
115171
}
172+
173+
fun onException(exception: Exception) {
174+
lifecycle.dispatchEvent<ArSceneLifecycleObserver> {
175+
onArSessionFailed(exception)
176+
}
177+
}
178+
179+
/** Check to see we have the necessary permissions for this app. */
180+
fun hasCameraPermission(context: Context) = ContextCompat.checkSelfPermission(
181+
context,
182+
Manifest.permission.CAMERA
183+
) == PackageManager.PERMISSION_GRANTED
184+
185+
fun checkCameraPermission(
186+
context: Context,
187+
permissionLauncher: ActivityResultLauncher<String>
188+
): Boolean {
189+
return if (!hasCameraPermission(context)) {
190+
permissionLauncher.launch(Manifest.permission.CAMERA)
191+
false
192+
} else {
193+
true
194+
}
195+
}
196+
197+
fun showCameraPermissionSettings(activity: Activity) {
198+
// Permission denied with checking "Do not ask again".
199+
Toast.makeText(
200+
activity,
201+
activity.getString(R.string.sceneview_camera_permission_required),
202+
Toast.LENGTH_LONG
203+
).show()
204+
// Launch Application Setting to grant permission
205+
appSettingsLauncher.launch(Intent().apply {
206+
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
207+
data = Uri.fromParts("package", activity.packageName, null)
208+
})
209+
}
210+
211+
fun checkInstall(activity: Activity, installRequested: Boolean): Boolean {
212+
// Request installation if necessary
213+
return isInstalled(activity) || !install(activity, installRequested)
214+
}
215+
216+
/** Check to see we have the necessary permissions for this app. */
217+
fun isInstalled(context: Context) = ArCoreApk.getInstance().checkAvailability(lifecycle.activity) == Availability.SUPPORTED_INSTALLED
218+
219+
fun install(activity: Activity, installRequested: Boolean) : Boolean {
220+
return ArCoreApk.getInstance().requestInstall(
221+
activity,
222+
!installRequested
223+
) == ArCoreApk.InstallStatus.INSTALL_REQUESTED
224+
}
225+
226+
fun createSession(
227+
cameraTextureId: Int,
228+
lifecycle: ArSceneLifecycle,
229+
features: Set<Session.Feature> = setOf(),
230+
config: Config.() -> Unit = {}
231+
): ArSession {
232+
// Create a session if Google Play Services for AR is installed and up to date.
233+
return ArSession(cameraTextureId, lifecycle, features, config)
234+
}
116235
}

0 commit comments

Comments
 (0)