Files

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 />