Migrar o app remetente do CCL para o Cast Application Framework (CAF)

O procedimento a seguir permite converter o app de transmissão do Android do SDK do Cast v2 com CCL para CAF. Todas as funcionalidades da CCL foram implementadas no CAF. Portanto, depois da migração, não será mais necessário usar a CCL.

O SDK do remetente do CAF do Google Cast usa o CastContext para gerenciar o GoogleAPIClient em seu nome. O CastContext gerencia ciclos de vida, erros e callbacks para você, o que simplifica bastante o desenvolvimento de um app Cast.

Introdução

  • Como o design do remetente do CAF foi influenciado pela Biblioteca complementar do Cast, a migração do CCL para o remetente do CAF envolve principalmente mapeamentos de classes de um para um e os métodos deles.
  • O remetente do CAF ainda é distribuído como parte do Google Play Services usando o Android SDK Manager.
  • Os novos pacotes (com.google.android.gms.cast.framework.*) que foram adicionados ao remetente do CAF, com funcionalidade semelhante ao CCL, assumem a responsabilidade de obedecer à lista de verificação de design do Google Cast.
  • O remetente do CAF fornece widgets que obedecem aos requisitos de UX do Cast. Esses widgets são semelhantes aos fornecidos pela CCL.
  • O remetente do CAF fornece callbacks assíncronos semelhantes ao CCL, para rastrear estados e receber dados. Ao contrário do CCL, o Remetente do CAF não fornece implementações do ambiente autônomo dos vários métodos de interface.

Nas seções a seguir, vamos nos concentrar principalmente nos aplicativos centradas em vídeo com base no VideoCastManager da CCL, mas em muitos casos os mesmos conceitos também se aplicam ao DataCastManager.

Dependências

O CCL e o CAF têm as mesmas dependências na biblioteca de suporte AppCompat, na Biblioteca de Suporte MediaRouter v7 e no Google Play Services. No entanto, a diferença é que o CAF depende do novo framework do Google Cast que está disponível no Google Play Services 9.2.0 ou mais recente.

No arquivo build.gradle, remova as dependências em com.google.android.gms:play-services-cast e com.google.android.libraries.cast.companionlibrary:ccl e adicione o novo framework do Cast:

dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:mediarouter-v7:23.4.0'
    compile 'com.google.android.gms:play-services-cast-framework:9.4.0'
}

Também é possível remover os metadados do Google Play Services:

<meta‐data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>

Todos os serviços, atividades e recursos que fazem parte do CAF são automaticamente integrados ao manifesto e aos recursos do app.

A versão mínima do SDK do Android compatível com o CAF é 9 (Gingerbread) e a versão mínima do SDK do CCL para Android é 10.

A CCL fornece um método de conveniência, BaseCastManager.checkGooglePlayServices(activity), para verificar se uma versão compatível do Google Play Services está disponível no dispositivo. O CAF não fornece isso como parte do SDK do Cast. Siga o procedimento Garantir que os dispositivos tenham o APK do Google Play Services para garantir que o APK do Google Play Services correto esteja instalado no dispositivo de um usuário, já que as atualizações podem não chegar a todos os usuários imediatamente.

Ainda é necessário usar uma variante de Theme.AppCompat para o tema do app.

Inicialização

Para a CCL, era preciso chamar VideoCastManager.initialize() no método onCreate() da instância do aplicativo. Essa lógica precisa ser removida do código de classe do aplicativo.

No CAF, também é necessária uma etapa de inicialização explícita para o framework do Cast. Isso envolve a inicialização do singleton CastContext, usando um OptionsProvider apropriado para especificar o ID do aplicativo receptor e quaisquer outras opções globais. O CastContext desempenha um papel semelhante ao VideoCastManager da CCL, fornecendo um Singleton com que os clientes interajam. O OptionsProvider é semelhante ao CastConfiguration da CCL para permitir que você configure os recursos do framework do Cast.

