Migrar o app de remetente do CCL para o Framework de aplicativos do Cast (CAF, na sigla em inglês)

O procedimento a seguir permite que você converta o app Android remetente de Transmita o SDK v2 com CCL para CAF. Toda a funcionalidade da CCL foi implementado no CAF, portanto, após a migração, não será mais necessário usar o CCL.

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

Introdução

  • Como o design do CAF Sender foi influenciado pela Cast Companion Library, a a migração do CCL para o remetente CAF envolve principalmente mapeamentos e seus métodos.
  • O remetente CAF ainda é distribuído como parte do Google Play Services usando o Android SDK Manager.
  • Novos pacotes (com.google.android.gms.cast.framework.*) que foram adicionados ao CAF Sender, com funcionalidade semelhante à CCL, assumem a responsabilidade de cumprir Lista de verificação de design do Google Cast.
  • O CAF Sender fornece widgets que obedecem aos requisitos do Cast UX. esses widgets são semelhantes aos fornecidos pelo CCL.
  • O remetente CAF fornece callbacks assíncronos semelhantes ao CCL para rastrear estados e obter dados. Ao contrário do CCL, o CAF Sender não fornece nenhum ambiente autônomo e implementações dos vários métodos de interface.

Nas próximas seções, vamos nos concentrar nos vídeos baseados no VideoCastManager da CCL, mas, em muitos casos, os mesmos também se aplicam ao DataCastManager.

Dependências

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

No arquivo build.gradle, remova as dependências do com.google.android.gms:play-services-cast e com.google.android.libraries.cast.companionlibrary:ccl, Depois adicione o novo framework do Google 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 do CAF são automaticamente integrado com o manifesto e os recursos do app.

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

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

Você ainda deve usar uma variante de Theme.AppCompat para o tema.

Inicialização

Para CCL, era necessário chamar VideoCastManager.initialize() no Método onCreate() da instância Applications. Essa lógica deve ser removido do código da classe do aplicativo.

No CAF, também é necessária uma etapa explícita de inicialização para o Cast de análise de dados em nuvem. Isso envolve a inicialização do Singleton CastContext, usando uma o OptionsProvider apropriado para especificar o ID do aplicativo receptor e quaisquer outras opções globais. O CastContext tem um papel semelhante ao do CCL. VideoCastManager fornecendo um Singleton com que os clientes interagem. O OptionsProvider é semelhante ao CastConfiguration da CCL, o que permite para configurar os recursos do framework do Google Cast.

Se a CastConfiguration.Builder do CCL atual for semelhante a esta:

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 aplicativo de exemplo para uma implementação completa do OptionsProvider.

Declarar o OptionsProvider no "aplicativo" do elemento 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>

Inicializar lentamente o CastContext no método onCreate de cada Activity. (e não a 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 VideoCastManager incrementUiCounter e decrementUiCounter da CCL devem será removido dos métodos onResume e onPause do seu Activities.

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

Botão "Transmitir" e caixa de diálogo "Transmitir"

Assim como na CCL, esses componentes são fornecidos pelo suporte do MediaRouter v7 biblioteca.

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

A declaração de MediaRouteActionProvider no XML do menu é a mesma que com o 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, substitua o método onCreateOptionMenu() de cada atividade, mas em vez de usar CastManager.addMediaRouterButton, use o CastButtonFactory do CAF para conectar o MediaRouteButton ao framework do Google 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 de dispositivos é amplamente gerenciado pelo framework. O aplicativo remetente não precisa processar (e não deve tentar fazer isso) conectando-se ao dispositivo e iniciando o aplicativo receptor usando GoogleApiClient:

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

Na CCL, é preciso estender a classe VideoCastConsumerImpl para rastrear o elenco. status da sessã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 remetente pode ser notificado sobre eventos de ciclo de vida da sessão registrando um SessionManagerListener com o SessionManager. A Os callbacks SessionManagerListener definem métodos de callback para todas as sessões. eventos de ciclo de vida.

Os seguintes métodos SessionManagerListener são mapeados a partir de CCLs Interface do VideoCastConsumer:

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

Declarar uma classe que implementa a interface SessionManagerListener e mover a lógica VideoCastConsumerImpl aos métodos de correspondência:

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 mudo, o que o CCL faz na 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:

VideoCastManager.getInstance().removeVideoCastConsumer(mCastConsumer);

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

mCastSessionManager.removeSessionManagerListener(mCastSessionManagerListener,
                    CastSession.class);

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

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

Para 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/silenciar ainda são entregues por callback no Cast.Listener. esses listeners são registrados CastSession. Todas as notificações de estado restantes do dispositivo são entregues por CastStateListener callbacks esses listeners são registrados CastSession. Não se esqueça de cancelar o registro de listeners quando o evento fragmentos, atividades ou apps ficam em segundo plano.

Lógica de reconexão

O CAF tenta restabelecer conexões de rede perdidas devido à à perda temporária de sinal Wi-Fi ou a outros erros de rede. Isso é feito na nível da sessão uma sessão pode entrar em um estado "suspenso" quando a conexão está perdidos e serão transferidos de volta para uma rede quando a conectividade está restauradas. O framework cuida da reconexão ao aplicativo receptor e reconectar canais de transmissão como parte desse processo.

O CAF fornece o próprio serviço de reconexão, para que você possa a 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 o 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 CAF é ativado por padrão, mas pode ser desativado usando o CastOptions:

Além disso, o CAF também adiciona retomada automática de sessão, que é ativada pelo padrão (e pode ser desativado por CastOptions). Se o aplicativo remetente for enviada para o segundo plano ou for encerrada (deslizando a tela ou devido a uma falha) enquanto uma sessão de transmissão estiver em andamento, o framework tentará retomar essa quando o aplicativo remetente volta para o primeiro plano ou é reiniciado. Isso é tratado automaticamente pelo SessionManager, que emite o evento os callbacks adequados em todas as instâncias de SessionManagerListener registradas.

Registro de canal personalizado

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

  • CastConfiguration permite especificar vários namespaces e o CCL e depois criaremos o canal para você.
  • DataCastManager é semelhante ao VideoCastManager, mas é focado em mídias que não são de mídia em diferentes casos de uso de negócios.

Nenhuma dessas formas de criar um canal personalizado é compatível com o CAF. Você terá que seguir o procedimento Adicionar um critério personalizado para o app remetente.

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

Controle de mídia

No CAF, a classe RemoteMediaClient é equivalente à VideoCastManager métodos de mídia. O RemoteMediaClient.Listener é equivalente a VideoCastConsumer. Especificamente, o onRemoteMediaPlayerMetadataUpdated e onRemoteMediaPlayerStatusUpdated VideoCastConsumer é mapeado para onMetadataUpdated e Métodos 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 RemoteMediaClient object; 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 aplicativo receptor conectado oferece suporte ao namespace de mídia.

O RemoteMediaClient pode ser acessado como o método getRemoteMediaClient de 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 com o RemoteMediaClient, que permite que vários componentes de remetentes compartilhem a única instância do RemoteMediaClient associado à sessão.

O VideoCastManager da CCL oferece 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 uma RemoteMediaClient.MediaChannelResult usando um callback PendingResult que pode ser usado para acompanhar o progresso e o resultado da solicitação.

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

Para carregar mídia na CCL, o VideoCastManager é usado:

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

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

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

Para acessar as informações e o status do 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 receber as mesmas informações:

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

Sobreposição introdutória

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

Em vez de usar o método VideoCastConsumer onCastAvailabilityChanged da CCL para saber quando mostrar a sobreposição, declare uma CastStateListener para determinar quando o botão Transmitir fica visível depois que os dispositivos de transmissão são 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);
    ...
}

