Перенос приложения CCL Sender на платформу Cast Application Framework (CAF)

Следующая процедура позволяет преобразовать приложение Android-отправителя из Cast SDK v2 с CCL в CAF. Все функции CCL реализованы в CAF, поэтому после миграции вам больше не понадобится использовать CCL.

SDK Cast CAF Sender использует CastContext для управления GoogleAPIClient от вашего имени. CastContext управляет жизненными циклами, ошибками и обратными вызовами, что значительно упрощает разработку приложения Cast.

Введение

  • Поскольку на дизайн CAF Sender повлияла Cast Companion Library, переход от CCL к CAF Sender включает в себя в основном однозначное сопоставление классов и их методов.
  • CAF Sender по-прежнему распространяется как часть сервисов Google Play с помощью менеджера Android SDK.
  • Новые пакеты ( com.google.android.gms.cast.framework.* ), добавленные в CAF Sender, с функциональностью, аналогичной CCL, берут на себя ответственность за соответствие контрольному списку Google Cast Design .
  • CAF Sender предоставляет виджеты, соответствующие требованиям Cast UX; эти виджеты аналогичны виджетам, предоставляемым CCL.
  • CAF Sender предоставляет асинхронные обратные вызовы, аналогичные CCL, для отслеживания состояний и получения данных. В отличие от CCL, CAF Sender не предоставляет никаких неоперативных реализаций различных методов интерфейса.

В следующих разделах мы в основном сосредоточимся на видеоприложениях, основанных на VideoCastManager от CCL, но во многих случаях те же концепции применимы и к DataCastManager.

Зависимости

CCL и CAF имеют одинаковые зависимости от библиотеки поддержки AppCompat, библиотеки поддержки MediaRouter v7 и сервисов Google Play. Однако разница в том, что CAF зависит от новой платформы Cast, доступной в сервисах Google Play 9.2.0 или более поздней версии.

В файле build.gradle удалите зависимости от com.google.android.gms:play-services-cast и com.google.android.libraries.cast.companionlibrary:ccl , затем добавьте новую платформу 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'
}

Вы также можете удалить метаданные сервиса Google Play:

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

Любые службы, действия и ресурсы, являющиеся частью CAF, автоматически объединяются с манифестом и ресурсами вашего приложения.

Минимальная версия Android SDK, которую поддерживает CAF, — 9 (Gingerbread); Минимальная версия Android SDK CCL — 10.

CCL предоставляет удобный метод BaseCastManager.checkGooglePlayServices(activity) для проверки доступности совместимой версии сервисов Google Play на устройстве. CAF не предоставляет это как часть Cast SDK. Выполните процедуру «Убедитесь, что на устройствах установлен APK-файл служб Google Play», чтобы убедиться, что на устройстве пользователя установлен правильный APK-файл служб Google Play, поскольку обновления могут не доходить до всех пользователей сразу.

Вам по-прежнему необходимо использовать вариант Theme.AppCompat для темы приложения.

Инициализация

Для CCL необходимо было вызвать VideoCastManager.initialize() в методе onCreate() экземпляра Applications. Эту логику следует удалить из кода класса приложения.

В CAF для платформы Cast также требуется явный этап инициализации. Это включает в себя инициализацию синглтона CastContext с использованием соответствующего OptionsProvider для указания идентификатора приложения-получателя и любых других глобальных параметров. CastContext играет аналогичную роль VideoCastManager CCL, предоставляя синглтон, с которым взаимодействуют клиенты. OptionsProvider аналогичен CastConfiguration CCL и позволяет вам настраивать функции платформы Cast.

Если ваш текущий CCL CastConfiguration.Builder выглядит так:

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

Тогда в CAF следующий CastOptionsProvider использующий CastOptions.Builder , будет аналогичен:

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

Взгляните на наш пример приложения , чтобы увидеть полную реализацию OptionsProvider.

Объявите OptionsProvider в элементе application файла 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>

Лениво инициализируйте CastContext в каждом методе onCreate Activity (а не в экземпляре Application ):

private CastContext mCastContext;

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

    mCastContext = CastContext.getSharedInstance(this);
}

Для доступа к синглтону CastContext используйте:

mCastContext = CastContext.getSharedInstance(this);

Обнаружение устройств