Se o CastConfiguration.Builder da CCL atual estiver assim:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(context.getString(R.string.app_id))
       .enableWifiReconnection()
       .enableAutoReconnect()
       .build());

Em seguida, no CAF, o CastOptionsProvider a seguir usando o CastOptions.Builder seria semelhante:

public class CastOptionsProvider implements OptionsProvider {

    @Override
    public CastOptions getCastOptions(Context context) {
        return new CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build();
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(
            Context context) {
        return null;
    }
}

Confira nosso app de exemplo para ver uma implementação completa do OptionsProvider.

Declare o OptionsProvider no elemento "application" do arquivo AndroidManifest.xml:

<application>
...
    <meta-data
        android:name=
          "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.google.sample.cast.refplayer.CastOptionsProvider"    
    />
</application>

Inicialize lentamente o CastContext no método onCreate de cada Activity, e não na instância Application:

private CastContext mCastContext;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.video_browser);
    setupActionBar();

    mCastContext = CastContext.getSharedInstance(this);
}

Para acessar o Singleton CastContext, use:

mCastContext = CastContext.getSharedInstance(this);

Descoberta de dispositivos

Os incrementUiCounter e decrementUiCounter de VideoCastManager da CCL precisam ser removidos dos métodos onResume e onPause do Activities.

No CAF, o processo de descoberta é iniciado e interrompido automaticamente pelo framework quando o app entra em primeiro plano e vai para o segundo plano, respectivamente.

Botão e caixa de diálogo de transmissão

Assim como na CCL, esses componentes são fornecidos pela Biblioteca de Suporte do MediaRouter v7.

O botão Transmitir ainda é implementado pelo MediaRouteButton e pode ser adicionado à sua atividade (usando um ActionBar ou um Toolbar) como um item de menu.

A declaração de MediaRouteActionProvider no XML de menu é a mesma da CCL:

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

Semelhante ao CCL, modifique o método onCreateOptionMenu() de cada atividade. No entanto, em vez de usar CastManager.addMediaRouterButton, use o CastButtonFactory do CAF para conectar o MediaRouteButton ao framework do Cast:

public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                                menu,
                                                R.id.media_route_menu_item);
    return true;
}

Controle do dispositivo

Semelhante ao CCL, no CAF, o controle do dispositivo é amplamente tratado pelo framework. O aplicativo remetente não precisa processar (e não tentar processar) a conexão com o dispositivo e a inicialização do app receptor usando GoogleApiClient.

A interação entre o remetente e o destinatário agora é representada como uma "sessão". A classe SessionManager processa o ciclo de vida da sessão e inicia e interrompe automaticamente as sessões em resposta a gestos do usuário. Uma sessão é iniciada quando o usuário seleciona um dispositivo de transmissão na caixa de diálogo de transmissão e termina quando ele toca no botão "Parar transmissão" na caixa de diálogo ou quando o próprio app remetente é encerrado.

Na CCL, é necessário estender a classe VideoCastConsumerImpl para rastrear o status da sessão de transmissão:

private final VideoCastConsumer mCastConsumer = new VideoCastConsumerImpl() {
  public void onApplicationConnected(ApplicationMetadata appMetadata, 
                                     String sessionId,
                                     boolean wasLaunched) {}
  public void onDisconnectionReason(int reason) {}
  public void onDisconnected() {}
}

No CAF, o aplicativo do remetente pode ser notificado sobre eventos do ciclo de vida da sessão registrando um SessionManagerListener com o SessionManager. Os callbacks SessionManagerListener definem métodos de callback para todos os eventos do ciclo de vida da sessão.

Os seguintes métodos SessionManagerListener são mapeados na interface VideoCastConsumer da CCL:

  • VideoCastConsumer.onApplicationConnected -> SessionManagerListener.onSessionStarted
  • VideoCastConsumer.onDisconnected -> SessionManagerListener.onSessionEnded