Monitore 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;
}

Confira se o MediaRouteButton está visível para que a sobreposição introdutória podem ser mostradas:

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 nossa app de exemplo para ver o código completo de trabalho para mostrar a sobreposição introdutória.

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

Minicontrole

Em vez do MiniController da CCL, use o MiniControllerFragment do CAF na sua arquivo de layout do app das atividades em que você quer mostrar a miniatura 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 oferece suporte à configuração manual compatível com o MiniController da CCL. e também não oferece suporte ao recurso Autoplay.

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

Notificação e tela de bloqueio

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

Remova do manifesto o seguinte:

  • VideoIntentReceiver
  • VideoCastNotificationService

A CCL oferece suporte ao fornecimento de um serviço de notificação personalizado com CastConfiguration.Builder que não é compatível com o CAF.

Considere a seguinte inicialização de CastManager usando a 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 ajudá-lo a criar controles de mídia para o notificação e tela de bloqueio no app remetente. A notificação e o bloqueio Os controles de tela 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, note que os botões "Reproduzir/pausar" e "Parar transmissão" são fornecidos por padrão. CAF vai rastrear automaticamente a visibilidade das atividades para decidir quando exibir a notificação de mídia, exceto para Gingerbread. Para o Gingerbread, consulte a nota anterior em usando registerLifecycleCallbacksBeforeIceCreamSandwich(); CCLs Chamadas VideoCastManager incrementUiCounter e decrementUiCounter deve ser removido.

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

Controle expandido

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

É possível remover a declaração VideoCastControllerActivity no manifesto.

No CAF, você precisa estender a ExpandControllerActivity e adicionar o botão Transmitir.

Para personalizar os estilos e botões que são exibidos na de controle, siga o procedimento Personalizar o controlador expandido.

Seleção de áudio

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

Controle do volume

Para o Gingerbread, é necessário usar dispatchKeyEvent, assim como o CCL. No ICS e acima para o controle de volume CCL e CAF é tratado automaticamente.

O CAF permite controlar o volume da transmissão usando o botão de volume do hardware no smartphone dentro das atividades dos apps e também mostra uma barra de volume visual quando a transmissão em versões com suporte. O CAF também processa a mudança de volume por meio do volume rígido mesmo se o app não estiver na frente, estiver bloqueado ou mesmo se a tela estiver

Legendas ocultas

No Android KitKat e superior, as legendas podem ser personalizadas por meio de legendas Configurações, em Configurações > Acessibilidade. Em versões anteriores do Android, mas não têm esse recurso. O CCL lida com isso fornecendo configurações de versões anteriores e delegar às configurações do sistema no KitKat e superiores.

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

O TracksChooserDialog do CCL não é mais necessário porque a mudança do estado as faixas de legendas são gerenciadas pela interface expandida do controle.

A API de closed caption no CAF é semelhante à v2.

Registros de depuração

O CAF não fornece configurações de registro de depuração.

Diversos

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

  • Conseguir autorização antes da reprodução com um MediaAuthService.
  • Mensagens de interface configuráveis

Apps de exemplo

Veja a diferença da migração do 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 (links em inglês) que usam o CAF.