CCL VideoCastManager и incrementUiCounter decrementUiCounter быть удалены из методов onResume и onPause ваших Activities .

В CAF процесс обнаружения запускается и останавливается платформой автоматически, когда приложение переходит на передний план и переходит в фоновый режим соответственно.

Кнопка трансляции и диалоговое окно трансляции

Как и в случае с CCL, эти компоненты предоставляются библиотекой поддержки MediaRouter v7.

Кнопка Cast по-прежнему реализуется с помощью MediaRouteButton и может быть добавлена ​​в вашу активность (с помощью ActionBar или Toolbar ) в качестве пункта меню в вашем меню.

Объявление MediaRouteActionProvider в меню xml такое же, как и в 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"/>

Подобно CCL, переопределите метод onCreateOptionMenu() каждого действия, но вместо использования CastManager.addMediaRouterButton используйте CastButtonFactory CAF для подключения MediaRouteButton к платформе 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;
}

Управление устройством

Как и в CCL, в CAF управление устройствами в основном осуществляется платформой. Приложению-отправителю не нужно обрабатывать (и не должно пытаться это делать) подключение к устройству и запуск приложения-получателя с помощью GoogleApiClient .

Взаимодействие между отправителем и получателем теперь представлено как «сессия». Класс SessionManager управляет жизненным циклом сеанса и автоматически запускает и останавливает сеансы в ответ на жесты пользователя: сеанс запускается, когда пользователь выбирает устройство трансляции в диалоговом окне трансляции, и завершается, когда пользователь нажимает кнопку «Остановить трансляцию» в диалоговом окне трансляции. диалоговое окно или когда само приложение-отправитель завершает работу.

В CCL вам необходимо расширить класс VideoCastConsumerImpl , чтобы отслеживать состояние сеанса трансляции:

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

В CAF приложение-отправитель может быть уведомлено о событиях жизненного цикла сеанса, зарегистрировав SessionManagerListener с помощью SessionManager . Обратные вызовы SessionManagerListener определяют методы обратного вызова для всех событий жизненного цикла сеанса.

Следующие методы SessionManagerListener отображаются из интерфейса VideoCastConsumer CCL:

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

Объявите класс, реализующий интерфейс SessionManagerListener , и переместите логику VideoCastConsumerImpl в соответствующие методы:

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) {}
  ...
}

Класс CastSession представляет сеанс с устройством Cast. В классе есть методы для управления громкостью устройства и состояниями отключения звука, что CCL делает в BaseCastManager .

Вместо использования CCL VideoCastManager для добавления потребителя:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Теперь зарегистрируйте свой SessionManagerListener :

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

Чтобы прекратить прослушивание событий в CCL:

VideoCastManager.getInstance().removeVideoCastConsumer(mCastConsumer);

Теперь используйте SessionManager , чтобы прекратить прослушивание событий сеанса:

mCastSessionManager.removeSessionManagerListener(mCastSessionManagerListener,
                    CastSession.class);

Чтобы явно отключиться от устройства Cast, CCL использовал:

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

Для CAF используйте SessionManager :

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

Чтобы определить, подключен ли отправитель к получателю, CCL предоставляет VideoCastManager.getInstance().isConnected() , но в CAF используйте SessionManager :

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

В CAF уведомления об изменении состояния громкости/отключения звука по-прежнему доставляются через методы обратного вызова в Cast.Listener ; эти прослушиватели зарегистрированы в CastSession . Все остальные уведомления о состоянии устройства доставляются через обратные вызовы CastStateListener ; эти прослушиватели зарегистрированы в CastSession . Убедитесь, что вы по-прежнему отменяете регистрацию прослушивателей, когда связанные фрагменты, действия или приложения переходят в фоновый режим.

Логика переподключения

CAF пытается восстановить сетевые соединения, потерянные из-за временной потери сигнала Wi-Fi или других сетевых ошибок. Теперь это делается на уровне сеанса; сеанс может перейти в «приостановленное» состояние при потере соединения и вернуться в «подключенное» состояние при восстановлении соединения. Платформа заботится о повторном подключении к приложению-приемнику и повторном подключении всех каналов Cast в рамках этого процесса.

CAF предоставляет собственную службу переподключения, поэтому вы можете удалить CCL ReconnectionService из своего манифеста:

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

