Plug-ins da interface do carro

Use os plug-ins da biblioteca Car UI para criar implementações completas de componentes. personalizações na biblioteca da interface do carro em vez de usar sobreposições de recursos no momento da execução. (RROs, na sigla em inglês). As RROs permitem mudar apenas os recursos XML da biblioteca Car UI o que limita a personalização.

Criar um plug-in

Um plug-in da biblioteca Car UI é um APK que contém classes que implementam um conjunto de APIs de plug-in. As APIs de plug-in podem ser compiladas em um como uma biblioteca estática.

Confira exemplos no Soong e no Gradle:

Soong

Confira este exemplo do 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

Confira este arquivo 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')

O plug-in precisa ter um provedor de conteúdo declarado no manifesto com o seguintes atributos:

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

android:authorities="com.android.car.ui.plugin" torna o plug-in detectável à biblioteca da interface do carro. O provedor precisa ser exportado para que possa ser consultado em no ambiente de execução. Além disso, se o atributo enabled for definido como false, o padrão implementação será usada no lugar da implementação do plug-in. O conteúdo a classe de provedor não precisa existir. Nesse caso, adicione tools:ignore="MissingClass" à definição do provedor. Ver o exemplo entrada do manifesto abaixo:

    <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 fim, como medida de segurança, Assinar o app.

Plug-ins como uma biblioteca compartilhada

Ao contrário das bibliotecas estáticas do Android, que são compiladas diretamente em apps, As bibliotecas compartilhadas do Android são compiladas em um APK autônomo que é referenciado por outros apps no momento da execução.

Os plug-ins implementados como uma biblioteca compartilhada do Android têm as classes deles adicionado automaticamente ao carregador de classe compartilhado entre os aplicativos. Quando um app que usa a biblioteca Car UI especifica um dependência de ambiente de execução na biblioteca compartilhada do plug-in, o O classloader pode acessar as classes da biblioteca compartilhada do plug-in. Plug-ins implementados já que os apps Android normais (e não uma biblioteca compartilhada) podem afetar negativamente o uso do app. horários de início.

Implementar e criar bibliotecas compartilhadas

O desenvolvimento com bibliotecas compartilhadas do Android é muito parecido com o desenvolvimento aplicativos, com algumas diferenças importantes.

  • Usar a tag library na tag application com o pacote de plug-ins no manifesto do app do plug-in:
    <application>
        <library android:name="com.chassis.car.ui.plugin" />
        ...
    </application>
  • Configurar a regra de build android_app do Soong (Android.bp) com o AAPT flag shared-lib, que é usada para criar uma biblioteca compartilhada:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Dependências de bibliotecas compartilhadas

Para cada app do sistema que usa a biblioteca Car UI, inclua o tag uses-library no manifesto do app em Tag application com o nome do pacote do plug-in:

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

Instalar um plug-in

Os plug-ins PRECISAM ser pré-instalados na partição do sistema, incluindo o módulo em PRODUCT_PACKAGES. O pacote pré-instalado pode ser atualizado de forma semelhante a qualquer outro app instalado.

Se você estiver atualizando um plug-in já existente no sistema, todos os aplicativos que o utilizam fechar automaticamente. Depois de reabertas pelo usuário, elas terão as alterações atualizadas. Se o app não estava em execução, na próxima vez que for iniciado, ele terá a versão atualizada plug-in.

Ao instalar um plug-in com o Android Studio, há alguns requisitos adicionais considerações a serem consideradas. No momento em que este artigo foi escrito, havia um bug em O processo de instalação de apps do Android Studio que causa atualizações em um plug-in para não entrarem em vigor. Isso pode ser corrigido selecionando a opção Sempre instalar Com o gerenciador de pacotes (desativa as otimizações de implantação no Android 11 e versões mais recentes) na configuração do build do plug-in.

Além disso, ao instalar o plug-in, o Android Studio informa um erro informando que não consegue encontrar uma atividade principal para iniciar. Isso é esperado, pois o plug-in não ter nenhuma atividade (exceto a intent vazia usada para resolver uma intent); Para elimine o erro, altere a opção Launch para Nothing no build configuração do Terraform.

Plug-in de configuração do Android Studio Figura 1. Plug-in de configuração do Android Studio

Plug-in de proxy

Personalização do de apps usando a biblioteca Car UI exige uma RRO direcionada a cada app que será modificado; inclusive quando as personalizações são idênticas em todos os apps. Isso significa que uma RRO por app é obrigatório. Confira quais apps usam a biblioteca da interface do carro.

