265 lines
11 KiB
Markdown
265 lines
11 KiB
Markdown
When using a Projected Context to launch the Glasses Projected activity on the
|
|
Projected Device, refer to the following source code in `ProjectedContext.kt`:
|
|
|
|
|
|
```kotlin
|
|
/*
|
|
* Copyright 2025 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
|
|
*
|
|
* http://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 androidx.xr.projected
|
|
|
|
import android.app.ActivityOptions
|
|
import android.companion.virtual.VirtualDeviceManager
|
|
import android.content.Context
|
|
import android.hardware.display.DisplayManager
|
|
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_ADDED
|
|
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED
|
|
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED
|
|
import android.os.Build
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.annotation.VisibleForTesting
|
|
import androidx.xr.projected.experimental.ExperimentalProjectedApi
|
|
import kotlin.coroutines.CoroutineContext
|
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
import kotlinx.coroutines.asExecutor
|
|
import kotlinx.coroutines.channels.awaitClose
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.callbackFlow
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
|
/**
|
|
* Helper for accessing Projected device [Context] and its features.
|
|
*
|
|
* Projected device is an XR device connected to an Android device (host). Host can project the
|
|
* application content to the Projected device and let users interact with it.
|
|
*
|
|
* The Projected device context will ensure Projected device system services are returned, when
|
|
* queried for system services from this object.
|
|
*
|
|
* Note: The application context's deviceId can switch between the Projected and host deviceId
|
|
* depending on which activity was most recently in the foreground. Prefer using the Activity
|
|
* context to minimize the risk of running into this problem.
|
|
*/
|
|
@ExperimentalProjectedApi
|
|
public object ProjectedContext {
|
|
|
|
private const val TAG = "ProjectedContext"
|
|
|
|
@VisibleForTesting internal const val PROJECTED_DEVICE_NAME = "ProjectionDevice"
|
|
@VisibleForTesting internal const val PROJECTED_DISPLAY_NAME = "ProjectionDisplay"
|
|
|
|
/**
|
|
* Explicitly create the Projected device context from any context object.
|
|
*
|
|
* @throws IllegalStateException if the projected device was not found.
|
|
*/
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
public fun createProjectedDeviceContext(context: Context): Context {
|
|
val deviceId =
|
|
getProjectedDeviceId(context)
|
|
?: throw IllegalStateException("Projected device not found.")
|
|
return context.createDeviceContext(deviceId)
|
|
}
|
|
|
|
/**
|
|
* Explicitly create the host device context from any context object. The host is the device
|
|
* that connects to a Projected device.
|
|
*
|
|
* If an application is using a Projected device context and it wants to use system services
|
|
* from the host (e.g. phone), it needs to use the host device context.
|
|
*/
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
public fun createHostDeviceContext(context: Context): Context =
|
|
context.createDeviceContext(Context.DEVICE_ID_DEFAULT)
|
|
|
|
/**
|
|
* Returns the name of the Projected device or null if either virtual device wasn't found or the
|
|
* name of the virtual device wasn't set.
|
|
*
|
|
* @throws IllegalArgumentException If another context is used (e.g. the host context).
|
|
*/
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
public fun getProjectedDeviceName(context: Context): String? =
|
|
// TODO: b/424812882 - Turn this into a lint check with an annotation.
|
|
if (isProjectedDeviceContext(context)) {
|
|
getVirtualDevice(context)?.name
|
|
} else {
|
|
throw IllegalArgumentException(
|
|
"Provided context is not the Projected device context. Can't get the device name."
|
|
)
|
|
}
|
|
|
|
/** Returns whether the provided context is the Projected device context. */
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
public fun isProjectedDeviceContext(context: Context): Boolean =
|
|
getVirtualDevice(context)?.name?.startsWith(PROJECTED_DEVICE_NAME) == true
|
|
|
|
/**
|
|
* Creates [ActivityOptions] that should be used to start an activity on the Projected device.
|
|
*
|
|
* @param context any [Context] object. If the provided context is not a Projected device
|
|
* context, a Projected device context will be created automatically and used to create
|
|
* Projected [ActivityOptions].
|
|
* @throws IllegalStateException if the projected display was not found or if the Projected
|
|
* device doesn't have any displays.
|
|
*/
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
public fun createProjectedActivityOptions(context: Context): ActivityOptions {
|
|
val projectedDeviceContext =
|
|
if (isProjectedDeviceContext(context)) {
|
|
context
|
|
} else {
|
|
createProjectedDeviceContext(context)
|
|
}
|
|
|
|
val projectedDisplayIds = getProjectedDisplayIds(projectedDeviceContext)
|
|
|
|
check(projectedDisplayIds.isNotEmpty()) { "Projected device doesn't have any displays." }
|
|
|
|
val projectedDisplay =
|
|
context.getSystemService(DisplayManager::class.java).displays.find {
|
|
it.name == PROJECTED_DISPLAY_NAME && projectedDisplayIds.contains(it.displayId)
|
|
} ?: throw IllegalStateException("No projected display found.")
|
|
|
|
return ActivityOptions.makeBasic().setLaunchDisplayId(projectedDisplay.displayId)
|
|
}
|
|
|
|
/**
|
|
* Observe whether a Projected device is connected to the host.
|
|
*
|
|
* @param context The context used to access the [VirtualDeviceManager]. It can be any context
|
|
* object.
|
|
* @param coroutineContext The CoroutineContext that includes CoroutineDispatcher which is where
|
|
* the Projected device connectivity observer is executed on.
|
|
* @throws IllegalArgumentException if provided coroutineContext doesn't include
|
|
* CoroutineDispatcher.
|
|
*/
|
|
@JvmStatic
|
|
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
|
|
public fun isProjectedDeviceConnected(
|
|
context: Context,
|
|
coroutineContext: CoroutineContext,
|
|
): Flow<Boolean> =
|
|
callbackFlow {
|
|
@OptIn(ExperimentalStdlibApi::class)
|
|
val coroutineDispatcher =
|
|
coroutineContext[CoroutineDispatcher]
|
|
?: throw IllegalArgumentException(
|
|
"CoroutineContext must contain a CoroutineDispatcher."
|
|
)
|
|
|
|
fun checkAndSend() {
|
|
trySend(isProjectedDisplayAvailable(context))
|
|
}
|
|
|
|
val virtualDeviceListener =
|
|
object : VirtualDeviceManager.VirtualDeviceListener {
|
|
override fun onVirtualDeviceCreated(deviceId: Int) {
|
|
checkAndSend()
|
|
}
|
|
|
|
override fun onVirtualDeviceClosed(deviceId: Int) {
|
|
checkAndSend()
|
|
}
|
|
}
|
|
|
|
val displayListener =
|
|
object : DisplayManager.DisplayListener {
|
|
override fun onDisplayAdded(displayId: Int) {
|
|
checkAndSend()
|
|
}
|
|
|
|
override fun onDisplayChanged(displayId: Int) {
|
|
checkAndSend()
|
|
}
|
|
|
|
override fun onDisplayRemoved(displayId: Int) {
|
|
checkAndSend()
|
|
}
|
|
}
|
|
|
|
checkAndSend()
|
|
|
|
val virtualDeviceManager =
|
|
context.getSystemService(VirtualDeviceManager::class.java)
|
|
virtualDeviceManager.registerVirtualDeviceListener(
|
|
coroutineDispatcher.asExecutor(),
|
|
virtualDeviceListener,
|
|
)
|
|
|
|
val displayManager = context.getSystemService(DisplayManager::class.java)
|
|
val eventFilter =
|
|
EVENT_TYPE_DISPLAY_ADDED or
|
|
EVENT_TYPE_DISPLAY_CHANGED or
|
|
EVENT_TYPE_DISPLAY_REMOVED
|
|
displayManager.registerDisplayListener(
|
|
coroutineDispatcher.asExecutor(),
|
|
eventFilter,
|
|
displayListener,
|
|
)
|
|
|
|
awaitClose {
|
|
virtualDeviceManager.unregisterVirtualDeviceListener(virtualDeviceListener)
|
|
displayManager.unregisterDisplayListener(displayListener)
|
|
}
|
|
}
|
|
.distinctUntilChanged()
|
|
|
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
private fun isProjectedDisplayAvailable(context: Context): Boolean {
|
|
val projectedDeviceContext =
|
|
try {
|
|
createProjectedDeviceContext(context)
|
|
} catch (e: IllegalStateException) {
|
|
return false
|
|
}
|
|
val projectedDisplayIds = getProjectedDisplayIds(projectedDeviceContext)
|
|
if (projectedDisplayIds.isEmpty()) {
|
|
return false
|
|
}
|
|
val displayManager = context.getSystemService(DisplayManager::class.java)
|
|
return displayManager.displays.any {
|
|
it.name == PROJECTED_DISPLAY_NAME && projectedDisplayIds.contains(it.displayId)
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
private fun getProjectedDeviceId(context: Context) =
|
|
context
|
|
.getSystemService(VirtualDeviceManager::class.java)
|
|
.virtualDevices
|
|
// TODO: b/424824481 - Replace the name matching with a better method.
|
|
.find { it.name?.startsWith(PROJECTED_DEVICE_NAME) ?: false }
|
|
?.deviceId
|
|
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
private fun getVirtualDevice(context: Context) =
|
|
context.getSystemService(VirtualDeviceManager::class.java).virtualDevices.find {
|
|
it.deviceId == context.deviceId
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
private fun getProjectedDisplayIds(context: Context) =
|
|
getVirtualDevice(context)?.displayIds ?: IntArray(size = 0)
|
|
}
|
|
```
|
|
|
|
<br /> |