Вам также не нужны следующие разрешения в вашем манифесте для логики повторного подключения:

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

Служба переподключения CAF включена по умолчанию, но ее можно отключить с помощью CastOptions .

Кроме того, CAF также добавляет автоматическое возобновление сеанса, которое включено по умолчанию (и может быть отключено с помощью CastOptions ). Если приложение-отправитель отправляется в фоновый режим или завершается (путем смахивания или из-за сбоя) во время сеанса трансляции, платформа попытается возобновить этот сеанс, когда приложение-отправитель вернется на передний план или перезапустится; это автоматически обрабатывается SessionManager , который выдает соответствующие обратные вызовы для всех зарегистрированных экземпляров SessionManagerListener .

Регистрация пользовательского канала

CCL предоставляет два способа создания пользовательского канала сообщений для получателя:

  • CastConfiguration позволяет вам указать несколько пространств имен, и CCL затем создаст для вас канал.
  • DataCastManager похож на VideoCastManager, но ориентирован на случаи использования, не связанные с мультимедиа.

Ни один из этих способов создания пользовательского канала не поддерживается CAF — вместо этого вам необходимо выполнить процедуру «Добавление пользовательского канала для вашего приложения-отправителя».

Как и в случае с CCL, для мультимедийных приложений нет необходимости явно регистрировать канал управления мультимедиа.

Контроль СМИ

В CAF класс RemoteMediaClient эквивалентен медиа-методам VideoCastManager . RemoteMediaClient.Listener эквивалентен методам VideoCastConsumer . В частности, методы onRemoteMediaPlayerMetadataUpdated и onRemoteMediaPlayerStatusUpdated объекта VideoCastConsumer сопоставляются с методами onMetadataUpdated и onStatusUpdated объекта RemoteMediaClient.Listener соответственно:

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() {
    }
}

Нет необходимости явно инициализировать или регистрировать объект RemoteMediaClient ; платформа автоматически создаст экземпляр объекта и зарегистрирует базовый медиаканал во время начала сеанса, если приложение-получатель, к которому подключается соединение, поддерживает пространство имен мультимедиа.

Доступ RemoteMediaClient можно получить как метод getRemoteMediaClient объекта CastSession .

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

Вместо CCL:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Теперь используйте CAF:

mRemoteMediaClient.addListener(mRemoteMediaClientListener);

В RemoteMediaClient можно зарегистрировать любое количество прослушивателей, что позволяет нескольким компонентам-отправителям совместно использовать один экземпляр RemoteMediaClient , связанный с сеансом.

VideoCastManager CCL предоставляет методы для управления воспроизведением мультимедиа:

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

Теперь они реализованы RemoteMediaClient в CAF:

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

В CAF все медиа-запросы, отправленные на RemoteMediaClient возвращают RemoteMediaClient.MediaChannelResult через обратный вызов PendingResult , который можно использовать для отслеживания хода выполнения и конечного результата запроса.

И CCL, и CAF используют классы MediaInfo и MediaMetadata для представления элементов мультимедиа и загрузки мультимедиа.

Для загрузки медиа в CCL используется VideoCastManager :

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

В CAF RemoteMediaClient используется для загрузки носителя:

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

Чтобы получить информацию Media и статус текущего сеанса мультимедиа на приемнике, CCL использует VideoCastManager :

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

В CAF используйте RemoteMediaClient для получения той же информации:

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

Вводное наложение

Подобно CCL, CAF предоставляет настраиваемое представление IntroductoryOverlay для выделения кнопки трансляции, когда она впервые отображается пользователям.

Вместо использования метода CCL VideoCastConsumer onCastAvailabilityChanged чтобы знать, когда отображать наложение, объявите CastStateListener , чтобы определить, когда кнопка Cast станет видимой после того, как MediaRouter обнаружит устройства Cast в локальной сети:

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);
    ...
}

Отслеживайте экземпляр 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;
}

Проверьте, отображается ли MediaRouteButton , чтобы можно было отобразить вводное наложение:

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();
            }
        });
    }
}

Взгляните на наш пример приложения , чтобы увидеть полный рабочий код для отображения вводного наложения.

Чтобы настроить стиль вводного наложения, выполните процедуру «Настройка вводного наложения» .