O plug-in de proxy da biblioteca Car UI é um exemplo uma biblioteca compartilhada do plug-in que delega as implementações dos componentes à biblioteca da biblioteca Car UI. Esse plug-in pode ser segmentado com uma RRO, que pode ser usada como um ponto único de personalização para apps que usam a biblioteca Car UI sem a necessidade de implementar um plug-in funcional. Para mais informações sobre RROs, consulte Mudar o valor dos recursos de um app em ambiente de execução.

O plug-in do proxy é apenas um exemplo e ponto de partida para fazer a personalização usando um plug-in. Para personalização além das RROs, é possível implementar um subconjunto de plug-ins e use o plug-in de proxy para o restante, ou implemente todos os componentes componentes totalmente do zero.

Embora o plug-in de proxy forneça um ponto único de personalização de RRO para apps, Os apps que recusam o uso do plug-in ainda vão exigir uma RRO que diretamente direciona o próprio app.

Implementar as APIs do plug-in

O ponto de entrada principal do plug-in é com.android.car.ui.plugin.PluginVersionProviderImpl. Todos os plug-ins devem inclua uma classe com esse nome exato e o nome do pacote. A classe precisa ter construtor padrão e implementar a interface PluginVersionProviderOEMV1.

Os plug-ins do CarUi precisam funcionar com apps mais antigos ou mais recentes que o plug-in. Para facilitar isso, todas as APIs de plug-in têm versões com um V# no final da classname. Se uma nova versão da biblioteca da interface do carro for lançada com novos recursos, elas fazem parte da versão V2 do componente. A biblioteca Car UI melhor fazer com que novos recursos funcionem dentro do escopo de um componente de plug-in mais antigo. Por exemplo, convertendo um novo tipo de botão na barra de ferramentas em MenuItems.

No entanto, um app com uma versão mais antiga da biblioteca Car UI não pode se adaptar a uma nova plug-in escrito em APIs mais recentes. Para resolver esse problema, permitimos que os plug-ins retornam implementações diferentes com base na versão da API do OEM compatíveis com os apps.

PluginVersionProviderOEMV1 tem um método:

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

Esse método retorna um objeto que implementa a versão mais recente do PluginFactoryOEMV# compatível com o plug-in, embora ainda seja menor que ou igual a maxVersion. Se um plug-in não tiver uma implementação de PluginFactory daquela antiga, ele poderá retornar null. Nesse caso, o estado implementação vinculada de componentes CarUi são usadas.

Para manter a compatibilidade com versões anteriores de apps que são compilados versões mais antigas da biblioteca estática Car Ui, é recomendável oferecer suporte maxVersions de 2, 5 ou superiores na implementação do plug-in do a classe PluginVersionProvider. As versões 1, 3 e 4 não são compatíveis. Para mais informações, consulte PluginVersionProviderImpl

O PluginFactory é a interface que cria todos os outros CarUi componentes de solução. Ele também define qual versão das interfaces precisa ser usada. Se o plug-in não pretende implementar nenhum desses componentes, ele pode retornar null na função de criação (exceto a barra de ferramentas, que tem uma função customizesBaseLayout() separada).

O pluginFactory limita quais versões dos componentes CarUi podem ser usadas juntas. Por exemplo, nunca haverá um pluginFactory que possa criar a versão 100 de uma Toolbar e a versão 1 de um RecyclerView, seria pouca garantia de que uma ampla variedade de versões de componentes trabalham juntas. Para usar a versão 100 da barra de ferramentas, espera-se que os desenvolvedores fornece uma implementação de uma versão do pluginFactory que cria um versão 100 da barra de ferramentas, o que limita as opções nas versões de outras componentes que podem ser criados. As versões de outros componentes não podem ser é igual. Por exemplo, uma pluginFactoryOEMV100 pode criar ToolbarControllerOEMV100 e um RecyclerViewOEMV70.

Barra de ferramentas

Layout básico

A barra de ferramentas e o "layout básico" são muito relacionadas, por isso a função que cria a barra de ferramentas é chamada installBaseLayoutAround. A layout básico permite que a barra de ferramentas seja posicionada em qualquer lugar ao redor do app para permitir uma barra de ferramentas na parte superior/inferior do app, verticalmente nas laterais, ou até mesmo uma barra de ferramentas circular que abrange todo o app. Isso é Isso pode ser feito passando uma visualização para installBaseLayoutAround para a barra de ferramentas/base e o layout padrão.

