summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2022-08-15 22:05:00 -0700
committerXin Li <delphij@google.com>2022-08-15 22:05:00 -0700
commitaad5eea70d817e1009648319a0716c0ff9829eda (patch)
tree9e78ee42e94bafd710a347d6ec32c0b943ff6569
parent26479648b9342845863628bb86afb2c7edef31ad (diff)
parenta2ab801ab3555676a32485cc58e07eb420fd6f25 (diff)
downloadMedia-aad5eea70d817e1009648319a0716c0ff9829eda.tar.gz
DO NOT MERGE - Merge Android 13
Bug: 242648940 Merged-In: I83952c087db24a978729f4cff5d2f2cc203ec38f Change-Id: I696b32e73e0a18a74cdd1c905e8ca409e9390b55
-rw-r--r--apex/aidl/private/android/media/IMediaCommunicationService.aidl3
-rw-r--r--apex/framework/Android.bp57
-rw-r--r--apex/framework/api/module-lib-current.txt1
-rw-r--r--apex/framework/java/android/media/BaseMediaParceledListSlice.java8
-rw-r--r--apex/framework/java/android/media/MediaCommunicationManager.java69
-rw-r--r--apex/framework/java/android/media/MediaSession2.java7
-rw-r--r--apex/framework/java/android/media/MediaSession2Service.java20
-rw-r--r--apex/framework/java/android/media/MediaTranscodingManager.java8
-rw-r--r--apex/framework/java/android/media/Session2Command.java5
-rw-r--r--apex/framework/java/android/media/Session2CommandGroup.java2
-rw-r--r--apex/framework/lint-baseline.xml324
-rw-r--r--apex/service/Android.bp7
-rw-r--r--apex/service/jarjar_rules.txt1
-rw-r--r--apex/service/java/com/android/server/media/MediaCommunicationService.java169
-rw-r--r--apex/service/java/com/android/server/media/SessionPriorityList.java199
-rw-r--r--apex/service/lint-baseline.xml4
16 files changed, 487 insertions, 397 deletions
diff --git a/apex/aidl/private/android/media/IMediaCommunicationService.aidl b/apex/aidl/private/android/media/IMediaCommunicationService.aidl
index fb3172b..e1c89e9 100644
--- a/apex/aidl/private/android/media/IMediaCommunicationService.aidl
+++ b/apex/aidl/private/android/media/IMediaCommunicationService.aidl
@@ -18,6 +18,7 @@ package android.media;
import android.media.Session2Token;
import android.media.IMediaCommunicationServiceCallback;
import android.media.MediaParceledListSlice;
+import android.view.KeyEvent;
/** {@hide} */
interface IMediaCommunicationService {
@@ -25,6 +26,8 @@ interface IMediaCommunicationService {
boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid);
MediaParceledListSlice getSession2Tokens(int userId);
+ void dispatchMediaKeyEvent(String packageName, in KeyEvent keyEvent, boolean asSystemService);
+
void registerCallback(IMediaCommunicationServiceCallback callback, String packageName);
void unregisterCallback(IMediaCommunicationServiceCallback callback);
}
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index a5e6df1..7bba456 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -17,17 +17,20 @@ package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// The dex jar produced by this is used in the APEX but otherwise this is not
+// used.
java_library {
name: "updatable-media",
- srcs: [
- ":updatable-media-srcs",
+ static_libs: [
+ "framework-media.impl",
],
permitted_packages: [
"android.media",
],
+ // Optimize the dex jar for the APEX.
optimize: {
enabled: true,
shrink: true,
@@ -37,19 +40,6 @@ java_library {
installable: true,
sdk_version: "module_current",
- libs: [
- "androidx.annotation_annotation",
- "framework-annotations-lib",
- ],
- static_libs: [
- "exoplayer2-extractor",
- "mediatranscoding_aidl_interface-java",
- "modules-annotation-minsdk",
- "modules-utils-build",
- ],
- jarjar_rules: "jarjar_rules.txt",
-
- plugins: ["java_api_finder"],
hostdex: true, // for hiddenapi check
apex_available: [
@@ -58,10 +48,9 @@ java_library {
],
min_sdk_version: "29",
visibility: [
+ // This MUST not be used for compilation within the media module, use
+ // framework-media.impl instead.
"//frameworks/av/apex:__subpackages__",
- "//frameworks/base/apex/media/service",
- "//frameworks/base/api", // For framework-all
- "//packages/modules/Media/apex/service",
],
}
@@ -126,14 +115,38 @@ java_sdk_library {
name: "framework-media",
defaults: ["framework-module-defaults"],
- // This is only used to define the APIs for updatable-media.
- api_only: true,
-
srcs: [
":updatable-media-srcs",
],
- impl_library_visibility: ["//frameworks/av/apex:__subpackages__"],
+ impl_library_visibility: [
+ "//frameworks/base/api", // For framework-all
+ "//packages/modules/Media:__subpackages__",
+ ],
+
+ sdk_version: "module_current",
+ impl_only_libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ ],
+ impl_only_static_libs: [
+ "exoplayer-media_apex",
+ "mediatranscoding_aidl_interface-java",
+ "modules-annotation-minsdk",
+ "modules-utils-build",
+ ],
+ jarjar_rules: "jarjar_rules.txt",
+
+ plugins: ["java_api_finder"],
+
+ apex_available: [
+ "com.android.media",
+ "test_com.android.media",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ },
+ min_sdk_version: "29",
}
cc_library_shared {
diff --git a/apex/framework/api/module-lib-current.txt b/apex/framework/api/module-lib-current.txt
index eb6397a..7317f14 100644
--- a/apex/framework/api/module-lib-current.txt
+++ b/apex/framework/api/module-lib-current.txt
@@ -2,6 +2,7 @@
package android.media {
public class MediaCommunicationManager {
+ method public void dispatchMediaKeyEvent(@NonNull android.view.KeyEvent, boolean);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaCommunicationManager.SessionCallback);
method public void unregisterSessionCallback(@NonNull android.media.MediaCommunicationManager.SessionCallback);
}
diff --git a/apex/framework/java/android/media/BaseMediaParceledListSlice.java b/apex/framework/java/android/media/BaseMediaParceledListSlice.java
index fb66609..11db293 100644
--- a/apex/framework/java/android/media/BaseMediaParceledListSlice.java
+++ b/apex/framework/java/android/media/BaseMediaParceledListSlice.java
@@ -43,7 +43,7 @@ import java.util.List;
*/
abstract class BaseMediaParceledListSlice<T> implements Parcelable {
private static String TAG = "BaseMediaParceledListSlice";
- private static boolean DEBUG = false;
+ private static final boolean DEBUG = false;
/*
* TODO get this number from somewhere else. For now set it to a quarter of
@@ -107,7 +107,11 @@ abstract class BaseMediaParceledListSlice<T> implements Parcelable {
}
while (i < N && reply.readInt() != 0) {
final T parcelable = readCreator(creator, reply, loader);
- verifySameType(listElementClass, parcelable.getClass());
+ if (listElementClass == null) {
+ listElementClass = parcelable.getClass();
+ } else {
+ verifySameType(listElementClass, parcelable.getClass());
+ }
mList.add(parcelable);
diff --git a/apex/framework/java/android/media/MediaCommunicationManager.java b/apex/framework/java/android/media/MediaCommunicationManager.java
index 40992f1..d6442d4 100644
--- a/apex/framework/java/android/media/MediaCommunicationManager.java
+++ b/apex/framework/java/android/media/MediaCommunicationManager.java
@@ -32,6 +32,9 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.service.media.MediaBrowserService;
import android.util.Log;
+import android.view.KeyEvent;
+
+import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresApi;
@@ -66,7 +69,8 @@ public class MediaCommunicationManager {
private static final int CURRENT_VERSION = VERSION_1;
private final Context mContext;
- private final IMediaCommunicationService mService;
+ // Do not access directly use getService().
+ private IMediaCommunicationService mService;
private final Object mLock = new Object();
private final CopyOnWriteArrayList<SessionCallbackRecord> mTokenCallbackRecords =
@@ -75,6 +79,9 @@ public class MediaCommunicationManager {
@GuardedBy("mLock")
private MediaCommunicationServiceCallbackStub mCallbackStub;
+ // TODO: remove this when MCS implements dispatchMediaKeyEvent.
+ private MediaSessionManager mMediaSessionManager;
+
/**
* @hide
*/
@@ -83,10 +90,6 @@ public class MediaCommunicationManager {
throw new UnsupportedOperationException("Android version must be S or greater.");
}
mContext = context;
- mService = IMediaCommunicationService.Stub.asInterface(
- MediaFrameworkInitializer.getMediaServiceManager()
- .getMediaCommunicationServiceRegisterer()
- .get());
}
/**
@@ -108,7 +111,7 @@ public class MediaCommunicationManager {
throw new IllegalArgumentException("token's type should be TYPE_SESSION");
}
try {
- mService.notifySession2Created(token);
+ getService().notifySession2Created(token);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
@@ -133,7 +136,7 @@ public class MediaCommunicationManager {
return false;
}
try {
- return mService.isTrusted(
+ return getService().isTrusted(
userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
} catch (RemoteException e) {
Log.w(TAG, "Cannot communicate with the service.", e);
@@ -185,7 +188,7 @@ public class MediaCommunicationManager {
MediaCommunicationServiceCallbackStub callbackStub =
new MediaCommunicationServiceCallbackStub();
try {
- mService.registerCallback(callbackStub, mContext.getPackageName());
+ getService().registerCallback(callbackStub, mContext.getPackageName());
mCallbackStub = callbackStub;
} catch (RemoteException ex) {
Log.e(TAG, "Failed to register callback.", ex);
@@ -208,7 +211,7 @@ public class MediaCommunicationManager {
synchronized (mLock) {
if (mCallbackStub != null && mTokenCallbackRecords.isEmpty()) {
try {
- mService.unregisterCallback(mCallbackStub);
+ getService().unregisterCallback(mCallbackStub);
} catch (RemoteException ex) {
Log.e(TAG, "Failed to unregister callback.", ex);
}
@@ -217,9 +220,27 @@ public class MediaCommunicationManager {
}
}
+ private IMediaCommunicationService getService() {
+ if (mService == null) {
+ mService = IMediaCommunicationService.Stub.asInterface(
+ MediaFrameworkInitializer.getMediaServiceManager()
+ .getMediaCommunicationServiceRegisterer()
+ .get());
+ }
+ return mService;
+ }
+
+ // TODO: remove this when MCS implements dispatchMediaKeyEvent.
+ private MediaSessionManager getMediaSessionManager() {
+ if (mMediaSessionManager == null) {
+ mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
+ }
+ return mMediaSessionManager;
+ }
+
private List<Session2Token> getSession2Tokens(int userId) {
try {
- MediaParceledListSlice slice = mService.getSession2Tokens(userId);
+ MediaParceledListSlice slice = getService().getSession2Tokens(userId);
return slice == null ? Collections.emptyList() : slice.getList();
} catch (RemoteException e) {
Log.e(TAG, "Failed to get session tokens", e);
@@ -228,6 +249,34 @@ public class MediaCommunicationManager {
}
/**
+ * Sends a media key event. The receiver will be selected automatically.
+ *
+ * @param keyEvent the key event to send, non-media key events will be ignored.
+ * @param asSystemService if {@code true}, the event is sent to the session as if it was come
+ * from the system service instead of the app process. It only affects
+ * {@link MediaSession.Callback#getCurrentControllerInfo()}.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean asSystemService) {
+ Objects.requireNonNull(keyEvent, "keyEvent shouldn't be null");
+
+ // When MCS handles this, caller is changed.
+ // TODO: remove this when MCS implementation is done.
+ if (!asSystemService) {
+ getMediaSessionManager().dispatchMediaKeyEvent(keyEvent, false);
+ return;
+ }
+
+ try {
+ getService().dispatchMediaKeyEvent(mContext.getPackageName(),
+ keyEvent, asSystemService);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send key event.", e);
+ }
+ }
+
+ /**
* Callback for listening to changes to the sessions.
* @see #registerSessionCallback(Executor, SessionCallback)
* @hide
diff --git a/apex/framework/java/android/media/MediaSession2.java b/apex/framework/java/android/media/MediaSession2.java
index e76d61c..7d07eb3 100644
--- a/apex/framework/java/android/media/MediaSession2.java
+++ b/apex/framework/java/android/media/MediaSession2.java
@@ -262,7 +262,7 @@ public class MediaSession2 implements AutoCloseable {
}
/**
- * Returns whehther the playback is active (i.e. playing something)
+ * Returns whether the playback is active (i.e. playing something)
*
* @return {@code true} if the playback active, {@code false} otherwise.
*/
@@ -302,8 +302,9 @@ public class MediaSession2 implements AutoCloseable {
parcel.setDataPosition(0);
Bundle out = parcel.readBundle(null);
- // Calling Bundle#size() will trigger Bundle#unparcel().
- out.size();
+ for (String key : out.keySet()) {
+ out.get(key);
+ }
} catch (BadParcelableException e) {
Log.d(TAG, "Custom parcelable in bundle.", e);
return true;
diff --git a/apex/framework/java/android/media/MediaSession2Service.java b/apex/framework/java/android/media/MediaSession2Service.java
index f6fd509..9f80c43 100644
--- a/apex/framework/java/android/media/MediaSession2Service.java
+++ b/apex/framework/java/android/media/MediaSession2Service.java
@@ -161,19 +161,19 @@ public abstract class MediaSession2Service extends Service {
public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo);
/**
- * Called when notification UI needs update. Override this method to show or cancel your own
- * notification UI.
+ * Called to update the media notification when the playback state changes.
* <p>
- * This would be called on {@link MediaSession2}'s callback executor when playback state is
- * changed.
+ * If playback is active and a notification is returned, the service uses it to become a
+ * foreground service. If playback is not active then the notification is still posted, but the
+ * service does not become a foreground service.
* <p>
- * With the notification returned here, the service becomes foreground service when the playback
- * is started. Apps must request the permission
- * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes
- * background service after the playback is stopped.
+ * Apps must request the {@link android.Manifest.permission#FOREGROUND_SERVICE} permission
+ * in order to use this API. For apps targeting {@link android.os.Build.VERSION_CODES#TIRAMISU}
+ * or later, notifications will only be posted if the app has also been granted the
+ * {@link android.Manifest.permission#POST_NOTIFICATIONS} permission.
*
- * @param session a session that needs notification update.
- * @return a {@link MediaNotification}. Can be {@code null}.
+ * @param session the session for which an updated media notification is required.
+ * @return the {@link MediaNotification}. Can be {@code null}.
*/
@Nullable
public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session);
diff --git a/apex/framework/java/android/media/MediaTranscodingManager.java b/apex/framework/java/android/media/MediaTranscodingManager.java
index ca35c01..aff3204 100644
--- a/apex/framework/java/android/media/MediaTranscodingManager.java
+++ b/apex/framework/java/android/media/MediaTranscodingManager.java
@@ -22,7 +22,6 @@ import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
-import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
@@ -124,7 +123,6 @@ public final class MediaTranscodingManager {
private final String mPackageName;
private final int mPid;
private final int mUid;
- private final boolean mIsLowRamDevice;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final HashMap<Integer, TranscodingSession> mPendingTranscodingSessions = new HashMap();
private final Object mLock = new Object();
@@ -211,10 +209,7 @@ public final class MediaTranscodingManager {
if (!SdkLevel.isAtLeastS()) {
return null;
}
- // Do not try to get the service on AndroidGo (low-ram) devices.
- if (mIsLowRamDevice) {
- return null;
- }
+
int retryCount = !retry ? 1 : CONNECT_SERVICE_RETRY_COUNT;
Log.i(TAG, "get service with retry " + retryCount);
for (int count = 1; count <= retryCount; count++) {
@@ -432,7 +427,6 @@ public final class MediaTranscodingManager {
mPackageName = mContext.getPackageName();
mUid = Os.getuid();
mPid = Os.getpid();
- mIsLowRamDevice = mContext.getSystemService(ActivityManager.class).isLowRamDevice();
}
/**
diff --git a/apex/framework/java/android/media/Session2Command.java b/apex/framework/java/android/media/Session2Command.java
index 26f4568..7e71591 100644
--- a/apex/framework/java/android/media/Session2Command.java
+++ b/apex/framework/java/android/media/Session2Command.java
@@ -37,9 +37,8 @@ import java.util.Objects;
* If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
* {@link #getCustomAction()} shouldn't be {@code null}.
* <p>
- * Refer to the
- * <a href="{@docRoot}reference/androidx/media2/SessionCommand2.html">AndroidX SessionCommand</a>
- * class for the list of valid commands.
+ * Refer to the <a href="{@docRoot}reference/androidx/media2/session/SessionCommand.html">
+ * AndroidX SessionCommand</a> class for the list of valid commands.
*/
public final class Session2Command implements Parcelable {
/**
diff --git a/apex/framework/java/android/media/Session2CommandGroup.java b/apex/framework/java/android/media/Session2CommandGroup.java
index 13aabfc..af8184a 100644
--- a/apex/framework/java/android/media/Session2CommandGroup.java
+++ b/apex/framework/java/android/media/Session2CommandGroup.java
@@ -68,7 +68,7 @@ public final class Session2CommandGroup implements Parcelable {
/**
* Used by parcelable creator.
*/
- @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @SuppressWarnings({"WeakerAccess", "UnsafeParcelApi"}) /* synthetic access */
Session2CommandGroup(Parcel in) {
Parcelable[] commands = in.readParcelableArray(Session2Command.class.getClassLoader());
if (commands != null) {
diff --git a/apex/framework/lint-baseline.xml b/apex/framework/lint-baseline.xml
index e1b1450..95eea45 100644
--- a/apex/framework/lint-baseline.xml
+++ b/apex/framework/lint-baseline.xml
@@ -1,312 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+<issues format="6" by="lint 7.2.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.2.0-dev">
<issue
- id="NewApi"
- message="Call requires API level 31 (current min is 29): `new android.media.ApplicationMediaCapabilities.Builder`"
- errorLine1=" new ApplicationMediaCapabilities.Builder();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ id="DefaultLocale"
+ message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
+ errorLine1=" if (mSupportedVideoMimeTypes.contains(videoMime.toLowerCase())) {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java"
- line="208"
- column="29"/>
+ line="121"
+ column="57"/>
</issue>
<issue
- id="NewApi"
- message="Call requires API level 31 (current min is 29): `new android.media.ApplicationMediaCapabilities.Builder`"
- errorLine1=" ApplicationMediaCapabilities.Builder builder = new ApplicationMediaCapabilities.Builder();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ id="DefaultLocale"
+ message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
+ errorLine1=" return String.format(&quot; session: {id: %d, status: %s, result: %s, progress: %d}&quot;,"
+ errorLine2=" ^">
<location
- file="frameworks/base/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java"
- line="314"
- column="56"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level R (current min is 29): `android.os.RemoteException#rethrowFromSystemServer`"
- errorLine1=" e.rethrowFromSystemServer();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaCommunicationManager.java"
- line="110"
- column="15"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level R (current min is 29): `android.os.Parcel#writeParcelableCreator`"
- errorLine1=" dest.writeParcelableCreator((Parcelable) parcelable);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParceledListSlice.java"
- line="77"
- column="14"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level R (current min is 29): `android.os.Parcel#readParcelableCreator`"
- errorLine1=" return from.readParcelableCreator(loader);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParceledListSlice.java"
- line="82"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.TrackData#mediaFormat`"
- errorLine1=" this.mediaFormat = mediaFormat;"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="273"
- column="13"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.TrackData#drmInitData`"
- errorLine1=" this.drmInitData = drmInitData;"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="274"
- column="13"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" this.timeMicros = timeMicros;"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="295"
- column="13"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" this.position = position;"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="296"
- column="13"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" return &quot;[timeMicros=&quot; + timeMicros + &quot;, position=&quot; + position + &quot;]&quot;;"
- errorLine2=" ~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="302"
- column="66"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" return &quot;[timeMicros=&quot; + timeMicros + &quot;, position=&quot; + position + &quot;]&quot;;"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="302"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level R (current min is 29): `android.media.MediaParser.SeekPoint`"
- errorLine1=" SeekPoint other = (SeekPoint) obj;"
- errorLine2=" ~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="313"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
- errorLine2=" ~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="314"
- column="54"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="314"
- column="66"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="314"
+ file="frameworks/base/apex/media/framework/java/android/media/MediaTranscodingManager.java"
+ line="1651"
column="20"/>
</issue>
<issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="314"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" int result = (int) timeMicros;"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="319"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" result = 31 * result + (int) position;"
- errorLine2=" ~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="320"
- column="42"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level R (current min is 29): `android.media.MediaParser.InputReader`"
- errorLine1=" public interface SeekableInputReader extends InputReader {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="352"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level 31 (current min is 29): `android.media.metrics.LogSessionId#LOG_SESSION_ID_NONE`"
- errorLine1=" @NonNull private LogSessionId mLogSessionId = LogSessionId.LOG_SESSION_ID_NONE;"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1071"
- column="51"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Cast from `SeekableInputReader` to `InputReader` requires API level 30 (current min is 29)"
- errorLine1=" mExoDataReader.mInputReader = seekableInputReader;"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1201"
- column="39"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" mPendingSeekPosition = seekPoint.position;"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1287"
- column="36"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" mPendingSeekTimeMicros = seekPoint.timeMicros;"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1288"
- column="38"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
- errorLine1=" mExtractor.seek(seekPoint.position, seekPoint.timeMicros);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1290"
- column="29"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
- errorLine1=" mExtractor.seek(seekPoint.position, seekPoint.timeMicros);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ id="ParcelClassLoader"
+ message="Passing null here (to use the default class loader) will not work if you are restoring your own classes. Consider using for example `getClass().getClassLoader()` instead."
+ errorLine1=" Bundle out = parcel.readBundle(null);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1290"
- column="49"/>
+ file="frameworks/base/apex/media/framework/java/android/media/MediaSession2.java"
+ line="303"
+ column="33"/>
</issue>
<issue
- id="NewApi"
- message="Field requires API level R (current min is 29): `android.media.DrmInitData.SchemeInitData#uuid`"
- errorLine1=" if (schemeInitData.uuid.equals(schemeUuid)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ id="ParcelClassLoader"
+ message="Using the default class loader will not work if you are restoring your own classes. Consider using for example `readBundle(getClass().getClassLoader())` instead."
+ errorLine1=" mCustomExtras = in.readBundle();"
+ errorLine2=" ~~~~~~~~~~~~">
<location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1579"
- column="21"/>
+ file="frameworks/base/apex/media/framework/java/android/media/Session2Command.java"
+ line="104"
+ column="28"/>
</issue>
<issue
- id="NewApi"
- message="Class requires API level R (current min is 29): `android.media.MediaParser.InputReader`"
- errorLine1=" private static final class DataReaderAdapter implements InputReader {"
- errorLine2=" ~~~~~~~~~~~">
+ id="ParcelClassLoader"
+ message="Passing null here (to use the default class loader) will not work if you are restoring your own classes. Consider using for example `getClass().getClassLoader()` instead."
+ errorLine1=" mSessionLink = in.readParcelable(null);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1872"
- column="61"/>
+ file="frameworks/base/apex/media/framework/java/android/media/Session2Token.java"
+ line="141"
+ column="27"/>
</issue>
<issue
- id="NewApi"
- message="Class requires API level R (current min is 29): `android.media.MediaParser.InputReader`"
- errorLine1=" private static final class ParsableByteArrayAdapter implements InputReader {"
- errorLine2=" ~~~~~~~~~~~">
+ id="ParcelClassLoader"
+ message="Using the default class loader will not work if you are restoring your own classes. Consider using for example `readBundle(getClass().getClassLoader())` instead."
+ errorLine1=" Bundle extras = in.readBundle();"
+ errorLine2=" ~~~~~~~~~~~~">
<location
- file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
- line="1905"
- column="68"/>
+ file="frameworks/base/apex/media/framework/java/android/media/Session2Token.java"
+ line="144"
+ column="28"/>
</issue>
</issues>
diff --git a/apex/service/Android.bp b/apex/service/Android.bp
index 60ff769..5f5eb54 100644
--- a/apex/service/Android.bp
+++ b/apex/service/Android.bp
@@ -36,11 +36,16 @@ java_sdk_library {
],
libs: [
"androidx.annotation_annotation",
- "updatable-media",
+ "framework-media.impl",
"modules-annotation-minsdk",
+ "modules-utils-build",
],
+ jarjar_rules: "jarjar_rules.txt",
sdk_version: "system_server_current",
min_sdk_version: "29", // TODO: We may need to bump this at some point.
+ lint: {
+ strict_updatability_linting: true,
+ },
apex_available: [
"com.android.media",
],
diff --git a/apex/service/jarjar_rules.txt b/apex/service/jarjar_rules.txt
new file mode 100644
index 0000000..7e37c2b
--- /dev/null
+++ b/apex/service/jarjar_rules.txt
@@ -0,0 +1 @@
+rule com.android.modules.** android.media.internal.@1
diff --git a/apex/service/java/com/android/server/media/MediaCommunicationService.java b/apex/service/java/com/android/server/media/MediaCommunicationService.java
index 0dc3e79..4223fa6 100644
--- a/apex/service/java/com/android/server/media/MediaCommunicationService.java
+++ b/apex/service/java/com/android/server/media/MediaCommunicationService.java
@@ -31,6 +31,7 @@ import android.media.MediaController2;
import android.media.MediaParceledListSlice;
import android.media.Session2CommandGroup;
import android.media.Session2Token;
+import android.media.session.MediaSessionManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
@@ -43,6 +44,7 @@ import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.view.KeyEvent;
import androidx.annotation.RequiresApi;
@@ -56,7 +58,6 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
/**
* A system service that manages {@link android.media.MediaSession2} creations
@@ -66,7 +67,7 @@ import java.util.stream.Collectors;
@MinSdk(Build.VERSION_CODES.S)
@RequiresApi(Build.VERSION_CODES.S)
public class MediaCommunicationService extends SystemService {
- private static final String TAG = "MediaCommunicationService";
+ private static final String TAG = "MediaCommunicationSrv";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
final Context mContext;
@@ -81,8 +82,9 @@ public class MediaCommunicationService extends SystemService {
final Executor mRecordExecutor = Executors.newSingleThreadExecutor();
@GuardedBy("mLock")
- final List<CallbackRecord> mCallbackRecords = new ArrayList<>();
+ final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<>();
final NotificationManager mNotificationManager;
+ MediaSessionManager mSessionManager;
public MediaCommunicationService(Context context) {
super(context);
@@ -97,6 +99,17 @@ public class MediaCommunicationService extends SystemService {
}
@Override
+ public void onBootPhase(int phase) {
+ super.onBootPhase(phase);
+ switch (phase) {
+ // This ensures MediaSessionService is started
+ case PHASE_BOOT_COMPLETED:
+ mSessionManager = mContext.getSystemService(MediaSessionManager.class);
+ break;
+ }
+ }
+
+ @Override
public void onUserStarting(@NonNull TargetUser user) {
if (DEBUG) Log.d(TAG, "onUserStarting: " + user);
updateUser();
@@ -140,8 +153,8 @@ public class MediaCommunicationService extends SystemService {
return null;
}
- List<Session2Token> getSession2TokensLocked(int userId) {
- List<Session2Token> list = new ArrayList<>();
+ ArrayList<Session2Token> getSession2TokensLocked(int userId) {
+ ArrayList<Session2Token> list = new ArrayList<>();
if (userId == ALL.getIdentifier()) {
int size = mUserRecords.size();
for (int i = 0; i < size; i++) {
@@ -227,28 +240,29 @@ public class MediaCommunicationService extends SystemService {
}
void dispatchSession2Changed(int userId) {
- MediaParceledListSlice<Session2Token> allSession2Tokens;
- MediaParceledListSlice<Session2Token> userSession2Tokens;
+ ArrayList<Session2Token> allSession2Tokens;
+ ArrayList<Session2Token> userSession2Tokens;
synchronized (mLock) {
- allSession2Tokens =
- new MediaParceledListSlice<>(getSession2TokensLocked(ALL.getIdentifier()));
- userSession2Tokens = new MediaParceledListSlice<>(getSession2TokensLocked(userId));
- }
- allSession2Tokens.setInlineCountLimit(1);
- userSession2Tokens.setInlineCountLimit(1);
+ allSession2Tokens = getSession2TokensLocked(ALL.getIdentifier());
+ userSession2Tokens = getSession2TokensLocked(userId);
- synchronized (mLock) {
for (CallbackRecord record : mCallbackRecords) {
if (record.mUserId == ALL.getIdentifier()) {
try {
- record.mCallback.onSession2Changed(allSession2Tokens);
+ MediaParceledListSlice<Session2Token> toSend =
+ new MediaParceledListSlice<>(allSession2Tokens);
+ toSend.setInlineCountLimit(0);
+ record.mCallback.onSession2Changed(toSend);
} catch (RemoteException e) {
Log.w(TAG, "Failed to notify session2 tokens changed " + record);
}
} else if (record.mUserId == userId) {
try {
- record.mCallback.onSession2Changed(userSession2Tokens);
+ MediaParceledListSlice<Session2Token> toSend =
+ new MediaParceledListSlice<>(userSession2Tokens);
+ toSend.setInlineCountLimit(0);
+ record.mCallback.onSession2Changed(toSend);
} catch (RemoteException e) {
Log.w(TAG, "Failed to notify session2 tokens changed " + record);
}
@@ -273,6 +287,34 @@ public class MediaCommunicationService extends SystemService {
session.close();
}
+ void onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority) {
+ FullUserRecord user = session.getFullUser();
+ if (user == null || !user.containsSession(session)) {
+ Log.d(TAG, "Unknown session changed playback state. Ignoring.");
+ return;
+ }
+ user.onPlaybackStateChanged(session, promotePriority);
+ }
+
+
+ static boolean isMediaSessionKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ return true;
+ }
+ return false;
+ }
+
private class Stub extends IMediaCommunicationService.Stub {
@Override
public void notifySession2Created(Session2Token sessionToken) {
@@ -344,7 +386,7 @@ public class MediaCommunicationService extends SystemService {
try {
// Check that they can make calls on behalf of the user and get the final user id
int resolvedUserId = handleIncomingUser(pid, uid, userId, null);
- List<Session2Token> result;
+ ArrayList<Session2Token> result;
synchronized (mLock) {
result = getSession2TokensLocked(resolvedUserId);
}
@@ -357,6 +399,25 @@ public class MediaCommunicationService extends SystemService {
}
@Override
+ public void dispatchMediaKeyEvent(String packageName, KeyEvent keyEvent,
+ boolean asSystemService) {
+ if (keyEvent == null || !isMediaSessionKey(keyEvent.getKeyCode())) {
+ Log.w(TAG, "Attempted to dispatch null or non-media key event.");
+ return;
+ }
+
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ //TODO: Dispatch key event to media session 2 if required
+ mSessionManager.dispatchMediaKeyEvent(keyEvent, asSystemService);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
public void registerCallback(IMediaCommunicationServiceCallback callback,
String packageName) throws RemoteException {
Objects.requireNonNull(callback, "callback should not be null");
@@ -475,26 +536,20 @@ public class MediaCommunicationService extends SystemService {
final class FullUserRecord {
private final int mFullUserId;
- private final Object mUserLock = new Object();
- @GuardedBy("mUserLock")
- private final List<Session2Record> mSessionRecords = new ArrayList<>();
+ private final SessionPriorityList mSessionPriorityList = new SessionPriorityList();
FullUserRecord(int fullUserId) {
mFullUserId = fullUserId;
}
public void addSession(Session2Record record) {
- synchronized (mUserLock) {
- mSessionRecords.add(record);
- }
+ mSessionPriorityList.addSession(record);
mHandler.post(() -> dispatchSession2Created(record.mSessionToken));
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
}
private void removeSession(Session2Record record) {
- synchronized (mUserLock) {
- mSessionRecords.remove(record);
- }
+ mSessionPriorityList.removeSession(record);
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
//TODO: Handle if the removed session was the media button session.
}
@@ -504,48 +559,31 @@ public class MediaCommunicationService extends SystemService {
}
public List<Session2Token> getAllSession2Tokens() {
- synchronized (mUserLock) {
- return mSessionRecords.stream()
- .map(Session2Record::getSessionToken)
- .collect(Collectors.toList());
- }
+ return mSessionPriorityList.getAllTokens();
}
public List<Session2Token> getSession2Tokens(int userId) {
- synchronized (mUserLock) {
- return mSessionRecords.stream()
- .filter(record -> record.getUserId() == userId)
- .map(Session2Record::getSessionToken)
- .collect(Collectors.toList());
- }
+ return mSessionPriorityList.getTokensByUserId(userId);
}
public void destroyAllSessions() {
- synchronized (mUserLock) {
- for (Session2Record session : mSessionRecords) {
- session.close();
- }
- mSessionRecords.clear();
- }
+ mSessionPriorityList.destroyAllSessions();
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
}
public void destroySessionsForUser(int userId) {
- boolean changed = false;
- synchronized (mUserLock) {
- for (int i = mSessionRecords.size() - 1; i >= 0; i--) {
- Session2Record session = mSessionRecords.get(i);
- if (session.getUserId() == userId) {
- mSessionRecords.remove(i);
- session.close();
- changed = true;
- }
- }
- }
- if (changed) {
+ if (mSessionPriorityList.destroySessionsByUserId(userId)) {
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
}
}
+
+ public boolean containsSession(Session2Record session) {
+ return mSessionPriorityList.contains(session);
+ }
+
+ public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) {
+ mSessionPriorityList.onPlaybackStateChanged(session, promotePriority);
+ }
}
static final class Session2Record {
@@ -561,6 +599,7 @@ public class MediaCommunicationService extends SystemService {
@GuardedBy("mSession2RecordLock")
private boolean mIsClosed;
+ //TODO: introduce policy (See MediaSessionPolicyProvider)
Session2Record(MediaCommunicationService service, FullUserRecord fullUser,
Session2Token token, Executor controllerExecutor) {
mServiceRef = new WeakReference<>(service);
@@ -596,6 +635,12 @@ public class MediaCommunicationService extends SystemService {
return mSessionToken;
}
+ public boolean checkPlaybackActiveState(boolean expected) {
+ synchronized (mSession2RecordLock) {
+ return mIsConnected && mController.isPlaybackActive() == expected;
+ }
+ }
+
private class Controller2Callback extends MediaController2.ControllerCallback {
@Override
public void onConnected(MediaController2 controller,
@@ -621,6 +666,20 @@ public class MediaCommunicationService extends SystemService {
service.onSessionDied(Session2Record.this);
}
}
+
+ @Override
+ public void onPlaybackActiveChanged(
+ @NonNull MediaController2 controller,
+ boolean playbackActive) {
+ if (DEBUG) {
+ Log.d(TAG, "playback active changed, " + mSessionToken + ", active="
+ + playbackActive);
+ }
+ MediaCommunicationService service = mServiceRef.get();
+ if (service != null) {
+ service.onSessionPlaybackStateChanged(Session2Record.this, playbackActive);
+ }
+ }
}
}
}
diff --git a/apex/service/java/com/android/server/media/SessionPriorityList.java b/apex/service/java/com/android/server/media/SessionPriorityList.java
new file mode 100644
index 0000000..8145861
--- /dev/null
+++ b/apex/service/java/com/android/server/media/SessionPriorityList.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2021 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 com.android.server.media;
+
+import android.annotation.Nullable;
+import android.media.Session2Token;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.annotation.MinSdk;
+import com.android.server.media.MediaCommunicationService.Session2Record;
+
+import java.util.ArrayList;
+import java.util.List;
+
+//TODO: Define the priority specifically.
+/**
+ * Keeps track of media sessions and their priority for notifications, media
+ * button dispatch, etc.
+ * Higher priority session has more chance to be selected as media button session,
+ * which receives the media button events.
+ */
+@MinSdk(Build.VERSION_CODES.S)
+@RequiresApi(Build.VERSION_CODES.S)
+class SessionPriorityList {
+ private static final String TAG = "SessionPriorityList";
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final List<Session2Record> mSessions = new ArrayList<>();
+
+ @Nullable
+ private Session2Record mMediaButtonSession;
+ @Nullable
+ private Session2Record mCachedVolumeSession;
+
+ //TODO: integrate AudioPlayerStateMonitor
+
+ public void addSession(Session2Record record) {
+ synchronized (mLock) {
+ mSessions.add(record);
+ }
+ }
+
+ public void removeSession(Session2Record record) {
+ synchronized (mLock) {
+ mSessions.remove(record);
+ }
+ if (record == mMediaButtonSession) {
+ updateMediaButtonSession(null);
+ }
+ }
+
+ public void destroyAllSessions() {
+ synchronized (mLock) {
+ for (Session2Record session : mSessions) {
+ session.close();
+ }
+ mSessions.clear();
+ }
+ }
+
+ public boolean destroySessionsByUserId(int userId) {
+ boolean changed = false;
+ synchronized (mLock) {
+ for (int i = mSessions.size() - 1; i >= 0; i--) {
+ Session2Record session = mSessions.get(i);
+ if (session.getUserId() == userId) {
+ mSessions.remove(i);
+ session.close();
+ changed = true;
+ }
+ }
+ }
+ return changed;
+ }
+
+ public List<Session2Token> getAllTokens() {
+ List<Session2Token> sessions = new ArrayList<>();
+ synchronized (mLock) {
+ for (Session2Record session : mSessions) {
+ sessions.add(session.getSessionToken());
+ }
+ }
+ return sessions;
+ }
+
+ public List<Session2Token> getTokensByUserId(int userId) {
+ List<Session2Token> sessions = new ArrayList<>();
+ synchronized (mLock) {
+ for (Session2Record session : mSessions) {
+ if (session.getUserId() == userId) {
+ sessions.add(session.getSessionToken());
+ }
+ }
+ }
+ return sessions;
+ }
+
+ /** Gets the media button session which receives the media button events. */
+ @Nullable
+ public Session2Record getMediaButtonSession() {
+ return mMediaButtonSession;
+ }
+
+ /** Gets the media volume session which receives the volume key events. */
+ @Nullable
+ public Session2Record getMediaVolumeSession() {
+ //TODO: if null, calculate it.
+ return mCachedVolumeSession;
+ }
+
+ public boolean contains(Session2Record session) {
+ synchronized (mLock) {
+ return mSessions.contains(session);
+ }
+ }
+
+ public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) {
+ if (promotePriority) {
+ synchronized (mLock) {
+ if (mSessions.remove(session)) {
+ mSessions.add(0, session);
+ } else {
+ Log.w(TAG, "onPlaybackStateChanged: Ignoring unknown session");
+ return;
+ }
+ }
+ } else if (session.checkPlaybackActiveState(false)) {
+ // Just clear the cached volume session when a state goes inactive
+ mCachedVolumeSession = null;
+ }
+
+ // In most cases, playback state isn't needed for finding the media button session,
+ // but we only use it as a hint if an app has multiple local media sessions.
+ // In that case, we pick the media session whose PlaybackState matches
+ // the audio playback configuration.
+ if (mMediaButtonSession != null
+ && mMediaButtonSession.getSessionToken().getUid()
+ == session.getSessionToken().getUid()) {
+ Session2Record newMediaButtonSession =
+ findMediaButtonSession(mMediaButtonSession.getSessionToken().getUid());
+ if (newMediaButtonSession != mMediaButtonSession) {
+ // Check if the policy states that this session should not be updated as a media
+ // button session.
+ updateMediaButtonSession(newMediaButtonSession);
+ }
+ }
+ }
+
+ private void updateMediaButtonSession(@Nullable Session2Record newSession) {
+ mMediaButtonSession = newSession;
+ //TODO: invoke callbacks for media button session changed listeners
+ }
+
+ /**
+ * Finds the media button session with the given {@param uid}.
+ * If the app has multiple media sessions, the media session whose playback state is not null
+ * and matches the audio playback state becomes the media button session. Otherwise the top
+ * priority session becomes the media button session.
+ *
+ * @return The media button session. Returns {@code null} if the app doesn't have a media
+ * session.
+ */
+ @Nullable
+ private Session2Record findMediaButtonSession(int uid) {
+ Session2Record mediaButtonSession = null;
+ synchronized (mLock) {
+ for (Session2Record session : mSessions) {
+ if (uid != session.getSessionToken().getUid()) {
+ continue;
+ }
+ // TODO: check audio player state monitor
+ if (mediaButtonSession == null) {
+ // Pick the top priority session as a default.
+ mediaButtonSession = session;
+ }
+ }
+ }
+ return mediaButtonSession;
+ }
+}
diff --git a/apex/service/lint-baseline.xml b/apex/service/lint-baseline.xml
new file mode 100644
index 0000000..def6baf
--- /dev/null
+++ b/apex/service/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 7.2.0-dev" type="baseline" client="" dependencies="true" name="" variant="all" version="7.2.0-dev">
+
+</issues>