Complementos de la IU del vehículo

Usa los complementos de la biblioteca de IU del vehículo para crear implementaciones completas de los componentes personalizadas en la biblioteca de la IU del vehículo en lugar de usar superposiciones de recursos de tiempo de ejecución (RRO). Las RRO te permiten cambiar solo los recursos XML de la biblioteca de la IU del vehículo componentes de seguridad, lo que limita la medida en que se puede personalizar.

Cómo crear un complemento

Un complemento de la biblioteca de IU del vehículo es un APK que contiene clases que implementan un conjunto de APIs de complementos. Las APIs de complementos pueden compilarse en un complemento como una biblioteca estática.

Consulta ejemplos en Soong y Gradle:

Soong

Considera este ejemplo de Soong:

android_app {
    name: "my-plugin",

    min_sdk_version: "28",
    target_sdk_version: "30",
    aaptflags: ["--shared-lib"],
    sdk_version: "current",

    manifest: "src/main/AndroidManifest.xml",
    srcs: ["src/main/java/**/*.java"],
    resource_dirs: ["src/main/res"],
    static_libs: [
        "car-ui-lib-oem-apis",
    ],
    // Disable optimization is mandatory to prevent R.java class from being
    // stripped out
    optimize: {
        enabled: false,
    },

    certificate: ":my-plugin-certificate",
}

Gradle

Consulta este archivo build.gradle:

apply plugin: 'com.android.application'

