Merge "Remove apex/media - migrated to packages/modules/Media" am: 8aa8ce4f91

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1987167

Change-Id: I13e1c0f75834c11957aaf5ed7e94da85ab325329
This commit is contained in:
Baligh Uddin
2022-02-18 01:34:37 +00:00
committed by Automerger Merge Worker
53 changed files with 3 additions and 10941 deletions

View File

@@ -176,7 +176,6 @@ java_defaults {
"sax/java", "sax/java",
"telecomm/java", "telecomm/java",
"apex/media/aidl/stable",
// TODO(b/147699819): remove this // TODO(b/147699819): remove this
"telephony/java", "telephony/java",
], ],
@@ -275,6 +274,7 @@ java_defaults {
// TODO: remove when moved to the below package // TODO: remove when moved to the below package
"frameworks/base/packages/ConnectivityT/framework-t/aidl-export", "frameworks/base/packages/ConnectivityT/framework-t/aidl-export",
"packages/modules/Connectivity/framework/aidl-export", "packages/modules/Connectivity/framework/aidl-export",
"packages/modules/Media/apex/aidl/stable",
], ],
}, },
dxflags: [ dxflags: [
@@ -552,11 +552,9 @@ stubs_defaults {
stubs_defaults { stubs_defaults {
name: "module-classpath-stubs-defaults", name: "module-classpath-stubs-defaults",
aidl: { aidl: {
local_include_dirs: [
"apex/media/aidl/stable",
],
include_dirs: [ include_dirs: [
"packages/modules/Connectivity/framework/aidl-export", "packages/modules/Connectivity/framework/aidl-export",
"packages/modules/Media/apex/aidl/stable",
], ],
}, },
libs: [ libs: [

View File

@@ -140,11 +140,9 @@ droidstubs {
"api-versions-jars-dir", "api-versions-jars-dir",
], ],
aidl: { aidl: {
local_include_dirs: [
"apex/media/aidl/stable",
],
include_dirs: [ include_dirs: [
"packages/modules/Connectivity/framework/aidl-export", "packages/modules/Connectivity/framework/aidl-export",
"packages/modules/Media/apex/aidl/stable",
], ],
}, },
} }

View File

@@ -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",
],
}

View File

@@ -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

View File

@@ -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",
],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",
}

View File

@@ -1,10 +0,0 @@
{
"presubmit": [
{
"name": "CtsMediaParserTestCases"
},
{
"name": "CtsMediaParserHostTestCases"
}
]
}

View File

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

View File

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

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

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

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

@@ -1,2 +0,0 @@
rule com.android.modules.** android.media.internal.@1
rule com.google.android.exoplayer2.** android.media.internal.exo.@1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
}
}

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &quot;[timeMicros=&quot; + timeMicros + &quot;, position=&quot; + position + &quot;]&quot;;"
errorLine2=" ~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="302"
column="66"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" return &quot;[timeMicros=&quot; + timeMicros + &quot;, position=&quot; + position + &quot;]&quot;;"
errorLine2=" ~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="302"
column="37"/>
</issue>
<issue
id="NewApi"
message="Class requires API level R (current min is 29): `android.media.MediaParser.SeekPoint`"
errorLine1=" SeekPoint other = (SeekPoint) obj;"
errorLine2=" ~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="313"
column="32"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
errorLine2=" ~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="314"
column="54"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="314"
column="66"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
errorLine2=" ~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="314"
column="20"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" return timeMicros == other.timeMicros &amp;&amp; position == other.position;"
errorLine2=" ~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="314"
column="34"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" int result = (int) timeMicros;"
errorLine2=" ~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="319"
column="32"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
errorLine1=" result = 31 * result + (int) position;"
errorLine2=" ~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="320"
column="42"/>
</issue>
<issue
id="NewApi"
message="Class requires API level R (current min is 29): `android.media.MediaParser.InputReader`"
errorLine1=" public interface SeekableInputReader extends InputReader {"
errorLine2=" ~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="352"
column="50"/>
</issue>
<issue
id="NewApi"
message="Field requires API level 31 (current min is 29): `android.media.metrics.LogSessionId#LOG_SESSION_ID_NONE`"
errorLine1=" @NonNull private LogSessionId mLogSessionId = LogSessionId.LOG_SESSION_ID_NONE;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="1071"
column="51"/>
</issue>
<issue
id="NewApi"
message="Cast from `SeekableInputReader` to `InputReader` requires API level 30 (current min is 29)"
errorLine1=" mExoDataReader.mInputReader = seekableInputReader;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="1201"
column="39"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
errorLine1=" mPendingSeekPosition = seekPoint.position;"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="1287"
column="36"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" mPendingSeekTimeMicros = seekPoint.timeMicros;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="1288"
column="38"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#position`"
errorLine1=" mExtractor.seek(seekPoint.position, seekPoint.timeMicros);"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/apex/media/framework/java/android/media/MediaParser.java"
line="1290"
column="29"/>
</issue>
<issue
id="NewApi"
message="Field requires API level R (current min is 29): `android.media.MediaParser.SeekPoint#timeMicros`"
errorLine1=" mExtractor.seek(seekPoint.position, seekPoint.timeMicros);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<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>

View File

@@ -1,2 +0,0 @@
# Keep all symbols in android.media.
-keep class android.media.* {*;}

View File

@@ -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",
],
}

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

@@ -1 +0,0 @@
// Signature format: 2.0

View File

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