summaryrefslogtreecommitdiff
path: root/media/java
diff options
context:
space:
mode:
Diffstat (limited to 'media/java')
-rw-r--r--media/java/android/media/AudioRecord.java55
-rw-r--r--media/java/android/media/AudioRecordingConfiguration.java168
-rw-r--r--media/java/android/media/AudioRecordingMonitor.java56
-rw-r--r--media/java/android/media/AudioRecordingMonitorClient.java28
-rw-r--r--media/java/android/media/AudioRecordingMonitorImpl.java250
-rw-r--r--media/java/android/media/AudioSystem.java19
-rw-r--r--media/java/android/media/FileDataSourceDesc.java59
-rw-r--r--media/java/android/media/MediaItem2.java324
-rw-r--r--media/java/android/media/MediaMetadataRetriever.java8
-rw-r--r--media/java/android/media/MediaPlayer.java32
-rw-r--r--media/java/android/media/MediaPlayer2.java166
-rw-r--r--media/java/android/media/MediaPlayerBase.java331
-rw-r--r--media/java/android/media/MediaRecorder.java61
-rw-r--r--media/java/android/media/ThumbnailUtils.java566
14 files changed, 1367 insertions, 756 deletions
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 4b2353c992f2..33f81f1db69c 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -16,8 +16,10 @@
package android.media;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityThread;
@@ -43,6 +45,7 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* The AudioRecord class manages the audio resources for Java applications
@@ -58,7 +61,7 @@ import java.util.List;
* been read yet. Data should be read from the audio hardware in chunks of sizes inferior to
* the total recording buffer size.
*/
-public class AudioRecord implements AudioRouting
+public class AudioRecord implements AudioRouting, AudioRecordingMonitor, AudioRecordingMonitorClient
{
//---------------------------------------------------------
// Constants
@@ -1654,6 +1657,56 @@ public class AudioRecord implements AudioRouting
return activeMicrophones;
}
+
+ //--------------------------------------------------------------------------
+ // Implementation of AudioRecordingMonitor interface
+ //--------------------
+
+ AudioRecordingMonitorImpl mRecordingInfoImpl =
+ new AudioRecordingMonitorImpl((AudioRecordingMonitorClient) this);
+
+ /**
+ * Register a callback to be notified of audio capture changes via a
+ * {@link AudioManager.AudioRecordingCallback}. A callback is received when the capture path
+ * configuration changes (pre-processing, format, sampling rate...) or capture is
+ * silenced/unsilenced by the system.
+ * @param executor {@link Executor} to handle the callbacks.
+ * @param cb non-null callback to register
+ */
+ public void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull AudioManager.AudioRecordingCallback cb) {
+ mRecordingInfoImpl.registerAudioRecordingCallback(executor, cb);
+ }
+
+ /**
+ * Unregister an audio recording callback previously registered with
+ * {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
+ * @param cb non-null callback to unregister
+ */
+ public void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb) {
+ mRecordingInfoImpl.unregisterAudioRecordingCallback(cb);
+ }
+
+ /**
+ * Returns the current active audio recording for this audio recorder.
+ * @return a valid {@link AudioRecordingConfiguration} if this recorder is active
+ * or null otherwise.
+ * @see AudioRecordingConfiguration
+ */
+ public @Nullable AudioRecordingConfiguration getActiveRecordingConfiguration() {
+ return mRecordingInfoImpl.getActiveRecordingConfiguration();
+ }
+
+ //---------------------------------------------------------
+ // Implementation of AudioRecordingMonitorClient interface
+ //--------------------
+ /**
+ * @hide
+ */
+ public int getPortId() {
+ return native_getPortId();
+ }
+
//---------------------------------------------------------
// Interface definitions
//--------------------
diff --git a/media/java/android/media/AudioRecordingConfiguration.java b/media/java/android/media/AudioRecordingConfiguration.java
index 9ada216d5e95..de76aeff82c4 100644
--- a/media/java/android/media/AudioRecordingConfiguration.java
+++ b/media/java/android/media/AudioRecordingConfiguration.java
@@ -18,7 +18,9 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
+import android.media.audiofx.AudioEffect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@@ -27,6 +29,8 @@ import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
/**
@@ -48,7 +52,7 @@ import java.util.Objects;
public final class AudioRecordingConfiguration implements Parcelable {
private final static String TAG = new String("AudioRecordingConfiguration");
- private final int mSessionId;
+ private final int mClientSessionId;
private final int mClientSource;
@@ -60,18 +64,50 @@ public final class AudioRecordingConfiguration implements Parcelable {
private final int mPatchHandle;
+ private final int mClientPortId;
+
+ private boolean mClientSilenced;
+
+ private final int mDeviceSource;
+
+ private final AudioEffect.Descriptor[] mClientEffects;
+
+ private final AudioEffect.Descriptor[] mDeviceEffects;
+
/**
* @hide
*/
+ @TestApi
public AudioRecordingConfiguration(int uid, int session, int source, AudioFormat clientFormat,
- AudioFormat devFormat, int patchHandle, String packageName) {
+ AudioFormat devFormat, int patchHandle, String packageName, int clientPortId,
+ boolean clientSilenced, int deviceSource,
+ AudioEffect.Descriptor[] clientEffects, AudioEffect.Descriptor[] deviceEffects) {
mClientUid = uid;
- mSessionId = session;
+ mClientSessionId = session;
mClientSource = source;
mClientFormat = clientFormat;
mDeviceFormat = devFormat;
mPatchHandle = patchHandle;
mClientPackageName = packageName;
+ mClientPortId = clientPortId;
+ mClientSilenced = clientSilenced;
+ mDeviceSource = deviceSource;
+ mClientEffects = clientEffects;
+ mDeviceEffects = deviceEffects;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public AudioRecordingConfiguration(int uid, int session, int source,
+ AudioFormat clientFormat, AudioFormat devFormat,
+ int patchHandle, String packageName) {
+ this(uid, session, source, clientFormat,
+ devFormat, patchHandle, packageName, 0 /*clientPortId*/,
+ false /*clientSilenced*/, MediaRecorder.AudioSource.DEFAULT /*deviceSource*/,
+ new AudioEffect.Descriptor[0] /*clientEffects*/,
+ new AudioEffect.Descriptor[0] /*deviceEffects*/);
}
/**
@@ -87,13 +123,26 @@ public final class AudioRecordingConfiguration implements Parcelable {
* @hide
*/
public static String toLogFriendlyString(AudioRecordingConfiguration arc) {
- return new String("session:" + arc.mSessionId
- + " -- source:" + MediaRecorder.toLogFriendlyAudioSource(arc.mClientSource)
+ String clientEffects = new String();
+ for (AudioEffect.Descriptor desc : arc.mClientEffects) {
+ clientEffects += "'" + desc.name + "' ";
+ }
+ String deviceEffects = new String();
+ for (AudioEffect.Descriptor desc : arc.mDeviceEffects) {
+ deviceEffects += "'" + desc.name + "' ";
+ }
+
+ return new String("session:" + arc.mClientSessionId
+ + " -- source client=" + MediaRecorder.toLogFriendlyAudioSource(arc.mClientSource)
+ + ", dev=" + arc.mDeviceFormat.toLogFriendlyString()
+ " -- uid:" + arc.mClientUid
+ " -- patch:" + arc.mPatchHandle
+ " -- pack:" + arc.mClientPackageName
+ " -- format client=" + arc.mClientFormat.toLogFriendlyString()
- + ", dev=" + arc.mDeviceFormat.toLogFriendlyString());
+ + ", dev=" + arc.mDeviceFormat.toLogFriendlyString()
+ + " -- silenced:" + arc.mClientSilenced
+ + " -- effects client=" + clientEffects
+ + ", dev=" + deviceEffects);
}
// Note that this method is called server side, so no "privileged" information is ever sent
@@ -106,8 +155,10 @@ public final class AudioRecordingConfiguration implements Parcelable {
*/
public static AudioRecordingConfiguration anonymizedCopy(AudioRecordingConfiguration in) {
return new AudioRecordingConfiguration( /*anonymized uid*/ -1,
- in.mSessionId, in.mClientSource, in.mClientFormat,
- in.mDeviceFormat, in.mPatchHandle, "" /*empty package name*/);
+ in.mClientSessionId, in.mClientSource, in.mClientFormat,
+ in.mDeviceFormat, in.mPatchHandle, "" /*empty package name*/,
+ in.mClientPortId, in.mClientSilenced, in.mDeviceSource, in.mClientEffects,
+ in.mDeviceEffects);
}
// matches the sources that return false in MediaRecorder.isSystemOnlyAudioSource(source)
@@ -129,16 +180,8 @@ public final class AudioRecordingConfiguration implements Parcelable {
// documented return values match the sources that return false
// in MediaRecorder.isSystemOnlyAudioSource(source)
/**
- * Returns the audio source being used for the recording.
- * @return one of {@link MediaRecorder.AudioSource#DEFAULT},
- * {@link MediaRecorder.AudioSource#MIC},
- * {@link MediaRecorder.AudioSource#VOICE_UPLINK},
- * {@link MediaRecorder.AudioSource#VOICE_DOWNLINK},
- * {@link MediaRecorder.AudioSource#VOICE_CALL},
- * {@link MediaRecorder.AudioSource#CAMCORDER},
- * {@link MediaRecorder.AudioSource#VOICE_RECOGNITION},
- * {@link MediaRecorder.AudioSource#VOICE_COMMUNICATION},
- * {@link MediaRecorder.AudioSource#UNPROCESSED}.
+ * Returns the audio source selected by the client.
+ * @return the audio source selected by the client.
*/
public @AudioSource int getClientAudioSource() { return mClientSource; }
@@ -146,7 +189,9 @@ public final class AudioRecordingConfiguration implements Parcelable {
* Returns the session number of the recording, see {@link AudioRecord#getAudioSessionId()}.
* @return the session number.
*/
- public int getClientAudioSessionId() { return mSessionId; }
+ public int getClientAudioSessionId() {
+ return mClientSessionId;
+ }
/**
* Returns the audio format at which audio is recorded on this Android device.
@@ -223,6 +268,54 @@ public final class AudioRecordingConfiguration implements Parcelable {
return null;
}
+ /**
+ * Returns the system unique ID assigned for the AudioRecord object corresponding to this
+ * AudioRecordingConfiguration client.
+ * @return the port ID.
+ */
+ int getClientPortId() {
+ return mClientPortId;
+ }
+
+ /**
+ * Returns true if the audio returned to the client is currently being silenced by the
+ * audio framework due to concurrent capture policy (e.g the capturing application does not have
+ * an active foreground process or service anymore).
+ * @return true if captured audio is silenced, false otherwise .
+ */
+ public boolean isClientSilenced() {
+ return mClientSilenced;
+ }
+
+ /**
+ * Returns the audio source currently used to configure the capture path. It can be different
+ * from the source returned by {@link #getClientAudioSource()} if another capture is active.
+ * @return the audio source active on the capture path.
+ */
+ public @AudioSource int getAudioSource() {
+ return mDeviceSource;
+ }
+
+ /**
+ * Returns the list of {@link AudioEffect.Descriptor} for all effects currently enabled on
+ * the audio capture client (e.g. {@link AudioRecord} or {@link MediaRecorder}).
+ * @return List of {@link AudioEffect.Descriptor} containing all effects enabled for the client.
+ */
+ public @NonNull List<AudioEffect.Descriptor> getClientEffects() {
+ return new ArrayList<AudioEffect.Descriptor>(Arrays.asList(mClientEffects));
+ }
+
+ /**
+ * Returns the list of {@link AudioEffect.Descriptor} for all effects currently enabled on
+ * the capture stream.
+ * @return List of {@link AudioEffect.Descriptor} containing all effects enabled on the
+ * capture stream. This can be different from the list returned by {@link #getClientEffects()}
+ * if another capture is active.
+ */
+ public @NonNull List<AudioEffect.Descriptor> getEffects() {
+ return new ArrayList<AudioEffect.Descriptor>(Arrays.asList(mDeviceEffects));
+ }
+
public static final Parcelable.Creator<AudioRecordingConfiguration> CREATOR
= new Parcelable.Creator<AudioRecordingConfiguration>() {
/**
@@ -240,7 +333,7 @@ public final class AudioRecordingConfiguration implements Parcelable {
@Override
public int hashCode() {
- return Objects.hash(mSessionId, mClientSource);
+ return Objects.hash(mClientSessionId, mClientSource);
}
@Override
@@ -250,23 +343,45 @@ public final class AudioRecordingConfiguration implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mSessionId);
+ dest.writeInt(mClientSessionId);
dest.writeInt(mClientSource);
mClientFormat.writeToParcel(dest, 0);
mDeviceFormat.writeToParcel(dest, 0);
dest.writeInt(mPatchHandle);
dest.writeString(mClientPackageName);
dest.writeInt(mClientUid);
+ dest.writeInt(mClientPortId);
+ dest.writeBoolean(mClientSilenced);
+ dest.writeInt(mDeviceSource);
+ dest.writeInt(mClientEffects.length);
+ for (int i = 0; i < mClientEffects.length; i++) {
+ mClientEffects[i].writeToParcel(dest, 0);
+ }
+ dest.writeInt(mDeviceEffects.length);
+ for (int i = 0; i < mDeviceEffects.length; i++) {
+ mDeviceEffects[i].writeToParcel(dest, 0);
+ }
}
private AudioRecordingConfiguration(Parcel in) {
- mSessionId = in.readInt();
+ mClientSessionId = in.readInt();
mClientSource = in.readInt();
mClientFormat = AudioFormat.CREATOR.createFromParcel(in);
mDeviceFormat = AudioFormat.CREATOR.createFromParcel(in);
mPatchHandle = in.readInt();
mClientPackageName = in.readString();
mClientUid = in.readInt();
+ mClientPortId = in.readInt();
+ mClientSilenced = in.readBoolean();
+ mDeviceSource = in.readInt();
+ mClientEffects = AudioEffect.Descriptor.CREATOR.newArray(in.readInt());
+ for (int i = 0; i < mClientEffects.length; i++) {
+ mClientEffects[i] = AudioEffect.Descriptor.CREATOR.createFromParcel(in);
+ }
+ mDeviceEffects = AudioEffect.Descriptor.CREATOR.newArray(in.readInt());
+ for (int i = 0; i < mClientEffects.length; i++) {
+ mDeviceEffects[i] = AudioEffect.Descriptor.CREATOR.createFromParcel(in);
+ }
}
@Override
@@ -277,11 +392,16 @@ public final class AudioRecordingConfiguration implements Parcelable {
AudioRecordingConfiguration that = (AudioRecordingConfiguration) o;
return ((mClientUid == that.mClientUid)
- && (mSessionId == that.mSessionId)
+ && (mClientSessionId == that.mClientSessionId)
&& (mClientSource == that.mClientSource)
&& (mPatchHandle == that.mPatchHandle)
&& (mClientFormat.equals(that.mClientFormat))
&& (mDeviceFormat.equals(that.mDeviceFormat))
- && (mClientPackageName.equals(that.mClientPackageName)));
+ && (mClientPackageName.equals(that.mClientPackageName))
+ && (mClientPortId == that.mClientPortId)
+ && (mClientSilenced == that.mClientSilenced)
+ && (mDeviceSource == that.mDeviceSource)
+ && (mClientEffects.equals(that.mClientEffects))
+ && (mDeviceEffects.equals(that.mDeviceEffects)));
}
}
diff --git a/media/java/android/media/AudioRecordingMonitor.java b/media/java/android/media/AudioRecordingMonitor.java
new file mode 100644
index 000000000000..e2605d074c86
--- /dev/null
+++ b/media/java/android/media/AudioRecordingMonitor.java
@@ -0,0 +1,56 @@
+/*
+ * 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.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * AudioRecordingMonitor defines an interface implemented by {@link AudioRecord} and
+ * {@link MediaRecorder} allowing applications to install a callback and be notified of changes
+ * in the capture path while recoding is active.
+ */
+public interface AudioRecordingMonitor {
+ /**
+ * Register a callback to be notified of audio capture changes via a
+ * {@link AudioManager.AudioRecordingCallback}. A callback is received when the capture path
+ * configuration changes (pre-processing, format, sampling rate...) or capture is
+ * silenced/unsilenced by the system.
+ * @param executor {@link Executor} to handle the callbacks.
+ * @param cb non-null callback to register
+ */
+ void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull AudioManager.AudioRecordingCallback cb);
+
+ /**
+ * Unregister an audio recording callback previously registered with
+ * {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
+ * @param cb non-null callback to unregister
+ */
+ void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb);
+
+ /**
+ * Returns the current active audio recording for this audio recorder.
+ * @return a valid {@link AudioRecordingConfiguration} if this recorder is active
+ * or null otherwise.
+ * @see AudioRecordingConfiguration
+ */
+ @Nullable AudioRecordingConfiguration getActiveRecordingConfiguration();
+}
diff --git a/media/java/android/media/AudioRecordingMonitorClient.java b/media/java/android/media/AudioRecordingMonitorClient.java
new file mode 100644
index 000000000000..7578d9b9a5fd
--- /dev/null
+++ b/media/java/android/media/AudioRecordingMonitorClient.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Interface implemented by classes using { @link AudioRecordingMonitor} interface.
+ * @hide
+ */
+public interface AudioRecordingMonitorClient {
+ /**
+ * @return the unique port ID allocated by audio framework to this recorder
+ */
+ int getPortId();
+}
diff --git a/media/java/android/media/AudioRecordingMonitorImpl.java b/media/java/android/media/AudioRecordingMonitorImpl.java
new file mode 100644
index 000000000000..c2cd4bc0b7eb
--- /dev/null
+++ b/media/java/android/media/AudioRecordingMonitorImpl.java
@@ -0,0 +1,250 @@
+/*
+ * 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.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of AudioRecordingMonitor interface.
+ * @hide
+ */
+public class AudioRecordingMonitorImpl implements AudioRecordingMonitor {
+
+ private static final String TAG = "android.media.AudioRecordingMonitor";
+
+ private static IAudioService sService; //lazy initialization, use getService()
+
+ private final AudioRecordingMonitorClient mClient;
+
+ AudioRecordingMonitorImpl(@NonNull AudioRecordingMonitorClient client) {
+ mClient = client;
+ }
+
+ /**
+ * Register a callback to be notified of audio capture changes via a
+ * {@link AudioManager.AudioRecordingCallback}. A callback is received when the capture path
+ * configuration changes (pre-processing, format, sampling rate...) or capture is
+ * silenced/unsilenced by the system.
+ * @param executor {@link Executor} to handle the callbacks.
+ * @param cb non-null callback to register
+ */
+ public void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull AudioManager.AudioRecordingCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null AudioRecordingCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor");
+ }
+ synchronized (mRecordCallbackLock) {
+ // check if eventCallback already in list
+ for (AudioRecordingCallbackInfo arci : mRecordCallbackList) {
+ if (arci.mCb == cb) {
+ throw new IllegalArgumentException(
+ "AudioRecordingCallback already registered");
+ }
+ }
+ beginRecordingCallbackHandling();
+ mRecordCallbackList.add(new AudioRecordingCallbackInfo(executor, cb));
+ }
+ }
+
+ /**
+ * Unregister an audio recording callback previously registered with
+ * {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
+ * @param cb non-null callback to unregister
+ */
+ public void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null AudioRecordingCallback argument");
+ }
+
+ synchronized (mRecordCallbackLock) {
+ for (AudioRecordingCallbackInfo arci : mRecordCallbackList) {
+ if (arci.mCb == cb) {
+ // ok to remove while iterating over list as we exit iteration
+ mRecordCallbackList.remove(arci);
+ if (mRecordCallbackList.size() == 0) {
+ endRecordingCallbackHandling();
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException("AudioRecordingCallback was not registered");
+ }
+ }
+
+ /**
+ * Returns the current active audio recording for this audio recorder.
+ * @return a valid {@link AudioRecordingConfiguration} if this recorder is active
+ * or null otherwise.
+ * @see AudioRecordingConfiguration
+ */
+ public @Nullable AudioRecordingConfiguration getActiveRecordingConfiguration() {
+ final IAudioService service = getService();
+ try {
+ List<AudioRecordingConfiguration> configs = service.getActiveRecordingConfigurations();
+ return getMyConfig(configs);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static class AudioRecordingCallbackInfo {
+ final AudioManager.AudioRecordingCallback mCb;
+ final Executor mExecutor;
+ AudioRecordingCallbackInfo(Executor e, AudioManager.AudioRecordingCallback cb) {
+ mExecutor = e;
+ mCb = cb;
+ }
+ }
+
+ private static final int MSG_RECORDING_CONFIG_CHANGE = 1;
+
+ private final Object mRecordCallbackLock = new Object();
+ @GuardedBy("mRecordCallbackLock")
+ @NonNull private LinkedList<AudioRecordingCallbackInfo> mRecordCallbackList =
+ new LinkedList<AudioRecordingCallbackInfo>();
+ @GuardedBy("mRecordCallbackLock")
+ private @Nullable HandlerThread mRecordingCallbackHandlerThread;
+ @GuardedBy("mRecordCallbackLock")
+ private @Nullable volatile Handler mRecordingCallbackHandler;
+
+ @GuardedBy("mRecordCallbackLock")
+ private final IRecordingConfigDispatcher mRecordingCallback =
+ new IRecordingConfigDispatcher.Stub() {
+ @Override
+ public void dispatchRecordingConfigChange(List<AudioRecordingConfiguration> configs) {
+ AudioRecordingConfiguration config = getMyConfig(configs);
+ if (config != null) {
+ synchronized (mRecordCallbackLock) {
+ if (mRecordingCallbackHandler != null) {
+ final Message m = mRecordingCallbackHandler.obtainMessage(
+ MSG_RECORDING_CONFIG_CHANGE/*what*/, config /*obj*/);
+ mRecordingCallbackHandler.sendMessage(m);
+ }
+ }
+ }
+ }
+ };
+
+ @GuardedBy("mRecordCallbackLock")
+ private void beginRecordingCallbackHandling() {
+ if (mRecordingCallbackHandlerThread == null) {
+ mRecordingCallbackHandlerThread = new HandlerThread(TAG + ".RecordingCallback");
+ mRecordingCallbackHandlerThread.start();
+ final Looper looper = mRecordingCallbackHandlerThread.getLooper();
+ if (looper != null) {
+ mRecordingCallbackHandler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_RECORDING_CONFIG_CHANGE: {
+ if (msg.obj == null) {
+ return;
+ }
+ ArrayList<AudioRecordingConfiguration> configs =
+ new ArrayList<AudioRecordingConfiguration>();
+ configs.add((AudioRecordingConfiguration) msg.obj);
+
+ final LinkedList<AudioRecordingCallbackInfo> cbInfoList;
+ synchronized (mRecordCallbackLock) {
+ if (mRecordCallbackList.size() == 0) {
+ return;
+ }
+ cbInfoList = new LinkedList<AudioRecordingCallbackInfo>(
+ mRecordCallbackList);
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (AudioRecordingCallbackInfo cbi : cbInfoList) {
+ cbi.mExecutor.execute(() ->
+ cbi.mCb.onRecordingConfigChanged(configs));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ } break;
+ default:
+ Log.e(TAG, "Unknown event " + msg.what);
+ break;
+ }
+ }
+ };
+ final IAudioService service = getService();
+ try {
+ service.registerRecordingCallback(mRecordingCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ @GuardedBy("mRecordCallbackLock")
+ private void endRecordingCallbackHandling() {
+ if (mRecordingCallbackHandlerThread != null) {
+ final IAudioService service = getService();
+ try {
+ service.unregisterRecordingCallback(mRecordingCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mRecordingCallbackHandlerThread.quit();
+ mRecordingCallbackHandlerThread = null;
+ }
+ }
+
+ AudioRecordingConfiguration getMyConfig(List<AudioRecordingConfiguration> configs) {
+ int portId = mClient.getPortId();
+ for (AudioRecordingConfiguration config : configs) {
+ if (config.getClientPortId() == portId) {
+ return config;
+ }
+ }
+ return null;
+ }
+
+ private static IAudioService getService() {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
+ sService = IAudioService.Stub.asInterface(b);
+ return sService;
+ }
+}
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 36f635a8e572..58fc1ab1c4dd 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.media.audiofx.AudioEffect;
import android.media.audiopolicy.AudioMix;
import android.os.Build;
import android.util.Log;
@@ -334,7 +335,9 @@ public class AudioSystem
* @param packName package name of the client app performing the recording. NOT SUPPORTED
*/
void onRecordingConfigurationChanged(int event, int uid, int session, int source,
- int[] recordingFormat, String packName);
+ int portId, boolean silenced, int[] recordingFormat,
+ AudioEffect.Descriptor[] clienteffects, AudioEffect.Descriptor[] effects,
+ int activeSource, String packName);
}
private static AudioRecordingCallback sRecordingCallback;
@@ -352,19 +355,27 @@ public class AudioSystem
* @param session
* @param source
* @param recordingFormat see
- * {@link AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int, int[])}
+ * {@link AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int, int,\
+ boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
* for the description of the record format.
*/
@UnsupportedAppUsage
private static void recordingCallbackFromNative(int event, int uid, int session, int source,
- int[] recordingFormat) {
+ int portId, boolean silenced, int[] recordingFormat,
+ AudioEffect.Descriptor[] clientEffects, AudioEffect.Descriptor[] effects,
+ int activeSource) {
AudioRecordingCallback cb = null;
synchronized (AudioSystem.class) {
cb = sRecordingCallback;
}
+
+ String clientEffectName = clientEffects.length == 0 ? "None" : clientEffects[0].name;
+ String effectName = effects.length == 0 ? "None" : effects[0].name;
+
if (cb != null) {
// TODO receive package name from native
- cb.onRecordingConfigurationChanged(event, uid, session, source, recordingFormat, "");
+ cb.onRecordingConfigurationChanged(event, uid, session, source, portId, silenced,
+ recordingFormat, clientEffects, effects, activeSource, "");
}
}
diff --git a/media/java/android/media/FileDataSourceDesc.java b/media/java/android/media/FileDataSourceDesc.java
index aca8dbed1a13..e29bd006c00a 100644
--- a/media/java/android/media/FileDataSourceDesc.java
+++ b/media/java/android/media/FileDataSourceDesc.java
@@ -44,6 +44,8 @@ public class FileDataSourceDesc extends DataSourceDesc {
private ParcelFileDescriptor mPFD;
private long mOffset = 0;
private long mLength = FD_LENGTH_UNKNOWN;
+ private int mCount = 0;
+ private boolean mClosed = false;
private FileDataSourceDesc() {
super();
@@ -55,27 +57,52 @@ public class FileDataSourceDesc extends DataSourceDesc {
@Override
void close() {
super.close();
- closeFD();
+ decCount();
}
/**
- * Releases the file descriptor held by this {@code FileDataSourceDesc} object.
+ * Decrements usage count by {@link MediaPlayer2}.
+ * If this is the last usage, also releases the file descriptor held by this
+ * {@code FileDataSourceDesc} object.
*/
- void closeFD() {
+ void decCount() {
synchronized (this) {
- if (mPFD != null) {
- try {
- mPFD.close();
- } catch (IOException e) {
- Log.e(TAG, "failed to close pfd: " + e);
- }
-
- mPFD = null;
+ --mCount;
+ if (mCount > 0) {
+ return;
+ }
+
+ try {
+ mPFD.close();
+ mClosed = true;
+ } catch (IOException e) {
+ Log.e(TAG, "failed to close pfd: " + e);
+ }
+ }
+ }
+
+ /**
+ * Increments usage count by {@link MediaPlayer2} if PFD has not been closed.
+ */
+ void incCount() {
+ synchronized (this) {
+ if (!mClosed) {
+ ++mCount;
}
}
}
/**
+ * Return the status of underline ParcelFileDescriptor
+ * @return true if underline ParcelFileDescriptor is closed, false otherwise.
+ */
+ boolean isPFDClosed() {
+ synchronized (this) {
+ return mClosed;
+ }
+ }
+
+ /**
* Return the ParcelFileDescriptor of this data source.
* @return the ParcelFileDescriptor of this data source
*/
@@ -150,6 +177,16 @@ public class FileDataSourceDesc extends DataSourceDesc {
* @return a new {@link FileDataSourceDesc} object
*/
public @NonNull FileDataSourceDesc build() {
+ if (mPFD == null) {
+ throw new IllegalStateException(
+ "underline ParcelFileDescriptor should not be null");
+ }
+ try {
+ mPFD.getFd();
+ } catch (IllegalStateException e) {
+ throw new IllegalStateException("ParcelFileDescriptor has been closed");
+ }
+
FileDataSourceDesc dsd = new FileDataSourceDesc();
super.build(dsd);
dsd.mPFD = mPFD;
diff --git a/media/java/android/media/MediaItem2.java b/media/java/android/media/MediaItem2.java
new file mode 100644
index 000000000000..aa2a937956c5
--- /dev/null
+++ b/media/java/android/media/MediaItem2.java
@@ -0,0 +1,324 @@
+/*
+ * 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.MediaMetadata.METADATA_KEY_MEDIA_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A class with information on a single media item with the metadata information. Here are use
+ * cases.
+ * <ul>
+ * <li>Specify media items to {@link SessionPlayer2} for playback.
+ * <li>Share media items across the processes.
+ * </ul>
+ * <p>
+ * Subclasses of the session player may only accept certain subclasses of the media items. Check
+ * the player documentation that you're interested in.
+ * <p>
+ * When it's shared across the processes, we cannot guarantee that they contain the right values
+ * because media items are application dependent especially for the metadata.
+ * <p>
+ * This object is thread safe.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link androidx.media2.MediaItem} for consistent behavior across all devices.
+ * </p>
+ * @hide
+ */
+public class MediaItem2 implements Parcelable {
+ private static final String TAG = "MediaItem2";
+
+ // intentionally less than long.MAX_VALUE.
+ // Declare this first to avoid 'illegal forward reference'.
+ static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ /**
+ * Used when a position is unknown.
+ *
+ * @see #getEndPosition()
+ */
+ public static final long POSITION_UNKNOWN = LONG_MAX;
+
+ public static final Parcelable.Creator<MediaItem2> CREATOR =
+ new Parcelable.Creator<MediaItem2>() {
+ @Override
+ public MediaItem2 createFromParcel(Parcel in) {
+ return new MediaItem2(in);
+ }
+
+ @Override
+ public MediaItem2[] newArray(int size) {
+ return new MediaItem2[size];
+ }
+ };
+
+ // TODO: Use SessionPlayer2.UNKNOWN_TIME instead
+ private static final long UNKNOWN_TIME = -1;
+
+ private final long mStartPositionMs;
+ private final long mEndPositionMs;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private MediaMetadata mMetadata;
+ @GuardedBy("mLock")
+ private final List<Pair<OnMetadataChangedListener, Executor>> mListeners = new ArrayList<>();
+
+ /**
+ * Used by {@link MediaItem2.Builder}.
+ */
+ // Note: Needs to be protected when we want to allow 3rd party player to define customized
+ // MediaItem2.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(Builder builder) {
+ this(builder.mMetadata, builder.mStartPositionMs, builder.mEndPositionMs);
+ }
+
+ /**
+ * Used by Parcelable.Creator.
+ */
+ // Note: Needs to be protected when we want to allow 3rd party player to define customized
+ // MediaItem2.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(Parcel in) {
+ this(in.readParcelable(MediaItem2.class.getClassLoader()), in.readLong(), in.readLong());
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(MediaItem2 item) {
+ this(item.mMetadata, item.mStartPositionMs, item.mEndPositionMs);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(@Nullable MediaMetadata metadata, long startPositionMs, long endPositionMs) {
+ if (startPositionMs > endPositionMs) {
+ throw new IllegalArgumentException("Illegal start/end position: "
+ + startPositionMs + " : " + endPositionMs);
+ }
+ if (metadata != null && metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+ long durationMs = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+ if (durationMs != UNKNOWN_TIME && endPositionMs != POSITION_UNKNOWN
+ && endPositionMs > durationMs) {
+ throw new IllegalArgumentException("endPositionMs shouldn't be greater than"
+ + " duration in the metdata, endPositionMs=" + endPositionMs
+ + ", durationMs=" + durationMs);
+ }
+ }
+ mMetadata = metadata;
+ mStartPositionMs = startPositionMs;
+ mEndPositionMs = endPositionMs;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
+ synchronized (mLock) {
+ sb.append("{mMetadata=").append(mMetadata);
+ sb.append(", mStartPositionMs=").append(mStartPositionMs);
+ sb.append(", mEndPositionMs=").append(mEndPositionMs);
+ sb.append('}');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Sets metadata. If the metadata is not {@code null}, its id should be matched with this
+ * instance's media id.
+ *
+ * @param metadata metadata to update
+ * @see MediaMetadata#METADATA_KEY_MEDIA_ID
+ */
+ public void setMetadata(@Nullable MediaMetadata metadata) {
+ List<Pair<OnMetadataChangedListener, Executor>> listeners = new ArrayList<>();
+ synchronized (mLock) {
+ if (mMetadata != null && metadata != null
+ && !TextUtils.equals(getMediaId(), metadata.getString(METADATA_KEY_MEDIA_ID))) {
+ Log.d(TAG, "MediaItem2's media ID shouldn't be changed");
+ return;
+ }
+ mMetadata = metadata;
+ listeners.addAll(mListeners);
+ }
+
+ for (Pair<OnMetadataChangedListener, Executor> pair : listeners) {
+ final OnMetadataChangedListener listener = pair.first;
+ pair.second.execute(new Runnable() {
+ @Override
+ public void run() {
+ listener.onMetadataChanged(MediaItem2.this);
+ }
+ });
+ }
+ }
+
+ /**
+ * Gets the metadata of the media.
+ *
+ * @return metadata from the session
+ */
+ public @Nullable MediaMetadata getMetadata() {
+ synchronized (mLock) {
+ return mMetadata;
+ }
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will start.
+ * @return the position in milliseconds at which the playback will start
+ */
+ public long getStartPosition() {
+ return mStartPositionMs;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will end.
+ * {@link #POSITION_UNKNOWN} means ending at the end of source content.
+ * @return the position in milliseconds at which the playback will end
+ */
+ public long getEndPosition() {
+ return mEndPositionMs;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mMetadata, 0);
+ dest.writeLong(mStartPositionMs);
+ dest.writeLong(mEndPositionMs);
+ }
+
+ /**
+ * Gets the media id for this item. If it's not {@code null}, it's a persistent unique key
+ * for the underlying media content.
+ *
+ * @return media Id from the session
+ */
+ @Nullable String getMediaId() {
+ synchronized (mLock) {
+ return mMetadata != null
+ ? mMetadata.getString(METADATA_KEY_MEDIA_ID) : null;
+ }
+ }
+
+ void addOnMetadataChangedListener(Executor executor, OnMetadataChangedListener listener) {
+ synchronized (mLock) {
+ for (Pair<OnMetadataChangedListener, Executor> pair : mListeners) {
+ if (pair.first == listener) {
+ return;
+ }
+ }
+ mListeners.add(new Pair<>(listener, executor));
+ }
+ }
+
+ void removeOnMetadataChangedListener(OnMetadataChangedListener listener) {
+ synchronized (mLock) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ if (mListeners.get(i).first == listener) {
+ mListeners.remove(i);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Builder for {@link MediaItem2}.
+ */
+ public static class Builder {
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaMetadata mMetadata;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ long mStartPositionMs = 0;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ long mEndPositionMs = POSITION_UNKNOWN;
+
+ /**
+ * Set the metadata of this instance. {@code null} for unset.
+ *
+ * @param metadata metadata
+ * @return this instance for chaining
+ */
+ public @NonNull Builder setMetadata(@Nullable MediaMetadata metadata) {
+ mMetadata = metadata;
+ return this;
+ }
+
+ /**
+ * Sets the start position in milliseconds at which the playback will start.
+ * Any negative number is treated as 0.
+ *
+ * @param position the start position in milliseconds at which the playback will start
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setStartPosition(long position) {
+ if (position < 0) {
+ position = 0;
+ }
+ mStartPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the end position in milliseconds at which the playback will end.
+ * Any negative number is treated as maximum length of the media item.
+ *
+ * @param position the end position in milliseconds at which the playback will end
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setEndPosition(long position) {
+ if (position < 0) {
+ position = POSITION_UNKNOWN;
+ }
+ mEndPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Build {@link MediaItem2}.
+ *
+ * @return a new {@link MediaItem2}.
+ */
+ public @NonNull MediaItem2 build() {
+ return new MediaItem2(this);
+ }
+ }
+
+ interface OnMetadataChangedListener {
+ void onMetadataChanged(MediaItem2 item);
+ }
+}
diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java
index 00a393a902b4..d656fa359826 100644
--- a/media/java/android/media/MediaMetadataRetriever.java
+++ b/media/java/android/media/MediaMetadataRetriever.java
@@ -40,8 +40,7 @@ import java.util.Map;
* MediaMetadataRetriever class provides a unified interface for retrieving
* frame and meta data from an input media file.
*/
-public class MediaMetadataRetriever
-{
+public class MediaMetadataRetriever implements AutoCloseable {
static {
System.loadLibrary("media_jni");
native_init();
@@ -672,6 +671,11 @@ public class MediaMetadataRetriever
@UnsupportedAppUsage
private native byte[] getEmbeddedPicture(int pictureType);
+ @Override
+ public void close() {
+ release();
+ }
+
/**
* Call it when one is done with the object. This method releases the memory
* allocated internally.
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 18d36eb1f753..0057875ec3f4 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -19,7 +19,6 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityThread;
import android.content.ContentProvider;
@@ -1680,37 +1679,6 @@ public class MediaPlayer extends PlayerBase
public native boolean isPlaying();
/**
- * Gets the current buffering management params used by the source component.
- * Calling it only after {@code setDataSource} has been called.
- * Each type of data source might have different set of default params.
- *
- * @return the current buffering management params used by the source component.
- * @throws IllegalStateException if the internal player engine has not been
- * initialized, or {@code setDataSource} has not been called.
- * @hide
- */
- @NonNull
- @TestApi
- public native BufferingParams getBufferingParams();
-
- /**
- * Sets buffering management params.
- * The object sets its internal BufferingParams to the input, except that the input is
- * invalid or not supported.
- * Call it only after {@code setDataSource} has been called.
- * The input is a hint to MediaPlayer.
- *
- * @param params the buffering management params.
- *
- * @throws IllegalStateException if the internal player engine has not been
- * initialized or has been released, or {@code setDataSource} has not been called.
- * @throws IllegalArgumentException if params is invalid or not supported.
- * @hide
- */
- @TestApi
- public native void setBufferingParams(@NonNull BufferingParams params);
-
- /**
* Change playback speed of audio by resampling the audio.
* <p>
* Specifies resampling as audio mode for variable rate playback, i.e.,
diff --git a/media/java/android/media/MediaPlayer2.java b/media/java/android/media/MediaPlayer2.java
index d4b1c7f868cb..34345b31ded1 100644
--- a/media/java/android/media/MediaPlayer2.java
+++ b/media/java/android/media/MediaPlayer2.java
@@ -544,32 +544,55 @@ public class MediaPlayer2 implements AutoCloseable
public native long getCurrentPosition();
/**
- * Gets the duration of the file.
+ * Gets the duration of the dsd.
*
+ * @param dsd the descriptor of data source of which you want to get duration
* @return the duration in milliseconds, if no duration is available
* (for example, if streaming live content), -1 is returned.
+ * @throws NullPointerException if dsd is null
*/
- public native long getDuration();
+ public long getDuration(@NonNull DataSourceDesc dsd) {
+ if (dsd == null) {
+ throw new NullPointerException("non-null dsd is expected");
+ }
+ SourceInfo sourceInfo = getSourceInfo(dsd);
+ if (sourceInfo == null) {
+ return -1;
+ }
+
+ return native_getDuration(sourceInfo.mId);
+ }
+
+ private native long native_getDuration(long srcId);
/**
- * Gets the current buffered media source position received through progressive downloading.
+ * Gets the buffered media source position of given dsd.
* For example a buffering update of 8000 milliseconds when 5000 milliseconds of the content
* has already been played indicates that the next 3000 milliseconds of the
* content to play has been buffered.
*
+ * @param dsd the descriptor of data source of which you want to get buffered position
* @return the current buffered media source position in milliseconds
+ * @throws NullPointerException if dsd is null
*/
- public long getBufferedPosition() {
+ public long getBufferedPosition(@NonNull DataSourceDesc dsd) {
+ if (dsd == null) {
+ throw new NullPointerException("non-null dsd is expected");
+ }
+ SourceInfo sourceInfo = getSourceInfo(dsd);
+ if (sourceInfo == null) {
+ return 0;
+ }
+
// Use cached buffered percent for now.
- int bufferedPercentage;
- synchronized (mSrcLock) {
- if (mCurrentSourceInfo == null) {
- bufferedPercentage = 0;
- } else {
- bufferedPercentage = mCurrentSourceInfo.mBufferedPercentage.get();
- }
+ int bufferedPercentage = sourceInfo.mBufferedPercentage.get();
+
+ long duration = getDuration(dsd);
+ if (duration < 0) {
+ duration = 0;
}
- return getDuration() * bufferedPercentage / 100;
+
+ return duration * bufferedPercentage / 100;
}
/**
@@ -673,7 +696,7 @@ public class MediaPlayer2 implements AutoCloseable
return addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
@Override
void process() throws IOException {
- Media2Utils.checkArgument(dsd != null, "the DataSourceDesc cannot be null");
+ checkDataSourceDesc(dsd);
int state = getState();
try {
if (state != PLAYER_STATE_ERROR && state != PLAYER_STATE_IDLE) {
@@ -706,7 +729,7 @@ public class MediaPlayer2 implements AutoCloseable
return addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
@Override
void process() {
- Media2Utils.checkArgument(dsd != null, "the DataSourceDesc cannot be null");
+ checkDataSourceDesc(dsd);
synchronized (mSrcLock) {
clearNextSourceInfos_l();
mNextSourceInfos.add(new SourceInfo(dsd));
@@ -732,22 +755,56 @@ public class MediaPlayer2 implements AutoCloseable
if (dsds == null || dsds.size() == 0) {
throw new IllegalArgumentException("data source list cannot be null or empty.");
}
+ boolean hasError = false;
+ for (DataSourceDesc dsd : dsds) {
+ if (dsd != null) {
+ hasError = true;
+ continue;
+ }
+ if (dsd instanceof FileDataSourceDesc) {
+ FileDataSourceDesc fdsd = (FileDataSourceDesc) dsd;
+ if (fdsd.isPFDClosed()) {
+ hasError = true;
+ continue;
+ }
- synchronized (mSrcLock) {
- clearNextSourceInfos_l();
+ fdsd.incCount();
+ }
+ }
+ if (hasError) {
for (DataSourceDesc dsd : dsds) {
if (dsd != null) {
- mNextSourceInfos.add(new SourceInfo(dsd));
- } else {
- Log.w(TAG, "DataSourceDesc in the source list shall not be null.");
+ dsd.close();
}
}
+ throw new IllegalArgumentException("invalid data source list");
+ }
+
+ synchronized (mSrcLock) {
+ clearNextSourceInfos_l();
+ for (DataSourceDesc dsd : dsds) {
+ mNextSourceInfos.add(new SourceInfo(dsd));
+ }
}
prepareNextDataSource();
}
});
}
+ // throws IllegalArgumentException if dsd is null or underline PFD of dsd has been closed.
+ private void checkDataSourceDesc(DataSourceDesc dsd) {
+ if (dsd == null) {
+ throw new IllegalArgumentException("dsd is expected to be non null");
+ }
+ if (dsd instanceof FileDataSourceDesc) {
+ FileDataSourceDesc fdsd = (FileDataSourceDesc) dsd;
+ if (fdsd.isPFDClosed()) {
+ throw new IllegalArgumentException("the underline FileDescriptor has been closed");
+ }
+ fdsd.incCount();
+ }
+ }
+
/**
* Removes all data sources pending to be played.
* @return a token which can be used to cancel the operation later with {@link #cancelCommand}.
@@ -1467,7 +1524,6 @@ public class MediaPlayer2 implements AutoCloseable
private native PersistableBundle native_getMetrics();
-
/**
* Gets the current buffering management params used by the source component.
* Calling it only after {@code setDataSource} has been called.
@@ -1505,7 +1561,6 @@ public class MediaPlayer2 implements AutoCloseable
private native void native_setBufferingParams(@NonNull BufferingParams params);
-
/**
* Sets playback rate using {@link PlaybackParams}. The object sets its internal
* PlaybackParams to the input. This allows the object to resume at previous speed
@@ -1969,19 +2024,31 @@ public class MediaPlayer2 implements AutoCloseable
/**
* Returns a List of track information.
*
+ * @param dsd the descriptor of data source of which you want to get track info
* @return List of track info. The total number of tracks is the array length.
* Must be called again if an external timed text source has been added after
* addTimedTextSource method is called.
* @throws IllegalStateException if it is called in an invalid state.
+ * @throws NullPointerException if dsd is null
*/
- public @NonNull List<TrackInfo> getTrackInfo() {
- TrackInfo[] trackInfo = getInbandTrackInfo();
+
+ public @NonNull List<TrackInfo> getTrackInfo(@NonNull DataSourceDesc dsd) {
+ if (dsd == null) {
+ throw new NullPointerException("non-null dsd is expected");
+ }
+ SourceInfo sourceInfo = getSourceInfo(dsd);
+ if (sourceInfo == null) {
+ return new ArrayList<TrackInfo>(0);
+ }
+
+ TrackInfo[] trackInfo = getInbandTrackInfo(sourceInfo);
return (trackInfo != null ? Arrays.asList(trackInfo) : new ArrayList<TrackInfo>(0));
}
- private TrackInfo[] getInbandTrackInfo() throws IllegalStateException {
+ private TrackInfo[] getInbandTrackInfo(SourceInfo sourceInfo) throws IllegalStateException {
PlayerMessage request = PlayerMessage.newBuilder()
.addValues(Value.newBuilder().setInt32Value(INVOKE_ID_GET_TRACK_INFO))
+ .addValues(Value.newBuilder().setInt64Value(sourceInfo.mId))
.build();
PlayerMessage response = invoke(request);
if (response == null) {
@@ -2001,9 +2068,10 @@ public class MediaPlayer2 implements AutoCloseable
/**
* Returns the index of the audio, video, or subtitle track currently selected for playback,
- * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
- * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ * The return value is an index into the array returned by {@link #getTrackInfo}, and can
+ * be used in calls to {@link #selectTrack} or {@link #deselectTrack}.
*
+ * @param dsd the descriptor of data source of which you want to get selected track
* @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
* {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
* {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
@@ -2011,14 +2079,24 @@ public class MediaPlayer2 implements AutoCloseable
* a negative integer is returned when there is no selected track for {@code trackType} or
* when {@code trackType} is not one of audio, video, or subtitle.
* @throws IllegalStateException if called after {@link #close()}
+ * @throws NullPointerException if dsd is null
*
- * @see #getTrackInfo()
- * @see #selectTrack(int)
- * @see #deselectTrack(int)
+ * @see #getTrackInfo
+ * @see #selectTrack
+ * @see #deselectTrack
*/
- public int getSelectedTrack(int trackType) {
+ public int getSelectedTrack(@NonNull DataSourceDesc dsd, int trackType) {
+ if (dsd == null) {
+ throw new NullPointerException("non-null dsd is expected");
+ }
+ SourceInfo sourceInfo = getSourceInfo(dsd);
+ if (sourceInfo == null) {
+ return -1;
+ }
+
PlayerMessage request = PlayerMessage.newBuilder()
.addValues(Value.newBuilder().setInt32Value(INVOKE_ID_GET_SELECTED_TRACK))
+ .addValues(Value.newBuilder().setInt64Value(sourceInfo.mId))
.addValues(Value.newBuilder().setInt32Value(trackType))
.build();
PlayerMessage response = invoke(request);
@@ -2049,19 +2127,20 @@ public class MediaPlayer2 implements AutoCloseable
* In addition, the support for selecting an audio track at runtime is pretty limited
* in that an audio track can only be selected in the <em>Prepared</em> state.
* </p>
+ * @param dsd the descriptor of data source of which you want to select track
* @param index the index of the track to be selected. The valid range of the index
* is 0..total number of track - 1. The total number of tracks as well as the type of
- * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * each individual track can be found by calling {@link #getTrackInfo} method.
* @return a token which can be used to cancel the operation later with {@link #cancelCommand}.
*
* @see MediaPlayer2#getTrackInfo
*/
// This is an asynchronous call.
- public Object selectTrack(int index) {
+ public Object selectTrack(@NonNull DataSourceDesc dsd, int index) {
return addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
@Override
void process() {
- selectOrDeselectTrack(index, true /* select */);
+ selectOrDeselectTrack(dsd, index, true /* select */);
}
});
}
@@ -2073,28 +2152,37 @@ public class MediaPlayer2 implements AutoCloseable
* deselected. If the timed text track identified by index has not been
* selected before, it throws an exception.
* </p>
+ * @param dsd the descriptor of data source of which you want to deselect track
* @param index the index of the track to be deselected. The valid range of the index
* is 0..total number of tracks - 1. The total number of tracks as well as the type of
- * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * each individual track can be found by calling {@link #getTrackInfo} method.
* @return a token which can be used to cancel the operation later with {@link #cancelCommand}.
*
* @see MediaPlayer2#getTrackInfo
*/
// This is an asynchronous call.
- public Object deselectTrack(int index) {
+ public Object deselectTrack(@NonNull DataSourceDesc dsd, int index) {
return addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
@Override
void process() {
- selectOrDeselectTrack(index, false /* select */);
+ selectOrDeselectTrack(dsd, index, false /* select */);
}
});
}
- private void selectOrDeselectTrack(int index, boolean select)
- throws IllegalStateException {
+ private void selectOrDeselectTrack(@NonNull DataSourceDesc dsd, int index, boolean select) {
+ if (dsd == null) {
+ throw new IllegalArgumentException("non-null dsd is expected");
+ }
+ SourceInfo sourceInfo = getSourceInfo(dsd);
+ if (sourceInfo == null) {
+ return;
+ }
+
PlayerMessage request = PlayerMessage.newBuilder()
.addValues(Value.newBuilder().setInt32Value(
select ? INVOKE_ID_SELECT_TRACK : INVOKE_ID_DESELECT_TRACK))
+ .addValues(Value.newBuilder().setInt64Value(sourceInfo.mId))
.addValues(Value.newBuilder().setInt32Value(index))
.build();
invoke(request);
@@ -2568,7 +2656,7 @@ public class MediaPlayer2 implements AutoCloseable
* Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates
* {@link TimedMetaData}.
*
- * @see MediaPlayer2#selectTrack(int)
+ * @see MediaPlayer2#selectTrack
* @see MediaPlayer2.OnTimedMetaDataAvailableListener
* @see TimedMetaData
*
diff --git a/media/java/android/media/MediaPlayerBase.java b/media/java/android/media/MediaPlayerBase.java
deleted file mode 100644
index a4265525fb6b..000000000000
--- a/media/java/android/media/MediaPlayerBase.java
+++ /dev/null
@@ -1,331 +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 java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * @hide
- * Base class for all media players that want media session.
- */
-public abstract class MediaPlayerBase implements AutoCloseable {
- /**
- * @hide
- */
- @IntDef({
- PLAYER_STATE_IDLE,
- PLAYER_STATE_PAUSED,
- PLAYER_STATE_PLAYING,
- PLAYER_STATE_ERROR })
- @Retention(RetentionPolicy.SOURCE)
- public @interface PlayerState {}
-
- /**
- * @hide
- */
- @IntDef({
- BUFFERING_STATE_UNKNOWN,
- BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
- BUFFERING_STATE_BUFFERING_AND_STARVED,
- BUFFERING_STATE_BUFFERING_COMPLETE })
- @Retention(RetentionPolicy.SOURCE)
- public @interface BuffState {}
-
- /**
- * State when the player is idle, and needs configuration to start playback.
- */
- public static final int PLAYER_STATE_IDLE = 0;
-
- /**
- * State when the player's playback is paused
- */
- public static final int PLAYER_STATE_PAUSED = 1;
-
- /**
- * State when the player's playback is ongoing
- */
- public static final int PLAYER_STATE_PLAYING = 2;
-
- /**
- * State when the player is in error state and cannot be recovered self.
- */
- public static final int PLAYER_STATE_ERROR = 3;
-
- /**
- * Buffering state is unknown.
- */
- public static final int BUFFERING_STATE_UNKNOWN = 0;
-
- /**
- * Buffering state indicating the player is buffering but enough has been buffered
- * for this player to be able to play the content.
- * See {@link #getBufferedPosition()} for how far is buffered already.
- */
- public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1;
-
- /**
- * Buffering state indicating the player is buffering, but the player is currently starved
- * for data, and cannot play.
- */
- public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2;
-
- /**
- * Buffering state indicating the player is done buffering, and the remainder of the content is
- * available for playback.
- */
- public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3;
-
- /**
- * Starts or resumes playback.
- */
- public abstract void play();
-
- /**
- * Prepares the player for playback.
- * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
- * notified when the preparation phase completed. During this time, the player may allocate
- * resources required to play, such as audio and video decoders.
- */
- public abstract void prepare();
-
- /**
- * Pauses playback.
- */
- public abstract void pause();
-
- /**
- * Resets the MediaPlayerBase to its uninitialized state.
- */
- public abstract void reset();
-
- /**
- *
- */
- public abstract void skipToNext();
-
- /**
- * Moves the playback head to the specified position
- * @param pos the new playback position expressed in ms.
- */
- public abstract void seekTo(long pos);
-
- public static final long UNKNOWN_TIME = -1;
-
- /**
- * Gets the current playback head position.
- * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown.
- */
- public long getCurrentPosition() { return UNKNOWN_TIME; }
-
- /**
- * Returns the duration of the current data source, or {@link #UNKNOWN_TIME} if unknown.
- * @return the duration in ms, or {@link #UNKNOWN_TIME}.
- */
- public long getDuration() { return UNKNOWN_TIME; }
-
- /**
- * Gets the buffered position of current playback, or {@link #UNKNOWN_TIME} if unknown.
- * @return the buffered position in ms, or {@link #UNKNOWN_TIME}.
- */
- public long getBufferedPosition() { return UNKNOWN_TIME; }
-
- /**
- * Returns the current player state.
- * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
- * notification of changes.
- * @return the current player state
- */
- public abstract @PlayerState int getPlayerState();
-
- /**
- * Returns the current buffering state of the player.
- * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
- * buffered.
- * @return the buffering state.
- */
- public abstract @BuffState int getBufferingState();
-
- /**
- * Sets the {@link AudioAttributes} to be used during the playback of the media.
- *
- * @param attributes non-null <code>AudioAttributes</code>.
- */
- public abstract void setAudioAttributes(@NonNull AudioAttributes attributes);
-
- /**
- * Returns AudioAttributes that media player has.
- */
- public abstract @Nullable AudioAttributes getAudioAttributes();
-
- /**
- * Sets the data source to be played.
- * @param dsd
- */
- public abstract void setDataSource(@NonNull DataSourceDesc dsd);
-
- /**
- * Sets the data source that will be played immediately after the current one is done playing.
- * @param dsd
- */
- public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
-
- /**
- * Sets the list of data sources that will be sequentially played after the current one. Each
- * data source is played immediately after the previous one is done playing.
- * @param dsds
- */
- public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
-
- /**
- * Returns the current data source.
- * @return the current data source, or null if none is set, or none available to play.
- */
- public abstract @Nullable DataSourceDesc getCurrentDataSource();
-
- /**
- * Configures the player to loop on the current data source.
- * @param loop true if the current data source is meant to loop.
- */
- public abstract void loopCurrent(boolean loop);
-
- /**
- * Sets the playback speed.
- * A value of 1.0f is the default playback value.
- * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
- * before using negative values.<br>
- * After changing the playback speed, it is recommended to query the actual speed supported
- * by the player, see {@link #getPlaybackSpeed()}.
- * @param speed
- */
- public abstract void setPlaybackSpeed(float speed);
-
- /**
- * Returns the actual playback speed to be used by the player when playing.
- * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
- * @return the actual playback speed
- */
- public float getPlaybackSpeed() { return 1.0f; }
-
- /**
- * Indicates whether reverse playback is supported.
- * Reverse playback is indicated by negative playback speeds, see
- * {@link #setPlaybackSpeed(float)}.
- * @return true if reverse playback is supported.
- */
- public boolean isReversePlaybackSupported() { return false; }
-
- /**
- * Sets the volume of the audio of the media to play, expressed as a linear multiplier
- * on the audio samples.
- * Note that this volume is specific to the player, and is separate from stream volume
- * used across the platform.<br>
- * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
- * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
- * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
- */
- public abstract void setPlayerVolume(float volume);
-
- /**
- * Returns the current volume of this player to this player.
- * Note that it does not take into account the associated stream volume.
- * @return the player volume.
- */
- public abstract float getPlayerVolume();
-
- /**
- * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
- */
- public float getMaxPlayerVolume() { return 1.0f; }
-
- /**
- * Adds a callback to be notified of events for this player.
- * @param e the {@link Executor} to be used for the events.
- * @param cb the callback to receive the events.
- */
- public abstract void registerPlayerEventCallback(@NonNull Executor e,
- @NonNull PlayerEventCallback cb);
-
- /**
- * Removes a previously registered callback for player events
- * @param cb the callback to remove
- */
- public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
-
- /**
- * A callback class to receive notifications for events on the media player.
- * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
- * register this callback.
- */
- public static abstract class PlayerEventCallback {
- /**
- * Called when the player's current data source has changed.
- *
- * @param mpb the player whose data source changed.
- * @param dsd the new current data source. null, if no more data sources available.
- */
- public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
- @Nullable DataSourceDesc dsd) { }
- /**
- * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
- * referenced by the given data source.
- * @param mpb the player that is prepared.
- * @param dsd the data source that the player is prepared to play.
- */
- public void onMediaPrepared(@NonNull MediaPlayerBase mpb, @NonNull DataSourceDesc dsd) { }
-
- /**
- * Called to indicate that the state of the player has changed.
- * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
- * @param mpb the player whose state has changed.
- * @param state the new state of the player.
- */
- public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
-
- /**
- * Called to report buffering events for a data source.
- * @param mpb the player that is buffering
- * @param dsd the data source for which buffering is happening.
- * @param state the new buffering state.
- */
- public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
- @NonNull DataSourceDesc dsd, @BuffState int state) { }
-
- /**
- * Called to indicate that the playback speed has changed.
- * @param mpb the player that has changed the playback speed.
- * @param speed the new playback speed.
- */
- public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
-
- /**
- * Called to indicate that {@link #seekTo(long)} is completed.
- *
- * @param mpb the player that has completed seeking.
- * @param position the previous seeking request.
- * @see #seekTo(long)
- */
- public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
- }
-
-}
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index 8ced021b1025..1cdc29102758 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -16,7 +16,9 @@
package android.media;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.UnsupportedAppUsage;
@@ -40,6 +42,8 @@ import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
+
/**
* Used to record audio and video. The recording control is based on a
@@ -83,7 +87,9 @@ import java.util.List;
* <a href="{@docRoot}guide/topics/media/audio-capture.html">Audio Capture</a> developer guide.</p>
* </div>
*/
-public class MediaRecorder implements AudioRouting
+public class MediaRecorder implements AudioRouting,
+ AudioRecordingMonitor,
+ AudioRecordingMonitorClient
{
static {
System.loadLibrary("media_jni");
@@ -304,7 +310,7 @@ public class MediaRecorder implements AudioRouting
/**
* Audio source for preemptible, low-priority software hotword detection
- * It presents the same gain and pre processing tuning as {@link #VOICE_RECOGNITION}.
+ * It presents the same gain and pre-processing tuning as {@link #VOICE_RECOGNITION}.
* <p>
* An application should use this audio source when it wishes to do
* always-on software hotword detection, while gracefully giving in to any other application
@@ -1471,6 +1477,57 @@ public class MediaRecorder implements AudioRouting
private native final int native_getActiveMicrophones(
ArrayList<MicrophoneInfo> activeMicrophones);
+ //--------------------------------------------------------------------------
+ // Implementation of AudioRecordingMonitor interface
+ //--------------------
+
+ AudioRecordingMonitorImpl mRecordingInfoImpl =
+ new AudioRecordingMonitorImpl((AudioRecordingMonitorClient) this);
+
+ /**
+ * Register a callback to be notified of audio capture changes via a
+ * {@link AudioManager.AudioRecordingCallback}. A callback is received when the capture path
+ * configuration changes (pre-processing, format, sampling rate...) or capture is
+ * silenced/unsilenced by the system.
+ * @param executor {@link Executor} to handle the callbacks.
+ * @param cb non-null callback to register
+ */
+ public void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull AudioManager.AudioRecordingCallback cb) {
+ mRecordingInfoImpl.registerAudioRecordingCallback(executor, cb);
+ }
+
+ /**
+ * Unregister an audio recording callback previously registered with
+ * {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
+ * @param cb non-null callback to unregister
+ */
+ public void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb) {
+ mRecordingInfoImpl.unregisterAudioRecordingCallback(cb);
+ }
+
+ /**
+ * Returns the current active audio recording for this audio recorder.
+ * @return a valid {@link AudioRecordingConfiguration} if this recorder is active
+ * or null otherwise.
+ * @see AudioRecordingConfiguration
+ */
+ public @Nullable AudioRecordingConfiguration getActiveRecordingConfiguration() {
+ return mRecordingInfoImpl.getActiveRecordingConfiguration();
+ }
+
+ //---------------------------------------------------------
+ // Implementation of AudioRecordingMonitorClient interface
+ //--------------------
+ /**
+ * @hide
+ */
+ public int getPortId() {
+ return native_getPortId();
+ }
+
+ private native int native_getPortId();
+
/**
* Called from native code when an interesting event happens. This method
* just uses the EventHandler system to post the event back to the main app thread.
diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java
index fd1406078e7a..f07076ad14aa 100644
--- a/media/java/android/media/ThumbnailUtils.java
+++ b/media/java/android/media/ThumbnailUtils.java
@@ -16,33 +16,53 @@
package android.media;
+import static android.media.MediaMetadataRetriever.METADATA_KEY_DURATION;
+import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT;
+import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH;
+import static android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC;
+import static android.os.Environment.MEDIA_UNKNOWN;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
+import android.graphics.ImageDecoder;
+import android.graphics.ImageDecoder.ImageInfo;
+import android.graphics.ImageDecoder.Source;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.Environment;
import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore.Images;
+import android.provider.MediaStore.ThumbnailConstants;
import android.util.Log;
+import android.util.Size;
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
+import com.android.internal.util.ArrayUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.ToIntFunction;
/**
- * Thumbnail generation routines for media provider.
+ * Utilities for generating visual thumbnails from files.
*/
-
public class ThumbnailUtils {
private static final String TAG = "ThumbnailUtils";
- /* Maximum pixels size for created bitmap. */
- private static final int MAX_NUM_PIXELS_THUMBNAIL = 512 * 384;
- private static final int MAX_NUM_PIXELS_MICRO_THUMBNAIL = 160 * 120;
- private static final int UNCONSTRAINED = -1;
+ /** @hide */
+ @Deprecated
+ @UnsupportedAppUsage
+ public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96;
/* Options used internally. */
private static final int OPTIONS_NONE = 0x0;
@@ -54,153 +74,252 @@ public class ThumbnailUtils {
*/
public static final int OPTIONS_RECYCLE_INPUT = 0x2;
+ private static Size convertKind(int kind) {
+ if (kind == ThumbnailConstants.MICRO_KIND) {
+ return Point.convert(ThumbnailConstants.MICRO_SIZE);
+ } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) {
+ return Point.convert(ThumbnailConstants.FULL_SCREEN_SIZE);
+ } else if (kind == ThumbnailConstants.MINI_KIND) {
+ return Point.convert(ThumbnailConstants.MINI_SIZE);
+ } else {
+ throw new IllegalArgumentException("Unsupported kind: " + kind);
+ }
+ }
+
+ private static class Resizer implements ImageDecoder.OnHeaderDecodedListener {
+ private final Size size;
+ private final CancellationSignal signal;
+
+ public Resizer(Size size, CancellationSignal signal) {
+ this.size = size;
+ this.signal = signal;
+ }
+
+ @Override
+ public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) {
+ // One last-ditch check to see if we've been canceled.
+ if (signal != null) signal.throwIfCanceled();
+
+ // We don't know how clients will use the decoded data, so we have
+ // to default to the more flexible "software" option.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+
+ // We requested a rough thumbnail size, but the remote size may have
+ // returned something giant, so defensively scale down as needed.
+ final int widthSample = info.getSize().getWidth() / size.getWidth();
+ final int heightSample = info.getSize().getHeight() / size.getHeight();
+ final int sample = Math.max(widthSample, heightSample);
+ if (sample > 1) {
+ decoder.setTargetSampleSize(sample);
+ }
+ }
+ }
+
/**
- * Constant used to indicate the dimension of mini thumbnail.
- * @hide Only used by media framework and media provider internally.
+ * Create a thumbnail for given audio file.
+ *
+ * @param filePath The audio file.
+ * @param kind The desired thumbnail kind, such as
+ * {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
*/
- public static final int TARGET_SIZE_MINI_THUMBNAIL = 320;
+ @Deprecated
+ public static @Nullable Bitmap createAudioThumbnail(@NonNull String filePath, int kind) {
+ try {
+ return createAudioThumbnail(new File(filePath), convertKind(kind), null);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ }
/**
- * Constant used to indicate the dimension of micro thumbnail.
- * @hide Only used by media framework and media provider internally.
+ * Create a thumbnail for given audio file.
+ *
+ * @param file The audio file.
+ * @param size The desired thumbnail size.
+ * @throws IOException If any trouble was encountered while generating or
+ * loading the thumbnail, or if
+ * {@link CancellationSignal#cancel()} was invoked.
*/
- @UnsupportedAppUsage
- public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96;
+ public static @NonNull Bitmap createAudioThumbnail(@NonNull File file, @NonNull Size size,
+ @Nullable CancellationSignal signal) throws IOException {
+ // Checkpoint before going deeper
+ if (signal != null) signal.throwIfCanceled();
+
+ final Resizer resizer = new Resizer(size, signal);
+ try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
+ retriever.setDataSource(file.getAbsolutePath());
+ final byte[] raw = retriever.getEmbeddedPicture();
+ if (raw != null) {
+ return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
+ }
+ } catch (RuntimeException e) {
+ throw new IOException("Failed to create thumbnail", e);
+ }
+
+ // Only poke around for files on external storage
+ if (MEDIA_UNKNOWN.equals(Environment.getExternalStorageState(file))) {
+ throw new IOException("No embedded album art found");
+ }
+
+ // Ignore "Downloads" or top-level directories
+ final File parent = file.getParentFile();
+ final File grandParent = parent != null ? parent.getParentFile() : null;
+ if (parent != null
+ && parent.getName().equals(Environment.DIRECTORY_DOWNLOADS)) {
+ throw new IOException("No thumbnails in Downloads directories");
+ }
+ if (grandParent != null
+ && MEDIA_UNKNOWN.equals(Environment.getExternalStorageState(grandParent))) {
+ throw new IOException("No thumbnails in top-level directories");
+ }
+
+ // If no embedded image found, look around for best standalone file
+ final File[] found = ArrayUtils
+ .defeatNullable(file.getParentFile().listFiles((dir, name) -> {
+ final String lower = name.toLowerCase();
+ return (lower.endsWith(".jpg") || lower.endsWith(".png"));
+ }));
+
+ final ToIntFunction<File> score = (f) -> {
+ final String lower = f.getName().toLowerCase();
+ if (lower.equals("albumart.jpg")) return 4;
+ if (lower.startsWith("albumart") && lower.endsWith(".jpg")) return 3;
+ if (lower.contains("albumart") && lower.endsWith(".jpg")) return 2;
+ if (lower.endsWith(".jpg")) return 1;
+ return 0;
+ };
+ final Comparator<File> bestScore = (a, b) -> {
+ return score.applyAsInt(a) - score.applyAsInt(b);
+ };
+
+ final File bestFile = Arrays.asList(found).stream().max(bestScore).orElse(null);
+ if (bestFile == null) {
+ throw new IOException("No album art found");
+ }
+
+ // Checkpoint before going deeper
+ if (signal != null) signal.throwIfCanceled();
+
+ return ImageDecoder.decodeBitmap(ImageDecoder.createSource(bestFile), resizer);
+ }
/**
- * This method first examines if the thumbnail embedded in EXIF is bigger than our target
- * size. If not, then it'll create a thumbnail from original image. Due to efficiency
- * consideration, we want to let MediaThumbRequest avoid calling this method twice for
- * both kinds, so it only requests for MICRO_KIND and set saveImage to true.
- *
- * This method always returns a "square thumbnail" for MICRO_KIND thumbnail.
+ * Create a thumbnail for given image file.
*
- * @param filePath the path of image file
- * @param kind could be MINI_KIND or MICRO_KIND
- * @return Bitmap, or null on failures
+ * @param filePath The image file.
+ * @param kind The desired thumbnail kind, such as
+ * {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
+ */
+ @Deprecated
+ public static @Nullable Bitmap createImageThumbnail(@NonNull String filePath, int kind) {
+ try {
+ return createImageThumbnail(new File(filePath), convertKind(kind), null);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ }
+
+ /**
+ * Create a thumbnail for given image file.
*
- * @hide This method is only used by media framework and media provider internally.
+ * @param file The audio file.
+ * @param size The desired thumbnail size.
+ * @throws IOException If any trouble was encountered while generating or
+ * loading the thumbnail, or if
+ * {@link CancellationSignal#cancel()} was invoked.
*/
- @UnsupportedAppUsage
- public static Bitmap createImageThumbnail(String filePath, int kind) {
- boolean wantMini = (kind == Images.Thumbnails.MINI_KIND);
- int targetSize = wantMini
- ? TARGET_SIZE_MINI_THUMBNAIL
- : TARGET_SIZE_MICRO_THUMBNAIL;
- int maxPixels = wantMini
- ? MAX_NUM_PIXELS_THUMBNAIL
- : MAX_NUM_PIXELS_MICRO_THUMBNAIL;
- SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap();
- Bitmap bitmap = null;
- String mimeType = MediaFile.getMimeTypeForFile(filePath);
+ public static @NonNull Bitmap createImageThumbnail(@NonNull File file, @NonNull Size size,
+ @Nullable CancellationSignal signal) throws IOException {
+ // Checkpoint before going deeper
+ if (signal != null) signal.throwIfCanceled();
+
+ final Resizer resizer = new Resizer(size, signal);
+ final String mimeType = MediaFile.getMimeTypeForFile(file.getName());
if (mimeType.equals("image/heif")
|| mimeType.equals("image/heif-sequence")
|| mimeType.equals("image/heic")
|| mimeType.equals("image/heic-sequence")) {
- bitmap = createThumbnailFromMetadataRetriever(filePath, targetSize, maxPixels);
+ try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
+ retriever.setDataSource(file.getAbsolutePath());
+ return retriever.getThumbnailImageAtIndex(-1,
+ new MediaMetadataRetriever.BitmapParams(), size.getWidth(),
+ size.getWidth() * size.getHeight());
+ } catch (RuntimeException e) {
+ throw new IOException("Failed to create thumbnail", e);
+ }
} else if (MediaFile.isExifMimeType(mimeType)) {
- createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap);
- bitmap = sizedThumbnailBitmap.mBitmap;
+ final ExifInterface exif = new ExifInterface(file);
+ final byte[] raw = exif.getThumbnailBytes();
+ if (raw != null) {
+ return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
+ }
}
- if (bitmap == null) {
- FileInputStream stream = null;
- try {
- stream = new FileInputStream(filePath);
- FileDescriptor fd = stream.getFD();
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 1;
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFileDescriptor(fd, null, options);
- if (options.mCancel || options.outWidth == -1
- || options.outHeight == -1) {
- return null;
- }
- options.inSampleSize = computeSampleSize(
- options, targetSize, maxPixels);
- options.inJustDecodeBounds = false;
-
- options.inDither = false;
- options.inPreferredConfig = Bitmap.Config.ARGB_8888;
- bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
- } catch (IOException ex) {
- Log.e(TAG, "", ex);
- } catch (OutOfMemoryError oom) {
- Log.e(TAG, "Unable to decode file " + filePath + ". OutOfMemoryError.", oom);
- } finally {
- try {
- if (stream != null) {
- stream.close();
- }
- } catch (IOException ex) {
- Log.e(TAG, "", ex);
- }
- }
+ // Checkpoint before going deeper
+ if (signal != null) signal.throwIfCanceled();
- }
+ return ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), resizer);
+ }
- if (kind == Images.Thumbnails.MICRO_KIND) {
- // now we make it a "square thumbnail" for MICRO_KIND thumbnail
- bitmap = extractThumbnail(bitmap,
- TARGET_SIZE_MICRO_THUMBNAIL,
- TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT);
+ /**
+ * Create a thumbnail for given video file.
+ *
+ * @param filePath The video file.
+ * @param kind The desired thumbnail kind, such as
+ * {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
+ */
+ @Deprecated
+ public static @Nullable Bitmap createVideoThumbnail(@NonNull String filePath, int kind) {
+ try {
+ return createVideoThumbnail(new File(filePath), convertKind(kind), null);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
}
- return bitmap;
}
/**
- * Create a video thumbnail for a video. May return null if the video is
- * corrupt or the format is not supported.
+ * Create a thumbnail for given video file.
*
- * @param filePath the path of video file
- * @param kind could be MINI_KIND or MICRO_KIND
+ * @param file The video file.
+ * @param size The desired thumbnail size.
+ * @throws IOException If any trouble was encountered while generating or
+ * loading the thumbnail, or if
+ * {@link CancellationSignal#cancel()} was invoked.
*/
- public static Bitmap createVideoThumbnail(String filePath, int kind) {
- Bitmap bitmap = null;
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- try {
- retriever.setDataSource(filePath);
- // First retrieve album art in metadata if set.
- byte[] embeddedPicture = retriever.getEmbeddedPicture();
- if (embeddedPicture != null && embeddedPicture.length > 0) {
- bitmap = BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture.length);
+ public static @NonNull Bitmap createVideoThumbnail(@NonNull File file, @NonNull Size size,
+ @Nullable CancellationSignal signal) throws IOException {
+ // Checkpoint before going deeper
+ if (signal != null) signal.throwIfCanceled();
+
+ final Resizer resizer = new Resizer(size, signal);
+ try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
+ mmr.setDataSource(file.getAbsolutePath());
+
+ // Try to retrieve thumbnail from metadata
+ final byte[] raw = mmr.getEmbeddedPicture();
+ if (raw != null) {
+ return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
}
- // Fall back to first frame of the video.
- if (bitmap == null) {
- bitmap = retriever.getFrameAtTime(-1);
- }
- } catch (IllegalArgumentException ex) {
- // Assume this is a corrupt video file
- } catch (RuntimeException ex) {
- // Assume this is a corrupt video file.
- } finally {
- try {
- retriever.release();
- } catch (RuntimeException ex) {
- // Ignore failures while cleaning up.
- }
- }
- if (bitmap == null) return null;
-
- if (kind == Images.Thumbnails.MINI_KIND) {
- // Scale down the bitmap if it's too large.
- int width = bitmap.getWidth();
- int height = bitmap.getHeight();
- int max = Math.max(width, height);
- if (max > 512) {
- float scale = 512f / max;
- int w = Math.round(scale * width);
- int h = Math.round(scale * height);
- bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
+ // Fall back to middle of video
+ final int width = Integer.parseInt(mmr.extractMetadata(METADATA_KEY_VIDEO_WIDTH));
+ final int height = Integer.parseInt(mmr.extractMetadata(METADATA_KEY_VIDEO_HEIGHT));
+ final long duration = Long.parseLong(mmr.extractMetadata(METADATA_KEY_DURATION));
+
+ // If we're okay with something larger than native format, just
+ // return a frame without up-scaling it
+ if (size.getWidth() > width && size.getHeight() > height) {
+ return mmr.getFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC);
+ } else {
+ return mmr.getScaledFrameAtTime(duration / 2, OPTION_CLOSEST_SYNC,
+ size.getWidth(), size.getHeight());
}
- } else if (kind == Images.Thumbnails.MICRO_KIND) {
- bitmap = extractThumbnail(bitmap,
- TARGET_SIZE_MICRO_THUMBNAIL,
- TARGET_SIZE_MICRO_THUMBNAIL,
- OPTIONS_RECYCLE_INPUT);
+ } catch (RuntimeException e) {
+ throw new IOException("Failed to create thumbnail", e);
}
- return bitmap;
}
/**
@@ -242,122 +361,27 @@ public class ThumbnailUtils {
return thumbnail;
}
- /*
- * Compute the sample size as a function of minSideLength
- * and maxNumOfPixels.
- * minSideLength is used to specify that minimal width or height of a
- * bitmap.
- * maxNumOfPixels is used to specify the maximal size in pixels that is
- * tolerable in terms of memory usage.
- *
- * The function returns a sample size based on the constraints.
- * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED,
- * which indicates no care of the corresponding constraint.
- * The functions prefers returning a sample size that
- * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED.
- *
- * Also, the function rounds up the sample size to a power of 2 or multiple
- * of 8 because BitmapFactory only honors sample size this way.
- * For example, BitmapFactory downsamples an image by 2 even though the
- * request is 3. So we round up the sample size to avoid OOM.
- */
+ @Deprecated
@UnsupportedAppUsage
private static int computeSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
- int initialSize = computeInitialSampleSize(options, minSideLength,
- maxNumOfPixels);
-
- int roundedSize;
- if (initialSize <= 8 ) {
- roundedSize = 1;
- while (roundedSize < initialSize) {
- roundedSize <<= 1;
- }
- } else {
- roundedSize = (initialSize + 7) / 8 * 8;
- }
-
- return roundedSize;
+ return 1;
}
+ @Deprecated
@UnsupportedAppUsage
private static int computeInitialSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
- double w = options.outWidth;
- double h = options.outHeight;
-
- int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
- (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
- int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
- (int) Math.min(Math.floor(w / minSideLength),
- Math.floor(h / minSideLength));
-
- if (upperBound < lowerBound) {
- // return the larger one when there is no overlapping zone.
- return lowerBound;
- }
-
- if ((maxNumOfPixels == UNCONSTRAINED) &&
- (minSideLength == UNCONSTRAINED)) {
- return 1;
- } else if (minSideLength == UNCONSTRAINED) {
- return lowerBound;
- } else {
- return upperBound;
- }
- }
-
- /**
- * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels.
- * The image data will be read from specified pfd if it's not null, otherwise
- * a new input stream will be created using specified ContentResolver.
- *
- * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A
- * new BitmapFactory.Options will be created if options is null.
- */
- private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
- Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
- BitmapFactory.Options options) {
- Bitmap b = null;
- try {
- if (pfd == null) pfd = makeInputStream(uri, cr);
- if (pfd == null) return null;
- if (options == null) options = new BitmapFactory.Options();
-
- FileDescriptor fd = pfd.getFileDescriptor();
- options.inSampleSize = 1;
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFileDescriptor(fd, null, options);
- if (options.mCancel || options.outWidth == -1
- || options.outHeight == -1) {
- return null;
- }
- options.inSampleSize = computeSampleSize(
- options, minSideLength, maxNumOfPixels);
- options.inJustDecodeBounds = false;
-
- options.inDither = false;
- options.inPreferredConfig = Bitmap.Config.ARGB_8888;
- b = BitmapFactory.decodeFileDescriptor(fd, null, options);
- } catch (OutOfMemoryError ex) {
- Log.e(TAG, "Got oom exception ", ex);
- return null;
- } finally {
- closeSilently(pfd);
- }
- return b;
+ return 1;
}
+ @Deprecated
@UnsupportedAppUsage
private static void closeSilently(ParcelFileDescriptor c) {
- if (c == null) return;
- try {
- c.close();
- } catch (Throwable t) {
- // do nothing
- }
+ IoUtils.closeQuietly(c);
}
+ @Deprecated
@UnsupportedAppUsage
private static ParcelFileDescriptor makeInputStream(
Uri uri, ContentResolver cr) {
@@ -371,6 +395,7 @@ public class ThumbnailUtils {
/**
* Transform source Bitmap to targeted width and height.
*/
+ @Deprecated
@UnsupportedAppUsage
private static Bitmap transform(Matrix scaler,
Bitmap source,
@@ -468,14 +493,7 @@ public class ThumbnailUtils {
return b2;
}
- /**
- * SizedThumbnailBitmap contains the bitmap, which is downsampled either from
- * the thumbnail in exif or the full image.
- * mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail
- * is not null.
- *
- * The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight.
- */
+ @Deprecated
private static class SizedThumbnailBitmap {
public byte[] mThumbnailData;
public Bitmap mBitmap;
@@ -483,81 +501,9 @@ public class ThumbnailUtils {
public int mThumbnailHeight;
}
- /**
- * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image.
- * The functions returns a SizedThumbnailBitmap,
- * which contains a downsampled bitmap and the thumbnail data in EXIF if exists.
- */
+ @Deprecated
@UnsupportedAppUsage
private static void createThumbnailFromEXIF(String filePath, int targetSize,
int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) {
- if (filePath == null) return;
-
- ExifInterface exif = null;
- byte [] thumbData = null;
- try {
- exif = new ExifInterface(filePath);
- thumbData = exif.getThumbnail();
- } catch (IOException ex) {
- Log.w(TAG, ex);
- }
-
- BitmapFactory.Options fullOptions = new BitmapFactory.Options();
- BitmapFactory.Options exifOptions = new BitmapFactory.Options();
- int exifThumbWidth = 0;
- int fullThumbWidth = 0;
-
- // Compute exifThumbWidth.
- if (thumbData != null) {
- exifOptions.inJustDecodeBounds = true;
- BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions);
- exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels);
- exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize;
- }
-
- // Compute fullThumbWidth.
- fullOptions.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(filePath, fullOptions);
- fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels);
- fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize;
-
- // Choose the larger thumbnail as the returning sizedThumbBitmap.
- if (thumbData != null && exifThumbWidth >= fullThumbWidth) {
- int width = exifOptions.outWidth;
- int height = exifOptions.outHeight;
- exifOptions.inJustDecodeBounds = false;
- sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0,
- thumbData.length, exifOptions);
- if (sizedThumbBitmap.mBitmap != null) {
- sizedThumbBitmap.mThumbnailData = thumbData;
- sizedThumbBitmap.mThumbnailWidth = width;
- sizedThumbBitmap.mThumbnailHeight = height;
- }
- } else {
- fullOptions.inJustDecodeBounds = false;
- sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions);
- }
- }
-
- private static Bitmap createThumbnailFromMetadataRetriever(
- String filePath, int targetSize, int maxPixels) {
- if (filePath == null) {
- return null;
- }
- Bitmap thumbnail = null;
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- try {
- retriever.setDataSource(filePath);
- MediaMetadataRetriever.BitmapParams params = new MediaMetadataRetriever.BitmapParams();
- params.setPreferredConfig(Bitmap.Config.ARGB_8888);
- thumbnail = retriever.getThumbnailImageAtIndex(-1, params, targetSize, maxPixels);
- } catch (RuntimeException ex) {
- // Assume this is a corrupt video file.
- } finally {
- if (retriever != null) {
- retriever.release();
- }
- }
- return thumbnail;
}
}