Declare uma classe que implemente a interface SessionManagerListener e mova a lógica VideoCastConsumerImpl para os métodos correspondentes:

private class CastSessionManagerListener implements SessionManagerListener<CastSession> {
  public void onSessionEnded(CastSession session, int error) {}
  public void onSessionStarted(CastSession session, String sessionId) {}
  public void onSessionEnding(CastSession session) {}
  ...
}

A classe CastSession representa uma sessão com um dispositivo de transmissão. A classe tem métodos para controlar o volume do dispositivo e os estados de silenciamento, o que a CCL faz no BaseCastManager.

Em vez de usar o CCL VideoCastManager para adicionar um consumidor:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Agora, registre seu SessionManagerListener:

mCastSessionManager = 
    CastContext.getSharedInstance(this).getSessionManager();
mCastSessionManagerListener = new CastSessionManagerListener();
mCastSessionManager.addSessionManagerListener(mCastSessionManagerListener,
                  CastSession.class);

Para parar de detectar eventos na CCL, faça o seguinte:

VideoCastManager.getInstance().removeVideoCastConsumer(mCastConsumer);

Agora, use o SessionManager para parar de detectar eventos de sessão:

mCastSessionManager.removeSessionManagerListener(mCastSessionManagerListener,
                    CastSession.class);

Para se desconectar explicitamente do dispositivo de transmissão, a CCL usou:

VideoCastManager.disconnectDevice(boolean stopAppOnExit, 
            boolean clearPersistedConnectionData,
            boolean setDefaultRoute)

Para o CAF, use o SessionManager:

CastContext.getSharedInstance(this).getSessionManager()
                                   .endCurrentSession(true);

Para determinar se o remetente está conectado ao destinatário, o CCL fornece VideoCastManager.getInstance().isConnected(), mas no CAF use o SessionManager:

public boolean isConnected() {
    CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                  .getSessionManager()
                                  .getCurrentCastSession();
    return (castSession != null && castSession.isConnected());
}

No CAF, as notificações de mudança de estado de volume/mudo ainda são entregues por métodos de callback na Cast.Listener. Esses listeners são registrados em CastSession. Todas as outras notificações de estado do dispositivo são entregues por callbacks CastStateListener. Esses listeners são registrados com o CastSession. Cancele o registro dos listeners quando os fragmentos, as atividades ou os apps associados ficarem em segundo plano.

Lógica de reconexão

O CAF tenta restabelecer conexões de rede perdidas devido a perda temporária de sinal Wi-Fi ou outros erros de rede. Agora, isso é feito no nível da sessão. Uma sessão pode entrar no estado "suspenso" quando a conexão for perdida e passar para o estado "conectado" quando a conectividade for restaurada. O framework cuida da reconexão ao app receptor e da reconexão de todos os canais de transmissão como parte desse processo.

O CAF fornece o próprio serviço de reconexão para que você possa remover o CCL ReconnectionService do manifesto:

<service android:name="com.google.android.libraries.cast.companionlibrary.cast.reconnection.ReconnectionService"/>

Você também não precisa das seguintes permissões no manifesto para a lógica de reconexão:

<uses‐permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses‐permission android:name="android.permission.ACCESS_WIFI_STATE"/>

O serviço de reconexão do CAF é ativado por padrão, mas pode ser desativado usando o CastOptions.

Além disso, o CAF também adiciona a retomada automática da sessão, que é ativada por padrão e pode ser desativada por CastOptions. Se o aplicativo remetente for enviado para o segundo plano ou for encerrado (deslizando para fora ou devido a uma falha) enquanto uma sessão de transmissão estiver em andamento, o framework tentará retomar essa sessão quando o aplicativo remetente voltar para o primeiro plano ou for reiniciado. Isso será processado automaticamente pelo SessionManager, que

Registro de canal personalizado