android {
  compileSdkVersion 30

  defaultConfig {
    minSdkVersion 28
    targetSdkVersion 30
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  signingConfigs {
    debug {
      storeFile file('chassis_upload_key.jks')
      storePassword 'chassis'
      keyAlias 'chassis'
      keyPassword 'chassis'
    }
  }
}

dependencies {
  implementation project(':oem-apis')
  // Or use the following if you'd like to use the maven artifact
  // implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}

Settings.gradle:

// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')

El complemento debe tener un proveedor de contenido declarado en su manifiesto que incluya siguientes atributos:

  android:authorities="com.android.car.ui.plugin"
  android:enabled="true"
  android:exported="true"

android:authorities="com.android.car.ui.plugin" hace que el complemento sea detectable. a la biblioteca de IU del vehículo. Se debe exportar el proveedor para que se pueda consultar en tiempo de ejecución. Además, si el atributo enabled se establece en false, el valor predeterminado en lugar de la del complemento. El contenido no es necesario que exista. En ese caso, asegúrate de agregar tools:ignore="MissingClass" a la definición del proveedor. Ver la muestra manifiesto a continuación:

    <application>
        <provider
            android:name="com.android.car.ui.plugin.PluginNameProvider"
            android:authorities="com.android.car.ui.plugin"
            android:enabled="false"
            android:exported="true"
            tools:ignore="MissingClass"/>
    </application>

Por último, como medida de seguridad, Firma la app.

Complementos como biblioteca compartida

A diferencia de las bibliotecas estáticas de Android, que se compilan directamente en apps, Las bibliotecas compartidas de Android se compilan en un APK independiente al que se hace referencia. por otras apps en el tiempo de ejecución.

Los complementos que se implementan como una biblioteca compartida de Android tienen sus clases. automáticamente al cargador de clases compartido entre apps. Cuando una app que usa la biblioteca de IU del vehículo especifica dependencia del tiempo de ejecución en la biblioteca compartida del complemento, su classloader puede acceder a las clases de la biblioteca compartida del complemento. Complementos implementados ya que las apps para Android normales (no una biblioteca compartida) pueden afectar negativamente el frío de la app. horas de inicio.

Cómo implementar y compilar bibliotecas compartidas

Desarrollar con bibliotecas compartidas de Android es muy similar al uso de aplicaciones normales de Android con algunas diferencias clave.

  • Usa la etiqueta library debajo de la etiqueta application con el paquete del complemento nombre en el manifiesto de la app del complemento:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Configura tu regla de compilación de android_app de Soong (Android.bp) con la AAPT la marca shared-lib, que se usa para compilar una biblioteca compartida:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dependencias de las bibliotecas compartidas

Para cada app del sistema que use la biblioteca de IU del vehículo, incluye la uses-library en el manifiesto de la app, en la etiqueta Etiqueta application con el nombre del paquete del complemento:

<manifest>
  <application
      android:name=".MyApp"
      ...>
    <uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
    ...
  </application>
</manifest>

Instala un complemento

Los complementos DEBEN estar preinstalados en la partición del sistema incluyendo el módulo en PRODUCT_PACKAGES. El paquete preinstalado se puede actualizar de manera similar a lo siguiente: ninguna otra app instalada.

Si actualizas un complemento existente en el sistema, todas las apps que lo usen se cierran automáticamente. Una vez que el usuario vuelve a abrirlo, tiene los cambios actualizados. Si la app no se estaba ejecutando, la próxima vez que se inicie, tendrá el archivo .

Cuando instalas un complemento con Android Studio, debes tener en cuenta y consideraciones clave para tener en cuenta. Al momento de esta redacción, hay un error en el proceso de instalación de la app de Android Studio que actualiza un complemento en caso de que no surta efecto. Para solucionar este problema, selecciona la opción Instalar siempre con el administrador de paquetes (inhabilita las optimizaciones de implementación en Android 11 y versiones posteriores) en la configuración de compilación del complemento.

Además, al instalar el complemento, Android Studio informa un error que indica no puede encontrar una actividad principal para iniciar. Esto es esperable, ya que el complemento no tener actividades (excepto el intent vacío que se usa para resolver un intent) Para eliminar el error, cambia la opción Launch a Nothing en la compilación configuración.

Configuración del complemento de Android Studio Figura 1: Configuración del complemento de Android Studio

Complemento de proxy

Personalización de apps que usan la biblioteca de la IU del vehículo requiere una RRO que se dirija a cada app específica que se modificará incluso cuando las personalizaciones son idénticas en todas las apps. Esto significa que una RRO por es obligatoria. Consulta qué apps usan la biblioteca de la IU del vehículo.

El complemento del proxy de la biblioteca de la IU del vehículo es un ejemplo. de complementos que delega las implementaciones de sus componentes al entorno de la biblioteca de la IU del vehículo. Este complemento se puede orientar con una RRO, que puede ser se usa como un punto único de personalización para apps que usan la biblioteca de IU del vehículo. sin tener que implementar un complemento funcional. Para obtener más información RRO, consulta Cambia el valor de los recursos de una app en entorno de ejecución.

El complemento del proxy es solo un ejemplo y punto de partida para hacer la personalización usando un complemento. Para la personalización más allá de los RRO, se puede implementar un subconjunto de complementos los componentes y usa el complemento proxy para el resto, o implementa todas los componentes completamente desde cero.

Aunque el complemento de proxy proporciona un punto único de personalización de RRO para las apps, las apps que rechacen el uso del complemento seguirán requiriendo una RRO que se orienta a la app en sí.

Cómo implementar las APIs del complemento

El punto de entrada principal al complemento es el Clase com.android.car.ui.plugin.PluginVersionProviderImpl. Todos los complementos deben Incluye una clase con este nombre exacto y este nombre de paquete. Esta clase debe tener un el constructor predeterminado y, luego, implementarás la interfaz PluginVersionProviderOEMV1.

Los complementos de CarUi deben funcionar con apps anteriores o nuevas que el complemento. Para para facilitar esto, todas las APIs del complemento tienen control de versiones con un V# al final de su de clase. Si se lanza una nueva versión de la biblioteca de la IU del vehículo con funciones nuevas, son parte de la versión V2 del componente. La biblioteca de IU del vehículo Es la mejor opción para que las funciones nuevas funcionen dentro del alcance de un componente de complemento anterior. Por ejemplo, puedes convertir un nuevo tipo de botón de la barra de herramientas en MenuItems.

Sin embargo, una app con una versión anterior de la biblioteca de la IU del vehículo no puede adaptarse a una nueva. complemento escrito con APIs más nuevas. Para resolver este problema, permitimos que los complementos devolver implementaciones propias basadas en la versión de la API de OEM compatibles con las apps.

PluginVersionProviderOEMV1 tiene un método:

Object getPluginFactory(int maxVersion, Context context, String packageName);

Este método muestra un objeto que implementa la versión más alta de PluginFactoryOEMV# es compatible con el complemento, y sigue siendo menor que o igual a maxVersion. Si un complemento no tiene una implementación de un PluginFactory tan antiguo, es posible que muestre null. En ese caso, el comando implementación vinculada de componentes CarUi.

Para mantener la retrocompatibilidad con apps que se compilan versiones anteriores de la biblioteca estática de Car UI, se recomienda admitir maxVersion de 2, 5 y superiores desde la implementación de tu complemento de la clase PluginVersionProvider. Las versiones 1, 3 y 4 no son compatibles. Para más información, consulta PluginVersionProviderImpl

PluginFactory es la interfaz que crea todos los demás CarUi o los componentes de la solución. También define qué versión de sus interfaces se debe usar. Si el complemento no busca implementar ninguno de estos componentes, es posible que null en su función de creación (excepto la barra de herramientas, que tiene una función customizesBaseLayout() independiente).

El pluginFactory limita las versiones de los componentes de CarUi que se pueden usar. entre sí. Por ejemplo, nunca habrá un pluginFactory que pueda crear la versión 100 de un Toolbar y también la versión 1 de un RecyclerView, ya que no garantizaría que una amplia variedad de versiones de componentes trabajan juntos. Para usar la versión 100 de la barra de herramientas, se espera que los desarrolladores proporcionan una implementación de una versión de pluginFactory que crea un barra de herramientas 100, que limita las opciones en las versiones de otras componentes que se pueden crear. Es posible que las versiones de otros componentes no se igual; por ejemplo, un pluginFactoryOEMV100 podría crear una ToolbarControllerOEMV100 y RecyclerViewOEMV70.

Barra de herramientas

Diseño básico

La barra de herramientas y el "diseño base" están muy relacionados, por lo que la función que crea la barra de herramientas se llama installBaseLayoutAround. El diseño básico es un concepto que permite que la barra de herramientas se ubique en cualquier lugar alrededor del contenido, para permitir una barra de herramientas en la parte superior/inferior de la app, verticalmente a los lados, o incluso una barra de herramientas circular que encierra toda la app. Este es lo cual se logra pasando una vista a installBaseLayoutAround para la barra de herramientas o base diseño para envolver.

El complemento debe tomar la vista proporcionada, separarla de su elemento superior, aumentarla el diseño propio del complemento en el mismo índice del elemento superior y con la misma LayoutParams como la vista que se acaba de desconectar y, luego, vuelve a conectarla en algún lugar dentro del diseño que se acaba de aumentar. El diseño inflado contener la barra de herramientas, si la aplicación lo solicita.

La app puede solicitar un diseño base sin una barra de herramientas. Si lo hace, installBaseLayoutAround debe mostrar un valor nulo. Para la mayoría de los complementos, eso es todo. debe suceder, pero si el autor del complemento desea aplicar, p.ej., una decoración alrededor del borde de la app, lo que aún podría hacerse con un diseño base. Estos decoraciones son particularmente útiles para dispositivos con pantallas no rectangulares, como pueden empujar la app a un espacio rectangular y agregar transiciones claras el espacio no rectangular.

installBaseLayoutAround también recibe un Consumer<InsetsOEMV1>. Esta consumidor se puede usar para comunicar a la aplicación que el complemento está parcialmente que cubre el contenido de la aplicación (con la barra de herramientas u otro modo). La app hará lo siguiente sé que deben seguir dibujando en este espacio, pero mantengan todas las críticas los componentes de ella. Este efecto se usa en nuestro diseño de referencia para que la semitransparente, y verás que las listas se desplazan debajo de ella. Si este atributo fuera implementado, el primer elemento de una lista estaría atascado debajo de la barra de herramientas. y no se puede hacer clic. Si este efecto no es necesario, el complemento puede ignorar el Consumidor

Desplazamiento del contenido debajo de la barra de herramientas Figura 2: Desplazamiento del contenido debajo de la barra de herramientas

Desde la perspectiva de la app, cuando el complemento envíe nuevas inserciones, recibirá desde cualquier actividad o fragmento que implemente InsetsChangedListener. Si una actividad o un fragmento no implementa InsetsChangedListener, la IU del vehículo controlará las inserciones de forma predeterminada aplicando las inserciones como relleno al Activity o FragmentActivity que contienen el fragmento. La biblioteca no aplica las inserciones a los fragmentos de forma predeterminada. Este es un fragmento de ejemplo de un implementación que aplica las inserciones como relleno en un elemento RecyclerView en el app:

public class MainActivity extends Activity implements InsetsChangedListener {
  @Override
  public void onCarUiInsetsChanged(Insets insets) {
    CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
    rv.setPadding(insets.getLeft(), insets.getTop(),
                  insets.getRight(), insets.getBottom());
  }
}

Por último, el complemento recibe una sugerencia fullscreen, que se usa para indicar si la vista que se debe unir ocupa toda la app o solo una pequeña sección. Esto se puede usar para evitar aplicar algunas decoraciones en el borde que solo tienen sentido si aparecen a lo largo de toda la pantalla. Una muestra que usa diseños básicos que no son de pantalla completa es Configuración, en la que cada panel el diseño de panel doble tiene su propia barra de herramientas.

Dado que se espera que installBaseLayoutAround muestre un valor nulo cuando toolbarEnabled es false, para que el complemento indique que no lo hace. deseas personalizar el diseño base, debe mostrar false de customizesBaseLayout

El diseño base debe contener un FocusParkingView y un FocusArea para completar admitir controles rotativos. Estas vistas se pueden omitir en dispositivos que no son compatibles con el sistema rotativo. Las FocusParkingView/FocusAreas se implementan en la biblioteca CarUi estática, por lo que se usa un setRotaryFactories para proporcionar fábricas a crear las vistas a partir de contextos.

Los contextos que se usan para crear vistas de enfoque deben ser el contexto de origen, no el contexto del complemento. El valor de FocusParkingView debe ser el más cercano a la primera vista en el árbol de la forma más razonablemente posible, ya que es lo que se enfoca cuando debería ningún enfoque visible para el usuario. El elemento FocusArea debe unir la barra de herramientas en el para indicar que es una zona de empuje rotativo. Si la FocusArea no es el usuario no puede navegar a ningún botón de la barra de herramientas con la control rotativo.

Controlador de la barra de herramientas

El ToolbarController real que se muestra debería ser mucho más sencillo de implementar que el diseño base. Su trabajo es llevar la información que se pasa métodos set y los mostrará en el diseño base. Consulta el Javadoc para obtener más información sobre de la mayoría de los métodos. A continuación, se analizan algunos de los métodos más complejos.

getImeSearchInterface se usa para mostrar los resultados de la búsqueda en el IME (teclado). en la ventana modal. Esto puede ser útil para mostrar/animar los resultados de la búsqueda junto al teclado, por ejemplo, si ocupaba solo la mitad de la pantalla. La mayoría de se implementa la funcionalidad en la biblioteca estática CarUi, la biblioteca del complemento solo proporciona métodos para que la biblioteca estática obtenga Devoluciones de llamada TextView y onPrivateIMECommand Para ello, el complemento debes usar una subclase TextView que anule a onPrivateIMECommand y pase la llamada al objeto de escucha proporcionado como el TextView de su barra de búsqueda.

setMenuItems simplemente muestra MenuItems en la pantalla, pero se lo llamará sorprendentemente a menudo. Como la API del complemento para MenuItems es inmutable, cada vez que un Se cambió MenuItem, se realizará una llamada a setMenuItems completamente nueva. Esto podría suceden por algo tan trivial como que un usuario hace clic en un switch MenuItem, clic en el interruptor para activarlo. Por razones de rendimiento y animación, por lo tanto, se recomienda calcular la diferencia entre el modelo de MenuItems y solo actualizar las vistas que realmente cambiaron. El elemento MenuItems proporciona un campo key que pueda ayudarte con esto, ya que la clave debe ser la misma en diferentes llamadas a setMenuItems para el mismo MenuItem.

Vista de estilo de la aplicación

AppStyledView es un contenedor para una vista que no está personalizada en absoluto. Integra se puede usar para proporcionar un borde alrededor de esa vista que haga que se destaque el resto de la app e indicarle al usuario que este es un tipo diferente de interfaz de usuario. La vista que une AppStyledView se proporciona en setContent El elemento AppStyledView también puede tener un botón Atrás o Cerrar. solicitada por la aplicación.

AppStyledView no inserta de inmediato sus vistas en la jerarquía de vistas. como lo hace installBaseLayoutAround, en cambio, solo devuelve su vista al biblioteca estática a través de getView, que luego realiza la inserción. La posición y el tamaño de AppStyledView también se puede controlar implementando getDialogWindowLayoutParam

Contextos

El complemento debe tener cuidado cuando se usan contextos, ya que existen plugin y "fuente" diferentes. El contexto del complemento se proporciona como un argumento para getPluginFactory y es el único contexto que tiene el elemento los recursos del complemento en ella. Esto significa que es el único contexto que se puede usar aumentar los diseños en el complemento.

Sin embargo, es posible que el contexto del complemento no tenga la configuración correcta establecida. Para obtener la configuración correcta, proporcionamos contextos de origen en los métodos que crean o los componentes de la solución. El contexto de origen suele ser una actividad, pero en algunos casos puede ser también un servicio u otro componente de Android. Para usar la configuración del un contexto de origen con los recursos del contexto del complemento, se debe crear creado con createConfigurationContext. Si la configuración correcta no es si se usa, se considerará una infracción del Modo estricto de Android y es posible que las vistas infladas pero no tienen las dimensiones correctas.

Context layoutInflationContext = pluginContext.createConfigurationContext(
        sourceContext.getResources().getConfiguration());

Cambios de modo

Algunos complementos pueden admitir varios modos para sus componentes, como un modo deportivo o un modo Eco que se vean visualmente distintos. No hay compatibilidad integrada para esa funcionalidad en CarUi, pero nada que el complemento lo implemente por completo internamente. El complemento puede supervisar las condiciones en las que quiera determinar cuándo cambiar de modo, por ejemplo, escuchando transmisiones. El complemento no puede activar un cambio de configuración. para cambiar los modos, pero no se recomienda depender de los cambios de configuración de todas formas, ya que actualizar manualmente el aspecto de cada componente es más para el usuario y también permite transiciones que no son posibles con cambios de configuración.

Jetpack Compose

Los complementos se pueden implementar con Jetpack Compose, pero este es un nivel alfa y no debe considerarse estable.

Los complementos pueden usar ComposeView para crear una plataforma habilitada para Compose en la que renderizar. Este ComposeView sería lo que se muestra a la app desde el método getView en los componentes

Un problema importante con el uso de ComposeView es que establece etiquetas en la vista raíz. en el diseño para almacenar variables globales que se comparten diferentes ComposeViews en la jerarquía. Como los IDs de recurso del complemento no son espacios de nombres separados de los de la app, esto podría causar conflictos cuando la app y las etiquetas del conjunto de complementos en la misma vista. Un ComposeViewWithLifecycle que mueve estas variables globales al A continuación, se proporciona ComposeView. Una vez más, esto no debe considerarse estable.

ComposeViewWithLifecycle:

class ComposeViewWithLifecycle @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
    LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {

  private val lifeCycle = LifecycleRegistry(this)
  private val modelStore = ViewModelStore()
  private val savedStateRegistryController = SavedStateRegistryController.create(this)
  private var composeView: ComposeView? = null
  private var content = @Composable {}

  init {
    ViewTreeLifecycleOwner.set(this, this)
    ViewTreeViewModelStoreOwner.set(this, this)
    ViewTreeSavedStateRegistryOwner.set(this, this)
    compositionContext = createCompositionContext()
  }

  fun setContent(content: @Composable () -> Unit) {
    this.content = content
    composeView?.setContent(content)
  }

  override fun getLifecycle(): Lifecycle {
    return lifeCycle
  }

  override fun getViewModelStore(): ViewModelStore {
    return modelStore
  }

  override fun getSavedStateRegistry(): SavedStateRegistry {
    return savedStateRegistryController.savedStateRegistry
  }

  override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    savedStateRegistryController.performRestore(Bundle())
    lifeCycle.currentState = Lifecycle.State.RESUMED
    composeView = ComposeView(context)
    composeView?.setContent(content)
    addView(composeView, LayoutParams(
      LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    lifeCycle.currentState = Lifecycle.State.DESTROYED
    modelStore.clear()
    removeAllViews()
    composeView = null
  }

  // Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
  private fun createCompositionContext(): CompositionContext {
    val currentThreadContext = AndroidUiDispatcher.CurrentThread
    val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
      PausableMonotonicFrameClock(it).apply { pause() }
    }
    val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
    val recomposer = Recomposer(contextWithClock)
    val runRecomposeScope = CoroutineScope(contextWithClock)
    val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
      "ViewTreeLifecycleOwner not found from $this"
    }
    viewTreeLifecycleOwner.lifecycle.addObserver(
      LifecycleEventObserver { _, event ->
        @Suppress("NON_EXHAUSTIVE_WHEN")
        when (event) {
          Lifecycle.Event.ON_CREATE ->
            // Undispatched launch since we've configured this scope
            // to be on the UI thread
            runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
              recomposer.runRecomposeAndApplyChanges()
            }
          Lifecycle.Event.ON_START -> pausableClock?.resume()
          Lifecycle.Event.ON_STOP -> pausableClock?.pause()
          Lifecycle.Event.ON_DESTROY -> {
            recomposer.cancel()
          }
        }
      }
    )
    return recomposer
  }

//  TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
//  override fun onSaveInstanceState(): Parcelable? {
//    val superState = super.onSaveInstanceState()
//    val bundle = Bundle()
//    savedStateRegistryController.performSave(bundle)
//  }
}