O plug-in deve pegar a visualização fornecida, removê-la do pai, inflar o próprio layout do plug-in no mesmo índice do pai e com a mesma LayoutParams como a visualização que acabou de ser removida e, em seguida, reanexou a visualização. em algum lugar dentro do layout que acabou de ser inflado. O layout inflado conter a barra de ferramentas, se solicitado pelo aplicativo.

O app pode solicitar um layout básico sem uma barra de ferramentas. Se isso acontecer, installBaseLayoutAround retornará um valor nulo. Para a maioria dos plug-ins, isso é tudo precisa acontecer, mas se o autor do plug-in quiser aplicar, por exemplo, uma decoração na borda do app, isso ainda poderia ser feito com um layout básico. Esses as decorações são particularmente úteis para dispositivos com telas não retangulares, como podem empurrar o app para um espaço retangular e adicionar transições claras ao o espaço não retangular.

installBaseLayoutAround também recebe um Consumer<InsetsOEMV1>. Isso consumidor pode ser usado para comunicar ao aplicativo que o plug-in está parcialmente cobrindo o conteúdo do app (com a barra de ferramentas ou não). O app vai saber continuar desenhando nesse espaço, mas mantenha qualquer elemento essencial os componentes dele. Esse efeito é usado em nosso design de referência para tornar barra de ferramentas semitransparente e faz com que as listas rolem abaixo dela. Se esse recurso foi não implementado, o primeiro item de uma lista ficaria preso abaixo da barra de ferramentas e não clicável. Se esse efeito não for necessário, o plug-in poderá ignorar o Consumidor.

Rolagem de conteúdo abaixo da barra de ferramentas Figura 2. Rolagem de conteúdo abaixo da barra de ferramentas

Do ponto de vista do aplicativo, quando o plug-in enviar novos encartes, ele receberá de qualquer atividade ou fragmento que implemente InsetsChangedListener. Se uma atividade ou um fragmento não implementa InsetsChangedListener, a interface do carro; vai tratar encartes por padrão aplicando os encartes como padding ao Activity ou FragmentActivity contendo o fragmento. A biblioteca não aplicar os encartes por padrão aos fragmentos. Aqui está um exemplo de snippet implementação que aplica os encartes como padding em um RecyclerView da aplicativo:

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 fim, o plug-in recebe uma dica fullscreen, que é usada para indicar se a visualização que precisa ser encapsulada ocupa todo o app ou apenas uma pequena seção. Isso pode ser usado para evitar aplicar algumas decorações na borda que só fazem sentido se aparecerem na borda da tela inteira. Uma amostra app que usa layouts básicos que não são em tela cheia são as Configurações, em que cada painel do layout de painel duplo tem a própria barra de ferramentas.

Como é esperado que installBaseLayoutAround retorne um valor nulo quando toolbarEnabled é false para o plug-in indicar que ele não quiser personalizar o layout base, ele deve retornar false de customizesBaseLayout.

O layout base precisa conter uma FocusParkingView e uma FocusArea para ser totalmente dão suporte a controles giratórios. Essas visualizações podem ser omitidas em dispositivos que não têm suporte ao seletor giratório. As FocusParkingView/FocusAreas são implementadas biblioteca CarUi estática, em que um setRotaryFactories é usado para fornecer fábricas para criar as visualizações a partir de contextos.

Os contextos usados para criar as visualizações de foco precisam ser o contexto da fonte, não o contexto do plug-in. O FocusParkingView precisa ser o mais próximo da primeira visualização da árvore da forma mais razoável possível, porque é o foco quando deveria não ter foco visível para o usuário. O FocusArea precisa ajustar a barra de ferramentas no layout básico para indicar que é uma zona de deslocamento giratório. Se o FocusArea não estiver fornecido, o usuário não poderá navegar para nenhum botão da barra de ferramentas com o controle giratório.

Controlador da barra de ferramentas

O ToolbarController real retornado será muito mais simples implementar do que o layout básico. A função dele é pegar as informações transmitidas para setters e exibi-lo no layout base. Consulte o Javadoc para mais informações sobre a maioria dos métodos. Alguns dos métodos mais complexos são discutidos abaixo.