O CCL oferece duas maneiras de criar um canal de mensagens personalizado para o destinatário:

  • CastConfiguration permite que você especifique vários namespaces, e o CCL criará o canal.
  • DataCastManager é semelhante ao VideoCastManager, mas focado em casos de uso que não sejam de mídia.

Nenhuma dessas formas de criar um canal personalizado é compatível com o CAF. Você precisa seguir o procedimento Adicionar um canal personalizado para seu app remetente.

Assim como na CCL, para aplicativos de mídia, não é necessário registrar explicitamente o canal de controle de mídia.

Controle de mídia

No CAF, a classe RemoteMediaClient é equivalente aos métodos de mídia VideoCastManager. O RemoteMediaClient.Listener é equivalente aos métodos VideoCastConsumer. Especificamente, os métodos onRemoteMediaPlayerMetadataUpdated e onRemoteMediaPlayerStatusUpdated de VideoCastConsumer são mapeados para os métodos onMetadataUpdated e onStatusUpdated de RemoteMediaClient.Listener, respectivamente:

private class CastMediaClientListener implements RemoteMediaClient.Listener {

    @Override
    public void onMetadataUpdated() {
        setMetadataFromRemote();
    }

    @Override
    public void onStatusUpdated() {
        updatePlaybackState();
    }

    @Override
    public void onSendingRemoteMediaRequest() {
    }

    @Override
    public void onQueueStatusUpdated() {
    }

    @Override
    public void onPreloadStatusUpdated() {
    }
}

Não é necessário inicializar ou registrar explicitamente o objeto RemoteMediaClient. O framework vai instanciar automaticamente o objeto e registrar o canal de mídia subjacente no horário de início da sessão se o app receptor que está sendo conectado for compatível com o namespace de mídia.

O RemoteMediaClient pode ser acessado como o método getRemoteMediaClient do objeto CastSession.

CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                     .getSessionManager()
                                     .getCurrentCastSession();
mRemoteMediaClient = castSession.getRemoteMediaClient();
mRemoteMediaClientListener = new CastMediaClientListener();

Em vez de CCLs:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Agora use o CAF:

mRemoteMediaClient.addListener(mRemoteMediaClientListener);

Qualquer número de listeners pode ser registrado no RemoteMediaClient, que permite que vários componentes de remetente compartilhem a única instância de RemoteMediaClient associada à sessão.

O VideoCastManager da CCL fornece métodos para processar a reprodução de mídia:

VideoCastManager manager = VideoCastManager.getInstance();
if (manager.isRemoteMediaLoaded()) {
    manager.pause();
    mCurrentPosition = (int) manager.getCurrentMediaPosition();
}

Agora, eles são implementados pelo RemoteMediaClient no CAF:

if (mRemoteMediaClient.hasMediaSession()) {
    mRemoteMediaClient.pause();
    mCurrentPosition = 
        (int)mRemoteMediaClient.getApproximateStreamPosition();
}

No CAF, todas as solicitações de mídia emitidas no RemoteMediaClient retornam um RemoteMediaClient.MediaChannelResult usando um callback PendingResult, que pode ser usado para rastrear o progresso e o resultado final da solicitação.

Tanto a CCL quanto o CAF usam as classes MediaInfo e MediaMetadata para representar itens de mídia e carregar mídia.

Para carregar mídia em CCL, use o VideoCastManager:

VideoCastManager.getInstance().loadMedia(media, autoPlay, mCurrentPosition, customData);

No CAF, o RemoteMediaClient é usado para carregar a mídia:

mRemoteMediaClient.load(media, autoPlay, mCurrentPosition, customData);

Para ver as informações e o status Media de uma sessão de mídia atual no receptor, a CCL usa o VideoCastManager:

MediaInfo mediaInfo = VideoCastManager.getInstance()
                                      .getRemoteMediaInformation();
int status = VideoCastManager.getInstance().getPlaybackStatus();
int idleReason = VideoCastManager.getInstance().getIdleReason();

