Merge "Remove apex/media - migrated to packages/modules/Media" am: 8aa8ce4f91 am: 15d4890f9c
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1987167 Change-Id: I55d97c0a74a528ee6950f68c7c04f0cee82461e0
This commit is contained in:
@@ -176,7 +176,6 @@ java_defaults {
|
||||
"sax/java",
|
||||
"telecomm/java",
|
||||
|
||||
"apex/media/aidl/stable",
|
||||
// TODO(b/147699819): remove this
|
||||
"telephony/java",
|
||||
],
|
||||
@@ -275,6 +274,7 @@ java_defaults {
|
||||
// TODO: remove when moved to the below package
|
||||
"frameworks/base/packages/ConnectivityT/framework-t/aidl-export",
|
||||
"packages/modules/Connectivity/framework/aidl-export",
|
||||
"packages/modules/Media/apex/aidl/stable",
|
||||
],
|
||||
},
|
||||
dxflags: [
|
||||
@@ -552,11 +552,9 @@ stubs_defaults {
|
||||
stubs_defaults {
|
||||
name: "module-classpath-stubs-defaults",
|
||||
aidl: {
|
||||
local_include_dirs: [
|
||||
"apex/media/aidl/stable",
|
||||
],
|
||||
include_dirs: [
|
||||
"packages/modules/Connectivity/framework/aidl-export",
|
||||
"packages/modules/Media/apex/aidl/stable",
|
||||
],
|
||||
},
|
||||
libs: [
|
||||
|
||||
@@ -140,11 +140,9 @@ droidstubs {
|
||||
"api-versions-jars-dir",
|
||||
],
|
||||
aidl: {
|
||||
local_include_dirs: [
|
||||
"apex/media/aidl/stable",
|
||||
],
|
||||
include_dirs: [
|
||||
"packages/modules/Connectivity/framework/aidl-export",
|
||||
"packages/modules/Media/apex/aidl/stable",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (C) 2020 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 {
|
||||
default_visibility: [
|
||||
":__subpackages__",
|
||||
"//frameworks/av/apex",
|
||||
"//frameworks/av/apex/testing",
|
||||
],
|
||||
// See: http://go/android-license-faq
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
sdk {
|
||||
name: "media-module-sdk",
|
||||
bootclasspath_fragments: ["com.android.media-bootclasspath-fragment"],
|
||||
systemserverclasspath_fragments: ["com.android.media-systemserverclasspath-fragment"],
|
||||
java_sdk_libs: [
|
||||
"framework-media",
|
||||
],
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# Bug component: 1344
|
||||
hdmoon@google.com
|
||||
jinpark@google.com
|
||||
klhyun@google.com
|
||||
lnilsson@google.com
|
||||
sungsoo@google.com
|
||||
|
||||
# go/android-fwk-media-solutions for info on areas of ownership.
|
||||
include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
|
||||
|
||||
# media reliability team packages/delivers the media mainline builds.
|
||||
include platform/frameworks/av:/media/janitors/reliability_mainline_OWNERS
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// Copyright 2020 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 {
|
||||
// See: http://go/android-license-faq
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "stable-media-aidl-srcs",
|
||||
srcs: ["stable/**/*.aidl"],
|
||||
path: "stable",
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "private-media-aidl-srcs",
|
||||
srcs: ["private/**/I*.aidl"],
|
||||
path: "private",
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "media-aidl-srcs",
|
||||
srcs: [
|
||||
":private-media-aidl-srcs",
|
||||
":stable-media-aidl-srcs",
|
||||
],
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
parcelable Controller2Link;
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright 2020 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 android.media;
|
||||
|
||||
import android.media.Session2Token;
|
||||
import android.media.IMediaCommunicationServiceCallback;
|
||||
import android.media.MediaParceledListSlice;
|
||||
|
||||
/** {@hide} */
|
||||
interface IMediaCommunicationService {
|
||||
void notifySession2Created(in Session2Token sessionToken);
|
||||
boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid);
|
||||
MediaParceledListSlice getSession2Tokens(int userId);
|
||||
|
||||
void registerCallback(IMediaCommunicationServiceCallback callback, String packageName);
|
||||
void unregisterCallback(IMediaCommunicationServiceCallback callback);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* 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 android.media;
|
||||
|
||||
import android.media.Session2Token;
|
||||
import android.media.MediaParceledListSlice;
|
||||
|
||||
/** {@hide} */
|
||||
oneway interface IMediaCommunicationServiceCallback {
|
||||
void onSession2Created(in Session2Token token);
|
||||
void onSession2Changed(in MediaParceledListSlice tokens);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.media.Session2Command;
|
||||
|
||||
/**
|
||||
* Interface from MediaSession2 to MediaController2.
|
||||
* <p>
|
||||
* Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
|
||||
* and holds calls from session to make session owner(s) frozen.
|
||||
* @hide
|
||||
*/
|
||||
// Code for AML only
|
||||
oneway interface IMediaController2 {
|
||||
void notifyConnected(int seq, in Bundle connectionResult) = 0;
|
||||
void notifyDisconnected(int seq) = 1;
|
||||
void notifyPlaybackActiveChanged(int seq, boolean playbackActive) = 2;
|
||||
void sendSessionCommand(int seq, in Session2Command command, in Bundle args,
|
||||
in ResultReceiver resultReceiver) = 3;
|
||||
void cancelSessionCommand(int seq) = 4;
|
||||
// Next Id : 5
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.media.Controller2Link;
|
||||
import android.media.Session2Command;
|
||||
|
||||
/**
|
||||
* Interface from MediaController2 to MediaSession2.
|
||||
* <p>
|
||||
* Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
|
||||
* and holds calls from session to make session owner(s) frozen.
|
||||
* @hide
|
||||
*/
|
||||
// Code for AML only
|
||||
oneway interface IMediaSession2 {
|
||||
void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
|
||||
void disconnect(in Controller2Link caller, int seq) = 1;
|
||||
void sendSessionCommand(in Controller2Link caller, int seq, in Session2Command sessionCommand,
|
||||
in Bundle args, in ResultReceiver resultReceiver) = 2;
|
||||
void cancelSessionCommand(in Controller2Link caller, int seq) = 3;
|
||||
// Next Id : 4
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 android.media;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.media.Controller2Link;
|
||||
|
||||
/**
|
||||
* Interface from MediaController2 to MediaSession2Service.
|
||||
* <p>
|
||||
* Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
|
||||
* and holds calls from controller to make controller owner(s) frozen.
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IMediaSession2Service {
|
||||
void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
|
||||
// Next Id : 1
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
parcelable Session2Command;
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020, 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 android.media;
|
||||
|
||||
parcelable MediaParceledListSlice<T>;
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 android.media;
|
||||
|
||||
parcelable Session2Token;
|
||||
@@ -1,161 +0,0 @@
|
||||
// Copyright (C) 2020 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 {
|
||||
// See: http://go/android-license-faq
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
java_library {
|
||||
name: "updatable-media",
|
||||
|
||||
srcs: [
|
||||
":updatable-media-srcs",
|
||||
],
|
||||
|
||||
permitted_packages: [
|
||||
"android.media",
|
||||
],
|
||||
|
||||
optimize: {
|
||||
enabled: true,
|
||||
shrink: true,
|
||||
proguard_flags_files: ["updatable-media-proguard.flags"],
|
||||
},
|
||||
|
||||
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: [
|
||||
"com.android.media",
|
||||
"test_com.android.media",
|
||||
],
|
||||
min_sdk_version: "29",
|
||||
visibility: [
|
||||
"//frameworks/av/apex:__subpackages__",
|
||||
"//frameworks/base/apex/media/service",
|
||||
"//frameworks/base/api", // For framework-all
|
||||
"//packages/modules/Media/apex/service",
|
||||
],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "updatable-media-srcs",
|
||||
srcs: [
|
||||
"java/android/media/MediaFrameworkInitializer.java",
|
||||
":media-aidl-srcs",
|
||||
":mediaparceledlistslice-java-srcs",
|
||||
":mediaparser-srcs",
|
||||
":mediasession2-java-srcs",
|
||||
":mediatranscoding-srcs",
|
||||
],
|
||||
visibility: ["//frameworks/base"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "mediasession2-java-srcs",
|
||||
srcs: [
|
||||
"java/android/media/Controller2Link.java",
|
||||
"java/android/media/MediaConstants.java",
|
||||
"java/android/media/MediaController2.java",
|
||||
"java/android/media/MediaSession2.java",
|
||||
"java/android/media/MediaSession2Service.java",
|
||||
"java/android/media/Session2Command.java",
|
||||
"java/android/media/Session2CommandGroup.java",
|
||||
"java/android/media/Session2Link.java",
|
||||
"java/android/media/Session2Token.java",
|
||||
"java/android/media/MediaCommunicationManager.java",
|
||||
],
|
||||
path: "java",
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "mediaparceledlistslice-java-srcs",
|
||||
srcs: [
|
||||
"java/android/media/MediaParceledListSlice.java",
|
||||
"java/android/media/BaseMediaParceledListSlice.java",
|
||||
],
|
||||
path: "java",
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "mediaparser-srcs",
|
||||
srcs: [
|
||||
"java/android/media/MediaParser.java",
|
||||
],
|
||||
path: "java",
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "mediatranscoding-srcs",
|
||||
srcs: [
|
||||
"java/android/media/ApplicationMediaCapabilities.java",
|
||||
"java/android/media/MediaFeature.java",
|
||||
"java/android/media/MediaTranscodingManager.java",
|
||||
],
|
||||
path: "java",
|
||||
}
|
||||
|
||||
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__"],
|
||||
}
|
||||
|
||||
cc_library_shared {
|
||||
name: "libmediaparser-jni",
|
||||
srcs: [
|
||||
"jni/android_media_MediaParserJNI.cpp",
|
||||
],
|
||||
header_libs: ["jni_headers"],
|
||||
shared_libs: [
|
||||
"libandroid",
|
||||
"liblog",
|
||||
"libmediametrics",
|
||||
],
|
||||
cflags: [
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
"-Wno-unused-parameter",
|
||||
"-Wunreachable-code",
|
||||
"-Wunused",
|
||||
],
|
||||
apex_available: [
|
||||
"com.android.media",
|
||||
],
|
||||
min_sdk_version: "29",
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "CtsMediaParserTestCases"
|
||||
},
|
||||
{
|
||||
"name": "CtsMediaParserHostTestCases"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
package android.media {
|
||||
|
||||
public final class ApplicationMediaCapabilities implements android.os.Parcelable {
|
||||
method @NonNull public static android.media.ApplicationMediaCapabilities createFromXml(@NonNull org.xmlpull.v1.XmlPullParser);
|
||||
method public int describeContents();
|
||||
method @NonNull public java.util.List<java.lang.String> getSupportedHdrTypes();
|
||||
method @NonNull public java.util.List<java.lang.String> getSupportedVideoMimeTypes();
|
||||
method @NonNull public java.util.List<java.lang.String> getUnsupportedHdrTypes();
|
||||
method @NonNull public java.util.List<java.lang.String> getUnsupportedVideoMimeTypes();
|
||||
method public boolean isFormatSpecified(@NonNull String);
|
||||
method public boolean isHdrTypeSupported(@NonNull String);
|
||||
method public boolean isVideoMimeTypeSupported(@NonNull String);
|
||||
method public void writeToParcel(@NonNull android.os.Parcel, int);
|
||||
field @NonNull public static final android.os.Parcelable.Creator<android.media.ApplicationMediaCapabilities> CREATOR;
|
||||
}
|
||||
|
||||
public static final class ApplicationMediaCapabilities.Builder {
|
||||
ctor public ApplicationMediaCapabilities.Builder();
|
||||
method @NonNull public android.media.ApplicationMediaCapabilities.Builder addSupportedHdrType(@NonNull String);
|
||||
method @NonNull public android.media.ApplicationMediaCapabilities.Builder addSupportedVideoMimeType(@NonNull String);
|
||||
method @NonNull public android.media.ApplicationMediaCapabilities.Builder addUnsupportedHdrType(@NonNull String);
|
||||
method @NonNull public android.media.ApplicationMediaCapabilities.Builder addUnsupportedVideoMimeType(@NonNull String);
|
||||
method @NonNull public android.media.ApplicationMediaCapabilities build();
|
||||
}
|
||||
|
||||
public class MediaCommunicationManager {
|
||||
method @NonNull public java.util.List<android.media.Session2Token> getSession2Tokens();
|
||||
method @IntRange(from=1) public int getVersion();
|
||||
}
|
||||
|
||||
public class MediaController2 implements java.lang.AutoCloseable {
|
||||
method public void cancelSessionCommand(@NonNull Object);
|
||||
method public void close();
|
||||
method @Nullable public android.media.Session2Token getConnectedToken();
|
||||
method public boolean isPlaybackActive();
|
||||
method @NonNull public Object sendSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle);
|
||||
}
|
||||
|
||||
public static final class MediaController2.Builder {
|
||||
ctor public MediaController2.Builder(@NonNull android.content.Context, @NonNull android.media.Session2Token);
|
||||
method @NonNull public android.media.MediaController2 build();
|
||||
method @NonNull public android.media.MediaController2.Builder setConnectionHints(@NonNull android.os.Bundle);
|
||||
method @NonNull public android.media.MediaController2.Builder setControllerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaController2.ControllerCallback);
|
||||
}
|
||||
|
||||
public abstract static class MediaController2.ControllerCallback {
|
||||
ctor public MediaController2.ControllerCallback();
|
||||
method public void onCommandResult(@NonNull android.media.MediaController2, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result);
|
||||
method public void onConnected(@NonNull android.media.MediaController2, @NonNull android.media.Session2CommandGroup);
|
||||
method public void onDisconnected(@NonNull android.media.MediaController2);
|
||||
method public void onPlaybackActiveChanged(@NonNull android.media.MediaController2, boolean);
|
||||
method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaController2, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
|
||||
}
|
||||
|
||||
public final class MediaFeature {
|
||||
ctor public MediaFeature();
|
||||
}
|
||||
|
||||
public static final class MediaFeature.HdrType {
|
||||
field public static final String DOLBY_VISION = "android.media.feature.hdr.dolby_vision";
|
||||
field public static final String HDR10 = "android.media.feature.hdr.hdr10";
|
||||
field public static final String HDR10_PLUS = "android.media.feature.hdr.hdr10_plus";
|
||||
field public static final String HLG = "android.media.feature.hdr.hlg";
|
||||
}
|
||||
|
||||
public final class MediaParser {
|
||||
method public boolean advance(@NonNull android.media.MediaParser.SeekableInputReader) throws java.io.IOException;
|
||||
method @NonNull public static android.media.MediaParser create(@NonNull android.media.MediaParser.OutputConsumer, @NonNull java.lang.String...);
|
||||
method @NonNull public static android.media.MediaParser createByName(@NonNull String, @NonNull android.media.MediaParser.OutputConsumer);
|
||||
method @NonNull public android.media.metrics.LogSessionId getLogSessionId();
|
||||
method @NonNull public String getParserName();
|
||||
method @NonNull public static java.util.List<java.lang.String> getParserNames(@NonNull android.media.MediaFormat);
|
||||
method public void release();
|
||||
method public void seek(@NonNull android.media.MediaParser.SeekPoint);
|
||||
method public void setLogSessionId(@NonNull android.media.metrics.LogSessionId);
|
||||
method @NonNull public android.media.MediaParser setParameter(@NonNull String, @NonNull Object);
|
||||
method public boolean supportsParameter(@NonNull String);
|
||||
field public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING = "android.media.mediaparser.adts.enableCbrSeeking";
|
||||
field public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING = "android.media.mediaparser.amr.enableCbrSeeking";
|
||||
field public static final String PARAMETER_FLAC_DISABLE_ID3 = "android.media.mediaparser.flac.disableId3";
|
||||
field public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING = "android.media.mediaparser.matroska.disableCuesSeeking";
|
||||
field public static final String PARAMETER_MP3_DISABLE_ID3 = "android.media.mediaparser.mp3.disableId3";
|
||||
field public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING = "android.media.mediaparser.mp3.enableCbrSeeking";
|
||||
field public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING = "android.media.mediaparser.mp3.enableIndexSeeking";
|
||||
field public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS = "android.media.mediaparser.mp4.ignoreEditLists";
|
||||
field public static final String PARAMETER_MP4_IGNORE_TFDT_BOX = "android.media.mediaparser.mp4.ignoreTfdtBox";
|
||||
field public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES = "android.media.mediaparser.mp4.treatVideoFramesAsKeyframes";
|
||||
field public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES = "android.media.mediaparser.ts.allowNonIdrAvcKeyframes";
|
||||
field public static final String PARAMETER_TS_DETECT_ACCESS_UNITS = "android.media.mediaparser.ts.ignoreDetectAccessUnits";
|
||||
field public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS = "android.media.mediaparser.ts.enableHdmvDtsAudioStreams";
|
||||
field public static final String PARAMETER_TS_IGNORE_AAC_STREAM = "android.media.mediaparser.ts.ignoreAacStream";
|
||||
field public static final String PARAMETER_TS_IGNORE_AVC_STREAM = "android.media.mediaparser.ts.ignoreAvcStream";
|
||||
field public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM = "android.media.mediaparser.ts.ignoreSpliceInfoStream";
|
||||
field public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode";
|
||||
field public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser";
|
||||
field public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser";
|
||||
field public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser";
|
||||
field public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser";
|
||||
field public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser";
|
||||
field public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser";
|
||||
field public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser";
|
||||
field public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser";
|
||||
field public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser";
|
||||
field public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser";
|
||||
field public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser";
|
||||
field public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser";
|
||||
field public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser";
|
||||
field public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN";
|
||||
field public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser";
|
||||
field public static final int SAMPLE_FLAG_DECODE_ONLY = -2147483648; // 0x80000000
|
||||
field public static final int SAMPLE_FLAG_ENCRYPTED = 1073741824; // 0x40000000
|
||||
field public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 268435456; // 0x10000000
|
||||
field public static final int SAMPLE_FLAG_KEY_FRAME = 1; // 0x1
|
||||
field public static final int SAMPLE_FLAG_LAST_SAMPLE = 536870912; // 0x20000000
|
||||
}
|
||||
|
||||
public static interface MediaParser.InputReader {
|
||||
method public long getLength();
|
||||
method public long getPosition();
|
||||
method public int read(@NonNull byte[], int, int) throws java.io.IOException;
|
||||
}
|
||||
|
||||
public static interface MediaParser.OutputConsumer {
|
||||
method public void onSampleCompleted(int, long, int, int, int, @Nullable android.media.MediaCodec.CryptoInfo);
|
||||
method public void onSampleDataFound(int, @NonNull android.media.MediaParser.InputReader) throws java.io.IOException;
|
||||
method public void onSeekMapFound(@NonNull android.media.MediaParser.SeekMap);
|
||||
method public void onTrackCountFound(int);
|
||||
method public void onTrackDataFound(int, @NonNull android.media.MediaParser.TrackData);
|
||||
}
|
||||
|
||||
public static final class MediaParser.ParsingException extends java.io.IOException {
|
||||
}
|
||||
|
||||
public static final class MediaParser.SeekMap {
|
||||
method public long getDurationMicros();
|
||||
method @NonNull public android.util.Pair<android.media.MediaParser.SeekPoint,android.media.MediaParser.SeekPoint> getSeekPoints(long);
|
||||
method public boolean isSeekable();
|
||||
field public static final int UNKNOWN_DURATION = -2147483648; // 0x80000000
|
||||
}
|
||||
|
||||
public static final class MediaParser.SeekPoint {
|
||||
field @NonNull public static final android.media.MediaParser.SeekPoint START;
|
||||
field public final long position;
|
||||
field public final long timeMicros;
|
||||
}
|
||||
|
||||
public static interface MediaParser.SeekableInputReader extends android.media.MediaParser.InputReader {
|
||||
method public void seekToPosition(long);
|
||||
}
|
||||
|
||||
public static final class MediaParser.TrackData {
|
||||
field @Nullable public final android.media.DrmInitData drmInitData;
|
||||
field @NonNull public final android.media.MediaFormat mediaFormat;
|
||||
}
|
||||
|
||||
public static final class MediaParser.UnrecognizedInputFormatException extends java.io.IOException {
|
||||
}
|
||||
|
||||
public class MediaSession2 implements java.lang.AutoCloseable {
|
||||
method public void broadcastSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle);
|
||||
method public void cancelSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object);
|
||||
method public void close();
|
||||
method @NonNull public java.util.List<android.media.MediaSession2.ControllerInfo> getConnectedControllers();
|
||||
method @NonNull public String getId();
|
||||
method @NonNull public android.media.Session2Token getToken();
|
||||
method public boolean isPlaybackActive();
|
||||
method @NonNull public Object sendSessionCommand(@NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
|
||||
method public void setPlaybackActive(boolean);
|
||||
}
|
||||
|
||||
public static final class MediaSession2.Builder {
|
||||
ctor public MediaSession2.Builder(@NonNull android.content.Context);
|
||||
method @NonNull public android.media.MediaSession2 build();
|
||||
method @NonNull public android.media.MediaSession2.Builder setExtras(@NonNull android.os.Bundle);
|
||||
method @NonNull public android.media.MediaSession2.Builder setId(@NonNull String);
|
||||
method @NonNull public android.media.MediaSession2.Builder setSessionActivity(@Nullable android.app.PendingIntent);
|
||||
method @NonNull public android.media.MediaSession2.Builder setSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaSession2.SessionCallback);
|
||||
}
|
||||
|
||||
public static final class MediaSession2.ControllerInfo {
|
||||
method @NonNull public android.os.Bundle getConnectionHints();
|
||||
method @NonNull public String getPackageName();
|
||||
method @NonNull public android.media.session.MediaSessionManager.RemoteUserInfo getRemoteUserInfo();
|
||||
method public int getUid();
|
||||
}
|
||||
|
||||
public abstract static class MediaSession2.SessionCallback {
|
||||
ctor public MediaSession2.SessionCallback();
|
||||
method public void onCommandResult(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result);
|
||||
method @Nullable public android.media.Session2CommandGroup onConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
|
||||
method public void onDisconnected(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
|
||||
method public void onPostConnect(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo);
|
||||
method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
|
||||
}
|
||||
|
||||
public abstract class MediaSession2Service extends android.app.Service {
|
||||
ctor public MediaSession2Service();
|
||||
method public final void addSession(@NonNull android.media.MediaSession2);
|
||||
method @NonNull public final java.util.List<android.media.MediaSession2> getSessions();
|
||||
method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
|
||||
method @Nullable public abstract android.media.MediaSession2 onGetSession(@NonNull android.media.MediaSession2.ControllerInfo);
|
||||
method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2);
|
||||
method public final void removeSession(@NonNull android.media.MediaSession2);
|
||||
field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
|
||||
}
|
||||
|
||||
public static class MediaSession2Service.MediaNotification {
|
||||
ctor public MediaSession2Service.MediaNotification(int, @NonNull android.app.Notification);
|
||||
method @NonNull public android.app.Notification getNotification();
|
||||
method public int getNotificationId();
|
||||
}
|
||||
|
||||
public final class Session2Command implements android.os.Parcelable {
|
||||
ctor public Session2Command(int);
|
||||
ctor public Session2Command(@NonNull String, @Nullable android.os.Bundle);
|
||||
method public int describeContents();
|
||||
method public int getCommandCode();
|
||||
method @Nullable public String getCustomAction();
|
||||
method @Nullable public android.os.Bundle getCustomExtras();
|
||||
method public void writeToParcel(@NonNull android.os.Parcel, int);
|
||||
field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0
|
||||
field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Command> CREATOR;
|
||||
}
|
||||
|
||||
public static final class Session2Command.Result {
|
||||
ctor public Session2Command.Result(int, @Nullable android.os.Bundle);
|
||||
method public int getResultCode();
|
||||
method @Nullable public android.os.Bundle getResultData();
|
||||
field public static final int RESULT_ERROR_UNKNOWN_ERROR = -1; // 0xffffffff
|
||||
field public static final int RESULT_INFO_SKIPPED = 1; // 0x1
|
||||
field public static final int RESULT_SUCCESS = 0; // 0x0
|
||||
}
|
||||
|
||||
public final class Session2CommandGroup implements android.os.Parcelable {
|
||||
method public int describeContents();
|
||||
method @NonNull public java.util.Set<android.media.Session2Command> getCommands();
|
||||
method public boolean hasCommand(@NonNull android.media.Session2Command);
|
||||
method public boolean hasCommand(int);
|
||||
method public void writeToParcel(@NonNull android.os.Parcel, int);
|
||||
field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2CommandGroup> CREATOR;
|
||||
}
|
||||
|
||||
public static final class Session2CommandGroup.Builder {
|
||||
ctor public Session2CommandGroup.Builder();
|
||||
ctor public Session2CommandGroup.Builder(@NonNull android.media.Session2CommandGroup);
|
||||
method @NonNull public android.media.Session2CommandGroup.Builder addCommand(@NonNull android.media.Session2Command);
|
||||
method @NonNull public android.media.Session2CommandGroup build();
|
||||
method @NonNull public android.media.Session2CommandGroup.Builder removeCommand(@NonNull android.media.Session2Command);
|
||||
}
|
||||
|
||||
public final class Session2Token implements android.os.Parcelable {
|
||||
ctor public Session2Token(@NonNull android.content.Context, @NonNull android.content.ComponentName);
|
||||
method public int describeContents();
|
||||
method @NonNull public android.os.Bundle getExtras();
|
||||
method @NonNull public String getPackageName();
|
||||
method @Nullable public String getServiceName();
|
||||
method public int getType();
|
||||
method public int getUid();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field @NonNull public static final android.os.Parcelable.Creator<android.media.Session2Token> CREATOR;
|
||||
field public static final int TYPE_SESSION = 0; // 0x0
|
||||
field public static final int TYPE_SESSION_SERVICE = 1; // 0x1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
package android.media {
|
||||
|
||||
public class MediaCommunicationManager {
|
||||
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);
|
||||
}
|
||||
|
||||
public static interface MediaCommunicationManager.SessionCallback {
|
||||
method public default void onSession2TokenCreated(@NonNull android.media.Session2Token);
|
||||
method public default void onSession2TokensChanged(@NonNull java.util.List<android.media.Session2Token>);
|
||||
}
|
||||
|
||||
public class MediaFrameworkInitializer {
|
||||
method public static void registerServiceWrappers();
|
||||
method public static void setMediaServiceManager(@NonNull android.media.MediaServiceManager);
|
||||
}
|
||||
|
||||
@Deprecated public final class MediaParceledListSlice<T extends android.os.Parcelable> implements android.os.Parcelable {
|
||||
ctor @Deprecated public MediaParceledListSlice(@NonNull java.util.List<T>);
|
||||
method @Deprecated public int describeContents();
|
||||
method @Deprecated @NonNull public static <T extends android.os.Parcelable> android.media.MediaParceledListSlice<T> emptyList();
|
||||
method @Deprecated public java.util.List<T> getList();
|
||||
method @Deprecated public void setInlineCountLimit(int);
|
||||
method @Deprecated public void writeToParcel(android.os.Parcel, int);
|
||||
field @Deprecated @NonNull public static final android.os.Parcelable.ClassLoaderCreator<android.media.MediaParceledListSlice> CREATOR;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1,68 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
package android.media {
|
||||
|
||||
public final class MediaTranscodingManager {
|
||||
method @Nullable public android.media.MediaTranscodingManager.TranscodingSession enqueueRequest(@NonNull android.media.MediaTranscodingManager.TranscodingRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaTranscodingManager.OnTranscodingFinishedListener);
|
||||
}
|
||||
|
||||
@java.lang.FunctionalInterface public static interface MediaTranscodingManager.OnTranscodingFinishedListener {
|
||||
method public void onTranscodingFinished(@NonNull android.media.MediaTranscodingManager.TranscodingSession);
|
||||
}
|
||||
|
||||
public abstract static class MediaTranscodingManager.TranscodingRequest {
|
||||
method public int getClientPid();
|
||||
method public int getClientUid();
|
||||
method @Nullable public android.os.ParcelFileDescriptor getDestinationFileDescriptor();
|
||||
method @NonNull public android.net.Uri getDestinationUri();
|
||||
method @Nullable public android.os.ParcelFileDescriptor getSourceFileDescriptor();
|
||||
method @NonNull public android.net.Uri getSourceUri();
|
||||
}
|
||||
|
||||
public static class MediaTranscodingManager.TranscodingRequest.VideoFormatResolver {
|
||||
ctor public MediaTranscodingManager.TranscodingRequest.VideoFormatResolver(@NonNull android.media.ApplicationMediaCapabilities, @NonNull android.media.MediaFormat);
|
||||
method @Nullable public android.media.MediaFormat resolveVideoFormat();
|
||||
method public boolean shouldTranscode();
|
||||
}
|
||||
|
||||
public static final class MediaTranscodingManager.TranscodingSession {
|
||||
method public boolean addClientUid(int);
|
||||
method public void cancel();
|
||||
method @NonNull public java.util.List<java.lang.Integer> getClientUids();
|
||||
method public int getErrorCode();
|
||||
method @IntRange(from=0, to=100) public int getProgress();
|
||||
method public int getResult();
|
||||
method public int getSessionId();
|
||||
method public int getStatus();
|
||||
method public void setOnProgressUpdateListener(@NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodingManager.TranscodingSession.OnProgressUpdateListener);
|
||||
field public static final int ERROR_DROPPED_BY_SERVICE = 1; // 0x1
|
||||
field public static final int ERROR_NONE = 0; // 0x0
|
||||
field public static final int ERROR_SERVICE_DIED = 2; // 0x2
|
||||
field public static final int RESULT_CANCELED = 4; // 0x4
|
||||
field public static final int RESULT_ERROR = 3; // 0x3
|
||||
field public static final int RESULT_NONE = 1; // 0x1
|
||||
field public static final int RESULT_SUCCESS = 2; // 0x2
|
||||
field public static final int STATUS_FINISHED = 3; // 0x3
|
||||
field public static final int STATUS_PAUSED = 4; // 0x4
|
||||
field public static final int STATUS_PENDING = 1; // 0x1
|
||||
field public static final int STATUS_RUNNING = 2; // 0x2
|
||||
}
|
||||
|
||||
@java.lang.FunctionalInterface public static interface MediaTranscodingManager.TranscodingSession.OnProgressUpdateListener {
|
||||
method public void onProgressUpdate(@NonNull android.media.MediaTranscodingManager.TranscodingSession, @IntRange(from=0, to=100) int);
|
||||
}
|
||||
|
||||
public static final class MediaTranscodingManager.VideoTranscodingRequest extends android.media.MediaTranscodingManager.TranscodingRequest {
|
||||
method @NonNull public android.media.MediaFormat getVideoTrackFormat();
|
||||
}
|
||||
|
||||
public static final class MediaTranscodingManager.VideoTranscodingRequest.Builder {
|
||||
ctor public MediaTranscodingManager.VideoTranscodingRequest.Builder(@NonNull android.net.Uri, @NonNull android.net.Uri, @NonNull android.media.MediaFormat);
|
||||
method @NonNull public android.media.MediaTranscodingManager.VideoTranscodingRequest build();
|
||||
method @NonNull public android.media.MediaTranscodingManager.VideoTranscodingRequest.Builder setClientPid(int);
|
||||
method @NonNull public android.media.MediaTranscodingManager.VideoTranscodingRequest.Builder setClientUid(int);
|
||||
method @NonNull public android.media.MediaTranscodingManager.VideoTranscodingRequest.Builder setDestinationFileDescriptor(@NonNull android.os.ParcelFileDescriptor);
|
||||
method @NonNull public android.media.MediaTranscodingManager.VideoTranscodingRequest.Builder setSourceFileDescriptor(@NonNull android.os.ParcelFileDescriptor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1,2 +0,0 @@
|
||||
rule com.android.modules.** android.media.internal.@1
|
||||
rule com.google.android.exoplayer2.** android.media.internal.exo.@1
|
||||
@@ -1,626 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.modules.annotation.MinSdk;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
ApplicationMediaCapabilities is an immutable class that encapsulates an application's capabilities
|
||||
for handling newer video codec format and media features.
|
||||
|
||||
<p>
|
||||
Android 12 introduces Compatible media transcoding feature. See
|
||||
<a href="https://developer.android.com/about/versions/12/features#compatible_media_transcoding">
|
||||
Compatible media transcoding</a>. By default, Android assumes apps can support playback of all
|
||||
media formats. Apps that would like to request that media be transcoded into a more compatible
|
||||
format should declare their media capabilities in a media_capabilities.xml resource file and add it
|
||||
as a property tag in the AndroidManifest.xml file. Here is a example:
|
||||
<pre>
|
||||
{@code
|
||||
<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<format android:name="HEVC" supported="true"/>
|
||||
<format android:name="HDR10" supported="false"/>
|
||||
<format android:name="HDR10Plus" supported="false"/>
|
||||
</media-capabilities>
|
||||
}
|
||||
</pre>
|
||||
The ApplicationMediaCapabilities class is generated from this xml and used by the platform to
|
||||
represent an application's media capabilities in order to determine whether modern media files need
|
||||
to be transcoded for that application.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
ApplicationMediaCapabilities objects can also be built by applications at runtime for use with
|
||||
{@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)} to provide more
|
||||
control over the transcoding that is built into the platform. ApplicationMediaCapabilities
|
||||
provided by applications at runtime like this override the default manifest capabilities for that
|
||||
media access.The object could be build either through {@link #createFromXml(XmlPullParser)} or
|
||||
through the builder class {@link ApplicationMediaCapabilities.Builder}
|
||||
|
||||
<h3> Video Codec Support</h3>
|
||||
<p>
|
||||
Newer video codes include HEVC, VP9 and AV1. Application only needs to indicate their support
|
||||
for newer format with this class as they are assumed to support older format like h.264.
|
||||
|
||||
<h3>Capability of handling HDR(high dynamic range) video</h3>
|
||||
<p>
|
||||
There are four types of HDR video(Dolby-Vision, HDR10, HDR10+, HLG) supported by the platform,
|
||||
application will only need to specify individual types they supported.
|
||||
*/
|
||||
@MinSdk(Build.VERSION_CODES.S)
|
||||
public final class ApplicationMediaCapabilities implements Parcelable {
|
||||
private static final String TAG = "ApplicationMediaCapabilities";
|
||||
|
||||
/** List of supported video codec mime types. */
|
||||
private Set<String> mSupportedVideoMimeTypes = new HashSet<>();
|
||||
|
||||
/** List of unsupported video codec mime types. */
|
||||
private Set<String> mUnsupportedVideoMimeTypes = new HashSet<>();
|
||||
|
||||
/** List of supported hdr types. */
|
||||
private Set<String> mSupportedHdrTypes = new HashSet<>();
|
||||
|
||||
/** List of unsupported hdr types. */
|
||||
private Set<String> mUnsupportedHdrTypes = new HashSet<>();
|
||||
|
||||
private boolean mIsSlowMotionSupported = false;
|
||||
|
||||
private ApplicationMediaCapabilities(Builder b) {
|
||||
mSupportedVideoMimeTypes.addAll(b.getSupportedVideoMimeTypes());
|
||||
mUnsupportedVideoMimeTypes.addAll(b.getUnsupportedVideoMimeTypes());
|
||||
mSupportedHdrTypes.addAll(b.getSupportedHdrTypes());
|
||||
mUnsupportedHdrTypes.addAll(b.getUnsupportedHdrTypes());
|
||||
mIsSlowMotionSupported = b.mIsSlowMotionSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if a video codec format is supported by the application.
|
||||
* <p>
|
||||
* If the application has not specified supporting the format or not, this will return false.
|
||||
* Use {@link #isFormatSpecified(String)} to query if a format is specified or not.
|
||||
*
|
||||
* @param videoMime The mime type of the video codec format. Must be the one used in
|
||||
* {@link MediaFormat#KEY_MIME}.
|
||||
* @return true if application supports the video codec format, false otherwise.
|
||||
*/
|
||||
public boolean isVideoMimeTypeSupported(
|
||||
@NonNull String videoMime) {
|
||||
if (mSupportedVideoMimeTypes.contains(videoMime.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if a HDR type is supported by the application.
|
||||
* <p>
|
||||
* If the application has not specified supporting the format or not, this will return false.
|
||||
* Use {@link #isFormatSpecified(String)} to query if a format is specified or not.
|
||||
*
|
||||
* @param hdrType The type of the HDR format.
|
||||
* @return true if application supports the HDR format, false otherwise.
|
||||
*/
|
||||
public boolean isHdrTypeSupported(
|
||||
@NonNull @MediaFeature.MediaHdrType String hdrType) {
|
||||
if (mSupportedHdrTypes.contains(hdrType)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if a format is specified by the application.
|
||||
* <p>
|
||||
* The format could be either the video format or the hdr format.
|
||||
*
|
||||
* @param format The name of the format.
|
||||
* @return true if application specifies the format, false otherwise.
|
||||
*/
|
||||
public boolean isFormatSpecified(@NonNull String format) {
|
||||
if (mSupportedVideoMimeTypes.contains(format) || mUnsupportedVideoMimeTypes.contains(format)
|
||||
|| mSupportedHdrTypes.contains(format) || mUnsupportedHdrTypes.contains(format)) {
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
// Write out the supported video mime types.
|
||||
dest.writeInt(mSupportedVideoMimeTypes.size());
|
||||
for (String cap : mSupportedVideoMimeTypes) {
|
||||
dest.writeString(cap);
|
||||
}
|
||||
// Write out the unsupported video mime types.
|
||||
dest.writeInt(mUnsupportedVideoMimeTypes.size());
|
||||
for (String cap : mUnsupportedVideoMimeTypes) {
|
||||
dest.writeString(cap);
|
||||
}
|
||||
// Write out the supported hdr types.
|
||||
dest.writeInt(mSupportedHdrTypes.size());
|
||||
for (String cap : mSupportedHdrTypes) {
|
||||
dest.writeString(cap);
|
||||
}
|
||||
// Write out the unsupported hdr types.
|
||||
dest.writeInt(mUnsupportedHdrTypes.size());
|
||||
for (String cap : mUnsupportedHdrTypes) {
|
||||
dest.writeString(cap);
|
||||
}
|
||||
// Write out the supported slow motion.
|
||||
dest.writeBoolean(mIsSlowMotionSupported);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String caps = new String(
|
||||
"Supported Video MimeTypes: " + mSupportedVideoMimeTypes.toString());
|
||||
caps += "Unsupported Video MimeTypes: " + mUnsupportedVideoMimeTypes.toString();
|
||||
caps += "Supported HDR types: " + mSupportedHdrTypes.toString();
|
||||
caps += "Unsupported HDR types: " + mUnsupportedHdrTypes.toString();
|
||||
caps += "Supported slow motion: " + mIsSlowMotionSupported;
|
||||
return caps;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static final Creator<ApplicationMediaCapabilities> CREATOR =
|
||||
new Creator<ApplicationMediaCapabilities>() {
|
||||
public ApplicationMediaCapabilities createFromParcel(Parcel in) {
|
||||
ApplicationMediaCapabilities.Builder builder =
|
||||
new ApplicationMediaCapabilities.Builder();
|
||||
|
||||
// Parse supported video codec mime types.
|
||||
int count = in.readInt();
|
||||
for (int readCount = 0; readCount < count; ++readCount) {
|
||||
builder.addSupportedVideoMimeType(in.readString());
|
||||
}
|
||||
|
||||
// Parse unsupported video codec mime types.
|
||||
count = in.readInt();
|
||||
for (int readCount = 0; readCount < count; ++readCount) {
|
||||
builder.addUnsupportedVideoMimeType(in.readString());
|
||||
}
|
||||
|
||||
// Parse supported hdr types.
|
||||
count = in.readInt();
|
||||
for (int readCount = 0; readCount < count; ++readCount) {
|
||||
builder.addSupportedHdrType(in.readString());
|
||||
}
|
||||
|
||||
// Parse unsupported hdr types.
|
||||
count = in.readInt();
|
||||
for (int readCount = 0; readCount < count; ++readCount) {
|
||||
builder.addUnsupportedHdrType(in.readString());
|
||||
}
|
||||
|
||||
boolean supported = in.readBoolean();
|
||||
builder.setSlowMotionSupported(supported);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public ApplicationMediaCapabilities[] newArray(int size) {
|
||||
return new ApplicationMediaCapabilities[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Query the video codec mime types supported by the application.
|
||||
* @return List of supported video codec mime types. The list will be empty if there are none.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getSupportedVideoMimeTypes() {
|
||||
return new ArrayList<>(mSupportedVideoMimeTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the video codec mime types that are not supported by the application.
|
||||
* @return List of unsupported video codec mime types. The list will be empty if there are none.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getUnsupportedVideoMimeTypes() {
|
||||
return new ArrayList<>(mUnsupportedVideoMimeTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all hdr types that are supported by the application.
|
||||
* @return List of supported hdr types. The list will be empty if there are none.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getSupportedHdrTypes() {
|
||||
return new ArrayList<>(mSupportedHdrTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all hdr types that are not supported by the application.
|
||||
* @return List of unsupported hdr types. The list will be empty if there are none.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getUnsupportedHdrTypes() {
|
||||
return new ArrayList<>(mUnsupportedHdrTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether handling of slow-motion video is supported
|
||||
* @hide
|
||||
*/
|
||||
public boolean isSlowMotionSupported() {
|
||||
return mIsSlowMotionSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link ApplicationMediaCapabilities} from an xml.
|
||||
*
|
||||
* The xml's syntax is the same as the media_capabilities.xml used by the AndroidManifest.xml.
|
||||
* <p> Here is an example:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* <media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <format android:name="HEVC" supported="true"/>
|
||||
* <format android:name="HDR10" supported="false"/>
|
||||
* <format android:name="HDR10Plus" supported="false"/>
|
||||
* </media-capabilities>
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
*
|
||||
* @param xmlParser The underlying {@link XmlPullParser} that will read the xml.
|
||||
* @return An ApplicationMediaCapabilities object.
|
||||
* @throws UnsupportedOperationException if the capabilities in xml config are invalid or
|
||||
* incompatible.
|
||||
*/
|
||||
// TODO: Add developer.android.com link for the format of the xml.
|
||||
@NonNull
|
||||
public static ApplicationMediaCapabilities createFromXml(@NonNull XmlPullParser xmlParser) {
|
||||
ApplicationMediaCapabilities.Builder builder = new ApplicationMediaCapabilities.Builder();
|
||||
builder.parseXml(xmlParser);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for {@link ApplicationMediaCapabilities} objects.
|
||||
* Use this class to configure and create an ApplicationMediaCapabilities instance. Builder
|
||||
* could be created from an existing ApplicationMediaCapabilities object, from a xml file or
|
||||
* MediaCodecList.
|
||||
* //TODO(hkuang): Add xml parsing support to the builder.
|
||||
*/
|
||||
public final static class Builder {
|
||||
/** List of supported video codec mime types. */
|
||||
private Set<String> mSupportedVideoMimeTypes = new HashSet<>();
|
||||
|
||||
/** List of supported hdr types. */
|
||||
private Set<String> mSupportedHdrTypes = new HashSet<>();
|
||||
|
||||
/** List of unsupported video codec mime types. */
|
||||
private Set<String> mUnsupportedVideoMimeTypes = new HashSet<>();
|
||||
|
||||
/** List of unsupported hdr types. */
|
||||
private Set<String> mUnsupportedHdrTypes = new HashSet<>();
|
||||
|
||||
private boolean mIsSlowMotionSupported = false;
|
||||
|
||||
/* Map to save the format read from the xml. */
|
||||
private Map<String, Boolean> mFormatSupportedMap = new HashMap<String, Boolean>();
|
||||
|
||||
/**
|
||||
* Constructs a new Builder with all the supports default to false.
|
||||
*/
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
private void parseXml(@NonNull XmlPullParser xmlParser)
|
||||
throws UnsupportedOperationException {
|
||||
if (xmlParser == null) {
|
||||
throw new IllegalArgumentException("XmlParser must not be null");
|
||||
}
|
||||
|
||||
try {
|
||||
while (xmlParser.next() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validates the tag is "media-capabilities".
|
||||
if (!xmlParser.getName().equals("media-capabilities")) {
|
||||
throw new UnsupportedOperationException("Invalid tag");
|
||||
}
|
||||
|
||||
xmlParser.next();
|
||||
while (xmlParser.getEventType() != XmlPullParser.END_TAG) {
|
||||
while (xmlParser.getEventType() != XmlPullParser.START_TAG) {
|
||||
if (xmlParser.getEventType() == XmlPullParser.END_DOCUMENT) {
|
||||
return;
|
||||
}
|
||||
xmlParser.next();
|
||||
}
|
||||
|
||||
// Validates the tag is "format".
|
||||
if (xmlParser.getName().equals("format")) {
|
||||
parseFormatTag(xmlParser);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Invalid tag");
|
||||
}
|
||||
while (xmlParser.getEventType() != XmlPullParser.END_TAG) {
|
||||
xmlParser.next();
|
||||
}
|
||||
xmlParser.next();
|
||||
}
|
||||
} catch (XmlPullParserException xppe) {
|
||||
throw new UnsupportedOperationException("Ill-formatted xml file");
|
||||
} catch (java.io.IOException ioe) {
|
||||
throw new UnsupportedOperationException("Unable to read xml file");
|
||||
}
|
||||
}
|
||||
|
||||
private void parseFormatTag(XmlPullParser xmlParser) {
|
||||
String name = null;
|
||||
String supported = null;
|
||||
for (int i = 0; i < xmlParser.getAttributeCount(); i++) {
|
||||
String attrName = xmlParser.getAttributeName(i);
|
||||
if (attrName.equals("name")) {
|
||||
name = xmlParser.getAttributeValue(i);
|
||||
} else if (attrName.equals("supported")) {
|
||||
supported = xmlParser.getAttributeValue(i);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Invalid attribute name " + attrName);
|
||||
}
|
||||
}
|
||||
|
||||
if (name != null && supported != null) {
|
||||
if (!supported.equals("true") && !supported.equals("false")) {
|
||||
throw new UnsupportedOperationException(
|
||||
("Supported value must be either true or false"));
|
||||
}
|
||||
boolean isSupported = Boolean.parseBoolean(supported);
|
||||
|
||||
// Check if the format is already found before.
|
||||
if (mFormatSupportedMap.get(name) != null && mFormatSupportedMap.get(name)
|
||||
!= isSupported) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Format: " + name + " has conflict supported value");
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "HEVC":
|
||||
if (isSupported) {
|
||||
mSupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
|
||||
} else {
|
||||
mUnsupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
|
||||
}
|
||||
break;
|
||||
case "VP9":
|
||||
if (isSupported) {
|
||||
mSupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
||||
} else {
|
||||
mUnsupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
||||
}
|
||||
break;
|
||||
case "AV1":
|
||||
if (isSupported) {
|
||||
mSupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||
} else {
|
||||
mUnsupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||
}
|
||||
break;
|
||||
case "HDR10":
|
||||
if (isSupported) {
|
||||
mSupportedHdrTypes.add(MediaFeature.HdrType.HDR10);
|
||||
} else {
|
||||
mUnsupportedHdrTypes.add(MediaFeature.HdrType.HDR10);
|
||||
}
|
||||
break;
|
||||
case "HDR10Plus":
|
||||
if (isSupported) {
|
||||
mSupportedHdrTypes.add(MediaFeature.HdrType.HDR10_PLUS);
|
||||
} else {
|
||||
mUnsupportedHdrTypes.add(MediaFeature.HdrType.HDR10_PLUS);
|
||||
}
|
||||
break;
|
||||
case "Dolby-Vision":
|
||||
if (isSupported) {
|
||||
mSupportedHdrTypes.add(MediaFeature.HdrType.DOLBY_VISION);
|
||||
} else {
|
||||
mUnsupportedHdrTypes.add(MediaFeature.HdrType.DOLBY_VISION);
|
||||
}
|
||||
break;
|
||||
case "HLG":
|
||||
if (isSupported) {
|
||||
mSupportedHdrTypes.add(MediaFeature.HdrType.HLG);
|
||||
} else {
|
||||
mUnsupportedHdrTypes.add(MediaFeature.HdrType.HLG);
|
||||
}
|
||||
break;
|
||||
case "SlowMotion":
|
||||
mIsSlowMotionSupported = isSupported;
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Invalid format name " + name);
|
||||
}
|
||||
// Save the name and isSupported into the map for validate later.
|
||||
mFormatSupportedMap.put(name, isSupported);
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Format name and supported must both be specified");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link ApplicationMediaCapabilities} object.
|
||||
*
|
||||
* @return a new {@link ApplicationMediaCapabilities} instance successfully initialized
|
||||
* with all the parameters set on this <code>Builder</code>.
|
||||
* @throws UnsupportedOperationException if the parameters set on the
|
||||
* <code>Builder</code> were incompatible, or if they
|
||||
* are not supported by the
|
||||
* device.
|
||||
*/
|
||||
@NonNull
|
||||
public ApplicationMediaCapabilities build() {
|
||||
Log.d(TAG,
|
||||
"Building ApplicationMediaCapabilities with: (Supported HDR: "
|
||||
+ mSupportedHdrTypes.toString() + " Unsupported HDR: "
|
||||
+ mUnsupportedHdrTypes.toString() + ") (Supported Codec: "
|
||||
+ " " + mSupportedVideoMimeTypes.toString() + " Unsupported Codec:"
|
||||
+ mUnsupportedVideoMimeTypes.toString() + ") "
|
||||
+ mIsSlowMotionSupported);
|
||||
|
||||
// If hdr is supported, application must also support hevc.
|
||||
if (!mSupportedHdrTypes.isEmpty() && !mSupportedVideoMimeTypes.contains(
|
||||
MediaFormat.MIMETYPE_VIDEO_HEVC)) {
|
||||
throw new UnsupportedOperationException("Only support HEVC mime type");
|
||||
}
|
||||
return new ApplicationMediaCapabilities(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a supported video codec mime type.
|
||||
*
|
||||
* @param codecMime Supported codec mime types. Must be one of the mime type defined
|
||||
* in {@link MediaFormat}.
|
||||
* @throws IllegalArgumentException if mime type is not valid.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder addSupportedVideoMimeType(
|
||||
@NonNull String codecMime) {
|
||||
mSupportedVideoMimeTypes.add(codecMime);
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getSupportedVideoMimeTypes() {
|
||||
return new ArrayList<>(mSupportedVideoMimeTypes);
|
||||
}
|
||||
|
||||
private boolean isValidVideoCodecMimeType(@NonNull String codecMime) {
|
||||
if (!codecMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)
|
||||
&& !codecMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)
|
||||
&& !codecMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an unsupported video codec mime type.
|
||||
*
|
||||
* @param codecMime Unsupported codec mime type. Must be one of the mime type defined
|
||||
* in {@link MediaFormat}.
|
||||
* @throws IllegalArgumentException if mime type is not valid.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder addUnsupportedVideoMimeType(
|
||||
@NonNull String codecMime) {
|
||||
if (!isValidVideoCodecMimeType(codecMime)) {
|
||||
throw new IllegalArgumentException("Invalid codec mime type: " + codecMime);
|
||||
}
|
||||
mUnsupportedVideoMimeTypes.add(codecMime);
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getUnsupportedVideoMimeTypes() {
|
||||
return new ArrayList<>(mUnsupportedVideoMimeTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a supported hdr type.
|
||||
*
|
||||
* @param hdrType Supported hdr type. Must be one of the String defined in
|
||||
* {@link MediaFeature.HdrType}.
|
||||
* @throws IllegalArgumentException if hdrType is not valid.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder addSupportedHdrType(
|
||||
@NonNull @MediaFeature.MediaHdrType String hdrType) {
|
||||
if (!isValidVideoCodecHdrType(hdrType)) {
|
||||
throw new IllegalArgumentException("Invalid hdr type: " + hdrType);
|
||||
}
|
||||
mSupportedHdrTypes.add(hdrType);
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getSupportedHdrTypes() {
|
||||
return new ArrayList<>(mSupportedHdrTypes);
|
||||
}
|
||||
|
||||
private boolean isValidVideoCodecHdrType(@NonNull String hdrType) {
|
||||
if (!hdrType.equals(MediaFeature.HdrType.DOLBY_VISION)
|
||||
&& !hdrType.equals(MediaFeature.HdrType.HDR10)
|
||||
&& !hdrType.equals(MediaFeature.HdrType.HDR10_PLUS)
|
||||
&& !hdrType.equals(MediaFeature.HdrType.HLG)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an unsupported hdr type.
|
||||
*
|
||||
* @param hdrType Unsupported hdr type. Must be one of the String defined in
|
||||
* {@link MediaFeature.HdrType}.
|
||||
* @throws IllegalArgumentException if hdrType is not valid.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder addUnsupportedHdrType(
|
||||
@NonNull @MediaFeature.MediaHdrType String hdrType) {
|
||||
if (!isValidVideoCodecHdrType(hdrType)) {
|
||||
throw new IllegalArgumentException("Invalid hdr type: " + hdrType);
|
||||
}
|
||||
mUnsupportedHdrTypes.add(hdrType);
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getUnsupportedHdrTypes() {
|
||||
return new ArrayList<>(mUnsupportedHdrTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether slow-motion video is supported.
|
||||
* If an application indicates support for slow-motion, it is application's responsibility
|
||||
* to parse the slow-motion videos using their own parser or using support library.
|
||||
* @see android.media.MediaFormat#KEY_SLOW_MOTION_MARKERS
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setSlowMotionSupported(boolean slowMotionSupported) {
|
||||
mIsSlowMotionSupported = slowMotionSupported;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 android.media;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a copied version of BaseParceledListSlice in framework with hidden API usages
|
||||
* removed.
|
||||
*
|
||||
* Transfer a large list of Parcelable objects across an IPC. Splits into
|
||||
* multiple transactions if needed.
|
||||
*
|
||||
* Caveat: for efficiency and security, all elements must be the same concrete type.
|
||||
* In order to avoid writing the class name of each object, we must ensure that
|
||||
* each object is the same type, or else unparceling then reparceling the data may yield
|
||||
* a different result if the class name encoded in the Parcelable is a Base type.
|
||||
* See b/17671747.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
abstract class BaseMediaParceledListSlice<T> implements Parcelable {
|
||||
private static String TAG = "BaseMediaParceledListSlice";
|
||||
private static boolean DEBUG = false;
|
||||
|
||||
/*
|
||||
* TODO get this number from somewhere else. For now set it to a quarter of
|
||||
* the 1MB limit.
|
||||
*/
|
||||
// private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes();
|
||||
private static final int MAX_IPC_SIZE = 64 * 1024;
|
||||
|
||||
private final List<T> mList;
|
||||
|
||||
private int mInlineCountLimit = Integer.MAX_VALUE;
|
||||
|
||||
public BaseMediaParceledListSlice(List<T> list) {
|
||||
mList = list;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
BaseMediaParceledListSlice(Parcel p, ClassLoader loader) {
|
||||
final int N = p.readInt();
|
||||
mList = new ArrayList<T>(N);
|
||||
if (DEBUG) Log.d(TAG, "Retrieving " + N + " items");
|
||||
if (N <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Parcelable.Creator<?> creator = readParcelableCreator(p, loader);
|
||||
Class<?> listElementClass = null;
|
||||
|
||||
int i = 0;
|
||||
while (i < N) {
|
||||
if (p.readInt() == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
final T parcelable = readCreator(creator, p, loader);
|
||||
if (listElementClass == null) {
|
||||
listElementClass = parcelable.getClass();
|
||||
} else {
|
||||
verifySameType(listElementClass, parcelable.getClass());
|
||||
}
|
||||
|
||||
mList.add(parcelable);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size()-1));
|
||||
i++;
|
||||
}
|
||||
if (i >= N) {
|
||||
return;
|
||||
}
|
||||
final IBinder retriever = p.readStrongBinder();
|
||||
while (i < N) {
|
||||
if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever);
|
||||
Parcel data = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
data.writeInt(i);
|
||||
try {
|
||||
retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failure retrieving array; only received " + i + " of " + N, e);
|
||||
return;
|
||||
}
|
||||
while (i < N && reply.readInt() != 0) {
|
||||
final T parcelable = readCreator(creator, reply, loader);
|
||||
verifySameType(listElementClass, parcelable.getClass());
|
||||
|
||||
mList.add(parcelable);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size()-1));
|
||||
i++;
|
||||
}
|
||||
reply.recycle();
|
||||
data.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private T readCreator(Parcelable.Creator<?> creator, Parcel p, ClassLoader loader) {
|
||||
if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
|
||||
Parcelable.ClassLoaderCreator<?> classLoaderCreator =
|
||||
(Parcelable.ClassLoaderCreator<?>) creator;
|
||||
return (T) classLoaderCreator.createFromParcel(p, loader);
|
||||
}
|
||||
return (T) creator.createFromParcel(p);
|
||||
}
|
||||
|
||||
private static void verifySameType(final Class<?> expected, final Class<?> actual) {
|
||||
if (!actual.equals(expected)) {
|
||||
throw new IllegalArgumentException("Can't unparcel type "
|
||||
+ (actual == null ? null : actual.getName()) + " in list of type "
|
||||
+ (expected == null ? null : expected.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return mList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a limit on the maximum number of entries in the array that will be included
|
||||
* inline in the initial parcelling of this object.
|
||||
*/
|
||||
public void setInlineCountLimit(int maxCount) {
|
||||
mInlineCountLimit = maxCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this to another Parcel. Note that this discards the internal Parcel
|
||||
* and should not be used anymore. This is so we can pass this to a Binder
|
||||
* where we won't have a chance to call recycle on this.
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
final int N = mList.size();
|
||||
final int callFlags = flags;
|
||||
dest.writeInt(N);
|
||||
if (DEBUG) Log.d(TAG, "Writing " + N + " items");
|
||||
if (N > 0) {
|
||||
final Class<?> listElementClass = mList.get(0).getClass();
|
||||
writeParcelableCreator(mList.get(0), dest);
|
||||
int i = 0;
|
||||
while (i < N && i < mInlineCountLimit && dest.dataSize() < MAX_IPC_SIZE) {
|
||||
dest.writeInt(1);
|
||||
|
||||
final T parcelable = mList.get(i);
|
||||
verifySameType(listElementClass, parcelable.getClass());
|
||||
writeElement(parcelable, dest, callFlags);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i));
|
||||
i++;
|
||||
}
|
||||
if (i < N) {
|
||||
dest.writeInt(0);
|
||||
Binder retriever = new Binder() {
|
||||
@Override
|
||||
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
||||
throws RemoteException {
|
||||
if (code != FIRST_CALL_TRANSACTION) {
|
||||
return super.onTransact(code, data, reply, flags);
|
||||
}
|
||||
int i = data.readInt();
|
||||
if (DEBUG) Log.d(TAG, "Writing more @" + i + " of " + N);
|
||||
while (i < N && reply.dataSize() < MAX_IPC_SIZE) {
|
||||
reply.writeInt(1);
|
||||
|
||||
final T parcelable = mList.get(i);
|
||||
verifySameType(listElementClass, parcelable.getClass());
|
||||
writeElement(parcelable, reply, callFlags);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i));
|
||||
i++;
|
||||
}
|
||||
if (i < N) {
|
||||
if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N);
|
||||
reply.writeInt(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever);
|
||||
dest.writeStrongBinder(retriever);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract void writeElement(T parcelable, Parcel reply, int callFlags);
|
||||
|
||||
abstract void writeParcelableCreator(T parcelable, Parcel dest);
|
||||
|
||||
abstract Parcelable.Creator<?> readParcelableCreator(Parcel from, ClassLoader loader);
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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 android.media;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Structure for source buffering management params.
|
||||
*
|
||||
* Used by {@link MediaPlayer#getBufferingParams()} and
|
||||
* {@link MediaPlayer#setBufferingParams(BufferingParams)}
|
||||
* to control source buffering behavior.
|
||||
*
|
||||
* <p>There are two stages of source buffering in {@link MediaPlayer}: initial buffering
|
||||
* (when {@link MediaPlayer} is being prepared) and rebuffering (when {@link MediaPlayer}
|
||||
* is playing back source). {@link BufferingParams} includes corresponding marks for each
|
||||
* stage of source buffering. The marks are time based (in milliseconds).
|
||||
*
|
||||
* <p>{@link MediaPlayer} source component has default marks which can be queried by
|
||||
* calling {@link MediaPlayer#getBufferingParams()} before any change is made by
|
||||
* {@link MediaPlayer#setBufferingParams()}.
|
||||
* <ul>
|
||||
* <li><strong>initial buffering:</strong> initialMarkMs is used when
|
||||
* {@link MediaPlayer} is being prepared. When cached data amount exceeds this mark
|
||||
* {@link MediaPlayer} is prepared. </li>
|
||||
* <li><strong>rebuffering during playback:</strong> resumePlaybackMarkMs is used when
|
||||
* {@link MediaPlayer} is playing back content.
|
||||
* <ul>
|
||||
* <li> {@link MediaPlayer} has internal mark, namely pausePlaybackMarkMs, to decide when
|
||||
* to pause playback if cached data amount runs low. This internal mark varies based on
|
||||
* type of data source. </li>
|
||||
* <li> When cached data amount exceeds resumePlaybackMarkMs, {@link MediaPlayer} will
|
||||
* resume playback if it has been paused due to low cached data amount. The internal mark
|
||||
* pausePlaybackMarkMs shall be less than resumePlaybackMarkMs. </li>
|
||||
* <li> {@link MediaPlayer} has internal mark, namely pauseRebufferingMarkMs, to decide
|
||||
* when to pause rebuffering. Apparently, this internal mark shall be no less than
|
||||
* resumePlaybackMarkMs. </li>
|
||||
* <li> {@link MediaPlayer} has internal mark, namely resumeRebufferingMarkMs, to decide
|
||||
* when to resume buffering. This internal mark varies based on type of data source. This
|
||||
* mark shall be larger than pausePlaybackMarkMs, and less than pauseRebufferingMarkMs.
|
||||
* </li>
|
||||
* </ul> </li>
|
||||
* </ul>
|
||||
* <p>Users should use {@link Builder} to change {@link BufferingParams}.
|
||||
* @hide
|
||||
*/
|
||||
public final class BufferingParams implements Parcelable {
|
||||
private static final int BUFFERING_NO_MARK = -1;
|
||||
|
||||
// params
|
||||
private int mInitialMarkMs = BUFFERING_NO_MARK;
|
||||
|
||||
private int mResumePlaybackMarkMs = BUFFERING_NO_MARK;
|
||||
|
||||
private BufferingParams() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return initial buffering mark in milliseconds.
|
||||
* @return initial buffering mark in milliseconds
|
||||
*/
|
||||
public int getInitialMarkMs() {
|
||||
return mInitialMarkMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mark in milliseconds for resuming playback.
|
||||
* @return the mark for resuming playback in milliseconds
|
||||
*/
|
||||
public int getResumePlaybackMarkMs() {
|
||||
return mResumePlaybackMarkMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for {@link BufferingParams} objects.
|
||||
* <p> Here is an example where <code>Builder</code> is used to define the
|
||||
* {@link BufferingParams} to be used by a {@link MediaPlayer} instance:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* BufferingParams myParams = mediaplayer.getDefaultBufferingParams();
|
||||
* myParams = new BufferingParams.Builder(myParams)
|
||||
* .setInitialMarkMs(10000)
|
||||
* .setResumePlaybackMarkMs(15000)
|
||||
* .build();
|
||||
* mediaplayer.setBufferingParams(myParams);
|
||||
* </pre>
|
||||
*/
|
||||
public static class Builder {
|
||||
private int mInitialMarkMs = BUFFERING_NO_MARK;
|
||||
private int mResumePlaybackMarkMs = BUFFERING_NO_MARK;
|
||||
|
||||
/**
|
||||
* Constructs a new Builder with the defaults.
|
||||
* By default, all marks are -1.
|
||||
*/
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Builder from a given {@link BufferingParams} instance
|
||||
* @param bp the {@link BufferingParams} object whose data will be reused
|
||||
* in the new Builder.
|
||||
*/
|
||||
public Builder(BufferingParams bp) {
|
||||
mInitialMarkMs = bp.mInitialMarkMs;
|
||||
mResumePlaybackMarkMs = bp.mResumePlaybackMarkMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines all of the fields that have been set and return a new
|
||||
* {@link BufferingParams} object. <code>IllegalStateException</code> will be
|
||||
* thrown if there is conflict between fields.
|
||||
* @return a new {@link BufferingParams} object
|
||||
*/
|
||||
public BufferingParams build() {
|
||||
BufferingParams bp = new BufferingParams();
|
||||
bp.mInitialMarkMs = mInitialMarkMs;
|
||||
bp.mResumePlaybackMarkMs = mResumePlaybackMarkMs;
|
||||
|
||||
return bp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time based mark in milliseconds for initial buffering.
|
||||
* @param markMs time based mark in milliseconds
|
||||
* @return the same Builder instance.
|
||||
*/
|
||||
public Builder setInitialMarkMs(int markMs) {
|
||||
mInitialMarkMs = markMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time based mark in milliseconds for resuming playback.
|
||||
* @param markMs time based mark in milliseconds for resuming playback
|
||||
* @return the same Builder instance.
|
||||
*/
|
||||
public Builder setResumePlaybackMarkMs(int markMs) {
|
||||
mResumePlaybackMarkMs = markMs;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private BufferingParams(Parcel in) {
|
||||
mInitialMarkMs = in.readInt();
|
||||
mResumePlaybackMarkMs = in.readInt();
|
||||
}
|
||||
|
||||
public static final @android.annotation.NonNull Parcelable.Creator<BufferingParams> CREATOR =
|
||||
new Parcelable.Creator<BufferingParams>() {
|
||||
@Override
|
||||
public BufferingParams createFromParcel(Parcel in) {
|
||||
return new BufferingParams(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferingParams[] newArray(int size) {
|
||||
return new BufferingParams[size];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(mInitialMarkMs);
|
||||
dest.writeInt(mResumePlaybackMarkMs);
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ResultReceiver;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Handles incoming commands from {@link MediaSession2} to {@link MediaController2}.
|
||||
* @hide
|
||||
*/
|
||||
// @SystemApi
|
||||
public final class Controller2Link implements Parcelable {
|
||||
private static final String TAG = "Controller2Link";
|
||||
private static final boolean DEBUG = MediaController2.DEBUG;
|
||||
|
||||
public static final @android.annotation.NonNull Parcelable.Creator<Controller2Link> CREATOR =
|
||||
new Parcelable.Creator<Controller2Link>() {
|
||||
@Override
|
||||
public Controller2Link createFromParcel(Parcel in) {
|
||||
return new Controller2Link(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller2Link[] newArray(int size) {
|
||||
return new Controller2Link[size];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private final MediaController2 mController;
|
||||
private final IMediaController2 mIController;
|
||||
|
||||
public Controller2Link(MediaController2 controller) {
|
||||
mController = controller;
|
||||
mIController = new Controller2Stub();
|
||||
}
|
||||
|
||||
Controller2Link(Parcel in) {
|
||||
mController = null;
|
||||
mIController = IMediaController2.Stub.asInterface(in.readStrongBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeStrongBinder(mIController.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mIController.asBinder().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof Controller2Link)) {
|
||||
return false;
|
||||
}
|
||||
Controller2Link other = (Controller2Link) obj;
|
||||
return Objects.equals(mIController.asBinder(), other.mIController.asBinder());
|
||||
}
|
||||
|
||||
/** Interface method for IMediaController2.notifyConnected */
|
||||
public void notifyConnected(int seq, Bundle connectionResult) {
|
||||
try {
|
||||
mIController.notifyConnected(seq, connectionResult);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaController2.notifyDisonnected */
|
||||
public void notifyDisconnected(int seq) {
|
||||
try {
|
||||
mIController.notifyDisconnected(seq);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaController2.notifyPlaybackActiveChanged */
|
||||
public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) {
|
||||
try {
|
||||
mIController.notifyPlaybackActiveChanged(seq, playbackActive);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaController2.sendSessionCommand */
|
||||
public void sendSessionCommand(int seq, Session2Command command, Bundle args,
|
||||
ResultReceiver resultReceiver) {
|
||||
try {
|
||||
mIController.sendSessionCommand(seq, command, args, resultReceiver);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaController2.cancelSessionCommand */
|
||||
public void cancelSessionCommand(int seq) {
|
||||
try {
|
||||
mIController.cancelSessionCommand(seq);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaController2.notifyConnected */
|
||||
public void onConnected(int seq, Bundle connectionResult) {
|
||||
if (connectionResult == null) {
|
||||
onDisconnected(seq);
|
||||
return;
|
||||
}
|
||||
mController.onConnected(seq, connectionResult);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaController2.notifyDisonnected */
|
||||
public void onDisconnected(int seq) {
|
||||
mController.onDisconnected(seq);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaController2.notifyPlaybackActiveChanged */
|
||||
public void onPlaybackActiveChanged(int seq, boolean playbackActive) {
|
||||
mController.onPlaybackActiveChanged(seq, playbackActive);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaController2.sendSessionCommand */
|
||||
public void onSessionCommand(int seq, Session2Command command, Bundle args,
|
||||
ResultReceiver resultReceiver) {
|
||||
mController.onSessionCommand(seq, command, args, resultReceiver);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaController2.cancelSessionCommand */
|
||||
public void onCancelCommand(int seq) {
|
||||
mController.onCancelCommand(seq);
|
||||
}
|
||||
|
||||
private class Controller2Stub extends IMediaController2.Stub {
|
||||
@Override
|
||||
public void notifyConnected(int seq, Bundle connectionResult) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Controller2Link.this.onConnected(seq, connectionResult);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDisconnected(int seq) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Controller2Link.this.onDisconnected(seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPlaybackActiveChanged(int seq, boolean playbackActive) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Controller2Link.this.onPlaybackActiveChanged(seq, playbackActive);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSessionCommand(int seq, Session2Command command, Bundle args,
|
||||
ResultReceiver resultReceiver) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Controller2Link.this.onSessionCommand(seq, command, args, resultReceiver);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelSessionCommand(int seq) {
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Controller2Link.this.onCancelCommand(seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* For supplying media data to the framework. Implement this if your app has
|
||||
* special requirements for the way media data is obtained.
|
||||
*
|
||||
* <p class="note">Methods of this interface may be called on multiple different
|
||||
* threads. There will be a thread synchronization point between each call to ensure that
|
||||
* modifications to the state of your DataSourceCallback are visible to future calls. This means
|
||||
* you don't need to do your own synchronization unless you're modifying the
|
||||
* DataSourceCallback from another thread while it's being used by the framework.</p>
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public abstract class DataSourceCallback implements Closeable {
|
||||
|
||||
public static final int END_OF_STREAM = -1;
|
||||
|
||||
/**
|
||||
* Called to request data from the given position.
|
||||
*
|
||||
* Implementations should should write up to {@code size} bytes into
|
||||
* {@code buffer}, and return the number of bytes written.
|
||||
*
|
||||
* Return {@code 0} if size is zero (thus no bytes are read).
|
||||
*
|
||||
* Return {@code -1} to indicate that end of stream is reached.
|
||||
*
|
||||
* @param position the position in the data source to read from.
|
||||
* @param buffer the buffer to read the data into.
|
||||
* @param offset the offset within buffer to read the data into.
|
||||
* @param size the number of bytes to read.
|
||||
* @throws IOException on fatal errors.
|
||||
* @return the number of bytes read, or {@link #END_OF_STREAM} if end of stream is reached.
|
||||
*/
|
||||
public abstract int readAt(long position, @NonNull byte[] buffer, int offset, int size)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Called to get the size of the data source.
|
||||
*
|
||||
* @throws IOException on fatal errors
|
||||
* @return the size of data source in bytes, or -1 if the size is unknown.
|
||||
*/
|
||||
public abstract long getSize() throws IOException;
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 android.media;
|
||||
|
||||
import static android.Manifest.permission.MEDIA_CONTENT_CONTROL;
|
||||
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
|
||||
|
||||
import android.annotation.CallbackExecutor;
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.SystemService;
|
||||
import android.content.Context;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.service.media.MediaBrowserService;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.modules.annotation.MinSdk;
|
||||
import com.android.modules.utils.build.SdkLevel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Provides support for interacting with {@link android.media.MediaSession2 MediaSession2s}
|
||||
* that applications have published to express their ongoing media playback state.
|
||||
*/
|
||||
@MinSdk(Build.VERSION_CODES.S)
|
||||
@SystemService(Context.MEDIA_COMMUNICATION_SERVICE)
|
||||
public class MediaCommunicationManager {
|
||||
private static final String TAG = "MediaCommunicationManager";
|
||||
|
||||
/**
|
||||
* The manager version used from beginning.
|
||||
*/
|
||||
private static final int VERSION_1 = 1;
|
||||
|
||||
/**
|
||||
* Current manager version.
|
||||
*/
|
||||
private static final int CURRENT_VERSION = VERSION_1;
|
||||
|
||||
private final Context mContext;
|
||||
private final IMediaCommunicationService mService;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
private final CopyOnWriteArrayList<SessionCallbackRecord> mTokenCallbackRecords =
|
||||
new CopyOnWriteArrayList<>();
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private MediaCommunicationServiceCallbackStub mCallbackStub;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public MediaCommunicationManager(@NonNull Context context) {
|
||||
if (!SdkLevel.isAtLeastS()) {
|
||||
throw new UnsupportedOperationException("Android version must be S or greater.");
|
||||
}
|
||||
mContext = context;
|
||||
mService = IMediaCommunicationService.Stub.asInterface(
|
||||
MediaFrameworkInitializer.getMediaServiceManager()
|
||||
.getMediaCommunicationServiceRegisterer()
|
||||
.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of this {@link MediaCommunicationManager}.
|
||||
*/
|
||||
public @IntRange(from = 1) int getVersion() {
|
||||
return CURRENT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a new {@link MediaSession2} with type {@link Session2Token#TYPE_SESSION} is
|
||||
* created.
|
||||
* @param token newly created session2 token
|
||||
* @hide
|
||||
*/
|
||||
public void notifySession2Created(@NonNull Session2Token token) {
|
||||
Objects.requireNonNull(token, "token shouldn't be null");
|
||||
if (token.getType() != Session2Token.TYPE_SESSION) {
|
||||
throw new IllegalArgumentException("token's type should be TYPE_SESSION");
|
||||
}
|
||||
try {
|
||||
mService.notifySession2Created(token);
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the remote user is a trusted app.
|
||||
* <p>
|
||||
* An app is trusted if the app holds the
|
||||
* {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission or has an enabled
|
||||
* notification listener.
|
||||
*
|
||||
* @param userInfo The remote user info from either
|
||||
* {@link MediaSession#getCurrentControllerInfo()} or
|
||||
* {@link MediaBrowserService#getCurrentBrowserInfo()}.
|
||||
* @return {@code true} if the remote user is trusted or {@code false} otherwise.
|
||||
* @hide
|
||||
*/
|
||||
public boolean isTrustedForMediaControl(@NonNull MediaSessionManager.RemoteUserInfo userInfo) {
|
||||
Objects.requireNonNull(userInfo, "userInfo shouldn't be null");
|
||||
if (userInfo.getPackageName() == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return mService.isTrusted(
|
||||
userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Cannot communicate with the service.", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the
|
||||
* current user.
|
||||
* <p>
|
||||
* Although this API can be used without any restriction, each session owners can accept or
|
||||
* reject your uses of {@link MediaSession2}.
|
||||
*
|
||||
* @return A list of {@link Session2Token}.
|
||||
*/
|
||||
@NonNull
|
||||
public List<Session2Token> getSession2Tokens() {
|
||||
return getSession2Tokens(UserHandle.myUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback to be notified when the list of active sessions changes.
|
||||
* <p>
|
||||
* This requires the {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission be
|
||||
* held by the calling app.
|
||||
* </p>
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client = MODULE_LIBRARIES)
|
||||
@RequiresPermission(MEDIA_CONTENT_CONTROL)
|
||||
public void registerSessionCallback(@CallbackExecutor @NonNull Executor executor,
|
||||
@NonNull SessionCallback callback) {
|
||||
Objects.requireNonNull(executor, "executor must not be null");
|
||||
Objects.requireNonNull(callback, "callback must not be null");
|
||||
|
||||
if (!mTokenCallbackRecords.addIfAbsent(
|
||||
new SessionCallbackRecord(executor, callback))) {
|
||||
Log.w(TAG, "registerSession2TokenCallback: Ignoring the same callback");
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mCallbackStub == null) {
|
||||
MediaCommunicationServiceCallbackStub callbackStub =
|
||||
new MediaCommunicationServiceCallbackStub();
|
||||
try {
|
||||
mService.registerCallback(callbackStub, mContext.getPackageName());
|
||||
mCallbackStub = callbackStub;
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "Failed to register callback.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops receiving active sessions updates on the specified callback.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client = MODULE_LIBRARIES)
|
||||
public void unregisterSessionCallback(@NonNull SessionCallback callback) {
|
||||
if (!mTokenCallbackRecords.remove(
|
||||
new SessionCallbackRecord(null, callback))) {
|
||||
Log.w(TAG, "unregisterSession2TokenCallback: Ignoring an unknown callback.");
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mCallbackStub != null && mTokenCallbackRecords.isEmpty()) {
|
||||
try {
|
||||
mService.unregisterCallback(mCallbackStub);
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "Failed to unregister callback.", ex);
|
||||
}
|
||||
mCallbackStub = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Session2Token> getSession2Tokens(int userId) {
|
||||
try {
|
||||
MediaParceledListSlice slice = mService.getSession2Tokens(userId);
|
||||
return slice == null ? Collections.emptyList() : slice.getList();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to get session tokens", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for listening to changes to the sessions.
|
||||
* @see #registerSessionCallback(Executor, SessionCallback)
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client = MODULE_LIBRARIES)
|
||||
public interface SessionCallback {
|
||||
/**
|
||||
* Called when a new {@link MediaSession2 media session2} is created.
|
||||
* @param token the newly created token
|
||||
*/
|
||||
default void onSession2TokenCreated(@NonNull Session2Token token) {}
|
||||
|
||||
/**
|
||||
* Called when {@link #getSession2Tokens() session tokens} are changed.
|
||||
*/
|
||||
default void onSession2TokensChanged(@NonNull List<Session2Token> tokens) {}
|
||||
}
|
||||
|
||||
private static final class SessionCallbackRecord {
|
||||
public final Executor executor;
|
||||
public final SessionCallback callback;
|
||||
|
||||
SessionCallbackRecord(Executor executor, SessionCallback callback) {
|
||||
this.executor = executor;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof SessionCallbackRecord)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(this.callback, ((SessionCallbackRecord) obj).callback);
|
||||
}
|
||||
}
|
||||
|
||||
class MediaCommunicationServiceCallbackStub extends IMediaCommunicationServiceCallback.Stub {
|
||||
@Override
|
||||
public void onSession2Created(Session2Token token) throws RemoteException {
|
||||
for (SessionCallbackRecord record : mTokenCallbackRecords) {
|
||||
record.executor.execute(() -> record.callback.onSession2TokenCreated(token));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSession2Changed(MediaParceledListSlice tokens) throws RemoteException {
|
||||
List<Session2Token> tokenList = tokens.getList();
|
||||
for (SessionCallbackRecord record : mTokenCallbackRecords) {
|
||||
record.executor.execute(() -> record.callback.onSession2TokensChanged(tokenList));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
class MediaConstants {
|
||||
// Bundle key for int
|
||||
static final String KEY_PID = "android.media.key.PID";
|
||||
|
||||
// Bundle key for String
|
||||
static final String KEY_PACKAGE_NAME = "android.media.key.PACKAGE_NAME";
|
||||
|
||||
// Bundle key for Parcelable
|
||||
static final String KEY_SESSION2LINK = "android.media.key.SESSION2LINK";
|
||||
static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS";
|
||||
static final String KEY_PLAYBACK_ACTIVE = "android.media.key.PLAYBACK_ACTIVE";
|
||||
static final String KEY_TOKEN_EXTRAS = "android.media.key.TOKEN_EXTRAS";
|
||||
static final String KEY_CONNECTION_HINTS = "android.media.key.CONNECTION_HINTS";
|
||||
|
||||
private MediaConstants() {
|
||||
}
|
||||
}
|
||||
@@ -1,637 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
|
||||
import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
|
||||
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
|
||||
import static android.media.MediaConstants.KEY_PID;
|
||||
import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
|
||||
import static android.media.MediaConstants.KEY_SESSION2LINK;
|
||||
import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
|
||||
import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
|
||||
import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
|
||||
import static android.media.Session2Token.TYPE_SESSION;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
*
|
||||
* Allows an app to interact with an active {@link MediaSession2} or a
|
||||
* {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other
|
||||
* commands can be sent to the session.
|
||||
*/
|
||||
public class MediaController2 implements AutoCloseable {
|
||||
static final String TAG = "MediaController2";
|
||||
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final ControllerCallback mCallback;
|
||||
|
||||
private final IBinder.DeathRecipient mDeathRecipient = () -> close();
|
||||
private final Context mContext;
|
||||
private final Session2Token mSessionToken;
|
||||
private final Executor mCallbackExecutor;
|
||||
private final Controller2Link mControllerStub;
|
||||
private final Handler mResultHandler;
|
||||
private final SessionServiceConnection mServiceConnection;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
//@GuardedBy("mLock")
|
||||
private boolean mClosed;
|
||||
//@GuardedBy("mLock")
|
||||
private int mNextSeqNumber;
|
||||
//@GuardedBy("mLock")
|
||||
private Session2Link mSessionBinder;
|
||||
//@GuardedBy("mLock")
|
||||
private Session2CommandGroup mAllowedCommands;
|
||||
//@GuardedBy("mLock")
|
||||
private Session2Token mConnectedToken;
|
||||
//@GuardedBy("mLock")
|
||||
private ArrayMap<ResultReceiver, Integer> mPendingCommands;
|
||||
//@GuardedBy("mLock")
|
||||
private ArraySet<Integer> mRequestedCommandSeqNumbers;
|
||||
//@GuardedBy("mLock")
|
||||
private boolean mPlaybackActive;
|
||||
|
||||
/**
|
||||
* Create a {@link MediaController2} from the {@link Session2Token}.
|
||||
* This connects to the session and may wake up the service if it's not available.
|
||||
*
|
||||
* @param context context
|
||||
* @param token token to connect to
|
||||
* @param connectionHints a session-specific argument to send to the session when connecting.
|
||||
* The contents of this bundle may affect the connection result.
|
||||
* @param executor executor to run callbacks on.
|
||||
* @param callback controller callback to receive changes in.
|
||||
*/
|
||||
MediaController2(@NonNull Context context, @NonNull Session2Token token,
|
||||
@NonNull Bundle connectionHints, @NonNull Executor executor,
|
||||
@NonNull ControllerCallback callback) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context shouldn't be null");
|
||||
}
|
||||
if (token == null) {
|
||||
throw new IllegalArgumentException("token shouldn't be null");
|
||||
}
|
||||
mContext = context;
|
||||
mSessionToken = token;
|
||||
mCallbackExecutor = (executor == null) ? context.getMainExecutor() : executor;
|
||||
mCallback = (callback == null) ? new ControllerCallback() {} : callback;
|
||||
mControllerStub = new Controller2Link(this);
|
||||
// NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
|
||||
mResultHandler = new Handler(context.getMainLooper());
|
||||
|
||||
mNextSeqNumber = 0;
|
||||
mPendingCommands = new ArrayMap<>();
|
||||
mRequestedCommandSeqNumbers = new ArraySet<>();
|
||||
|
||||
boolean connectRequested;
|
||||
if (token.getType() == TYPE_SESSION) {
|
||||
mServiceConnection = null;
|
||||
connectRequested = requestConnectToSession(connectionHints);
|
||||
} else {
|
||||
mServiceConnection = new SessionServiceConnection(connectionHints);
|
||||
connectRequested = requestConnectToService();
|
||||
}
|
||||
if (!connectRequested) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (mLock) {
|
||||
if (mClosed) {
|
||||
// Already closed. Ignore rest of clean up code.
|
||||
// Note: unbindService() throws IllegalArgumentException when it's called twice.
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "closing " + this);
|
||||
}
|
||||
mClosed = true;
|
||||
if (mServiceConnection != null) {
|
||||
// Note: This should be called even when the bindService() has returned false.
|
||||
mContext.unbindService(mServiceConnection);
|
||||
}
|
||||
if (mSessionBinder != null) {
|
||||
try {
|
||||
mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
|
||||
mSessionBinder.unlinkToDeath(mDeathRecipient, 0);
|
||||
} catch (RuntimeException e) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
mConnectedToken = null;
|
||||
mPendingCommands.clear();
|
||||
mRequestedCommandSeqNumbers.clear();
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onDisconnected(MediaController2.this);
|
||||
});
|
||||
mSessionBinder = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link Session2Token} of the connected session.
|
||||
* If it is not connected yet, it returns {@code null}.
|
||||
* <p>
|
||||
* This may differ with the {@link Session2Token} from the constructor. For example, if the
|
||||
* controller is created with the token for {@link MediaSession2Service}, this would return
|
||||
* token for the {@link MediaSession2} in the service.
|
||||
*
|
||||
* @return Session2Token of the connected session, or {@code null} if not connected
|
||||
*/
|
||||
@Nullable
|
||||
public Session2Token getConnectedToken() {
|
||||
synchronized (mLock) {
|
||||
return mConnectedToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the session's playback is active.
|
||||
*
|
||||
* @return {@code true} if playback active. {@code false} otherwise.
|
||||
* @see ControllerCallback#onPlaybackActiveChanged(MediaController2, boolean)
|
||||
*/
|
||||
public boolean isPlaybackActive() {
|
||||
synchronized (mLock) {
|
||||
return mPlaybackActive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a session command to the session
|
||||
* <p>
|
||||
* @param command the session command
|
||||
* @param args optional arguments
|
||||
* @return a token which will be sent together in {@link ControllerCallback#onCommandResult}
|
||||
* when its result is received.
|
||||
*/
|
||||
@NonNull
|
||||
public Object sendSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
|
||||
ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
synchronized (mLock) {
|
||||
mPendingCommands.remove(this);
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onCommandResult(MediaController2.this, this,
|
||||
command, new Session2Command.Result(resultCode, resultData));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
synchronized (mLock) {
|
||||
if (mSessionBinder != null) {
|
||||
int seq = getNextSeqNumber();
|
||||
mPendingCommands.put(resultReceiver, seq);
|
||||
try {
|
||||
mSessionBinder.sendSessionCommand(mControllerStub, seq, command, args,
|
||||
resultReceiver);
|
||||
} catch (RuntimeException e) {
|
||||
mPendingCommands.remove(resultReceiver);
|
||||
resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultReceiver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the session command previously sent.
|
||||
*
|
||||
* @param token the token which is returned from {@link #sendSessionCommand}.
|
||||
*/
|
||||
public void cancelSessionCommand(@NonNull Object token) {
|
||||
if (token == null) {
|
||||
throw new IllegalArgumentException("token shouldn't be null");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mSessionBinder == null) return;
|
||||
Integer seq = mPendingCommands.remove(token);
|
||||
if (seq != null) {
|
||||
mSessionBinder.cancelSessionCommand(mControllerStub, seq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called by Controller2Link.onConnected
|
||||
void onConnected(int seq, Bundle connectionResult) {
|
||||
Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK);
|
||||
Session2CommandGroup allowedCommands =
|
||||
connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
|
||||
boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE);
|
||||
|
||||
Bundle tokenExtras = connectionResult.getBundle(KEY_TOKEN_EXTRAS);
|
||||
if (tokenExtras == null) {
|
||||
Log.w(TAG, "extras shouldn't be null.");
|
||||
tokenExtras = Bundle.EMPTY;
|
||||
} else if (MediaSession2.hasCustomParcelable(tokenExtras)) {
|
||||
Log.w(TAG, "extras contain custom parcelable. Ignoring.");
|
||||
tokenExtras = Bundle.EMPTY;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "notifyConnected sessionBinder=" + sessionBinder
|
||||
+ ", allowedCommands=" + allowedCommands);
|
||||
}
|
||||
if (sessionBinder == null || allowedCommands == null) {
|
||||
// Connection rejected.
|
||||
close();
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mSessionBinder = sessionBinder;
|
||||
mAllowedCommands = allowedCommands;
|
||||
mPlaybackActive = playbackActive;
|
||||
|
||||
// Implementation for the local binder is no-op,
|
||||
// so can be used without worrying about deadlock.
|
||||
sessionBinder.linkToDeath(mDeathRecipient, 0);
|
||||
mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION,
|
||||
mSessionToken.getPackageName(), sessionBinder, tokenExtras);
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onConnected(MediaController2.this, allowedCommands);
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Controller2Link.onDisconnected
|
||||
void onDisconnected(int seq) {
|
||||
// close() will call mCallback.onDisconnected
|
||||
close();
|
||||
}
|
||||
|
||||
// Called by Controller2Link.onPlaybackActiveChanged
|
||||
void onPlaybackActiveChanged(int seq, boolean playbackActive) {
|
||||
synchronized (mLock) {
|
||||
mPlaybackActive = playbackActive;
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onPlaybackActiveChanged(MediaController2.this, playbackActive);
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Controller2Link.onSessionCommand
|
||||
void onSessionCommand(int seq, Session2Command command, Bundle args,
|
||||
@Nullable ResultReceiver resultReceiver) {
|
||||
synchronized (mLock) {
|
||||
mRequestedCommandSeqNumbers.add(seq);
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
boolean isCanceled;
|
||||
synchronized (mLock) {
|
||||
isCanceled = !mRequestedCommandSeqNumbers.remove(seq);
|
||||
}
|
||||
if (isCanceled) {
|
||||
if (resultReceiver != null) {
|
||||
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Session2Command.Result result = mCallback.onSessionCommand(
|
||||
MediaController2.this, command, args);
|
||||
if (resultReceiver != null) {
|
||||
if (result == null) {
|
||||
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||||
} else {
|
||||
resultReceiver.send(result.getResultCode(), result.getResultData());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Controller2Link.onSessionCommand
|
||||
void onCancelCommand(int seq) {
|
||||
synchronized (mLock) {
|
||||
mRequestedCommandSeqNumbers.remove(seq);
|
||||
}
|
||||
}
|
||||
|
||||
private int getNextSeqNumber() {
|
||||
synchronized (mLock) {
|
||||
return mNextSeqNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle createConnectionRequest(@NonNull Bundle connectionHints) {
|
||||
Bundle connectionRequest = new Bundle();
|
||||
connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
|
||||
connectionRequest.putInt(KEY_PID, Process.myPid());
|
||||
connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints);
|
||||
return connectionRequest;
|
||||
}
|
||||
|
||||
private boolean requestConnectToSession(@NonNull Bundle connectionHints) {
|
||||
Session2Link sessionBinder = mSessionToken.getSessionLink();
|
||||
Bundle connectionRequest = createConnectionRequest(connectionHints);
|
||||
try {
|
||||
sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to call connection request", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean requestConnectToService() {
|
||||
// Service. Needs to get fresh binder whenever connection is needed.
|
||||
final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
|
||||
intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());
|
||||
|
||||
// Use bindService() instead of startForegroundService() to start session service for three
|
||||
// reasons.
|
||||
// 1. Prevent session service owner's stopSelf() from destroying service.
|
||||
// With the startForegroundService(), service's call of stopSelf() will trigger immediate
|
||||
// onDestroy() calls on the main thread even when onConnect() is running in another
|
||||
// thread.
|
||||
// 2. Minimize APIs for developers to take care about.
|
||||
// With bindService(), developers only need to take care about Service.onBind()
|
||||
// but Service.onStartCommand() should be also taken care about with the
|
||||
// startForegroundService().
|
||||
// 3. Future support for UI-less playback
|
||||
// If a service wants to keep running, it should be either foreground service or
|
||||
// bound service. But there had been request for the feature for system apps
|
||||
// and using bindService() will be better fit with it.
|
||||
synchronized (mLock) {
|
||||
boolean result = mContext.bindService(
|
||||
intent, mServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
if (!result) {
|
||||
Log.w(TAG, "bind to " + mSessionToken + " failed");
|
||||
return false;
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "bind to " + mSessionToken + " succeeded");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Builder for {@link MediaController2}.
|
||||
* <p>
|
||||
* Any incoming event from the {@link MediaSession2} will be handled on the callback
|
||||
* executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private Context mContext;
|
||||
private Session2Token mToken;
|
||||
private Bundle mConnectionHints;
|
||||
private Executor mCallbackExecutor;
|
||||
private ControllerCallback mCallback;
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MediaController2}.
|
||||
*
|
||||
* @param context context
|
||||
* @param token token of the session to connect to
|
||||
*/
|
||||
public Builder(@NonNull Context context, @NonNull Session2Token token) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context shouldn't be null");
|
||||
}
|
||||
if (token == null) {
|
||||
throw new IllegalArgumentException("token shouldn't be null");
|
||||
}
|
||||
mContext = context;
|
||||
mToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection hints for the controller.
|
||||
* <p>
|
||||
* {@code connectionHints} is a session-specific argument to send to the session when
|
||||
* connecting. The contents of this bundle may affect the connection result.
|
||||
* <p>
|
||||
* An {@link IllegalArgumentException} will be thrown if the bundle contains any
|
||||
* non-framework Parcelable objects.
|
||||
*
|
||||
* @param connectionHints a bundle which contains the connection hints
|
||||
* @return The Builder to allow chaining
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setConnectionHints(@NonNull Bundle connectionHints) {
|
||||
if (connectionHints == null) {
|
||||
throw new IllegalArgumentException("connectionHints shouldn't be null");
|
||||
}
|
||||
if (MediaSession2.hasCustomParcelable(connectionHints)) {
|
||||
throw new IllegalArgumentException("connectionHints shouldn't contain any custom "
|
||||
+ "parcelables");
|
||||
}
|
||||
mConnectionHints = new Bundle(connectionHints);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for the controller and its executor.
|
||||
*
|
||||
* @param executor callback executor
|
||||
* @param callback session callback.
|
||||
* @return The Builder to allow chaining
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setControllerCallback(@NonNull Executor executor,
|
||||
@NonNull ControllerCallback callback) {
|
||||
if (executor == null) {
|
||||
throw new IllegalArgumentException("executor shouldn't be null");
|
||||
}
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("callback shouldn't be null");
|
||||
}
|
||||
mCallbackExecutor = executor;
|
||||
mCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {@link MediaController2}.
|
||||
*
|
||||
* @return a new controller
|
||||
*/
|
||||
@NonNull
|
||||
public MediaController2 build() {
|
||||
if (mCallbackExecutor == null) {
|
||||
mCallbackExecutor = mContext.getMainExecutor();
|
||||
}
|
||||
if (mCallback == null) {
|
||||
mCallback = new ControllerCallback() {};
|
||||
}
|
||||
if (mConnectionHints == null) {
|
||||
mConnectionHints = Bundle.EMPTY;
|
||||
}
|
||||
return new MediaController2(
|
||||
mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Interface for listening to change in activeness of the {@link MediaSession2}.
|
||||
*/
|
||||
public abstract static class ControllerCallback {
|
||||
/**
|
||||
* Called when the controller is successfully connected to the session. The controller
|
||||
* becomes available afterwards.
|
||||
*
|
||||
* @param controller the controller for this event
|
||||
* @param allowedCommands commands that's allowed by the session.
|
||||
*/
|
||||
public void onConnected(@NonNull MediaController2 controller,
|
||||
@NonNull Session2CommandGroup allowedCommands) {}
|
||||
|
||||
/**
|
||||
* Called when the session refuses the controller or the controller is disconnected from
|
||||
* the session. The controller becomes unavailable afterwards and the callback wouldn't
|
||||
* be called.
|
||||
* <p>
|
||||
* It will be also called after the {@link #close()}, so you can put clean up code here.
|
||||
* You don't need to call {@link #close()} after this.
|
||||
*
|
||||
* @param controller the controller for this event
|
||||
*/
|
||||
public void onDisconnected(@NonNull MediaController2 controller) {}
|
||||
|
||||
/**
|
||||
* Called when the session's playback activeness is changed.
|
||||
*
|
||||
* @param controller the controller for this event
|
||||
* @param playbackActive {@code true} if the session's playback is active.
|
||||
* {@code false} otherwise.
|
||||
* @see MediaController2#isPlaybackActive()
|
||||
*/
|
||||
public void onPlaybackActiveChanged(@NonNull MediaController2 controller,
|
||||
boolean playbackActive) {}
|
||||
|
||||
/**
|
||||
* Called when the connected session sent a session command.
|
||||
*
|
||||
* @param controller the controller for this event
|
||||
* @param command the session command
|
||||
* @param args optional arguments
|
||||
* @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
|
||||
* will be sent to the session.
|
||||
*/
|
||||
@Nullable
|
||||
public Session2Command.Result onSessionCommand(@NonNull MediaController2 controller,
|
||||
@NonNull Session2Command command, @Nullable Bundle args) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the command sent to the connected session is finished.
|
||||
*
|
||||
* @param controller the controller for this event
|
||||
* @param token the token got from {@link MediaController2#sendSessionCommand}
|
||||
* @param command the session command
|
||||
* @param result the result of the session command
|
||||
*/
|
||||
public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token,
|
||||
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
|
||||
}
|
||||
|
||||
// This will be called on the main thread.
|
||||
private class SessionServiceConnection implements ServiceConnection {
|
||||
private final Bundle mConnectionHints;
|
||||
|
||||
SessionServiceConnection(@Nullable Bundle connectionHints) {
|
||||
mConnectionHints = connectionHints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// Note that it's always main-thread.
|
||||
boolean connectRequested = false;
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onServiceConnected " + name + " " + this);
|
||||
}
|
||||
if (!mSessionToken.getPackageName().equals(name.getPackageName())) {
|
||||
Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName()
|
||||
+ " but is connected to " + name);
|
||||
return;
|
||||
}
|
||||
IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service);
|
||||
if (iService == null) {
|
||||
Log.wtf(TAG, "Service interface is missing.");
|
||||
return;
|
||||
}
|
||||
Bundle connectionRequest = createConnectionRequest(mConnectionHints);
|
||||
iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
|
||||
connectRequested = true;
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Service " + name + " has died prematurely", e);
|
||||
} finally {
|
||||
if (!connectRequested) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
// Temporal lose of the binding because of the service crash. System will automatically
|
||||
// rebind, so just no-op.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Session service " + name + " is disconnected.");
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
// Permanent lose of the binding because of the service package update or removed.
|
||||
// This SessionServiceRecord will be removed accordingly, but forget session binder here
|
||||
// for sure.
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 android.media;
|
||||
|
||||
import android.annotation.StringDef;
|
||||
import android.os.Build;
|
||||
|
||||
import com.android.modules.annotation.MinSdk;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* MediaFeature defines various media features, e.g. hdr type.
|
||||
*/
|
||||
@MinSdk(Build.VERSION_CODES.S)
|
||||
public final class MediaFeature {
|
||||
/**
|
||||
* Defines tye type of HDR(high dynamic range) video.
|
||||
*/
|
||||
public static final class HdrType {
|
||||
private HdrType() {
|
||||
}
|
||||
|
||||
/**
|
||||
* HDR type for dolby-vision.
|
||||
*/
|
||||
public static final String DOLBY_VISION = "android.media.feature.hdr.dolby_vision";
|
||||
/**
|
||||
* HDR type for hdr10.
|
||||
*/
|
||||
public static final String HDR10 = "android.media.feature.hdr.hdr10";
|
||||
/**
|
||||
* HDR type for hdr10+.
|
||||
*/
|
||||
public static final String HDR10_PLUS = "android.media.feature.hdr.hdr10_plus";
|
||||
/**
|
||||
* HDR type for hlg.
|
||||
*/
|
||||
public static final String HLG = "android.media.feature.hdr.hlg";
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@StringDef({
|
||||
HdrType.DOLBY_VISION,
|
||||
HdrType.HDR10,
|
||||
HdrType.HDR10_PLUS,
|
||||
HdrType.HLG,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface MediaHdrType {
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.SystemApi.Client;
|
||||
import android.app.SystemServiceRegistry;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import com.android.modules.annotation.MinSdk;
|
||||
import com.android.modules.utils.build.SdkLevel;
|
||||
|
||||
/**
|
||||
* Class for performing registration for all media services on com.android.media apex.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@MinSdk(Build.VERSION_CODES.S)
|
||||
@SystemApi(client = Client.MODULE_LIBRARIES)
|
||||
public class MediaFrameworkInitializer {
|
||||
private MediaFrameworkInitializer() {
|
||||
}
|
||||
|
||||
private static volatile MediaServiceManager sMediaServiceManager;
|
||||
|
||||
/**
|
||||
* Sets an instance of {@link MediaServiceManager} that allows
|
||||
* the media mainline module to register/obtain media binder services. This is called
|
||||
* by the platform during the system initialization.
|
||||
*
|
||||
* @param mediaServiceManager instance of {@link MediaServiceManager} that allows
|
||||
* the media mainline module to register/obtain media binder services.
|
||||
*/
|
||||
public static void setMediaServiceManager(
|
||||
@NonNull MediaServiceManager mediaServiceManager) {
|
||||
if (sMediaServiceManager != null) {
|
||||
throw new IllegalStateException("setMediaServiceManager called twice!");
|
||||
}
|
||||
|
||||
if (mediaServiceManager == null) {
|
||||
throw new NullPointerException("mediaServiceManager is null!");
|
||||
}
|
||||
|
||||
sMediaServiceManager = mediaServiceManager;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public static MediaServiceManager getMediaServiceManager() {
|
||||
return sMediaServiceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link SystemServiceRegistry}'s static initializer and registers all media
|
||||
* services to {@link Context}, so that {@link Context#getSystemService} can return them.
|
||||
*
|
||||
* @throws IllegalStateException if this is called from anywhere besides
|
||||
* {@link SystemServiceRegistry}
|
||||
*/
|
||||
public static void registerServiceWrappers() {
|
||||
SystemServiceRegistry.registerContextAwareService(
|
||||
Context.MEDIA_TRANSCODING_SERVICE,
|
||||
MediaTranscodingManager.class,
|
||||
context -> new MediaTranscodingManager(context)
|
||||
);
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
SystemServiceRegistry.registerContextAwareService(
|
||||
Context.MEDIA_COMMUNICATION_SERVICE,
|
||||
MediaCommunicationManager.class,
|
||||
context -> new MediaCommunicationManager(context)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.SystemApi;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a copied version of MediaParceledListSlice in framework with hidden API usages removed,
|
||||
* and also with some lint error fixed.
|
||||
*
|
||||
* Transfer a large list of Parcelable objects across an IPC. Splits into
|
||||
* multiple transactions if needed.
|
||||
*
|
||||
* TODO: Remove this from @SystemApi once all the MediaSession related classes are moved
|
||||
* to apex (or ParceledListSlice moved to apex). This class is temporaily added to system API
|
||||
* for moving classes step by step.
|
||||
*
|
||||
* @param <T> The type of the elements in the list.
|
||||
* @see BaseMediaParceledListSlice
|
||||
* @deprecated This is temporary marked as @SystemApi. Should be removed from the API surface.
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
|
||||
public final class MediaParceledListSlice<T extends Parcelable>
|
||||
extends BaseMediaParceledListSlice<T> {
|
||||
public MediaParceledListSlice(@NonNull List<T> list) {
|
||||
super(list);
|
||||
}
|
||||
|
||||
private MediaParceledListSlice(Parcel in, ClassLoader loader) {
|
||||
super(in, loader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T extends Parcelable> MediaParceledListSlice<T> emptyList() {
|
||||
return new MediaParceledListSlice<T>(Collections.<T> emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
int contents = 0;
|
||||
final List<T> list = getList();
|
||||
for (int i=0; i<list.size(); i++) {
|
||||
contents |= list.get(i).describeContents();
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
void writeElement(T parcelable, Parcel dest, int callFlags) {
|
||||
parcelable.writeToParcel(dest, callFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
void writeParcelableCreator(T parcelable, Parcel dest) {
|
||||
dest.writeParcelableCreator((Parcelable) parcelable);
|
||||
}
|
||||
|
||||
@Override
|
||||
Parcelable.Creator<?> readParcelableCreator(Parcel from, ClassLoader loader) {
|
||||
return from.readParcelableCreator(loader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public static final Parcelable.ClassLoaderCreator<MediaParceledListSlice> CREATOR =
|
||||
new Parcelable.ClassLoaderCreator<MediaParceledListSlice>() {
|
||||
public MediaParceledListSlice createFromParcel(Parcel in) {
|
||||
return new MediaParceledListSlice(in, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaParceledListSlice createFromParcel(Parcel in, ClassLoader loader) {
|
||||
return new MediaParceledListSlice(in, loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaParceledListSlice[] newArray(int size) {
|
||||
return new MediaParceledListSlice[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,931 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
|
||||
import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
|
||||
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
|
||||
import static android.media.MediaConstants.KEY_PID;
|
||||
import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
|
||||
import static android.media.MediaConstants.KEY_SESSION2LINK;
|
||||
import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
|
||||
import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
|
||||
import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
|
||||
import static android.media.Session2Token.TYPE_SESSION;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.MediaSessionManager.RemoteUserInfo;
|
||||
import android.os.BadParcelableException;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Parcel;
|
||||
import android.os.Process;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.modules.utils.build.SdkLevel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Allows a media app to expose its transport controls and playback information in a process to
|
||||
* other processes including the Android framework and other apps.
|
||||
*/
|
||||
public class MediaSession2 implements AutoCloseable {
|
||||
static final String TAG = "MediaSession2";
|
||||
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
// Note: This checks the uniqueness of a session ID only in a single process.
|
||||
// When the framework becomes able to check the uniqueness, this logic should be removed.
|
||||
//@GuardedBy("MediaSession.class")
|
||||
private static final List<String> SESSION_ID_LIST = new ArrayList<>();
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final Object mLock = new Object();
|
||||
//@GuardedBy("mLock")
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final Context mContext;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final Executor mCallbackExecutor;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final SessionCallback mCallback;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final Session2Link mSessionStub;
|
||||
|
||||
private final String mSessionId;
|
||||
private final PendingIntent mSessionActivity;
|
||||
private final Session2Token mSessionToken;
|
||||
private final MediaSessionManager mMediaSessionManager;
|
||||
private final MediaCommunicationManager mCommunicationManager;
|
||||
private final Handler mResultHandler;
|
||||
|
||||
//@GuardedBy("mLock")
|
||||
private boolean mClosed;
|
||||
//@GuardedBy("mLock")
|
||||
private boolean mPlaybackActive;
|
||||
//@GuardedBy("mLock")
|
||||
private ForegroundServiceEventCallback mForegroundServiceEventCallback;
|
||||
|
||||
MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity,
|
||||
@NonNull Executor callbackExecutor, @NonNull SessionCallback callback,
|
||||
@NonNull Bundle tokenExtras) {
|
||||
synchronized (MediaSession2.class) {
|
||||
if (SESSION_ID_LIST.contains(id)) {
|
||||
throw new IllegalStateException("Session ID must be unique. ID=" + id);
|
||||
}
|
||||
SESSION_ID_LIST.add(id);
|
||||
}
|
||||
|
||||
mContext = context;
|
||||
mSessionId = id;
|
||||
mSessionActivity = sessionActivity;
|
||||
mCallbackExecutor = callbackExecutor;
|
||||
mCallback = callback;
|
||||
mSessionStub = new Session2Link(this);
|
||||
mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
|
||||
mSessionStub, tokenExtras);
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class);
|
||||
mMediaSessionManager = null;
|
||||
} else {
|
||||
mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
|
||||
mCommunicationManager = null;
|
||||
}
|
||||
// NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
|
||||
mResultHandler = new Handler(context.getMainLooper());
|
||||
mClosed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
List<ControllerInfo> controllerInfos;
|
||||
ForegroundServiceEventCallback callback;
|
||||
synchronized (mLock) {
|
||||
if (mClosed) {
|
||||
return;
|
||||
}
|
||||
mClosed = true;
|
||||
controllerInfos = getConnectedControllers();
|
||||
mConnectedControllers.clear();
|
||||
callback = mForegroundServiceEventCallback;
|
||||
mForegroundServiceEventCallback = null;
|
||||
}
|
||||
synchronized (MediaSession2.class) {
|
||||
SESSION_ID_LIST.remove(mSessionId);
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.onSessionClosed(this);
|
||||
}
|
||||
for (ControllerInfo info : controllerInfos) {
|
||||
info.notifyDisconnected();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Should not be here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID
|
||||
*/
|
||||
@NonNull
|
||||
public String getId() {
|
||||
return mSessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Session2Token} for creating {@link MediaController2}.
|
||||
*/
|
||||
@NonNull
|
||||
public Session2Token getToken() {
|
||||
return mSessionToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a session command to all the connected controllers
|
||||
* <p>
|
||||
* @param command the session command
|
||||
* @param args optional arguments
|
||||
*/
|
||||
public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
List<ControllerInfo> controllerInfos = getConnectedControllers();
|
||||
for (ControllerInfo controller : controllerInfos) {
|
||||
controller.sendSessionCommand(command, args, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a session command to a specific controller
|
||||
* <p>
|
||||
* @param controller the controller to get the session command
|
||||
* @param command the session command
|
||||
* @param args optional arguments
|
||||
* @return a token which will be sent together in {@link SessionCallback#onCommandResult}
|
||||
* when its result is received.
|
||||
*/
|
||||
@NonNull
|
||||
public Object sendSessionCommand(@NonNull ControllerInfo controller,
|
||||
@NonNull Session2Command command, @Nullable Bundle args) {
|
||||
if (controller == null) {
|
||||
throw new IllegalArgumentException("controller shouldn't be null");
|
||||
}
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
controller.receiveCommandResult(this);
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onCommandResult(MediaSession2.this, controller, this,
|
||||
command, new Session2Command.Result(resultCode, resultData));
|
||||
});
|
||||
}
|
||||
};
|
||||
controller.sendSessionCommand(command, args, resultReceiver);
|
||||
return resultReceiver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the session command previously sent.
|
||||
*
|
||||
* @param controller the controller to get the session command
|
||||
* @param token the token which is returned from {@link #sendSessionCommand}.
|
||||
*/
|
||||
public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) {
|
||||
if (controller == null) {
|
||||
throw new IllegalArgumentException("controller shouldn't be null");
|
||||
}
|
||||
if (token == null) {
|
||||
throw new IllegalArgumentException("token shouldn't be null");
|
||||
}
|
||||
controller.cancelSessionCommand(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the playback is active (i.e. playing something)
|
||||
*
|
||||
* @param playbackActive {@code true} if the playback active, {@code false} otherwise.
|
||||
**/
|
||||
public void setPlaybackActive(boolean playbackActive) {
|
||||
final ForegroundServiceEventCallback serviceCallback;
|
||||
synchronized (mLock) {
|
||||
if (mPlaybackActive == playbackActive) {
|
||||
return;
|
||||
}
|
||||
mPlaybackActive = playbackActive;
|
||||
serviceCallback = mForegroundServiceEventCallback;
|
||||
}
|
||||
if (serviceCallback != null) {
|
||||
serviceCallback.onPlaybackActiveChanged(this, playbackActive);
|
||||
}
|
||||
List<ControllerInfo> controllerInfos = getConnectedControllers();
|
||||
for (ControllerInfo controller : controllerInfos) {
|
||||
controller.notifyPlaybackActiveChanged(playbackActive);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whehther the playback is active (i.e. playing something)
|
||||
*
|
||||
* @return {@code true} if the playback active, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isPlaybackActive() {
|
||||
synchronized (mLock) {
|
||||
return mPlaybackActive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of the connected controllers
|
||||
*
|
||||
* @return list of the connected controllers.
|
||||
*/
|
||||
@NonNull
|
||||
public List<ControllerInfo> getConnectedControllers() {
|
||||
List<ControllerInfo> controllers = new ArrayList<>();
|
||||
synchronized (mLock) {
|
||||
controllers.addAll(mConnectedControllers.values());
|
||||
}
|
||||
return controllers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given bundle includes non-framework Parcelables.
|
||||
*/
|
||||
static boolean hasCustomParcelable(@Nullable Bundle bundle) {
|
||||
if (bundle == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try writing the bundle to parcel, and read it with framework classloader.
|
||||
Parcel parcel = null;
|
||||
try {
|
||||
parcel = Parcel.obtain();
|
||||
parcel.writeBundle(bundle);
|
||||
parcel.setDataPosition(0);
|
||||
Bundle out = parcel.readBundle(null);
|
||||
|
||||
// Calling Bundle#size() will trigger Bundle#unparcel().
|
||||
out.size();
|
||||
} catch (BadParcelableException e) {
|
||||
Log.d(TAG, "Custom parcelable in bundle.", e);
|
||||
return true;
|
||||
} finally {
|
||||
if (parcel != null) {
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isClosed() {
|
||||
synchronized (mLock) {
|
||||
return mClosed;
|
||||
}
|
||||
}
|
||||
|
||||
SessionCallback getCallback() {
|
||||
return mCallback;
|
||||
}
|
||||
|
||||
boolean isTrustedForMediaControl(RemoteUserInfo remoteUserInfo) {
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
return mCommunicationManager.isTrustedForMediaControl(remoteUserInfo);
|
||||
} else {
|
||||
return mMediaSessionManager.isTrustedForMediaControl(remoteUserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
|
||||
synchronized (mLock) {
|
||||
if (mForegroundServiceEventCallback == callback) {
|
||||
return;
|
||||
}
|
||||
if (mForegroundServiceEventCallback != null && callback != null) {
|
||||
throw new IllegalStateException("A session cannot be added to multiple services");
|
||||
}
|
||||
mForegroundServiceEventCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
|
||||
void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
|
||||
Bundle connectionRequest) {
|
||||
if (callingPid == 0) {
|
||||
// The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
|
||||
// the remote process. If it's the case, use PID from the connectionRequest.
|
||||
callingPid = connectionRequest.getInt(KEY_PID);
|
||||
}
|
||||
String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
|
||||
|
||||
RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
|
||||
|
||||
Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
|
||||
if (connectionHints == null) {
|
||||
Log.w(TAG, "connectionHints shouldn't be null.");
|
||||
connectionHints = Bundle.EMPTY;
|
||||
} else if (hasCustomParcelable(connectionHints)) {
|
||||
Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
|
||||
connectionHints = Bundle.EMPTY;
|
||||
}
|
||||
|
||||
final ControllerInfo controllerInfo = new ControllerInfo(
|
||||
remoteUserInfo,
|
||||
isTrustedForMediaControl(remoteUserInfo),
|
||||
controller,
|
||||
connectionHints);
|
||||
mCallbackExecutor.execute(() -> {
|
||||
boolean connected = false;
|
||||
try {
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.mAllowedCommands =
|
||||
mCallback.onConnect(MediaSession2.this, controllerInfo);
|
||||
// Don't reject connection for the request from trusted app.
|
||||
// Otherwise server will fail to retrieve session's information to dispatch
|
||||
// media keys to.
|
||||
if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) {
|
||||
return;
|
||||
}
|
||||
if (controllerInfo.mAllowedCommands == null) {
|
||||
// For trusted apps, send non-null allowed commands to keep
|
||||
// connection.
|
||||
controllerInfo.mAllowedCommands =
|
||||
new Session2CommandGroup.Builder().build();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Accepting connection: " + controllerInfo);
|
||||
}
|
||||
// If connection is accepted, notify the current state to the controller.
|
||||
// It's needed because we cannot call synchronous calls between
|
||||
// session/controller.
|
||||
Bundle connectionResult = new Bundle();
|
||||
connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
|
||||
connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
|
||||
controllerInfo.mAllowedCommands);
|
||||
connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
|
||||
connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras());
|
||||
|
||||
// Double check if session is still there, because close() can be called in
|
||||
// another thread.
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.notifyConnected(connectionResult);
|
||||
synchronized (mLock) {
|
||||
if (mConnectedControllers.containsKey(controller)) {
|
||||
Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
|
||||
+ " request multiple times");
|
||||
}
|
||||
mConnectedControllers.put(controller, controllerInfo);
|
||||
}
|
||||
mCallback.onPostConnect(MediaSession2.this, controllerInfo);
|
||||
connected = true;
|
||||
} finally {
|
||||
if (!connected || isClosed()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Rejecting connection or notifying that session is closed"
|
||||
+ ", controllerInfo=" + controllerInfo);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mConnectedControllers.remove(controller);
|
||||
}
|
||||
controllerInfo.notifyDisconnected();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onDisconnect
|
||||
void onDisconnect(@NonNull final Controller2Link controller, int seq) {
|
||||
final ControllerInfo controllerInfo;
|
||||
synchronized (mLock) {
|
||||
controllerInfo = mConnectedControllers.remove(controller);
|
||||
}
|
||||
if (controllerInfo == null) {
|
||||
return;
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onDisconnected(MediaSession2.this, controllerInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onSessionCommand
|
||||
void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
|
||||
final Session2Command command, final Bundle args,
|
||||
@Nullable ResultReceiver resultReceiver) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
final ControllerInfo controllerInfo;
|
||||
synchronized (mLock) {
|
||||
controllerInfo = mConnectedControllers.get(controller);
|
||||
}
|
||||
if (controllerInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: check allowed commands.
|
||||
synchronized (mLock) {
|
||||
controllerInfo.addRequestedCommandSeqNumber(seq);
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
|
||||
if (resultReceiver != null) {
|
||||
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Session2Command.Result result = mCallback.onSessionCommand(
|
||||
MediaSession2.this, controllerInfo, command, args);
|
||||
if (resultReceiver != null) {
|
||||
if (result == null) {
|
||||
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||||
} else {
|
||||
resultReceiver.send(result.getResultCode(), result.getResultData());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onCancelCommand
|
||||
void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
|
||||
final ControllerInfo controllerInfo;
|
||||
synchronized (mLock) {
|
||||
controllerInfo = mConnectedControllers.get(controller);
|
||||
}
|
||||
if (controllerInfo == null) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.removeRequestedCommandSeqNumber(seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Builder for {@link MediaSession2}.
|
||||
* <p>
|
||||
* Any incoming event from the {@link MediaController2} will be handled on the callback
|
||||
* executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private Context mContext;
|
||||
private String mId;
|
||||
private PendingIntent mSessionActivity;
|
||||
private Executor mCallbackExecutor;
|
||||
private SessionCallback mCallback;
|
||||
private Bundle mExtras;
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MediaSession2}.
|
||||
*
|
||||
* @param context Context
|
||||
* @throws IllegalArgumentException if context is {@code null}.
|
||||
*/
|
||||
public Builder(@NonNull Context context) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context shouldn't be null");
|
||||
}
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an intent for launching UI for this Session. This can be used as a
|
||||
* quick link to an ongoing media screen. The intent should be for an
|
||||
* activity that may be started using {@link Context#startActivity(Intent)}.
|
||||
*
|
||||
* @param pi The intent to launch to show UI for this session.
|
||||
* @return The Builder to allow chaining
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setSessionActivity(@Nullable PendingIntent pi) {
|
||||
mSessionActivity = pi;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ID of the session. If it's not set, an empty string will be used to create a session.
|
||||
* <p>
|
||||
* Use this if and only if your app supports multiple playback at the same time and also
|
||||
* wants to provide external apps to have finer controls of them.
|
||||
*
|
||||
* @param id id of the session. Must be unique per package.
|
||||
* @throws IllegalArgumentException if id is {@code null}.
|
||||
* @return The Builder to allow chaining
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setId(@NonNull String id) {
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("id shouldn't be null");
|
||||
}
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for the session and its executor.
|
||||
*
|
||||
* @param executor callback executor
|
||||
* @param callback session callback.
|
||||
* @return The Builder to allow chaining
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setSessionCallback(@NonNull Executor executor,
|
||||
@NonNull SessionCallback callback) {
|
||||
mCallbackExecutor = executor;
|
||||
mCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extras for the session token. If null or not set, {@link Session2Token#getExtras()}
|
||||
* will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown
|
||||
* if the bundle contains any non-framework Parcelable objects.
|
||||
*
|
||||
* @return The Builder to allow chaining
|
||||
* @see Session2Token#getExtras()
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setExtras(@NonNull Bundle extras) {
|
||||
if (extras == null) {
|
||||
throw new NullPointerException("extras shouldn't be null");
|
||||
}
|
||||
if (hasCustomParcelable(extras)) {
|
||||
throw new IllegalArgumentException(
|
||||
"extras shouldn't contain any custom parcelables");
|
||||
}
|
||||
mExtras = new Bundle(extras);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {@link MediaSession2}.
|
||||
*
|
||||
* @return a new session
|
||||
* @throws IllegalStateException if the session with the same id is already exists for the
|
||||
* package.
|
||||
*/
|
||||
@NonNull
|
||||
public MediaSession2 build() {
|
||||
if (mCallbackExecutor == null) {
|
||||
mCallbackExecutor = mContext.getMainExecutor();
|
||||
}
|
||||
if (mCallback == null) {
|
||||
mCallback = new SessionCallback() {};
|
||||
}
|
||||
if (mId == null) {
|
||||
mId = "";
|
||||
}
|
||||
if (mExtras == null) {
|
||||
mExtras = Bundle.EMPTY;
|
||||
}
|
||||
MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity,
|
||||
mCallbackExecutor, mCallback, mExtras);
|
||||
|
||||
// Notify framework about the newly create session after the constructor is finished.
|
||||
// Otherwise, framework may access the session before the initialization is finished.
|
||||
try {
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
MediaCommunicationManager manager =
|
||||
mContext.getSystemService(MediaCommunicationManager.class);
|
||||
manager.notifySession2Created(session2.getToken());
|
||||
} else {
|
||||
MediaSessionManager manager =
|
||||
mContext.getSystemService(MediaSessionManager.class);
|
||||
manager.notifySession2Created(session2.getToken());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
session2.close();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return session2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Information of a controller.
|
||||
*/
|
||||
public static final class ControllerInfo {
|
||||
private final RemoteUserInfo mRemoteUserInfo;
|
||||
private final boolean mIsTrusted;
|
||||
private final Controller2Link mControllerBinder;
|
||||
private final Bundle mConnectionHints;
|
||||
private final Object mLock = new Object();
|
||||
//@GuardedBy("mLock")
|
||||
private int mNextSeqNumber;
|
||||
//@GuardedBy("mLock")
|
||||
private ArrayMap<ResultReceiver, Integer> mPendingCommands;
|
||||
//@GuardedBy("mLock")
|
||||
private ArraySet<Integer> mRequestedCommandSeqNumbers;
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
Session2CommandGroup mAllowedCommands;
|
||||
|
||||
/**
|
||||
* @param remoteUserInfo remote user info
|
||||
* @param trusted {@code true} if trusted, {@code false} otherwise
|
||||
* @param controllerBinder Controller2Link for the connected controller.
|
||||
* @param connectionHints a session-specific argument sent from the controller for the
|
||||
* connection. The contents of this bundle may affect the
|
||||
* connection result.
|
||||
*/
|
||||
ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
|
||||
@Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) {
|
||||
mRemoteUserInfo = remoteUserInfo;
|
||||
mIsTrusted = trusted;
|
||||
mControllerBinder = controllerBinder;
|
||||
mConnectionHints = connectionHints;
|
||||
mPendingCommands = new ArrayMap<>();
|
||||
mRequestedCommandSeqNumbers = new ArraySet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return remote user info of the controller.
|
||||
*/
|
||||
@NonNull
|
||||
public RemoteUserInfo getRemoteUserInfo() {
|
||||
return mRemoteUserInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return package name of the controller.
|
||||
*/
|
||||
@NonNull
|
||||
public String getPackageName() {
|
||||
return mRemoteUserInfo.getPackageName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return uid of the controller. Can be a negative value if the uid cannot be obtained.
|
||||
*/
|
||||
public int getUid() {
|
||||
return mRemoteUserInfo.getUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return connection hints sent from controller.
|
||||
*/
|
||||
@NonNull
|
||||
public Bundle getConnectionHints() {
|
||||
return new Bundle(mConnectionHints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
|
||||
* has a enabled notification listener so can be trusted to accept connection and incoming
|
||||
* command request.
|
||||
*
|
||||
* @return {@code true} if the controller is trusted.
|
||||
* @hide
|
||||
*/
|
||||
public boolean isTrusted() {
|
||||
return mIsTrusted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mControllerBinder, mRemoteUserInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof ControllerInfo)) return false;
|
||||
if (this == obj) return true;
|
||||
|
||||
ControllerInfo other = (ControllerInfo) obj;
|
||||
if (mControllerBinder != null || other.mControllerBinder != null) {
|
||||
return Objects.equals(mControllerBinder, other.mControllerBinder);
|
||||
}
|
||||
return mRemoteUserInfo.equals(other.mRemoteUserInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
|
||||
+ mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
|
||||
}
|
||||
|
||||
void notifyConnected(Bundle connectionResult) {
|
||||
if (mControllerBinder == null) return;
|
||||
|
||||
try {
|
||||
mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult);
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
}
|
||||
}
|
||||
|
||||
void notifyDisconnected() {
|
||||
if (mControllerBinder == null) return;
|
||||
|
||||
try {
|
||||
mControllerBinder.notifyDisconnected(getNextSeqNumber());
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
}
|
||||
}
|
||||
|
||||
void notifyPlaybackActiveChanged(boolean playbackActive) {
|
||||
if (mControllerBinder == null) return;
|
||||
|
||||
try {
|
||||
mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive);
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
}
|
||||
}
|
||||
|
||||
void sendSessionCommand(Session2Command command, Bundle args,
|
||||
ResultReceiver resultReceiver) {
|
||||
if (mControllerBinder == null) return;
|
||||
|
||||
try {
|
||||
int seq = getNextSeqNumber();
|
||||
synchronized (mLock) {
|
||||
mPendingCommands.put(resultReceiver, seq);
|
||||
}
|
||||
mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver);
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
synchronized (mLock) {
|
||||
mPendingCommands.remove(resultReceiver);
|
||||
}
|
||||
resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
void cancelSessionCommand(@NonNull Object token) {
|
||||
if (mControllerBinder == null) return;
|
||||
Integer seq;
|
||||
synchronized (mLock) {
|
||||
seq = mPendingCommands.remove(token);
|
||||
}
|
||||
if (seq != null) {
|
||||
mControllerBinder.cancelSessionCommand(seq);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveCommandResult(ResultReceiver resultReceiver) {
|
||||
synchronized (mLock) {
|
||||
mPendingCommands.remove(resultReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
void addRequestedCommandSeqNumber(int seq) {
|
||||
synchronized (mLock) {
|
||||
mRequestedCommandSeqNumbers.add(seq);
|
||||
}
|
||||
}
|
||||
|
||||
boolean removeRequestedCommandSeqNumber(int seq) {
|
||||
synchronized (mLock) {
|
||||
return mRequestedCommandSeqNumbers.remove(seq);
|
||||
}
|
||||
}
|
||||
|
||||
private int getNextSeqNumber() {
|
||||
synchronized (mLock) {
|
||||
return mNextSeqNumber++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Callback to be called for all incoming commands from {@link MediaController2}s.
|
||||
*/
|
||||
public abstract static class SessionCallback {
|
||||
/**
|
||||
* Called when a controller is created for this session. Return allowed commands for
|
||||
* controller. By default it returns {@code null}.
|
||||
* <p>
|
||||
* You can reject the connection by returning {@code null}. In that case, controller
|
||||
* receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
|
||||
* and cannot be used.
|
||||
* <p>
|
||||
* The controller hasn't connected yet in this method, so calls to the controller
|
||||
* (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for
|
||||
* the custom initialization for the controller instead.
|
||||
*
|
||||
* @param session the session for this event
|
||||
* @param controller controller information.
|
||||
* @return allowed commands. Can be {@code null} to reject connection.
|
||||
*/
|
||||
@Nullable
|
||||
public Session2CommandGroup onConnect(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after a controller is connected. This is a convenient method to add
|
||||
* custom initialization between the session and a controller.
|
||||
* <p>
|
||||
* Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't
|
||||
* work in {@link #onConnect} because the controller hasn't connected yet in
|
||||
* {@link #onConnect}.
|
||||
*
|
||||
* @param session the session for this event
|
||||
* @param controller controller information.
|
||||
*/
|
||||
public void onPostConnect(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a controller is disconnected
|
||||
*
|
||||
* @param session the session for this event
|
||||
* @param controller controller information
|
||||
*/
|
||||
public void onDisconnected(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller) {}
|
||||
|
||||
/**
|
||||
* Called when a controller sent a session command.
|
||||
*
|
||||
* @param session the session for this event
|
||||
* @param controller controller information
|
||||
* @param command the session command
|
||||
* @param args optional arguments
|
||||
* @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
|
||||
* will be sent to the session.
|
||||
*/
|
||||
@Nullable
|
||||
public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller, @NonNull Session2Command command,
|
||||
@Nullable Bundle args) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the command sent to the controller is finished.
|
||||
*
|
||||
* @param session the session for this event
|
||||
* @param controller controller information
|
||||
* @param token the token got from {@link MediaSession2#sendSessionCommand}
|
||||
* @param command the session command
|
||||
* @param result the result of the session command
|
||||
*/
|
||||
public void onCommandResult(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller, @NonNull Object token,
|
||||
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
|
||||
}
|
||||
|
||||
abstract static class ForegroundServiceEventCallback {
|
||||
public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
|
||||
public void onSessionClosed(MediaSession2 session) {}
|
||||
}
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
/*
|
||||
* 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 android.media;
|
||||
|
||||
import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
|
||||
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
|
||||
import static android.media.MediaConstants.KEY_PID;
|
||||
|
||||
import android.annotation.CallSuper;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.MediaSession2.ControllerInfo;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.MediaSessionManager.RemoteUserInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Service containing {@link MediaSession2}.
|
||||
*/
|
||||
public abstract class MediaSession2Service extends Service {
|
||||
/**
|
||||
* The {@link Intent} that must be declared as handled by the service.
|
||||
*/
|
||||
public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
|
||||
|
||||
private static final String TAG = "MediaSession2Service";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback =
|
||||
new MediaSession2.ForegroundServiceEventCallback() {
|
||||
@Override
|
||||
public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
|
||||
MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionClosed(MediaSession2 session) {
|
||||
removeSession(session);
|
||||
}
|
||||
};
|
||||
|
||||
private final Object mLock = new Object();
|
||||
//@GuardedBy("mLock")
|
||||
private NotificationManager mNotificationManager;
|
||||
//@GuardedBy("mLock")
|
||||
private MediaSessionManager mMediaSessionManager;
|
||||
//@GuardedBy("mLock")
|
||||
private Intent mStartSelfIntent;
|
||||
//@GuardedBy("mLock")
|
||||
private Map<String, MediaSession2> mSessions = new ArrayMap<>();
|
||||
//@GuardedBy("mLock")
|
||||
private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>();
|
||||
//@GuardedBy("mLock")
|
||||
private MediaSession2ServiceStub mStub;
|
||||
|
||||
/**
|
||||
* Called by the system when the service is first created. Do not call this method directly.
|
||||
* <p>
|
||||
* Override this method if you need your own initialization. Derived classes MUST call through
|
||||
* to the super class's implementation of this method.
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
synchronized (mLock) {
|
||||
mStub = new MediaSession2ServiceStub(this);
|
||||
mStartSelfIntent = new Intent(this, this.getClass());
|
||||
mNotificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mMediaSessionManager =
|
||||
(MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
@Nullable
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
if (SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
synchronized (mLock) {
|
||||
return mStub;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the system to notify that it is no longer used and is being removed. Do not call
|
||||
* this method directly.
|
||||
* <p>
|
||||
* Override this method if you need your own clean up. Derived classes MUST call through
|
||||
* to the super class's implementation of this method.
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
synchronized (mLock) {
|
||||
List<MediaSession2> sessions = getSessions();
|
||||
for (MediaSession2 session : sessions) {
|
||||
removeSession(session);
|
||||
}
|
||||
mSessions.clear();
|
||||
mNotifications.clear();
|
||||
}
|
||||
mStub.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a {@link MediaController2} is created with the this service's
|
||||
* {@link Session2Token}. Return the session for telling the controller which session to
|
||||
* connect. Return {@code null} to reject the connection from this controller.
|
||||
* <p>
|
||||
* Session returned here will be added to this service automatically. You don't need to call
|
||||
* {@link #addSession(MediaSession2)} for that.
|
||||
* <p>
|
||||
* This method is always called on the main thread.
|
||||
*
|
||||
* @param controllerInfo information of the controller which is trying to connect.
|
||||
* @return a {@link MediaSession2} instance for the controller to connect to, or {@code null}
|
||||
* to reject connection
|
||||
* @see MediaSession2.Builder
|
||||
* @see #getSessions()
|
||||
*/
|
||||
@Nullable
|
||||
public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo);
|
||||
|
||||
/**
|
||||
* Called when notification UI needs update. Override this method to show or cancel your own
|
||||
* notification UI.
|
||||
* <p>
|
||||
* This would be called on {@link MediaSession2}'s callback executor when playback state is
|
||||
* changed.
|
||||
* <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.
|
||||
*
|
||||
* @param session a session that needs notification update.
|
||||
* @return a {@link MediaNotification}. Can be {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session);
|
||||
|
||||
/**
|
||||
* Adds a session to this service.
|
||||
* <p>
|
||||
* Added session will be removed automatically when it's closed, or removed when
|
||||
* {@link #removeSession} is called.
|
||||
*
|
||||
* @param session a session to be added.
|
||||
* @see #removeSession(MediaSession2)
|
||||
*/
|
||||
public final void addSession(@NonNull MediaSession2 session) {
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("session shouldn't be null");
|
||||
}
|
||||
if (session.isClosed()) {
|
||||
throw new IllegalArgumentException("session is already closed");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
MediaSession2 previousSession = mSessions.get(session.getId());
|
||||
if (previousSession != null) {
|
||||
if (previousSession != session) {
|
||||
Log.w(TAG, "Session ID should be unique, ID=" + session.getId()
|
||||
+ ", previous=" + previousSession + ", session=" + session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
mSessions.put(session.getId(), session);
|
||||
session.setForegroundServiceEventCallback(mForegroundServiceEventCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a session from this service.
|
||||
*
|
||||
* @param session a session to be removed.
|
||||
* @see #addSession(MediaSession2)
|
||||
*/
|
||||
public final void removeSession(@NonNull MediaSession2 session) {
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("session shouldn't be null");
|
||||
}
|
||||
MediaNotification notification;
|
||||
synchronized (mLock) {
|
||||
if (mSessions.get(session.getId()) != session) {
|
||||
// Session isn't added or removed already.
|
||||
return;
|
||||
}
|
||||
mSessions.remove(session.getId());
|
||||
notification = mNotifications.remove(session);
|
||||
}
|
||||
session.setForegroundServiceEventCallback(null);
|
||||
if (notification != null) {
|
||||
mNotificationManager.cancel(notification.getNotificationId());
|
||||
}
|
||||
if (getSessions().isEmpty()) {
|
||||
stopForeground(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of {@link MediaSession2}s that you've added to this service.
|
||||
*
|
||||
* @return sessions
|
||||
*/
|
||||
public final @NonNull List<MediaSession2> getSessions() {
|
||||
List<MediaSession2> list = new ArrayList<>();
|
||||
synchronized (mLock) {
|
||||
list.addAll(mSessions.values());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MediaSessionManager}.
|
||||
*/
|
||||
@NonNull
|
||||
MediaSessionManager getMediaSessionManager() {
|
||||
synchronized (mLock) {
|
||||
return mMediaSessionManager;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by registered {@link MediaSession2.ForegroundServiceEventCallback}
|
||||
*
|
||||
* @param session session with change
|
||||
* @param playbackActive {@code true} if playback is active.
|
||||
*/
|
||||
void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
|
||||
MediaNotification mediaNotification = onUpdateNotification(session);
|
||||
if (mediaNotification == null) {
|
||||
// The service implementation doesn't want to use the automatic start/stopForeground
|
||||
// feature.
|
||||
return;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mNotifications.put(session, mediaNotification);
|
||||
}
|
||||
int id = mediaNotification.getNotificationId();
|
||||
Notification notification = mediaNotification.getNotification();
|
||||
if (!playbackActive) {
|
||||
mNotificationManager.notify(id, notification);
|
||||
return;
|
||||
}
|
||||
// playbackActive == true
|
||||
startForegroundService(mStartSelfIntent);
|
||||
startForeground(id, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Returned by {@link #onUpdateNotification(MediaSession2)} for making session service
|
||||
* foreground service to keep playback running in the background. It's highly recommended to
|
||||
* show media style notification here.
|
||||
*/
|
||||
public static class MediaNotification {
|
||||
private final int mNotificationId;
|
||||
private final Notification mNotification;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param notificationId notification id to be used for
|
||||
* {@link NotificationManager#notify(int, Notification)}.
|
||||
* @param notification a notification to make session service run in the foreground. Media
|
||||
* style notification is recommended here.
|
||||
*/
|
||||
public MediaNotification(int notificationId, @NonNull Notification notification) {
|
||||
if (notification == null) {
|
||||
throw new IllegalArgumentException("notification shouldn't be null");
|
||||
}
|
||||
mNotificationId = notificationId;
|
||||
mNotification = notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id of the notification.
|
||||
*
|
||||
* @return the notification id
|
||||
*/
|
||||
public int getNotificationId() {
|
||||
return mNotificationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the notification.
|
||||
*
|
||||
* @return the notification
|
||||
*/
|
||||
@NonNull
|
||||
public Notification getNotification() {
|
||||
return mNotification;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub
|
||||
implements AutoCloseable {
|
||||
final WeakReference<MediaSession2Service> mService;
|
||||
final Handler mHandler;
|
||||
|
||||
MediaSession2ServiceStub(MediaSession2Service service) {
|
||||
mService = new WeakReference<>(service);
|
||||
mHandler = new Handler(service.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
if (mService.get() == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Service is already destroyed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (caller == null || connectionRequest == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Ignoring calls with illegal arguments, caller=" + caller
|
||||
+ ", connectionRequest=" + connectionRequest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
mHandler.post(() -> {
|
||||
boolean shouldNotifyDisconnected = true;
|
||||
try {
|
||||
final MediaSession2Service service = mService.get();
|
||||
if (service == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Service isn't available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
|
||||
// The Binder.getCallingPid() can be 0 for an oneway call from the
|
||||
// remote process. If it's the case, use PID from the connectionRequest.
|
||||
RemoteUserInfo remoteUserInfo = new RemoteUserInfo(
|
||||
callingPkg,
|
||||
pid == 0 ? connectionRequest.getInt(KEY_PID) : pid,
|
||||
uid);
|
||||
|
||||
Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
|
||||
if (connectionHints == null) {
|
||||
Log.w(TAG, "connectionHints shouldn't be null.");
|
||||
connectionHints = Bundle.EMPTY;
|
||||
} else if (MediaSession2.hasCustomParcelable(connectionHints)) {
|
||||
Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
|
||||
connectionHints = Bundle.EMPTY;
|
||||
}
|
||||
|
||||
final ControllerInfo controllerInfo = new ControllerInfo(
|
||||
remoteUserInfo,
|
||||
service.getMediaSessionManager()
|
||||
.isTrustedForMediaControl(remoteUserInfo),
|
||||
caller,
|
||||
connectionHints);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Handling incoming connection request from the"
|
||||
+ " controller=" + controllerInfo);
|
||||
}
|
||||
|
||||
final MediaSession2 session;
|
||||
session = service.onGetSession(controllerInfo);
|
||||
|
||||
if (session == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Rejecting incoming connection request from the"
|
||||
+ " controller=" + controllerInfo);
|
||||
}
|
||||
// Note: Trusted controllers also can be rejected according to the
|
||||
// service implementation.
|
||||
return;
|
||||
}
|
||||
service.addSession(session);
|
||||
shouldNotifyDisconnected = false;
|
||||
session.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
} catch (Exception e) {
|
||||
// Don't propagate exception in service to the controller.
|
||||
Log.w(TAG, "Failed to add a session to session service", e);
|
||||
} finally {
|
||||
// Trick to call onDisconnected() in one place.
|
||||
if (shouldNotifyDisconnected) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Notifying the controller of its disconnection");
|
||||
}
|
||||
try {
|
||||
caller.notifyDisconnected(0);
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
// Not an issue because we'll ignore it anyway.
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
mService.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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 android.media;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A DataSourceCallback that is backed by a ParcelFileDescriptor.
|
||||
*/
|
||||
class ProxyDataSourceCallback extends DataSourceCallback {
|
||||
private static final String TAG = "TestDataSourceCallback";
|
||||
|
||||
ParcelFileDescriptor mPFD;
|
||||
FileDescriptor mFD;
|
||||
|
||||
ProxyDataSourceCallback(ParcelFileDescriptor pfd) throws IOException {
|
||||
mPFD = pfd.dup();
|
||||
mFD = mPFD.getFileDescriptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int readAt(long position, byte[] buffer, int offset, int size)
|
||||
throws IOException {
|
||||
try {
|
||||
Os.lseek(mFD, position, OsConstants.SEEK_SET);
|
||||
int ret = Os.read(mFD, buffer, offset, size);
|
||||
return (ret == 0) ? END_OF_STREAM : ret;
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized long getSize() throws IOException {
|
||||
return mPFD.getStatSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
try {
|
||||
mPFD.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to close the PFD.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 android.media;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
class RoutingDelegate implements AudioRouting.OnRoutingChangedListener {
|
||||
private AudioRouting mAudioRouting;
|
||||
private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
|
||||
private Handler mHandler;
|
||||
|
||||
RoutingDelegate(final AudioRouting audioRouting,
|
||||
final AudioRouting.OnRoutingChangedListener listener,
|
||||
Handler handler) {
|
||||
mAudioRouting = audioRouting;
|
||||
mOnRoutingChangedListener = listener;
|
||||
mHandler = handler;
|
||||
}
|
||||
|
||||
public AudioRouting.OnRoutingChangedListener getListener() {
|
||||
return mOnRoutingChangedListener;
|
||||
}
|
||||
|
||||
public Handler getHandler() {
|
||||
return mHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoutingChanged(AudioRouting router) {
|
||||
if (mOnRoutingChangedListener != null) {
|
||||
mOnRoutingChangedListener.onRoutingChanged(mAudioRouting);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
|
||||
* <p>
|
||||
* If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
|
||||
* 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.
|
||||
*/
|
||||
public final class Session2Command implements Parcelable {
|
||||
/**
|
||||
* Command code for the custom command which can be defined by string action in the
|
||||
* {@link Session2Command}.
|
||||
*/
|
||||
public static final int COMMAND_CODE_CUSTOM = 0;
|
||||
|
||||
public static final @android.annotation.NonNull Parcelable.Creator<Session2Command> CREATOR =
|
||||
new Parcelable.Creator<Session2Command>() {
|
||||
@Override
|
||||
public Session2Command createFromParcel(Parcel in) {
|
||||
return new Session2Command(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session2Command[] newArray(int size) {
|
||||
return new Session2Command[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final int mCommandCode;
|
||||
// Nonnull if it's custom command
|
||||
private final String mCustomAction;
|
||||
private final Bundle mCustomExtras;
|
||||
|
||||
/**
|
||||
* Constructor for creating a command predefined in AndroidX media2.
|
||||
*
|
||||
* @param commandCode A command code for a command predefined in AndroidX media2.
|
||||
*/
|
||||
public Session2Command(int commandCode) {
|
||||
if (commandCode == COMMAND_CODE_CUSTOM) {
|
||||
throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
|
||||
}
|
||||
mCommandCode = commandCode;
|
||||
mCustomAction = null;
|
||||
mCustomExtras = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a custom command.
|
||||
*
|
||||
* @param action The action of this custom command.
|
||||
* @param extras An extra bundle for this custom command.
|
||||
*/
|
||||
public Session2Command(@NonNull String action, @Nullable Bundle extras) {
|
||||
if (action == null) {
|
||||
throw new IllegalArgumentException("action shouldn't be null");
|
||||
}
|
||||
mCommandCode = COMMAND_CODE_CUSTOM;
|
||||
mCustomAction = action;
|
||||
mCustomExtras = extras;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by parcelable creator.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
Session2Command(Parcel in) {
|
||||
mCommandCode = in.readInt();
|
||||
mCustomAction = in.readString();
|
||||
mCustomExtras = in.readBundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the command code of a predefined command.
|
||||
* This will return {@link #COMMAND_CODE_CUSTOM} for a custom command.
|
||||
*/
|
||||
public int getCommandCode() {
|
||||
return mCommandCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the action of a custom command.
|
||||
* This will return {@code null} for a predefined command.
|
||||
*/
|
||||
@Nullable
|
||||
public String getCustomAction() {
|
||||
return mCustomAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extra bundle of a custom command.
|
||||
* This will return {@code null} for a predefined command.
|
||||
*/
|
||||
@Nullable
|
||||
public Bundle getCustomExtras() {
|
||||
return mCustomExtras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
if (dest == null) {
|
||||
throw new IllegalArgumentException("parcel shouldn't be null");
|
||||
}
|
||||
dest.writeInt(mCommandCode);
|
||||
dest.writeString(mCustomAction);
|
||||
dest.writeBundle(mCustomExtras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Session2Command)) {
|
||||
return false;
|
||||
}
|
||||
Session2Command other = (Session2Command) obj;
|
||||
return mCommandCode == other.mCommandCode
|
||||
&& TextUtils.equals(mCustomAction, other.mCustomAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mCustomAction, mCommandCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Contains the result of {@link Session2Command}.
|
||||
*/
|
||||
public static final class Result {
|
||||
private final int mResultCode;
|
||||
private final Bundle mResultData;
|
||||
|
||||
/**
|
||||
* Result code representing that the command is skipped or canceled. For an example, a seek
|
||||
* command can be skipped if it is followed by another seek command.
|
||||
*/
|
||||
public static final int RESULT_INFO_SKIPPED = 1;
|
||||
|
||||
/**
|
||||
* Result code representing that the command is successfully completed.
|
||||
*/
|
||||
public static final int RESULT_SUCCESS = 0;
|
||||
|
||||
/**
|
||||
* Result code represents that call is ended with an unknown error.
|
||||
*/
|
||||
public static final int RESULT_ERROR_UNKNOWN_ERROR = -1;
|
||||
|
||||
/**
|
||||
* Constructor of {@link Result}.
|
||||
*
|
||||
* @param resultCode result code
|
||||
* @param resultData result data
|
||||
*/
|
||||
public Result(int resultCode, @Nullable Bundle resultData) {
|
||||
mResultCode = resultCode;
|
||||
mResultData = resultData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result code.
|
||||
*/
|
||||
public int getResultCode() {
|
||||
return mResultCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result data.
|
||||
*/
|
||||
@Nullable
|
||||
public Bundle getResultData() {
|
||||
return mResultData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import static android.media.Session2Command.COMMAND_CODE_CUSTOM;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* A set of {@link Session2Command} which represents a command group.
|
||||
*/
|
||||
public final class Session2CommandGroup implements Parcelable {
|
||||
private static final String TAG = "Session2CommandGroup";
|
||||
|
||||
public static final @android.annotation.NonNull Parcelable.Creator<Session2CommandGroup>
|
||||
CREATOR = new Parcelable.Creator<Session2CommandGroup>() {
|
||||
@Override
|
||||
public Session2CommandGroup createFromParcel(Parcel in) {
|
||||
return new Session2CommandGroup(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session2CommandGroup[] newArray(int size) {
|
||||
return new Session2CommandGroup[size];
|
||||
}
|
||||
};
|
||||
|
||||
Set<Session2Command> mCommands = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Creates a new Session2CommandGroup with commands copied from another object.
|
||||
*
|
||||
* @param commands The collection of commands to copy.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
Session2CommandGroup(@Nullable Collection<Session2Command> commands) {
|
||||
if (commands != null) {
|
||||
mCommands.addAll(commands);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by parcelable creator.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
Session2CommandGroup(Parcel in) {
|
||||
Parcelable[] commands = in.readParcelableArray(Session2Command.class.getClassLoader());
|
||||
if (commands != null) {
|
||||
for (Parcelable command : commands) {
|
||||
mCommands.add((Session2Command) command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this command group has a command that matches given {@code command}.
|
||||
*
|
||||
* @param command A command to find. Shouldn't be {@code null}.
|
||||
*/
|
||||
public boolean hasCommand(@NonNull Session2Command command) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
return mCommands.contains(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this command group has a command that matches given {@code commandCode}.
|
||||
*
|
||||
* @param commandCode A command code to find.
|
||||
* Shouldn't be {@link Session2Command#COMMAND_CODE_CUSTOM}.
|
||||
*/
|
||||
public boolean hasCommand(int commandCode) {
|
||||
if (commandCode == COMMAND_CODE_CUSTOM) {
|
||||
throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
|
||||
}
|
||||
for (Session2Command command : mCommands) {
|
||||
if (command.getCommandCode() == commandCode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all commands of this command group.
|
||||
*/
|
||||
@NonNull
|
||||
public Set<Session2Command> getCommands() {
|
||||
return new HashSet<>(mCommands);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
if (dest == null) {
|
||||
throw new IllegalArgumentException("parcel shouldn't be null");
|
||||
}
|
||||
dest.writeParcelableArray(mCommands.toArray(new Session2Command[0]), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Builds a {@link Session2CommandGroup} object.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private Set<Session2Command> mCommands;
|
||||
|
||||
public Builder() {
|
||||
mCommands = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for {@link Session2CommandGroup} with commands copied from another
|
||||
* {@link Session2CommandGroup} object.
|
||||
* @param commandGroup
|
||||
*/
|
||||
public Builder(@NonNull Session2CommandGroup commandGroup) {
|
||||
if (commandGroup == null) {
|
||||
throw new IllegalArgumentException("command group shouldn't be null");
|
||||
}
|
||||
mCommands = commandGroup.getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a command to this command group.
|
||||
*
|
||||
* @param command A command to add. Shouldn't be {@code null}.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder addCommand(@NonNull Session2Command command) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
mCommands.add(command);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a command from this group which matches given {@code command}.
|
||||
*
|
||||
* @param command A command to find. Shouldn't be {@code null}.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder removeCommand(@NonNull Session2Command command) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
mCommands.remove(command);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds {@link Session2CommandGroup}.
|
||||
*
|
||||
* @return a new {@link Session2CommandGroup}.
|
||||
*/
|
||||
@NonNull
|
||||
public Session2CommandGroup build() {
|
||||
return new Session2CommandGroup(mCommands);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Handles incoming commands from {@link MediaController2} to {@link MediaSession2}.
|
||||
* @hide
|
||||
*/
|
||||
// @SystemApi
|
||||
public final class Session2Link implements Parcelable {
|
||||
private static final String TAG = "Session2Link";
|
||||
private static final boolean DEBUG = MediaSession2.DEBUG;
|
||||
|
||||
public static final @android.annotation.NonNull Parcelable.Creator<Session2Link> CREATOR =
|
||||
new Parcelable.Creator<Session2Link>() {
|
||||
@Override
|
||||
public Session2Link createFromParcel(Parcel in) {
|
||||
return new Session2Link(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session2Link[] newArray(int size) {
|
||||
return new Session2Link[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final MediaSession2 mSession;
|
||||
private final IMediaSession2 mISession;
|
||||
|
||||
public Session2Link(MediaSession2 session) {
|
||||
mSession = session;
|
||||
mISession = new Session2Stub();
|
||||
}
|
||||
|
||||
Session2Link(Parcel in) {
|
||||
mSession = null;
|
||||
mISession = IMediaSession2.Stub.asInterface(in.readStrongBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeStrongBinder(mISession.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mISession.asBinder().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof Session2Link)) {
|
||||
return false;
|
||||
}
|
||||
Session2Link other = (Session2Link) obj;
|
||||
return Objects.equals(mISession.asBinder(), other.mISession.asBinder());
|
||||
}
|
||||
|
||||
/** Link to death with mISession */
|
||||
public void linkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
|
||||
if (mISession != null) {
|
||||
try {
|
||||
mISession.asBinder().linkToDeath(recipient, flags);
|
||||
} catch (RemoteException e) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Session died too early.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Unlink to death with mISession */
|
||||
public boolean unlinkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
|
||||
if (mISession != null) {
|
||||
return mISession.asBinder().unlinkToDeath(recipient, flags);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Interface method for IMediaSession2.connect */
|
||||
public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
try {
|
||||
mISession.connect(caller, seq, connectionRequest);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaSession2.disconnect */
|
||||
public void disconnect(final Controller2Link caller, int seq) {
|
||||
try {
|
||||
mISession.disconnect(caller, seq);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaSession2.sendSessionCommand */
|
||||
public void sendSessionCommand(final Controller2Link caller, final int seq,
|
||||
final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
|
||||
try {
|
||||
mISession.sendSessionCommand(caller, seq, command, args, resultReceiver);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interface method for IMediaSession2.sendSessionCommand */
|
||||
public void cancelSessionCommand(final Controller2Link caller, final int seq) {
|
||||
try {
|
||||
mISession.cancelSessionCommand(caller, seq);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.connect */
|
||||
public void onConnect(final Controller2Link caller, int pid, int uid, int seq,
|
||||
Bundle connectionRequest) {
|
||||
mSession.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.disconnect */
|
||||
public void onDisconnect(final Controller2Link caller, int seq) {
|
||||
mSession.onDisconnect(caller, seq);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.sendSessionCommand */
|
||||
public void onSessionCommand(final Controller2Link caller, final int seq,
|
||||
final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
|
||||
mSession.onSessionCommand(caller, seq, command, args, resultReceiver);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.cancelSessionCommand */
|
||||
public void onCancelCommand(final Controller2Link caller, final int seq) {
|
||||
mSession.onCancelCommand(caller, seq);
|
||||
}
|
||||
|
||||
private class Session2Stub extends IMediaSession2.Stub {
|
||||
@Override
|
||||
public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
if (caller == null || connectionRequest == null) {
|
||||
return;
|
||||
}
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final Controller2Link caller, int seq) {
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onDisconnect(caller, seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSessionCommand(final Controller2Link caller, final int seq,
|
||||
final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onSessionCommand(caller, seq, command, args, resultReceiver);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelSessionCommand(final Controller2Link caller, final int seq) {
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onCancelCommand(caller, seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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 android.media;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* This API is not generally intended for third party application developers.
|
||||
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||||
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
|
||||
* Library</a> for consistent behavior across all devices.
|
||||
* <p>
|
||||
* Represents an ongoing {@link MediaSession2} or a {@link MediaSession2Service}.
|
||||
* If it's representing a session service, it may not be ongoing.
|
||||
* <p>
|
||||
* This may be passed to apps by the session owner to allow them to create a
|
||||
* {@link MediaController2} to communicate with the session.
|
||||
* <p>
|
||||
* It can be also obtained by {@link android.media.session.MediaSessionManager}.
|
||||
*/
|
||||
public final class Session2Token implements Parcelable {
|
||||
private static final String TAG = "Session2Token";
|
||||
|
||||
public static final @android.annotation.NonNull Creator<Session2Token> CREATOR =
|
||||
new Creator<Session2Token>() {
|
||||
@Override
|
||||
public Session2Token createFromParcel(Parcel p) {
|
||||
return new Session2Token(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session2Token[] newArray(int size) {
|
||||
return new Session2Token[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(prefix = "TYPE_", value = {TYPE_SESSION, TYPE_SESSION_SERVICE})
|
||||
public @interface TokenType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for {@link MediaSession2}.
|
||||
*/
|
||||
public static final int TYPE_SESSION = 0;
|
||||
|
||||
/**
|
||||
* Type for {@link MediaSession2Service}.
|
||||
*/
|
||||
public static final int TYPE_SESSION_SERVICE = 1;
|
||||
|
||||
private final int mUid;
|
||||
@TokenType
|
||||
private final int mType;
|
||||
private final String mPackageName;
|
||||
private final String mServiceName;
|
||||
private final Session2Link mSessionLink;
|
||||
private final ComponentName mComponentName;
|
||||
private final Bundle mExtras;
|
||||
|
||||
/**
|
||||
* Constructor for the token with type {@link #TYPE_SESSION_SERVICE}.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param serviceComponent The component name of the service.
|
||||
*/
|
||||
public Session2Token(@NonNull Context context, @NonNull ComponentName serviceComponent) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context shouldn't be null");
|
||||
}
|
||||
if (serviceComponent == null) {
|
||||
throw new IllegalArgumentException("serviceComponent shouldn't be null");
|
||||
}
|
||||
|
||||
final PackageManager manager = context.getPackageManager();
|
||||
final int uid = getUid(manager, serviceComponent.getPackageName());
|
||||
|
||||
if (!isInterfaceDeclared(manager, MediaSession2Service.SERVICE_INTERFACE,
|
||||
serviceComponent)) {
|
||||
Log.w(TAG, serviceComponent + " doesn't implement MediaSession2Service.");
|
||||
}
|
||||
mComponentName = serviceComponent;
|
||||
mPackageName = serviceComponent.getPackageName();
|
||||
mServiceName = serviceComponent.getClassName();
|
||||
mUid = uid;
|
||||
mType = TYPE_SESSION_SERVICE;
|
||||
mSessionLink = null;
|
||||
mExtras = Bundle.EMPTY;
|
||||
}
|
||||
|
||||
Session2Token(int uid, int type, String packageName, Session2Link sessionLink,
|
||||
@NonNull Bundle tokenExtras) {
|
||||
mUid = uid;
|
||||
mType = type;
|
||||
mPackageName = packageName;
|
||||
mServiceName = null;
|
||||
mComponentName = null;
|
||||
mSessionLink = sessionLink;
|
||||
mExtras = tokenExtras;
|
||||
}
|
||||
|
||||
Session2Token(Parcel in) {
|
||||
mUid = in.readInt();
|
||||
mType = in.readInt();
|
||||
mPackageName = in.readString();
|
||||
mServiceName = in.readString();
|
||||
mSessionLink = in.readParcelable(null);
|
||||
mComponentName = ComponentName.unflattenFromString(in.readString());
|
||||
|
||||
Bundle extras = in.readBundle();
|
||||
if (extras == null) {
|
||||
Log.w(TAG, "extras shouldn't be null.");
|
||||
extras = Bundle.EMPTY;
|
||||
} else if (MediaSession2.hasCustomParcelable(extras)) {
|
||||
Log.w(TAG, "extras contain custom parcelable. Ignoring.");
|
||||
extras = Bundle.EMPTY;
|
||||
}
|
||||
mExtras = extras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(mUid);
|
||||
dest.writeInt(mType);
|
||||
dest.writeString(mPackageName);
|
||||
dest.writeString(mServiceName);
|
||||
dest.writeParcelable(mSessionLink, flags);
|
||||
dest.writeString(mComponentName == null ? "" : mComponentName.flattenToString());
|
||||
dest.writeBundle(mExtras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mType, mUid, mPackageName, mServiceName, mSessionLink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof Session2Token)) {
|
||||
return false;
|
||||
}
|
||||
Session2Token other = (Session2Token) obj;
|
||||
return mUid == other.mUid
|
||||
&& TextUtils.equals(mPackageName, other.mPackageName)
|
||||
&& TextUtils.equals(mServiceName, other.mServiceName)
|
||||
&& mType == other.mType
|
||||
&& Objects.equals(mSessionLink, other.mSessionLink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session2Token {pkg=" + mPackageName + " type=" + mType
|
||||
+ " service=" + mServiceName + " Session2Link=" + mSessionLink + "}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return uid of the session
|
||||
*/
|
||||
public int getUid() {
|
||||
return mUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return package name of the session
|
||||
*/
|
||||
@NonNull
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return service name of the session. Can be {@code null} for {@link #TYPE_SESSION}.
|
||||
*/
|
||||
@Nullable
|
||||
public String getServiceName() {
|
||||
return mServiceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return type of the token
|
||||
* @see #TYPE_SESSION
|
||||
* @see #TYPE_SESSION_SERVICE
|
||||
*/
|
||||
public @TokenType int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return extras of the token
|
||||
* @see MediaSession2.Builder#setExtras(Bundle)
|
||||
*/
|
||||
@NonNull
|
||||
public Bundle getExtras() {
|
||||
return new Bundle(mExtras);
|
||||
}
|
||||
|
||||
Session2Link getSessionLink() {
|
||||
return mSessionLink;
|
||||
}
|
||||
|
||||
private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,
|
||||
ComponentName serviceComponent) {
|
||||
Intent serviceIntent = new Intent(serviceInterface);
|
||||
// Use queryIntentServices to find services with MediaSession2Service.SERVICE_INTERFACE.
|
||||
// We cannot use resolveService with intent specified class name, because resolveService
|
||||
// ignores actions if Intent.setClassName() is specified.
|
||||
serviceIntent.setPackage(serviceComponent.getPackageName());
|
||||
|
||||
List<ResolveInfo> list = manager.queryIntentServices(
|
||||
serviceIntent, PackageManager.GET_META_DATA);
|
||||
if (list != null) {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
ResolveInfo resolveInfo = list.get(i);
|
||||
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
|
||||
continue;
|
||||
}
|
||||
if (TextUtils.equals(
|
||||
resolveInfo.serviceInfo.name, serviceComponent.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int getUid(PackageManager manager, String packageName) {
|
||||
try {
|
||||
return manager.getApplicationInfo(packageName, 0).uid;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalArgumentException("Cannot find package " + packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020, 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.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <media/MediaMetrics.h>
|
||||
|
||||
#define JNI_FUNCTION(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE Java_android_media_MediaParser_##NAME(JNIEnv* env, jobject thiz, \
|
||||
##__VA_ARGS__); \
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE Java_android_media_MediaParser_##NAME(JNIEnv* env, jobject thiz, \
|
||||
##__VA_ARGS__)
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMediaMetricsKey[] = "mediaparser";
|
||||
|
||||
constexpr char kAttributeLogSessionId[] = "android.media.mediaparser.logSessionId";
|
||||
constexpr char kAttributeParserName[] = "android.media.mediaparser.parserName";
|
||||
constexpr char kAttributeCreatedByName[] = "android.media.mediaparser.createdByName";
|
||||
constexpr char kAttributeParserPool[] = "android.media.mediaparser.parserPool";
|
||||
constexpr char kAttributeLastException[] = "android.media.mediaparser.lastException";
|
||||
constexpr char kAttributeResourceByteCount[] = "android.media.mediaparser.resourceByteCount";
|
||||
constexpr char kAttributeDurationMillis[] = "android.media.mediaparser.durationMillis";
|
||||
constexpr char kAttributeTrackMimeTypes[] = "android.media.mediaparser.trackMimeTypes";
|
||||
constexpr char kAttributeTrackCodecs[] = "android.media.mediaparser.trackCodecs";
|
||||
constexpr char kAttributeAlteredParameters[] = "android.media.mediaparser.alteredParameters";
|
||||
constexpr char kAttributeVideoWidth[] = "android.media.mediaparser.videoWidth";
|
||||
constexpr char kAttributeVideoHeight[] = "android.media.mediaparser.videoHeight";
|
||||
|
||||
// Util class to handle string resource management.
|
||||
class JstringHandle {
|
||||
public:
|
||||
JstringHandle(JNIEnv* env, jstring value) : mEnv(env), mJstringValue(value) {
|
||||
mCstringValue = env->GetStringUTFChars(value, /* isCopy= */ nullptr);
|
||||
}
|
||||
|
||||
~JstringHandle() {
|
||||
if (mCstringValue != nullptr) {
|
||||
mEnv->ReleaseStringUTFChars(mJstringValue, mCstringValue);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* value() const {
|
||||
return mCstringValue != nullptr ? mCstringValue : "";
|
||||
}
|
||||
|
||||
JNIEnv* mEnv;
|
||||
jstring mJstringValue;
|
||||
const char* mCstringValue;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
JNI_FUNCTION(void, nativeSubmitMetrics, jstring logSessionIdJstring, jstring parserNameJstring,
|
||||
jboolean createdByName, jstring parserPoolJstring, jstring lastExceptionJstring,
|
||||
jlong resourceByteCount, jlong durationMillis, jstring trackMimeTypesJstring,
|
||||
jstring trackCodecsJstring, jstring alteredParameters, jint videoWidth,
|
||||
jint videoHeight) {
|
||||
mediametrics_handle_t item(mediametrics_create(kMediaMetricsKey));
|
||||
mediametrics_setCString(item, kAttributeLogSessionId,
|
||||
JstringHandle(env, logSessionIdJstring).value());
|
||||
mediametrics_setCString(item, kAttributeParserName,
|
||||
JstringHandle(env, parserNameJstring).value());
|
||||
mediametrics_setInt32(item, kAttributeCreatedByName, createdByName ? 1 : 0);
|
||||
mediametrics_setCString(item, kAttributeParserPool,
|
||||
JstringHandle(env, parserPoolJstring).value());
|
||||
mediametrics_setCString(item, kAttributeLastException,
|
||||
JstringHandle(env, lastExceptionJstring).value());
|
||||
mediametrics_setInt64(item, kAttributeResourceByteCount, resourceByteCount);
|
||||
mediametrics_setInt64(item, kAttributeDurationMillis, durationMillis);
|
||||
mediametrics_setCString(item, kAttributeTrackMimeTypes,
|
||||
JstringHandle(env, trackMimeTypesJstring).value());
|
||||
mediametrics_setCString(item, kAttributeTrackCodecs,
|
||||
JstringHandle(env, trackCodecsJstring).value());
|
||||
mediametrics_setCString(item, kAttributeAlteredParameters,
|
||||
JstringHandle(env, alteredParameters).value());
|
||||
mediametrics_setInt32(item, kAttributeVideoWidth, videoWidth);
|
||||
mediametrics_setInt32(item, kAttributeVideoHeight, videoHeight);
|
||||
mediametrics_selfRecord(item);
|
||||
mediametrics_delete(item);
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
|
||||
|
||||
<issue
|
||||
id="NewApi"
|
||||
message="Call requires API level 31 (current min is 29): `new android.media.ApplicationMediaCapabilities.Builder`"
|
||||
errorLine1=" new ApplicationMediaCapabilities.Builder();"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java"
|
||||
line="208"
|
||||
column="29"/>
|
||||
</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=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<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 "[timeMicros=" + timeMicros + ", position=" + position + "]";"
|
||||
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 "[timeMicros=" + timeMicros + ", position=" + position + "]";"
|
||||
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 && 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 && 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 && position == other.position;"
|
||||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
|
||||
line="314"
|
||||
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 && 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=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
|
||||
line="1290"
|
||||
column="49"/>
|
||||
</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=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
|
||||
line="1579"
|
||||
column="21"/>
|
||||
</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=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
|
||||
line="1872"
|
||||
column="61"/>
|
||||
</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=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
|
||||
line="1905"
|
||||
column="68"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
@@ -1,2 +0,0 @@
|
||||
# Keep all symbols in android.media.
|
||||
-keep class android.media.* {*;}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2020 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 {
|
||||
// See: http://go/android-license-faq
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "service-media-s-sources",
|
||||
srcs: [
|
||||
"java/**/*.java",
|
||||
],
|
||||
path: "java",
|
||||
visibility: ["//visibility:private"],
|
||||
}
|
||||
|
||||
java_sdk_library {
|
||||
name: "service-media-s",
|
||||
permitted_packages: [
|
||||
"com.android.server.media",
|
||||
],
|
||||
defaults: ["framework-system-server-module-defaults"],
|
||||
srcs: [
|
||||
":service-media-s-sources",
|
||||
],
|
||||
libs: [
|
||||
"updatable-media",
|
||||
],
|
||||
sdk_version: "system_server_current",
|
||||
min_sdk_version: "29", // TODO: We may need to bump this at some point.
|
||||
apex_available: [
|
||||
"com.android.media",
|
||||
],
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1 +0,0 @@
|
||||
// Signature format: 2.0
|
||||
@@ -1,620 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
|
||||
import static android.os.UserHandle.ALL;
|
||||
import static android.os.UserHandle.getUserHandleForUid;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.IMediaCommunicationService;
|
||||
import android.media.IMediaCommunicationServiceCallback;
|
||||
import android.media.MediaController2;
|
||||
import android.media.MediaParceledListSlice;
|
||||
import android.media.Session2CommandGroup;
|
||||
import android.media.Session2Token;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.SystemService;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
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
|
||||
* and their ongoing media playback state.
|
||||
* @hide
|
||||
*/
|
||||
public class MediaCommunicationService extends SystemService {
|
||||
private static final String TAG = "MediaCommunicationService";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
final Context mContext;
|
||||
|
||||
final Object mLock = new Object();
|
||||
final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final SparseIntArray mFullUserIds = new SparseIntArray();
|
||||
@GuardedBy("mLock")
|
||||
private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<>();
|
||||
|
||||
final Executor mRecordExecutor = Executors.newSingleThreadExecutor();
|
||||
@GuardedBy("mLock")
|
||||
final List<CallbackRecord> mCallbackRecords = new ArrayList<>();
|
||||
final NotificationManager mNotificationManager;
|
||||
|
||||
public MediaCommunicationService(Context context) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
publishBinderService(Context.MEDIA_COMMUNICATION_SERVICE, new Stub());
|
||||
updateUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserStarting(@NonNull TargetUser user) {
|
||||
if (DEBUG) Log.d(TAG, "onUserStarting: " + user);
|
||||
updateUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
|
||||
if (DEBUG) Log.d(TAG, "onUserSwitching: " + to);
|
||||
updateUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserStopped(@NonNull TargetUser targetUser) {
|
||||
int userId = targetUser.getUserHandle().getIdentifier();
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onUserStopped: " + userId);
|
||||
synchronized (mLock) {
|
||||
FullUserRecord user = getFullUserRecordLocked(userId);
|
||||
if (user != null) {
|
||||
if (user.getFullUserId() == userId) {
|
||||
user.destroyAllSessions();
|
||||
mUserRecords.remove(userId);
|
||||
} else {
|
||||
user.destroySessionsForUser(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateUser();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
CallbackRecord findCallbackRecordLocked(@Nullable IMediaCommunicationServiceCallback callback) {
|
||||
if (callback == null) {
|
||||
return null;
|
||||
}
|
||||
for (CallbackRecord record : mCallbackRecords) {
|
||||
if (Objects.equals(callback.asBinder(), record.mCallback.asBinder())) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Session2Token> getSession2TokensLocked(int userId) {
|
||||
List<Session2Token> list = new ArrayList<>();
|
||||
if (userId == ALL.getIdentifier()) {
|
||||
int size = mUserRecords.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
list.addAll(mUserRecords.valueAt(i).getAllSession2Tokens());
|
||||
}
|
||||
} else {
|
||||
FullUserRecord user = getFullUserRecordLocked(userId);
|
||||
if (user != null) {
|
||||
list.addAll(user.getSession2Tokens(userId));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private FullUserRecord getFullUserRecordLocked(int userId) {
|
||||
int fullUserId = mFullUserIds.get(userId, -1);
|
||||
if (fullUserId < 0) {
|
||||
return null;
|
||||
}
|
||||
return mUserRecords.get(fullUserId);
|
||||
}
|
||||
|
||||
private boolean hasMediaControlPermission(int pid, int uid) {
|
||||
// Check if it's system server or has MEDIA_CONTENT_CONTROL.
|
||||
// Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra
|
||||
// check here.
|
||||
if (uid == Process.SYSTEM_UID || mContext.checkPermission(
|
||||
android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "uid(" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateUser() {
|
||||
UserManager manager = mContext.getSystemService(UserManager.class);
|
||||
List<UserHandle> allUsers = manager.getUserHandles(/*excludeDying=*/false);
|
||||
|
||||
synchronized (mLock) {
|
||||
mFullUserIds.clear();
|
||||
if (allUsers != null) {
|
||||
for (UserHandle user : allUsers) {
|
||||
UserHandle parent = manager.getProfileParent(user);
|
||||
if (parent != null) {
|
||||
mFullUserIds.put(user.getIdentifier(), parent.getIdentifier());
|
||||
} else {
|
||||
mFullUserIds.put(user.getIdentifier(), user.getIdentifier());
|
||||
if (mUserRecords.get(user.getIdentifier()) == null) {
|
||||
mUserRecords.put(user.getIdentifier(),
|
||||
new FullUserRecord(user.getIdentifier()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure that the current full user exists.
|
||||
int currentFullUserId = ActivityManager.getCurrentUser();
|
||||
FullUserRecord currentFullUserRecord = mUserRecords.get(currentFullUserId);
|
||||
if (currentFullUserRecord == null) {
|
||||
Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
|
||||
currentFullUserRecord = new FullUserRecord(currentFullUserId);
|
||||
mUserRecords.put(currentFullUserId, currentFullUserRecord);
|
||||
}
|
||||
mFullUserIds.put(currentFullUserId, currentFullUserId);
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchSession2Created(Session2Token token) {
|
||||
synchronized (mLock) {
|
||||
for (CallbackRecord record : mCallbackRecords) {
|
||||
if (record.mUserId != ALL.getIdentifier()
|
||||
&& record.mUserId != getUserHandleForUid(token.getUid()).getIdentifier()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
record.mCallback.onSession2Created(token);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to notify session2 token created " + record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchSession2Changed(int userId) {
|
||||
MediaParceledListSlice<Session2Token> allSession2Tokens;
|
||||
MediaParceledListSlice<Session2Token> userSession2Tokens;
|
||||
|
||||
synchronized (mLock) {
|
||||
allSession2Tokens =
|
||||
new MediaParceledListSlice<>(getSession2TokensLocked(ALL.getIdentifier()));
|
||||
userSession2Tokens = new MediaParceledListSlice<>(getSession2TokensLocked(userId));
|
||||
}
|
||||
allSession2Tokens.setInlineCountLimit(1);
|
||||
userSession2Tokens.setInlineCountLimit(1);
|
||||
|
||||
synchronized (mLock) {
|
||||
for (CallbackRecord record : mCallbackRecords) {
|
||||
if (record.mUserId == ALL.getIdentifier()) {
|
||||
try {
|
||||
record.mCallback.onSession2Changed(allSession2Tokens);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to notify session2 tokens changed " + record);
|
||||
}
|
||||
} else if (record.mUserId == userId) {
|
||||
try {
|
||||
record.mCallback.onSession2Changed(userSession2Tokens);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to notify session2 tokens changed " + record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onSessionDied(Session2Record session) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Destroying " + session);
|
||||
}
|
||||
if (session.isClosed()) {
|
||||
Log.w(TAG, "Destroying already destroyed session. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
FullUserRecord user = session.getFullUser();
|
||||
if (user != null) {
|
||||
user.removeSession(session);
|
||||
}
|
||||
session.close();
|
||||
}
|
||||
|
||||
private class Stub extends IMediaCommunicationService.Stub {
|
||||
@Override
|
||||
public void notifySession2Created(Session2Token sessionToken) {
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Session2 is created " + sessionToken);
|
||||
}
|
||||
if (uid != sessionToken.getUid()) {
|
||||
throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
|
||||
+ " but actually=" + sessionToken.getUid());
|
||||
}
|
||||
FullUserRecord user;
|
||||
int userId = getUserHandleForUid(sessionToken.getUid()).getIdentifier();
|
||||
synchronized (mLock) {
|
||||
user = getFullUserRecordLocked(userId);
|
||||
}
|
||||
if (user == null) {
|
||||
Log.w(TAG, "notifySession2Created: Ignore session of an unknown user");
|
||||
return;
|
||||
}
|
||||
user.addSession(new Session2Record(MediaCommunicationService.this,
|
||||
user, sessionToken, mRecordExecutor));
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL
|
||||
* permission or an enabled notification listener)
|
||||
*
|
||||
* @param controllerPackageName package name of the controller app
|
||||
* @param controllerPid pid of the controller app
|
||||
* @param controllerUid uid of the controller app
|
||||
*/
|
||||
@Override
|
||||
public boolean isTrusted(String controllerPackageName, int controllerPid,
|
||||
int controllerUid) {
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
// Don't perform check between controllerPackageName and controllerUid.
|
||||
// When an (activity|service) runs on the another apps process by specifying
|
||||
// android:process in the AndroidManifest.xml, then PID and UID would have the
|
||||
// running process' information instead of the (activity|service) that has created
|
||||
// MediaController.
|
||||
// Note that we can use Context#getOpPackageName() instead of
|
||||
// Context#getPackageName() for getting package name that matches with the PID/UID,
|
||||
// but it doesn't tell which package has created the MediaController, so useless.
|
||||
return hasMediaControlPermission(controllerPid, controllerUid)
|
||||
|| hasEnabledNotificationListener(
|
||||
userId, controllerPackageName, controllerUid);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaParceledListSlice getSession2Tokens(int userId) {
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
|
||||
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;
|
||||
synchronized (mLock) {
|
||||
result = getSession2TokensLocked(resolvedUserId);
|
||||
}
|
||||
MediaParceledListSlice parceledListSlice = new MediaParceledListSlice<>(result);
|
||||
parceledListSlice.setInlineCountLimit(1);
|
||||
return parceledListSlice;
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCallback(IMediaCommunicationServiceCallback callback,
|
||||
String packageName) throws RemoteException {
|
||||
Objects.requireNonNull(callback, "callback should not be null");
|
||||
Objects.requireNonNull(packageName, "packageName should not be null");
|
||||
|
||||
synchronized (mLock) {
|
||||
if (findCallbackRecordLocked(callback) == null) {
|
||||
|
||||
CallbackRecord record = new CallbackRecord(callback, packageName,
|
||||
Binder.getCallingUid(), Binder.getCallingPid());
|
||||
mCallbackRecords.add(record);
|
||||
try {
|
||||
callback.asBinder().linkToDeath(record, 0);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to register callback", e);
|
||||
mCallbackRecords.remove(record);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "registerCallback is called with already registered callback. "
|
||||
+ "packageName=" + packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterCallback(IMediaCommunicationServiceCallback callback)
|
||||
throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
CallbackRecord existingRecord = findCallbackRecordLocked(callback);
|
||||
if (existingRecord != null) {
|
||||
mCallbackRecords.remove(existingRecord);
|
||||
callback.asBinder().unlinkToDeath(existingRecord, 0);
|
||||
} else {
|
||||
Log.e(TAG, "unregisterCallback is called with unregistered callback.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasEnabledNotificationListener(int callingUserId,
|
||||
String controllerPackageName, int controllerUid) {
|
||||
int controllerUserId = UserHandle.getUserHandleForUid(controllerUid).getIdentifier();
|
||||
if (callingUserId != controllerUserId) {
|
||||
// Enabled notification listener only works within the same user.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mNotificationManager.hasEnabledNotificationListener(controllerPackageName,
|
||||
UserHandle.getUserHandleForUid(controllerUid))) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, controllerPackageName + " (uid=" + controllerUid
|
||||
+ ") doesn't have an enabled notification listener");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles incoming user by checking whether the caller has permission to access the
|
||||
// given user id's information or not. Permission is not necessary if the given user id is
|
||||
// equal to the caller's user id, but if not, the caller needs to have the
|
||||
// INTERACT_ACROSS_USERS_FULL permission. Otherwise, a security exception will be thrown.
|
||||
// The return value will be the given user id, unless the given user id is
|
||||
// UserHandle.CURRENT, which will return the ActivityManager.getCurrentUser() value instead.
|
||||
private int handleIncomingUser(int pid, int uid, int userId, String packageName) {
|
||||
int callingUserId = UserHandle.getUserHandleForUid(uid).getIdentifier();
|
||||
if (userId == callingUserId) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
boolean canInteractAcrossUsersFull = mContext.checkPermission(
|
||||
INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED;
|
||||
if (canInteractAcrossUsersFull) {
|
||||
if (userId == UserHandle.CURRENT.getIdentifier()) {
|
||||
return ActivityManager.getCurrentUser();
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
throw new SecurityException("Permission denied while calling from " + packageName
|
||||
+ " with user id: " + userId + "; Need to run as either the calling user id ("
|
||||
+ callingUserId + "), or with " + INTERACT_ACROSS_USERS_FULL + " permission");
|
||||
}
|
||||
}
|
||||
|
||||
final class CallbackRecord implements IBinder.DeathRecipient {
|
||||
private final IMediaCommunicationServiceCallback mCallback;
|
||||
private final String mPackageName;
|
||||
private final int mUid;
|
||||
private int mPid;
|
||||
private final int mUserId;
|
||||
|
||||
CallbackRecord(IMediaCommunicationServiceCallback callback,
|
||||
String packageName, int uid, int pid) {
|
||||
mCallback = callback;
|
||||
mPackageName = packageName;
|
||||
mUid = uid;
|
||||
mPid = pid;
|
||||
mUserId = (mContext.checkPermission(
|
||||
INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED)
|
||||
? ALL.getIdentifier() : UserHandle.getUserHandleForUid(mUid).getIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CallbackRecord[callback=" + mCallback + ", pkg=" + mPackageName
|
||||
+ ", uid=" + mUid + ", pid=" + mPid + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
synchronized (mLock) {
|
||||
mCallbackRecords.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class FullUserRecord {
|
||||
private final int mFullUserId;
|
||||
private final Object mUserLock = new Object();
|
||||
@GuardedBy("mUserLock")
|
||||
private final List<Session2Record> mSessionRecords = new ArrayList<>();
|
||||
|
||||
FullUserRecord(int fullUserId) {
|
||||
mFullUserId = fullUserId;
|
||||
}
|
||||
|
||||
public void addSession(Session2Record record) {
|
||||
synchronized (mUserLock) {
|
||||
mSessionRecords.add(record);
|
||||
}
|
||||
mHandler.post(() -> dispatchSession2Created(record.mSessionToken));
|
||||
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
|
||||
}
|
||||
|
||||
private void removeSession(Session2Record record) {
|
||||
synchronized (mUserLock) {
|
||||
mSessionRecords.remove(record);
|
||||
}
|
||||
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
|
||||
//TODO: Handle if the removed session was the media button session.
|
||||
}
|
||||
|
||||
public int getFullUserId() {
|
||||
return mFullUserId;
|
||||
}
|
||||
|
||||
public List<Session2Token> getAllSession2Tokens() {
|
||||
synchronized (mUserLock) {
|
||||
return mSessionRecords.stream()
|
||||
.map(Session2Record::getSessionToken)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
public List<Session2Token> getSession2Tokens(int userId) {
|
||||
synchronized (mUserLock) {
|
||||
return mSessionRecords.stream()
|
||||
.filter(record -> record.getUserId() == userId)
|
||||
.map(Session2Record::getSessionToken)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
public void destroyAllSessions() {
|
||||
synchronized (mUserLock) {
|
||||
for (Session2Record session : mSessionRecords) {
|
||||
session.close();
|
||||
}
|
||||
mSessionRecords.clear();
|
||||
}
|
||||
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) {
|
||||
mHandler.post(() -> dispatchSession2Changed(mFullUserId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class Session2Record {
|
||||
final Session2Token mSessionToken;
|
||||
final Object mSession2RecordLock = new Object();
|
||||
final WeakReference<MediaCommunicationService> mServiceRef;
|
||||
final WeakReference<FullUserRecord> mFullUserRef;
|
||||
@GuardedBy("mSession2RecordLock")
|
||||
private final MediaController2 mController;
|
||||
|
||||
@GuardedBy("mSession2RecordLock")
|
||||
boolean mIsConnected;
|
||||
@GuardedBy("mSession2RecordLock")
|
||||
private boolean mIsClosed;
|
||||
|
||||
Session2Record(MediaCommunicationService service, FullUserRecord fullUser,
|
||||
Session2Token token, Executor controllerExecutor) {
|
||||
mServiceRef = new WeakReference<>(service);
|
||||
mFullUserRef = new WeakReference<>(fullUser);
|
||||
mSessionToken = token;
|
||||
mController = new MediaController2.Builder(service.getContext(), token)
|
||||
.setControllerCallback(controllerExecutor, new Controller2Callback())
|
||||
.build();
|
||||
}
|
||||
|
||||
public int getUserId() {
|
||||
return UserHandle.getUserHandleForUid(mSessionToken.getUid()).getIdentifier();
|
||||
}
|
||||
|
||||
public FullUserRecord getFullUser() {
|
||||
return mFullUserRef.get();
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
synchronized (mSession2RecordLock) {
|
||||
return mIsClosed;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
synchronized (mSession2RecordLock) {
|
||||
mIsClosed = true;
|
||||
mController.close();
|
||||
}
|
||||
}
|
||||
|
||||
public Session2Token getSessionToken() {
|
||||
return mSessionToken;
|
||||
}
|
||||
|
||||
private class Controller2Callback extends MediaController2.ControllerCallback {
|
||||
@Override
|
||||
public void onConnected(MediaController2 controller,
|
||||
Session2CommandGroup allowedCommands) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands);
|
||||
}
|
||||
synchronized (mSession2RecordLock) {
|
||||
mIsConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(MediaController2 controller) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "disconnected from " + mSessionToken);
|
||||
}
|
||||
synchronized (mSession2RecordLock) {
|
||||
mIsConnected = false;
|
||||
}
|
||||
MediaCommunicationService service = mServiceRef.get();
|
||||
if (service != null) {
|
||||
service.onSessionDied(Session2Record.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user