Мини-контроллер

Вместо MiniController CCL используйте MiniControllerFragment CAF в файле макета приложения для действий, в которых вы хотите отобразить мини-контроллер:

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

CAF не поддерживает ручную настройку, поддерживаемую MiniController CCL, а также не поддерживает функцию Autoplay .

Чтобы настроить стиль и кнопки мини-контроллера, выполните процедуру «Настройка мини-контроллера» .

Уведомления и экран блокировки

Подобно VideoCastNotificationService CCL, CAF предоставляет MediaNotificationService для управления отображением мультимедийных уведомлений при трансляции.

Вам необходимо удалить из манифеста следующее:

  • VideoIntentReceiver
  • VideoCastNotificationService

CCL поддерживает предоставление настраиваемой службы уведомлений с помощью CastConfiguration.Builder ; это не поддерживается CAF.

Рассмотрим следующую инициализацию CastManager с использованием 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());

Для эквивалентной конфигурации в CAF SDK предоставляет NotificationsOptions.Builder , который поможет вам создать элементы управления мультимедиа для уведомлений и экрана блокировки в приложении отправителя. Элементы управления уведомлениями и экраном блокировки можно включить с помощью CastOptions при инициализации 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();
}

Уведомления и элементы управления экраном блокировки всегда включены в CAF. Также обратите внимание, что кнопки воспроизведения/паузы и остановки трансляции предусмотрены по умолчанию. CAF будет автоматически отслеживать видимость действий, чтобы решить, когда отображать мультимедийное уведомление, за исключением Gingerbread. (Для Gingerbread см. предыдущее примечание об использовании registerLifecycleCallbacksBeforeIceCreamSandwich() ; вызовы CCL VideoCastManager incrementUiCounter и decrementUiCounter должны быть удалены.)

Чтобы настроить кнопки, отображаемые в уведомлениях, выполните процедуру «Добавление элементов управления мультимедиа на экран уведомлений и блокировки» .

Расширенный контроллер

CCL предоставляет VideoCastControllerActivity и VideoCastControllerFragment для отображения расширенного контроллера при трансляции мультимедиа.

Вы можете удалить объявление VideoCastControllerActivity в манифесте.

В CAF вам придется расширить ExpandedControllerActivity и добавить кнопку Cast .

Чтобы настроить стили и кнопки, отображаемые в расширенном контроллере, выполните процедуру «Настройка расширенного контроллера» .

Аудио фокус

Как и в случае с CCL, фокус звука управляется автоматически.

Регулятор громкости

Для Gingerbread требуется dispatchKeyEvent , как и для CCL. В ICS и выше для CCL и CAF регулировка громкости осуществляется автоматически.

CAF позволяет управлять громкостью трансляции с помощью жесткой кнопки громкости на телефоне в действиях ваших приложений, а также отображает визуальную панель громкости при трансляции в поддерживаемых версиях. CAF также обрабатывает изменение громкости с помощью жесткого тома, даже если ваше приложение не на переднем плане, заблокировано или даже если экран выключен.

Скрытые субтитры

В Android KitKat и более поздних версиях подписи можно настроить в настройках подписей, которые находятся в разделе «Настройки» > «Доступность». Однако более ранние версии Android не имеют такой возможности. CCL решает эту проблему, предоставляя пользовательские настройки для более ранних версий и делегируя их системным настройкам в KitKat и выше.

CAF не предоставляет пользовательских настроек для изменения предпочтений подписей. Вам следует удалить ссылки CaptionsPreferenceActivity в своем манифесте и XML-файле настроек.

TracksChooserDialog CCL больше не нужен, поскольку изменение дорожек субтитров обрабатывается расширенным пользовательским интерфейсом контроллера.

API субтитров в CAF аналогичен версии 2.

Ведение журнала отладки

CAF не предоставляет настройки ведения журнала отладки.

Разное

Следующие функции CCL не поддерживаются в CAF:

  • Получение авторизации перед воспроизведением путем предоставления MediaAuthService
  • Настраиваемые сообщения пользовательского интерфейса

Примеры приложений

Взгляните на разницу при переносе нашего примера приложения Universal Music Player для Android (uamp) из CCL в CAF.

У нас также есть учебные пособия по кодированию и примеры приложений , использующих CAF.