diff options
Diffstat (limited to 'media/java')
-rw-r--r-- | media/java/android/media/AudioRecord.java | 55 | ||||
-rw-r--r-- | media/java/android/media/AudioRecordingConfiguration.java | 168 | ||||
-rw-r--r-- | media/java/android/media/AudioRecordingMonitor.java | 56 | ||||
-rw-r--r-- | media/java/android/media/AudioRecordingMonitorClient.java | 28 | ||||
-rw-r--r-- | media/java/android/media/AudioRecordingMonitorImpl.java | 250 | ||||
-rw-r--r-- | media/java/android/media/AudioSystem.java | 19 | ||||
-rw-r--r-- | media/java/android/media/FileDataSourceDesc.java | 59 | ||||
-rw-r--r-- | media/java/android/media/MediaItem2.java | 324 | ||||
-rw-r--r-- | media/java/android/media/MediaMetadataRetriever.java | 8 | ||||
-rw-r--r-- | media/java/android/media/MediaPlayer.java | 32 | ||||
-rw-r--r-- | media/java/android/media/MediaPlayer2.java | 166 | ||||
-rw-r--r-- | media/java/android/media/MediaPlayerBase.java | 331 | ||||
-rw-r--r-- | media/java/android/media/MediaRecorder.java | 61 | ||||
-rw-r--r-- | media/java/android/media/ThumbnailUtils.java | 566 |
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; } } |