A propriedade getImeSearchInterface é usada para mostrar os resultados da pesquisa no IME (teclado). janela. Isso pode ser útil para exibir/animar resultados de pesquisa junto com a do teclado, por exemplo, se ele ocupou apenas metade da tela. A maioria a funcionalidade é implementada na biblioteca CarUi estática, a pesquisa no plug-in fornece apenas métodos para que a biblioteca estática obtenha a Callbacks TextView e onPrivateIMECommand. Para isso, o plug-in precisa usar uma subclasse TextView que substitua onPrivateIMECommand e transmita a chamada para o listener fornecido como o TextView da barra de pesquisa.

O setMenuItems simplesmente mostra MenuItems na tela, mas será chamado. com muita frequência. Como a API do plug-in para MenuItems é imutável, sempre que um O MenuItem foi modificado, e uma chamada setMenuItems totalmente nova será realizada. Isso poderia acontecer para algo tão trivial, como um usuário clicou em um botão MenuItem, e clique fez a chave alternar. Por motivos de desempenho e animação, é recomendável calcular a diferença entre a versão antiga e a nova MenuItems e atualizar apenas as visualizações que realmente mudaram. Os MenuItems forneça um campo key que possa ajudar com isso, já que a chave precisa ser a mesma. em chamadas diferentes para setMenuItems para o mesmo MenuItem.

Visualização de estilo de aplicativo

O AppStyledView é um contêiner para uma visualização que não é personalizada. Ela pode ser usado para fornecer uma borda em torno da visualização que a faz se destacar o restante do aplicativo e indicar ao usuário que se trata de um tipo diferente de interface gráfica do usuário. A visualização encapsulada pelo AppStyledView é fornecida em setContent: O AppStyledView também pode ter um botão "Voltar" ou "Fechar" como solicitado pelo app.

O AppStyledView não insere as visualizações imediatamente na hierarquia de visualizações. como installBaseLayoutAround faz, em vez disso, ele apenas retorna a visualização para o biblioteca estática por meio de getView, que faz a inserção. A posição e o tamanho do AppStyledView também pode ser controlado pela implementação getDialogWindowLayoutParam.

Contextos

O plug-in precisa ter cuidado ao usar contextos, porque há plugin e "fonte" contextos de negócios diferentes. O contexto do plug-in é fornecido como um argumento para getPluginFactory, e é o único contexto que tem a os recursos do plug-in nele. Isso significa que é o único contexto que pode ser usado para inflar layouts no plug-in.

No entanto, o contexto do plug-in pode não ter a configuração correta definida. Para tenham a configuração correta, fornecemos contextos de origem em métodos que criam componentes de solução. O contexto de origem geralmente é uma atividade, mas em alguns casos pode também pode ser um serviço ou outro componente do Android. Para usar a configuração do contexto de origem com os recursos do contexto do plug-in, um novo contexto deve ser criado usando createConfigurationContext. Se a configuração correta não for usado, haverá uma violação do modo estrito do Android, e as visualizações infladas podem não têm as dimensões corretas.

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

Alterações de modo

Alguns plug-ins aceitam vários modos para seus componentes, como: um modo esportivo ou um modo econômico que tenham uma aparência visualmente diferente. Não há o suporte integrado para essa funcionalidade no CarUi, mas nada pode parar plug-in de implementá-la totalmente internamente. O plug-in pode monitorar quaisquer condições que queira descobrir quando mudar de modo, como para ouvir transmissões. O plug-in não pode acionar uma mudança de configuração. alterar os modos, mas não é recomendável depender de mudanças de configuração porque atualizar manualmente a aparência de cada componente é para o usuário e também permite transições que não são possíveis mudanças na configuração.

Jetpack Compose

Plug-ins podem ser implementados usando o Jetpack Compose, mas este é um nível Alfa e não deve ser considerado estável.

Os plug-ins podem usar ComposeView para criar uma plataforma ativada para o Compose para renderização. Esse ComposeView seria o que é retornado ao app pelo método getView nos componentes.

Um grande problema com o uso da ComposeView é que ela define tags na visualização raiz. no layout para armazenar variáveis globais que são compartilhadas ComposeViews diferentes na hierarquia. Como os IDs de recurso do plug-in não são com namespace separados dos do aplicativo, isso poderia causar conflitos quando os app e o plug-in definem tags na mesma visualização. Um personalizado ComposeViewWithLifecycle, que move essas variáveis globais para a seção O ComposeView é fornecido abaixo. Novamente, isso não é considerado estável.

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