No CAF, use o RemoteMediaClient para acessar as mesmas informações:

MediaInfo mediaInfo = mRemoteMediaClient.getMediaInfo();
int status = mRemoteMediaClient.getPlayerState();
int idleReason = mRemoteMediaClient.getIdleReason();

Sobreposição introdutória

Semelhante à CCL, o CAF fornece uma visualização personalizada IntroductoryOverlay para destacar o botão Transmitir quando ele é mostrado aos usuários pela primeira vez.

Em vez de usar o método onCastAvailabilityChanged da VideoCastConsumer da CLC para saber quando exibir a sobreposição, declare um CastStateListener para determinar quando o botão Transmitir ficará visível quando os dispositivos de transmissão forem descobertos na rede local pelo MediaRouter:

private IntroductoryOverlay mIntroductoryOverlay;
private MenuItem mMediaRouteMenuItem;

protected void onCreate(Bundle savedInstanceState) {
    ...
    mCastStateListener = new CastStateListener() {
        @Override
        public void onCastStateChanged(int newState) {
            if (newState != CastState.NO_DEVICES_AVAILABLE) {
                showIntroductoryOverlay();
            }
        }
    };
    mCastContext = CastContext.getSharedInstance(this);
    mCastContext.registerLifecycleCallbacksBeforeIceCreamSandwich(this, 
        savedInstanceState);
}

protected void onResume() {
    mCastContext.addCastStateListener(mCastStateListener);
    ...
}

protected void onPause() {
    mCastContext.removeCastStateListener(mCastStateListener);
    ...
}

Acompanhe a instância MediaRouteMenuItem:

public boolean onCreateOptionsMenu(Menu menu) {
   super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    mMediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(
            getApplicationContext(), menu,
            R.id.media_route_menu_item);
    showIntroductoryOverlay();
    return true;
}

Verifique se o MediaRouteButton está visível para que a sobreposição introdutória seja exibida:

private void showIntroductoryOverlay() {
    if (mIntroductoryOverlay != null) {
        mIntroductoryOverlay.remove();
    }
    if ((mMediaRouteMenuItem != null) && mMediaRouteMenuItem.isVisible()) {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mIntroductoryOverlay = new IntroductoryOverlay.Builder(
                        VideoBrowserActivity.this, mMediaRouteMenuItem)
                        .setTitleText(getString(R.string.introducing_cast))
                        .setOverlayColor(R.color.primary)
                        .setSingleTime()
                        .setOnOverlayDismissedListener(
                                new IntroductoryOverlay
                                    .OnOverlayDismissedListener() {
                                        @Override
                                        public void onOverlayDismissed() {
                                            mIntroductoryOverlay = null;
                                        }
                                })
                        .build();
                mIntroductoryOverlay.show();
            }
        });
    }
}

Confira nosso app de exemplo para ver o código de trabalho completo para mostrar a sobreposição introdutória.

Para personalizar o estilo da sobreposição introdutória, siga o procedimento Personalizar sobreposição introdutória.

Minicontrole

Em vez de MiniController do CCL, use o MiniControllerFragment do CAF no arquivo de layout do app das atividades em que você quer mostrar o mini controlador:

<fragment
        android:id="@+id/cast_mini_controller"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:castShowImageThumbnail="true"
        android:visibility="gone"
        class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

O CAF não é compatível com a configuração manual aceita pelo MiniController da CCL e também com o recurso Autoplay.

Para personalizar o estilo e os botões do minicontrole, siga o procedimento Personalizar minicontrole.

Notificação e tela de bloqueio

Semelhante ao VideoCastNotificationService do CCL, o CAF fornece um MediaNotificationService para gerenciar a exibição de notificações de mídia durante a transmissão.

Você precisa remover o seguinte do seu manifesto:

  • VideoIntentReceiver
  • VideoCastNotificationService

O CCL oferece suporte a um serviço de notificação personalizado com o CastConfiguration.Builder, o que não é suportado pelo CAF.

