MediaControllerStub.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.session;

import static androidx.media3.common.util.Util.postOrRun;

import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.media3.common.Player.Commands;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import java.lang.ref.WeakReference;
import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull;

/* package */ class MediaControllerStub extends IMediaController.Stub {

  private static final String TAG = "MediaControllerStub";

  /** The version of the IMediaController interface. */
  public static final int VERSION_INT = 1;

  private final WeakReference<MediaControllerImplBase> controller;

  public MediaControllerStub(MediaControllerImplBase controller) {
    this.controller = new WeakReference<>(controller);
  }

  @Override
  public void onSessionResult(int sequenceNum, Bundle sessionResultBundle) {
    SessionResult result;
    try {
      result = SessionResult.CREATOR.fromBundle(sessionResultBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionResult", e);
      return;
    }
    // Don't post setting future result so the result can be obtained on the application looper.
    // For an example, {@code MediaController.setRating(rating).get()} wouldn't return if the
    // result is posted.
    setControllerFutureResult(sequenceNum, result);
  }

  @Override
  public void onLibraryResult(int sequenceNum, Bundle libraryResultBundle) {
    LibraryResult<?> result;
    try {
      result = LibraryResult.UNKNOWN_TYPE_CREATOR.fromBundle(libraryResultBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for LibraryResult", e);
      return;
    }
    // Don't post setting future result so the result can be obtained on the application looper.
    // For an example, {@code MediaBrowser.getLibraryRoot(params).get()} wouldn't return if the
    // result is posted.
    setControllerFutureResult(sequenceNum, result);
  }

  @Override
  public void onConnected(int seq, Bundle connectionResultBundle) {
    ConnectionState connectionState;
    try {
      connectionState = ConnectionState.CREATOR.fromBundle(connectionResultBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Malformed Bundle for ConnectionResult. Disconnected from the session.", e);
      onDisconnected(seq);
      return;
    }
    dispatchControllerTaskOnHandler(controller -> controller.onConnected(connectionState));
  }

  @Override
  public void onDisconnected(int seq) {
    dispatchControllerTaskOnHandler(
        controller ->
            controller.getInstance().runOnApplicationLooper(controller.getInstance()::release));
  }

  @Override
  public void onSetCustomLayout(int seq, List<Bundle> commandButtonBundleList) {
    List<CommandButton> layout;
    try {
      layout = BundleableUtil.fromBundleList(CommandButton.CREATOR, commandButtonBundleList);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for CommandButton", e);
      return;
    }
    dispatchControllerTaskOnHandler(controller -> controller.onSetCustomLayout(seq, layout));
  }

  @Override
  public void onAvailableCommandsChangedFromSession(
      int seq, Bundle sessionCommandsBundle, Bundle playerCommandsBundle) {
    SessionCommands sessionCommands;
    try {
      sessionCommands = SessionCommands.CREATOR.fromBundle(sessionCommandsBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionCommands", e);
      return;
    }
    Commands playerCommands;
    try {
      playerCommands = Commands.CREATOR.fromBundle(playerCommandsBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for Commands", e);
      return;
    }
    dispatchControllerTaskOnHandler(
        controller ->
            controller.onAvailableCommandsChangedFromSession(sessionCommands, playerCommands));
  }

  @Override
  public void onAvailableCommandsChangedFromPlayer(int seq, Bundle commandsBundle) {
    Commands commandsFromPlayer;
    try {
      commandsFromPlayer = Commands.CREATOR.fromBundle(commandsBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for Commands", e);
      return;
    }
    dispatchControllerTaskOnHandler(
        controller -> controller.onAvailableCommandsChangedFromPlayer(commandsFromPlayer));
  }

  @Override
  public void onCustomCommand(int seq, Bundle commandBundle, Bundle args) {
    if (args == null) {
      Log.w(TAG, "Ignoring custom command with null args.");
      return;
    }
    SessionCommand command;
    try {
      command = SessionCommand.CREATOR.fromBundle(commandBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionCommand", e);
      return;
    }
    dispatchControllerTaskOnHandler(controller -> controller.onCustomCommand(seq, command, args));
  }

  @Override
  public void onPeriodicSessionPositionInfoChanged(int seq, Bundle sessionPositionInfoBundle) {
    SessionPositionInfo sessionPositionInfo;
    try {
      sessionPositionInfo = SessionPositionInfo.CREATOR.fromBundle(sessionPositionInfoBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionPositionInfo", e);
      return;
    }
    dispatchControllerTaskOnHandler(
        controller -> controller.notifyPeriodicSessionPositionInfoChanged(sessionPositionInfo));
  }

  @Override
  public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTimelineExcluded) {
    PlayerInfo playerInfo;
    try {
      playerInfo = PlayerInfo.CREATOR.fromBundle(playerInfoBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for PlayerInfo", e);
      return;
    }
    dispatchControllerTaskOnHandler(
        controller -> controller.onPlayerInfoChanged(playerInfo, isTimelineExcluded));
  }

  @Override
  public void onExtrasChanged(int seq, Bundle extras) {
    dispatchControllerTaskOnHandler(controller -> controller.onExtrasChanged(extras));
  }

  @Override
  public void onRenderedFirstFrame(int seq) {
    dispatchControllerTaskOnHandler(MediaControllerImplBase::onRenderedFirstFrame);
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  // MediaBrowser specific
  ////////////////////////////////////////////////////////////////////////////////////////////
  @Override
  public void onSearchResultChanged(
      int seq, String query, int itemCount, @Nullable Bundle libraryParams)
      throws RuntimeException {
    if (TextUtils.isEmpty(query)) {
      Log.w(TAG, "onSearchResultChanged(): Ignoring empty query");
      return;
    }
    if (itemCount < 0) {
      Log.w(TAG, "onSearchResultChanged(): Ignoring negative itemCount: " + itemCount);
      return;
    }
    dispatchControllerTaskOnHandler(
        (ControllerTask<MediaBrowserImplBase>)
            browser ->
                browser.notifySearchResultChanged(
                    query,
                    itemCount,
                    libraryParams == null
                        ? null
                        : LibraryParams.CREATOR.fromBundle(libraryParams)));
  }

  @Override
  public void onChildrenChanged(
      int seq, String parentId, int itemCount, @Nullable Bundle libraryParams) {
    if (TextUtils.isEmpty(parentId)) {
      Log.w(TAG, "onChildrenChanged(): Ignoring empty parentId");
      return;
    }
    if (itemCount < 0) {
      Log.w(TAG, "onChildrenChanged(): Ignoring negative itemCount: " + itemCount);
      return;
    }
    dispatchControllerTaskOnHandler(
        (ControllerTask<MediaBrowserImplBase>)
            browser ->
                browser.notifyChildrenChanged(
                    parentId,
                    itemCount,
                    libraryParams == null
                        ? null
                        : LibraryParams.CREATOR.fromBundle(libraryParams)));
  }

  public void destroy() {
    controller.clear();
  }

  private <T extends @NonNull Object> void setControllerFutureResult(
      int sequenceNum, T futureResult) {
    long token = Binder.clearCallingIdentity();
    try {
      @Nullable MediaControllerImplBase controller = this.controller.get();
      if (controller == null) {
        return;
      }
      controller.setFutureResult(sequenceNum, futureResult);
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  private <T extends MediaControllerImplBase> void dispatchControllerTaskOnHandler(
      ControllerTask<T> task) {
    long token = Binder.clearCallingIdentity();
    try {
      @Nullable MediaControllerImplBase controller = this.controller.get();
      if (controller == null) {
        return;
      }
      Handler handler = controller.getInstance().applicationHandler;
      postOrRun(
          handler,
          () -> {
            if (controller.isReleased()) {
              return;
            }
            @SuppressWarnings("unchecked")
            T castedController = (T) controller;
            task.run(castedController);
          });
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  /* @FunctionalInterface */
  private interface ControllerTask<T extends MediaControllerImplBase> {

    void run(T controller);
  }
}