Considere a seguinte inicialização de CastManager usando CCL:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(
           context.getString(R.string.app_id))
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_PLAY_PAUSE,true)
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_DISCONNECT,true)
       .build());

Para a configuração equivalente no CAF, o SDK fornece um NotificationsOptions.Builder para ajudar você a criar controles de mídia para a notificação e a tela de bloqueio no app remetente. Os controles da tela de notificação e bloqueio podem ser ativados com o CastOptions ao inicializar o CastContext.

public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = 
        new NotificationOptions.Builder()
            .setActions(Arrays.asList(
                MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK,
                MediaIntentReceiver.ACTION_STOP_CASTING), new int[]{0, 1})
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
             .setNotificationOptions(notificationOptions)
             .build();
    return new CastOptions.Builder()
             .setReceiverApplicationId(context.getString(R.string.app_id))
             .setCastMediaOptions(mediaOptions)
             .build();
}

As notificações e os controles da tela de bloqueio estão sempre ativados no CAF. Além disso, os botões de reproduzir/pausar e interromper a transmissão são fornecidos por padrão. O CAF vai rastrear automaticamente a visibilidade das atividades para decidir quando exibir a notificação de mídia, exceto o Gingerbread. Para o Gingerbread, consulte a observação anterior sobre o uso de registerLifecycleCallbacksBeforeIceCreamSandwich(). As chamadas VideoCastManager incrementUiCounter e decrementUiCounter do CCL precisam ser removidas.

Para personalizar os botões mostrados nas notificações, siga o procedimento Adicionar controles de mídia à tela de bloqueio e notificação.

Controle expandido

A CCL fornece a VideoCastControllerActivity e a VideoCastControllerFragment para exibir um controlador expandido ao transmitir mídia.

Você pode remover a declaração VideoCastControllerActivity no manifesto.

No CAF, é necessário estender a ExpandedControllerActivity e adicionar o botão "Transmitir".

Para personalizar os estilos e botões exibidos no controle expandido, siga o procedimento Personalizar controle expandido.

Seleção de áudio

Assim como na CCL, a seleção de áudio é gerenciada automaticamente.

Controle do volume

Para o Gingerbread, o dispatchKeyEvent é obrigatório, assim como o CCL. No ICS e em versões posteriores, o controle de volume do CCL e do CAF é processado automaticamente.

O CAF permite controlar o volume de transmissão usando o botão de volume rígido no smartphone das atividades do app e também mostra uma barra de volume visual ao transmitir em versões compatíveis. O CAF também processa a mudança de volume com o volume rígido, mesmo que o app não esteja na frente, esteja bloqueado ou mesmo se a tela esteja desativada.

Closed captions

No Android KitKat e em versões mais recentes, as legendas podem ser personalizadas nas configurações de legendas, em "Configurações > Acessibilidade". No entanto, as versões anteriores do Android não têm esse recurso. O CCL lida com isso fornecendo configurações personalizadas para versões anteriores e delegando às configurações do sistema no KitKat e versões mais recentes.

O CAF não oferece configurações personalizadas para alterar as preferências de legenda. Remova as referências CaptionsPreferenceActivity do manifesto e do XML de preferências.

O TracksChooserDialog do CCL não é mais necessário, já que a mudança das faixas de legenda fechada é processada pela IU do controlador expandida.

A API de legenda no CAF é semelhante à v2.

Registro de depuração

O CAF não oferece configurações de geração de registros de depuração.

Diversos

Os seguintes recursos do CCL não são compatíveis com o CAF:

  • Como conseguir autorização antes da reprodução fornecendo um MediaAuthService
  • Mensagens de IU configuráveis

Apps de exemplo

Confira a diferença para migrar nosso app de exemplo Universal Music Player para Android (uamp) do CCL para o CAF.

Também temos tutoriais do codelab e apps de exemplo que usam o CAF.