diff options
Diffstat (limited to 'media')
77 files changed, 4387 insertions, 1350 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 97919a9..4526839 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -27,7 +27,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.Objects; import java.util.Set; @@ -280,6 +279,7 @@ public final class AudioAttributes implements Parcelable { * Internal use only * @return a combined mask of all flags */ + @SystemApi public int getAllFlags() { return (mFlags & FLAG_ALL); } @@ -542,14 +542,15 @@ public final class AudioAttributes implements Parcelable { /** * @hide * Same as {@link #setCapturePreset(int)} but authorizes the use of HOTWORD, - * REMOTE_SUBMIX and FM_TUNER. + * REMOTE_SUBMIX and RADIO_TUNER. * @param preset * @return the same Builder instance. */ + @SystemApi public Builder setInternalCapturePreset(int preset) { if ((preset == MediaRecorder.AudioSource.HOTWORD) || (preset == MediaRecorder.AudioSource.REMOTE_SUBMIX) - || (preset == MediaRecorder.AudioSource.FM_TUNER)) { + || (preset == MediaRecorder.AudioSource.RADIO_TUNER)) { mSource = preset; } else { setCapturePreset(preset); diff --git a/media/java/android/media/AudioDevicesManager.java b/media/java/android/media/AudioDevicesManager.java index bce2100..ee11eef 100644 --- a/media/java/android/media/AudioDevicesManager.java +++ b/media/java/android/media/AudioDevicesManager.java @@ -22,7 +22,6 @@ import android.util.Slog; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; -import android.content.Context; /** @hide * API candidate diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 9876995..28941b9 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -26,9 +26,7 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.media.RemoteController.OnClientUpdateListener; import android.media.audiopolicy.AudioPolicy; -import android.media.audiopolicy.AudioPolicyConfig; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionLegacyHelper; @@ -58,12 +56,10 @@ import java.util.Iterator; */ public class AudioManager { - private final Context mContext; + private final Context mApplicationContext; private long mVolumeKeyUpTime; - private final boolean mUseMasterVolume; private final boolean mUseVolumeKeySounds; private final boolean mUseFixedVolume; - private final Binder mToken = new Binder(); private static String TAG = "AudioManager"; private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler(); @@ -149,17 +145,6 @@ public class AudioManager { "android.media.STREAM_MUTE_CHANGED_ACTION"; /** - * @hide Broadcast intent when the master volume changes. - * Includes the new volume - * - * @see #EXTRA_MASTER_VOLUME_VALUE - * @see #EXTRA_PREV_MASTER_VOLUME_VALUE - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String MASTER_VOLUME_CHANGED_ACTION = - "android.media.MASTER_VOLUME_CHANGED_ACTION"; - - /** * @hide Broadcast intent when the master mute state changes. * Includes the the new volume * @@ -211,20 +196,6 @@ public class AudioManager { "android.media.EXTRA_PREV_VOLUME_STREAM_VALUE"; /** - * @hide The new master volume value for the master volume changed intent. - * Value is integer between 0 and 100 inclusive. - */ - public static final String EXTRA_MASTER_VOLUME_VALUE = - "android.media.EXTRA_MASTER_VOLUME_VALUE"; - - /** - * @hide The previous master volume value for the master volume changed intent. - * Value is integer between 0 and 100 inclusive. - */ - public static final String EXTRA_PREV_MASTER_VOLUME_VALUE = - "android.media.EXTRA_PREV_MASTER_VOLUME_VALUE"; - - /** * @hide The new master volume mute state for the master mute changed intent. * Value is boolean */ @@ -259,7 +230,7 @@ public class AudioManager { "android.intent.action.HEADSET_PLUG"; /** - * Broadcast Action: A sticky broadcast indicating an HMDI cable was plugged or unplugged + * Broadcast Action: A sticky broadcast indicating an HDMI cable was plugged or unplugged. * * The intent will have the following extra values: {@link #EXTRA_AUDIO_PLUG_STATE}, * {@link #EXTRA_MAX_CHANNEL_COUNT}, {@link #EXTRA_ENCODINGS}. @@ -467,6 +438,12 @@ public class AudioManager { */ public static final int FLAG_SHOW_VIBRATE_HINT = 1 << 11; + /** + * Adjusting the volume due to a hardware key press. + * @hide + */ + public static final int FLAG_FROM_KEY = 1 << 12; + private static final String[] FLAG_NAMES = { "FLAG_SHOW_UI", "FLAG_ALLOW_RINGER_MODES", @@ -480,6 +457,7 @@ public class AudioManager { "FLAG_ACTIVE_MEDIA_ONLY", "FLAG_SHOW_UI_WARNINGS", "FLAG_SHOW_VIBRATE_HINT", + "FLAG_FROM_KEY", }; /** @hide */ @@ -604,12 +582,10 @@ public class AudioManager { * @hide */ public AudioManager(Context context) { - mContext = context; - mUseMasterVolume = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_useMasterVolume); - mUseVolumeKeySounds = mContext.getResources().getBoolean( + mApplicationContext = context; + mUseVolumeKeySounds = mApplicationContext.getResources().getBoolean( com.android.internal.R.bool.config_useVolumeKeySounds); - mUseFixedVolume = mContext.getResources().getBoolean( + mUseFixedVolume = mApplicationContext.getResources().getBoolean( com.android.internal.R.bool.config_useFixedVolume); sAudioPortEventHandler.init(); } @@ -648,7 +624,7 @@ public class AudioManager { * or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}. */ public void dispatchMediaKeyEvent(KeyEvent keyEvent) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mApplicationContext); helper.sendMediaButtonEvent(keyEvent, false); } @@ -668,12 +644,8 @@ public class AudioManager { * The user has hit another key during the delay (e.g., 300ms) * since the last volume key up, so cancel any sounds. */ - if (mUseMasterVolume) { - adjustMasterVolume(ADJUST_SAME, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); - } else { - adjustSuggestedStreamVolume(ADJUST_SAME, - stream, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); - } + adjustSuggestedStreamVolume(ADJUST_SAME, + stream, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); } } @@ -689,26 +661,17 @@ public class AudioManager { * Adjust the volume in on key down since it is more * responsive to the user. */ - int flags = FLAG_SHOW_UI | FLAG_VIBRATE; - - if (mUseMasterVolume) { - adjustMasterVolume( - keyCode == KeyEvent.KEYCODE_VOLUME_UP - ? ADJUST_RAISE - : ADJUST_LOWER, - flags); - } else { - adjustSuggestedStreamVolume( - keyCode == KeyEvent.KEYCODE_VOLUME_UP - ? ADJUST_RAISE - : ADJUST_LOWER, - stream, - flags); - } + adjustSuggestedStreamVolume( + keyCode == KeyEvent.KEYCODE_VOLUME_UP + ? ADJUST_RAISE + : ADJUST_LOWER, + stream, + FLAG_SHOW_UI | FLAG_VIBRATE); break; case KeyEvent.KEYCODE_VOLUME_MUTE: if (event.getRepeatCount() == 0) { - MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + MediaSessionLegacyHelper.getHelper(mApplicationContext) + .sendVolumeKeyEvent(event, false); } break; } @@ -727,20 +690,16 @@ public class AudioManager { * sound to play when a user holds down volume down to mute. */ if (mUseVolumeKeySounds) { - if (mUseMasterVolume) { - adjustMasterVolume(ADJUST_SAME, FLAG_PLAY_SOUND); - } else { - int flags = FLAG_PLAY_SOUND; - adjustSuggestedStreamVolume( - ADJUST_SAME, - stream, - flags); - } + adjustSuggestedStreamVolume( + ADJUST_SAME, + stream, + FLAG_PLAY_SOUND); } mVolumeKeyUpTime = SystemClock.uptimeMillis(); break; case KeyEvent.KEYCODE_VOLUME_MUTE: - MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + MediaSessionLegacyHelper.getHelper(mApplicationContext) + .sendVolumeKeyEvent(event, false); break; } } @@ -784,12 +743,8 @@ public class AudioManager { public void adjustStreamVolume(int streamType, int direction, int flags) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - service.adjustMasterVolume(direction, flags, mContext.getOpPackageName()); - } else { - service.adjustStreamVolume(streamType, direction, flags, - mContext.getOpPackageName()); - } + service.adjustStreamVolume(streamType, direction, flags, + mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in adjustStreamVolume", e); } @@ -819,17 +774,8 @@ public class AudioManager { * @see #isVolumeFixed() */ public void adjustVolume(int direction, int flags) { - IAudioService service = getService(); - try { - if (mUseMasterVolume) { - service.adjustMasterVolume(direction, flags, mContext.getOpPackageName()); - } else { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags); - } - } catch (RemoteException e) { - Log.e(TAG, "Dead object in adjustVolume", e); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mApplicationContext); + helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags); } /** @@ -857,34 +803,17 @@ public class AudioManager { * @see #isVolumeFixed() */ public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) { - IAudioService service = getService(); - try { - if (mUseMasterVolume) { - service.adjustMasterVolume(direction, flags, mContext.getOpPackageName()); - } else { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags); - } - } catch (RemoteException e) { - Log.e(TAG, "Dead object in adjustSuggestedStreamVolume", e); - } + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mApplicationContext); + helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags); } - /** - * Adjusts the master volume for the device's audio amplifier. - * <p> - * - * @param steps The number of volume steps to adjust. A positive - * value will raise the volume. - * @param flags One or more flags. - * @hide - */ - public void adjustMasterVolume(int steps, int flags) { + /** @hide */ + public void setMasterMute(boolean mute, int flags) { IAudioService service = getService(); try { - service.adjustMasterVolume(steps, flags, mContext.getOpPackageName()); + service.setMasterMute(mute, flags, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { - Log.e(TAG, "Dead object in adjustMasterVolume", e); + Log.e(TAG, "Dead object in setMasterMute", e); } } @@ -936,11 +865,7 @@ public class AudioManager { public int getStreamMaxVolume(int streamType) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - return service.getMasterMaxVolume(); - } else { - return service.getStreamMaxVolume(streamType); - } + return service.getStreamMaxVolume(streamType); } catch (RemoteException e) { Log.e(TAG, "Dead object in getStreamMaxVolume", e); return 0; @@ -948,6 +873,24 @@ public class AudioManager { } /** + * Returns the minimum volume index for a particular stream. + * + * @param streamType The stream type whose minimum volume index is returned. + * @return The minimum valid volume index for the stream. + * @see #getStreamVolume(int) + * @hide + */ + public int getStreamMinVolume(int streamType) { + IAudioService service = getService(); + try { + return service.getStreamMinVolume(streamType); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getStreamMinVolume", e); + return 0; + } + } + + /** * Returns the current volume index for a particular stream. * * @param streamType The stream type whose volume index is returned. @@ -958,11 +901,7 @@ public class AudioManager { public int getStreamVolume(int streamType) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - return service.getMasterVolume(); - } else { - return service.getStreamVolume(streamType); - } + return service.getStreamVolume(streamType); } catch (RemoteException e) { Log.e(TAG, "Dead object in getStreamVolume", e); return 0; @@ -977,11 +916,7 @@ public class AudioManager { public int getLastAudibleStreamVolume(int streamType) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - return service.getLastAudibleMasterVolume(); - } else { - return service.getLastAudibleStreamVolume(streamType); - } + return service.getLastAudibleStreamVolume(streamType); } catch (RemoteException e) { Log.e(TAG, "Dead object in getLastAudibleStreamVolume", e); return 0; @@ -994,12 +929,12 @@ public class AudioManager { * It is assumed that this stream type is also tied to ringer mode changes. * @hide */ - public int getMasterStreamType() { + public int getUiSoundsStreamType() { IAudioService service = getService(); try { - return service.getMasterStreamType(); + return service.getUiSoundsStreamType(); } catch (RemoteException e) { - Log.e(TAG, "Dead object in getMasterStreamType", e); + Log.e(TAG, "Dead object in getUiSoundsStreamType", e); return STREAM_RING; } } @@ -1023,7 +958,7 @@ public class AudioManager { } IAudioService service = getService(); try { - service.setRingerModeExternal(ringerMode, mContext.getOpPackageName()); + service.setRingerModeExternal(ringerMode, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in setRingerMode", e); } @@ -1044,82 +979,13 @@ public class AudioManager { public void setStreamVolume(int streamType, int index, int flags) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - service.setMasterVolume(index, flags, mContext.getOpPackageName()); - } else { - service.setStreamVolume(streamType, index, flags, mContext.getOpPackageName()); - } + service.setStreamVolume(streamType, index, flags, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in setStreamVolume", e); } } /** - * Returns the maximum volume index for master volume. - * - * @hide - */ - public int getMasterMaxVolume() { - IAudioService service = getService(); - try { - return service.getMasterMaxVolume(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in getMasterMaxVolume", e); - return 0; - } - } - - /** - * Returns the current volume index for master volume. - * - * @return The current volume index for master volume. - * @hide - */ - public int getMasterVolume() { - IAudioService service = getService(); - try { - return service.getMasterVolume(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in getMasterVolume", e); - return 0; - } - } - - /** - * Get last audible volume before master volume was muted. - * - * @hide - */ - public int getLastAudibleMasterVolume() { - IAudioService service = getService(); - try { - return service.getLastAudibleMasterVolume(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in getLastAudibleMasterVolume", e); - return 0; - } - } - - /** - * Sets the volume index for master volume. - * - * @param index The volume index to set. See - * {@link #getMasterMaxVolume()} for the largest valid value. - * @param flags One or more flags. - * @see #getMasterMaxVolume() - * @see #getMasterVolume() - * @hide - */ - public void setMasterVolume(int index, int flags) { - IAudioService service = getService(); - try { - service.setMasterVolume(index, flags, mContext.getOpPackageName()); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in setMasterVolume", e); - } - } - - /** * Solo or unsolo a particular stream. * <p> * Do not use. This method has been deprecated and is now a no-op. @@ -1190,11 +1056,7 @@ public class AudioManager { public boolean isStreamMute(int streamType) { IAudioService service = getService(); try { - if (mUseMasterVolume) { - return service.isMasterMute(); - } else { - return service.isStreamMute(streamType); - } + return service.isStreamMute(streamType); } catch (RemoteException e) { Log.e(TAG, "Dead object in isStreamMute", e); return false; @@ -1224,9 +1086,6 @@ public class AudioManager { * @hide */ public void forceVolumeControlStream(int streamType) { - if (mUseMasterVolume) { - return; - } IAudioService service = getService(); try { service.forceVolumeControlStream(streamType, mICallBack); @@ -1433,7 +1292,7 @@ public class AudioManager { * @see #startBluetoothSco() */ public boolean isBluetoothScoAvailableOffCall() { - return mContext.getResources().getBoolean( + return mApplicationContext.getResources().getBoolean( com.android.internal.R.bool.config_bluetooth_sco_off_call); } @@ -1485,7 +1344,8 @@ public class AudioManager { public void startBluetoothSco(){ IAudioService service = getService(); try { - service.startBluetoothSco(mICallBack, mContext.getApplicationInfo().targetSdkVersion); + service.startBluetoothSco(mICallBack, + mApplicationContext.getApplicationInfo().targetSdkVersion); } catch (RemoteException e) { Log.e(TAG, "Dead object in startBluetoothSco", e); } @@ -1633,7 +1493,7 @@ public class AudioManager { public void setMicrophoneMute(boolean on){ IAudioService service = getService(); try { - service.setMicrophoneMute(on, mContext.getOpPackageName()); + service.setMicrophoneMute(on, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in setMicrophoneMute", e); } @@ -1666,7 +1526,7 @@ public class AudioManager { public void setMode(int mode) { IAudioService service = getService(); try { - service.setMode(mode, mICallBack); + service.setMode(mode, mICallBack, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in setMode", e); } @@ -2064,7 +1924,7 @@ public class AudioManager { * Settings has an in memory cache, so this is fast. */ private boolean querySoundEffectsEnabled(int user) { - return Settings.System.getIntForUser(mContext.getContentResolver(), + return Settings.System.getIntForUser(mApplicationContext.getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0; } @@ -2476,7 +2336,7 @@ public class AudioManager { try { status = service.requestAudioFocus(requestAttributes, durationHint, mICallBack, mAudioFocusDispatcher, getIdForAudioFocusListener(l), - mContext.getOpPackageName() /* package name */, flags, + mApplicationContext.getOpPackageName() /* package name */, flags, ap != null ? ap.cb() : null); } catch (RemoteException e) { Log.e(TAG, "Can't call requestAudioFocus() on AudioService:", e); @@ -2501,7 +2361,7 @@ public class AudioManager { .setInternalLegacyStreamType(streamType).build(), durationHint, mICallBack, null, AudioSystem.IN_VOICE_COMM_FOCUS_ID, - mContext.getOpPackageName(), + mApplicationContext.getOpPackageName(), AUDIOFOCUS_FLAG_LOCK, null /* policy token */); } catch (RemoteException e) { @@ -2570,7 +2430,7 @@ public class AudioManager { if (eventReceiver == null) { return; } - if (!eventReceiver.getPackageName().equals(mContext.getPackageName())) { + if (!eventReceiver.getPackageName().equals(mApplicationContext.getPackageName())) { Log.e(TAG, "registerMediaButtonEventReceiver() error: " + "receiver and context package names don't match"); return; @@ -2579,7 +2439,7 @@ public class AudioManager { Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); // the associated intent will be handled by the component being registered mediaButtonIntent.setComponent(eventReceiver); - PendingIntent pi = PendingIntent.getBroadcast(mContext, + PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/); registerMediaButtonIntent(pi, eventReceiver); } @@ -2613,8 +2473,8 @@ public class AudioManager { Log.e(TAG, "Cannot call registerMediaButtonIntent() with a null parameter"); return; } - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); - helper.addMediaButtonListener(pi, eventReceiver, mContext); + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mApplicationContext); + helper.addMediaButtonListener(pi, eventReceiver, mApplicationContext); } /** @@ -2632,7 +2492,7 @@ public class AudioManager { Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); // the associated intent will be handled by the component being registered mediaButtonIntent.setComponent(eventReceiver); - PendingIntent pi = PendingIntent.getBroadcast(mContext, + PendingIntent pi = PendingIntent.getBroadcast(mApplicationContext, 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/); unregisterMediaButtonIntent(pi); } @@ -2655,7 +2515,7 @@ public class AudioManager { * @hide */ public void unregisterMediaButtonIntent(PendingIntent pi) { - MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mApplicationContext); helper.removeMediaButtonListener(pi); } @@ -2672,7 +2532,7 @@ public class AudioManager { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } - rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mContext)); + rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mApplicationContext)); } /** @@ -2687,7 +2547,7 @@ public class AudioManager { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } - rcClient.unregisterWithSession(MediaSessionLegacyHelper.getHelper(mContext)); + rcClient.unregisterWithSession(MediaSessionLegacyHelper.getHelper(mApplicationContext)); } /** @@ -2695,7 +2555,7 @@ public class AudioManager { * metadata updates and playback state information from applications using * {@link RemoteControlClient}, and control their playback. * <p> - * Registration requires the {@link OnClientUpdateListener} listener to be + * Registration requires the {@link RemoteController.OnClientUpdateListener} listener to be * one of the enabled notification listeners (see * {@link android.service.notification.NotificationListenerService}). * @@ -3205,7 +3065,8 @@ public class AudioManager { public void setWiredDeviceConnectionState(int type, int state, String address, String name) { IAudioService service = getService(); try { - service.setWiredDeviceConnectionState(type, state, address, name); + service.setWiredDeviceConnectionState(type, state, address, name, + mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.e(TAG, "Dead object in setWiredDeviceConnectionState "+e); } @@ -3230,9 +3091,8 @@ public class AudioManager { delay = service.setBluetoothA2dpDeviceConnectionState(device, state, profile); } catch (RemoteException e) { Log.e(TAG, "Dead object in setBluetoothA2dpDeviceConnectionState "+e); - } finally { - return delay; } + return delay; } /** {@hide} */ @@ -3349,7 +3209,7 @@ public class AudioManager { */ public void disableSafeMediaVolume() { try { - getService().disableSafeMediaVolume(); + getService().disableSafeMediaVolume(mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.w(TAG, "Error disabling safe media volume", e); } @@ -3361,7 +3221,7 @@ public class AudioManager { */ public void setRingerModeInternal(int ringerMode) { try { - getService().setRingerModeInternal(ringerMode, mContext.getOpPackageName()); + getService().setRingerModeInternal(ringerMode, mApplicationContext.getOpPackageName()); } catch (RemoteException e) { Log.w(TAG, "Error calling setRingerModeInternal", e); } @@ -3381,6 +3241,18 @@ public class AudioManager { } /** + * Only useful for volume controllers. + * @hide + */ + public void setVolumePolicy(VolumePolicy policy) { + try { + getService().setVolumePolicy(policy); + } catch (RemoteException e) { + Log.w(TAG, "Error calling setVolumePolicy", e); + } + } + + /** * Set Hdmi Cec system audio mode. * * @param on whether to be on system audio mode diff --git a/media/java/android/media/AudioManagerInternal.java b/media/java/android/media/AudioManagerInternal.java index ef5710c..abb4257 100644 --- a/media/java/android/media/AudioManagerInternal.java +++ b/media/java/android/media/AudioManagerInternal.java @@ -27,8 +27,7 @@ import com.android.server.LocalServices; public abstract class AudioManagerInternal { public abstract void adjustSuggestedStreamVolumeForUid(int streamType, int direction, - int flags, - String callingPackage, int uid); + int flags, String callingPackage, int uid); public abstract void adjustStreamVolumeForUid(int streamType, int direction, int flags, String callingPackage, int uid); @@ -36,9 +35,6 @@ public abstract class AudioManagerInternal { public abstract void setStreamVolumeForUid(int streamType, int direction, int flags, String callingPackage, int uid); - public abstract void adjustMasterVolumeForUid(int steps, int flags, String callingPackage, - int uid); - public abstract void setRingerModeDelegate(RingerModeDelegate delegate); public abstract int getRingerModeInternal(); @@ -50,10 +46,10 @@ public abstract class AudioManagerInternal { public interface RingerModeDelegate { /** Called when external ringer mode is evaluated, returns the new internal ringer mode */ int onSetRingerModeExternal(int ringerModeOld, int ringerModeNew, String caller, - int ringerModeInternal); + int ringerModeInternal, VolumePolicy policy); /** Called when internal ringer mode is evaluated, returns the new external ringer mode */ int onSetRingerModeInternal(int ringerModeOld, int ringerModeNew, String caller, - int ringerModeExternal); + int ringerModeExternal, VolumePolicy policy); } } diff --git a/media/java/android/media/AudioPort.java b/media/java/android/media/AudioPort.java index b046791..88e784a 100644 --- a/media/java/android/media/AudioPort.java +++ b/media/java/android/media/AudioPort.java @@ -15,7 +15,6 @@ */ package android.media; -import android.util.Slog; /** * An audio port is a node of the audio framework or hardware that can be connected to or diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java index ba2a59d..c05fd77 100644 --- a/media/java/android/media/AudioPortEventHandler.java +++ b/media/java/android/media/AudioPortEventHandler.java @@ -19,8 +19,6 @@ package android.media; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.util.Log; - import java.util.ArrayList; import java.lang.ref.WeakReference; diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index de10ef9..259fe37 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -20,6 +20,7 @@ import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.Iterator; +import android.annotation.SystemApi; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -238,7 +239,6 @@ public class AudioRecord /** * @hide - * CANDIDATE FOR PUBLIC API * Class constructor with {@link AudioAttributes} and {@link AudioFormat}. * @param attributes a non-null {@link AudioAttributes} instance. Use * {@link AudioAttributes.Builder#setCapturePreset(int)} for configuring the capture @@ -257,6 +257,7 @@ public class AudioRecord * construction. * @throws IllegalArgumentException */ + @SystemApi public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int sessionId) throws IllegalArgumentException { mRecordingState = RECORDSTATE_STOPPED; @@ -376,7 +377,7 @@ public class AudioRecord // audio source if ( (audioSource < MediaRecorder.AudioSource.DEFAULT) || ((audioSource > MediaRecorder.getAudioSourceMax()) && - (audioSource != MediaRecorder.AudioSource.FM_TUNER) && + (audioSource != MediaRecorder.AudioSource.RADIO_TUNER) && (audioSource != MediaRecorder.AudioSource.HOTWORD)) ) { throw new IllegalArgumentException("Invalid audio source."); } diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index cd78234..93e2cbe 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -21,9 +21,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.NioUtils; -import java.util.Iterator; -import java.util.Set; - import android.annotation.IntDef; import android.app.ActivityThread; import android.app.AppOpsManager; diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 17f5b59..8e96218 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -29,6 +29,7 @@ import android.media.IRemoteVolumeObserver; import android.media.IRingtonePlayer; import android.media.IVolumeController; import android.media.Rating; +import android.media.VolumePolicy; import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.IAudioPolicyCallback; import android.net.Uri; @@ -40,36 +41,30 @@ import android.view.KeyEvent; interface IAudioService { void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, - String callingPackage); + String callingPackage, String caller); void adjustStreamVolume(int streamType, int direction, int flags, String callingPackage); - void adjustMasterVolume(int direction, int flags, String callingPackage); - void setStreamVolume(int streamType, int index, int flags, String callingPackage); oneway void setRemoteStreamVolume(int index); - void setMasterVolume(int index, int flags, String callingPackage); - boolean isStreamMute(int streamType); void forceRemoteSubmixFullVolume(boolean startForcing, IBinder cb); boolean isMasterMute(); + void setMasterMute(boolean mute, int flags, String callingPackage); + int getStreamVolume(int streamType); - int getMasterVolume(); + int getStreamMinVolume(int streamType); int getStreamMaxVolume(int streamType); - int getMasterMaxVolume(); - int getLastAudibleStreamVolume(int streamType); - int getLastAudibleMasterVolume(); - void setMicrophoneMute(boolean on, String callingPackage); void setRingerModeExternal(int ringerMode, String caller); @@ -88,7 +83,7 @@ interface IAudioService { boolean shouldVibrate(int vibrateType); - void setMode(int mode, IBinder cb); + void setMode(int mode, IBinder cb, String callingPackage); int getMode(); @@ -187,9 +182,11 @@ interface IAudioService { void setRingtonePlayer(IRingtonePlayer player); IRingtonePlayer getRingtonePlayer(); - int getMasterStreamType(); + int getUiSoundsStreamType(); + + void setWiredDeviceConnectionState(int type, int state, String address, String name, + String caller); - void setWiredDeviceConnectionState(int type, int state, String address, String name); int setBluetoothA2dpDeviceConnectionState(in BluetoothDevice device, int state, int profile); AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer); @@ -204,15 +201,18 @@ interface IAudioService { boolean isStreamAffectedByMute(int streamType); - void disableSafeMediaVolume(); + void disableSafeMediaVolume(String callingPackage); int setHdmiSystemAudioSupported(boolean on); boolean isHdmiSystemAudioSupported(); - String registerAudioPolicy(in AudioPolicyConfig policyConfig, - in IAudioPolicyCallback pcb, boolean hasFocusListener); + String registerAudioPolicy(in AudioPolicyConfig policyConfig, + in IAudioPolicyCallback pcb, boolean hasFocusListener); + oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb); - int setFocusPropertiesForPolicy(int duckingBehavior, in IAudioPolicyCallback pcb); + int setFocusPropertiesForPolicy(int duckingBehavior, in IAudioPolicyCallback pcb); + + void setVolumePolicy(in VolumePolicy policy); } diff --git a/media/java/android/media/IVolumeController.aidl b/media/java/android/media/IVolumeController.aidl index e3593a6..90ac416 100644 --- a/media/java/android/media/IVolumeController.aidl +++ b/media/java/android/media/IVolumeController.aidl @@ -27,8 +27,6 @@ oneway interface IVolumeController { void volumeChanged(int streamType, int flags); - void masterVolumeChanged(int flags); - void masterMuteChanged(int flags); void setLayoutDirection(int layoutDirection); diff --git a/media/java/android/media/Image.java b/media/java/android/media/Image.java index 53ab264..9d07492 100644 --- a/media/java/android/media/Image.java +++ b/media/java/android/media/Image.java @@ -115,14 +115,49 @@ public abstract class Image implements AutoCloseable { /** * Get the timestamp associated with this frame. * <p> - * The timestamp is measured in nanoseconds, and is monotonically - * increasing. However, the zero point and whether the timestamp can be - * compared against other sources of time or images depend on the source of - * this image. + * The timestamp is measured in nanoseconds, and is normally monotonically + * increasing. However, the behavior of the timestamp depends on the source + * of this image. See {@link android.hardware.Camera Camera}, + * {@link android.hardware.camera2.CameraDevice CameraDevice}, {@link MediaPlayer} and + * {@link MediaCodec} for more details. * </p> */ public abstract long getTimestamp(); + /** + * Set the timestamp associated with this frame. + * <p> + * The timestamp is measured in nanoseconds, and is normally monotonically + * increasing. However, However, the behavior of the timestamp depends on + * the destination of this image. See {@link android.hardware.Camera Camera} + * , {@link android.hardware.camera2.CameraDevice CameraDevice}, + * {@link MediaPlayer} and {@link MediaCodec} for more details. + * </p> + * <p> + * For images dequeued from {@link ImageWriter} via + * {@link ImageWriter#dequeueInputImage()}, it's up to the application to + * set the timestamps correctly before sending them back to the + * {@link ImageWriter}, or the timestamp will be generated automatically when + * {@link ImageWriter#queueInputImage queueInputImage()} is called. + * </p> + * + * @param timestamp The timestamp to be set for this image. + */ + public void setTimestamp(long timestamp) { + return; + } + + /** + * <p>Check if the image is opaque.</p> + * + * <p>The pixel data of opaque images are not accessible to the application, + * and therefore {@link #getPlanes} will return an empty array for an opaque image. + * </p> + */ + public boolean isOpaque() { + return false; + } + private Rect mCropRect; /** @@ -155,7 +190,10 @@ public abstract class Image implements AutoCloseable { /** * Get the array of pixel planes for this Image. The number of planes is - * determined by the format of the Image. + * determined by the format of the Image. The application will get an + * empty array if the image is opaque because the opaque image pixel data + * is not directly accessible. The application can check if an image is + * opaque by calling {@link Image#isOpaque}. */ public abstract Plane[] getPlanes(); @@ -164,14 +202,54 @@ public abstract class Image implements AutoCloseable { * <p> * After calling this method, calling any methods on this {@code Image} will * result in an {@link IllegalStateException}, and attempting to read from - * {@link ByteBuffer ByteBuffers} returned by an earlier - * {@link Plane#getBuffer} call will have undefined behavior. + * or write to {@link ByteBuffer ByteBuffers} returned by an earlier + * {@link Plane#getBuffer} call will have undefined behavior. If the image + * was obtained from {@link ImageWriter} via + * {@link ImageWriter#dequeueInputImage()}, after calling this method, any + * image data filled by the application will be lost and the image will be + * returned to {@link ImageWriter} for reuse. Images given to + * {@link ImageWriter#queueInputImage queueInputImage()} are automatically + * closed. * </p> */ @Override public abstract void close(); /** + * <p> + * Check if the image can be attached to a new owner (e.g. {@link ImageWriter}). + * </p> + * <p> + * This is a package private method that is only used internally. + * </p> + * + * @return true if the image is attachable to a new owner, false if the image is still attached + * to its current owner, or the image is a stand-alone image and is not attachable to + * a new owner. + */ + boolean isAttachable() { + return false; + } + + /** + * <p> + * Get the owner of the {@link Image}. + * </p> + * <p> + * The owner of an {@link Image} could be {@link ImageReader}, {@link ImageWriter}, + * {@link MediaCodec} etc. This method returns the owner that produces this image, or null + * if the image is stand-alone image or the owner is unknown. + * </p> + * <p> + * This is a package private method that is only used internally. + * </p> + * + * @return The owner of the Image. + */ + Object getOwner() { + return null; + } + /** * <p>A single color plane of image data.</p> * * <p>The number and meaning of the planes in an Image are determined by the diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java index 8d6a588..b2f7a20 100644 --- a/media/java/android/media/ImageReader.java +++ b/media/java/android/media/ImageReader.java @@ -27,6 +27,7 @@ import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.NioUtils; +import java.util.concurrent.atomic.AtomicBoolean; /** * <p>The ImageReader class allows direct application access to image data @@ -34,7 +35,7 @@ import java.nio.NioUtils; * * <p>Several Android media API classes accept Surface objects as targets to * render to, including {@link MediaPlayer}, {@link MediaCodec}, - * {@link android.hardware.camera2.CameraDevice}, and + * {@link android.hardware.camera2.CameraDevice}, {@link ImageWriter} and * {@link android.renderscript.Allocation RenderScript Allocations}. The image * sizes and formats that can be used with each source vary, and should be * checked in the documentation for the specific API.</p> @@ -97,10 +98,60 @@ public class ImageReader implements AutoCloseable { * @see Image */ public static ImageReader newInstance(int width, int height, int format, int maxImages) { + if (format == PixelFormat.OPAQUE) { + throw new IllegalArgumentException("To obtain an opaque ImageReader, please use" + + " newOpaqueInstance rather than newInstance"); + } return new ImageReader(width, height, format, maxImages); } /** + * <p> + * Create a new opaque reader for images of the desired size. + * </p> + * <p> + * An opaque {@link ImageReader} produces images that are not directly + * accessible by the application. The application can still acquire images + * from an opaque image reader, and send them to the + * {@link android.hardware.camera2.CameraDevice camera} for reprocessing via + * {@link ImageWriter} interface. However, the {@link Image#getPlanes() + * getPlanes()} will return an empty array for opaque images. The + * application can check if an existing reader is an opaque reader by + * calling {@link #isOpaque()}. + * </p> + * <p> + * The {@code maxImages} parameter determines the maximum number of + * {@link Image} objects that can be be acquired from the + * {@code ImageReader} simultaneously. Requesting more buffers will use up + * more memory, so it is important to use only the minimum number necessary. + * </p> + * <p> + * The valid sizes and formats depend on the source of the image data. + * </p> + * <p> + * Opaque ImageReaders are more efficient to use when application access to + * image data is not necessary, comparing to ImageReaders using a non-opaque + * format such as {@link ImageFormat#YUV_420_888 YUV_420_888}. + * </p> + * + * @param width The default width in pixels of the Images that this reader + * will produce. + * @param height The default height in pixels of the Images that this reader + * will produce. + * @param maxImages The maximum number of images the user will want to + * access simultaneously. This should be as small as possible to + * limit memory use. Once maxImages Images are obtained by the + * user, one of them has to be released before a new Image will + * become available for access through + * {@link #acquireLatestImage()} or {@link #acquireNextImage()}. + * Must be greater than 0. + * @see Image + */ + public static ImageReader newOpaqueInstance(int width, int height, int maxImages) { + return new ImageReader(width, height, PixelFormat.OPAQUE, maxImages); + } + + /** * @hide */ protected ImageReader(int width, int height, int format, int maxImages) { @@ -197,6 +248,23 @@ public class ImageReader implements AutoCloseable { } /** + * <p> + * Check if the {@link ImageReader} is an opaque reader. + * </p> + * <p> + * An opaque image reader produces opaque images, see {@link Image#isOpaque} + * for more details. + * </p> + * + * @return true if the ImageReader is opaque. + * @see Image#isOpaque + * @see ImageReader#newOpaqueInstance + */ + public boolean isOpaque() { + return mFormat == PixelFormat.OPAQUE; + } + + /** * <p>Get a {@link Surface} that can be used to produce {@link Image Images} for this * {@code ImageReader}.</p> * @@ -443,6 +511,7 @@ public class ImageReader implements AutoCloseable { @Override public void close() { setOnImageAvailableListener(null, null); + if (mSurface != null) mSurface.release(); nativeClose(); } @@ -456,6 +525,58 @@ public class ImageReader implements AutoCloseable { } /** + * <p> + * Remove the ownership of this image from the ImageReader. + * </p> + * <p> + * After this call, the ImageReader no longer owns this image, and the image + * ownership can be transfered to another entity like {@link ImageWriter} + * via {@link ImageWriter#queueInputImage}. It's up to the new owner to + * release the resources held by this image. For example, if the ownership + * of this image is transfered to an {@link ImageWriter}, the image will be + * freed by the ImageWriter after the image data consumption is done. + * </p> + * <p> + * This method can be used to achieve zero buffer copy for use cases like + * {@link android.hardware.camera2.CameraDevice Camera2 API} OPAQUE and YUV + * reprocessing, where the application can select an output image from + * {@link ImageReader} and transfer this image directly to + * {@link ImageWriter}, where this image can be consumed by camera directly. + * For OPAQUE reprocessing, this is the only way to send input buffers to + * the {@link android.hardware.camera2.CameraDevice camera} for + * reprocessing. + * </p> + * <p> + * This is a package private method that is only used internally. + * </p> + * + * @param image The image to be detached from this ImageReader. + * @throws IllegalStateException If the ImageReader or image have been + * closed, or the has been detached, or has not yet been + * acquired. + */ + void detachImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("input image must not be null"); + } + if (!isImageOwnedbyMe(image)) { + throw new IllegalArgumentException("Trying to detach an image that is not owned by" + + " this ImageReader"); + } + + SurfaceImage si = (SurfaceImage) image; + if (!si.isImageValid()) { + throw new IllegalStateException("Image is no longer valid"); + } + if (si.isAttachable()) { + throw new IllegalStateException("Image was already detached from this ImageReader"); + } + + nativeDetachImage(image); + si.setDetached(true); + } + + /** * Only a subset of the formats defined in * {@link android.graphics.ImageFormat ImageFormat} and * {@link android.graphics.PixelFormat PixelFormat} are supported by @@ -483,13 +604,25 @@ public class ImageReader implements AutoCloseable { case ImageFormat.Y16: case ImageFormat.RAW_SENSOR: case ImageFormat.RAW10: + case ImageFormat.DEPTH16: + case ImageFormat.DEPTH_POINT_CLOUD: return 1; + case PixelFormat.OPAQUE: + return 0; default: throw new UnsupportedOperationException( String.format("Invalid format specified %d", mFormat)); } } + private boolean isImageOwnedbyMe(Image image) { + if (!(image instanceof SurfaceImage)) { + return false; + } + SurfaceImage si = (SurfaceImage) image; + return si.getReader() == this; + } + /** * Called from Native code when an Event happens. * @@ -558,7 +691,11 @@ public class ImageReader implements AutoCloseable { @Override public void close() { if (mIsImageValid) { - ImageReader.this.releaseImage(this); + if (!mIsDetached.get()) { + // For detached images, the new owner is responsible for + // releasing the resources + ImageReader.this.releaseImage(this); + } } } @@ -611,6 +748,15 @@ public class ImageReader implements AutoCloseable { } @Override + public void setTimestamp(long timestampNs) { + if (mIsImageValid) { + mTimestamp = timestampNs; + } else { + throw new IllegalStateException("Image is already released"); + } + } + + @Override public Plane[] getPlanes() { if (mIsImageValid) { // Shallow copy is fine. @@ -621,6 +767,11 @@ public class ImageReader implements AutoCloseable { } @Override + public boolean isOpaque() { + return mFormat == PixelFormat.OPAQUE; + } + + @Override protected final void finalize() throws Throwable { try { close(); @@ -629,6 +780,20 @@ public class ImageReader implements AutoCloseable { } } + @Override + boolean isAttachable() { + return mIsDetached.get(); + } + + @Override + ImageReader getOwner() { + return ImageReader.this; + } + + private void setDetached(boolean detached) { + mIsDetached.getAndSet(detached); + } + private void setImageValid(boolean isValid) { mIsImageValid = isValid; } @@ -731,6 +896,8 @@ public class ImageReader implements AutoCloseable { private boolean mIsImageValid; private int mHeight = -1; private int mWidth = -1; + // If this image is detached from the ImageReader. + private AtomicBoolean mIsDetached = new AtomicBoolean(false); private synchronized native ByteBuffer nativeImageGetBuffer(int idx, int readerFormat); private synchronized native SurfacePlane nativeCreatePlane(int idx, int readerFormat); @@ -743,6 +910,7 @@ public class ImageReader implements AutoCloseable { private synchronized native void nativeClose(); private synchronized native void nativeReleaseImage(Image i); private synchronized native Surface nativeGetSurface(); + private synchronized native void nativeDetachImage(Image i); /** * @return A return code {@code ACQUIRE_*} diff --git a/media/java/android/media/ImageUtils.java b/media/java/android/media/ImageUtils.java new file mode 100644 index 0000000..89313bf --- /dev/null +++ b/media/java/android/media/ImageUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015 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.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.media.Image.Plane; +import android.util.Size; + +import java.nio.ByteBuffer; + +/** + * Package private utility class for hosting commonly used Image related methods. + */ +class ImageUtils { + + /** + * Only a subset of the formats defined in + * {@link android.graphics.ImageFormat ImageFormat} and + * {@link android.graphics.PixelFormat PixelFormat} are supported by + * ImageReader. When reading RGB data from a surface, the formats defined in + * {@link android.graphics.PixelFormat PixelFormat} can be used; when + * reading YUV, JPEG or raw sensor data (for example, from the camera or video + * decoder), formats from {@link android.graphics.ImageFormat ImageFormat} + * are used. + */ + public static int getNumPlanesForFormat(int format) { + switch (format) { + case ImageFormat.YV12: + case ImageFormat.YUV_420_888: + case ImageFormat.NV21: + return 3; + case ImageFormat.NV16: + return 2; + case PixelFormat.RGB_565: + case PixelFormat.RGBA_8888: + case PixelFormat.RGBX_8888: + case PixelFormat.RGB_888: + case ImageFormat.JPEG: + case ImageFormat.YUY2: + case ImageFormat.Y8: + case ImageFormat.Y16: + case ImageFormat.RAW_SENSOR: + case ImageFormat.RAW10: + return 1; + case PixelFormat.OPAQUE: + return 0; + default: + throw new UnsupportedOperationException( + String.format("Invalid format specified %d", format)); + } + } + + /** + * <p> + * Copy source image data to destination Image. + * </p> + * <p> + * Only support the copy between two non-opaque images with same properties + * (format, size, etc.). The data from the source image will be copied to + * the byteBuffers from the destination Image starting from position zero, + * and the destination image will be rewound to zero after copy is done. + * </p> + * + * @param src The source image to be copied from. + * @param dst The destination image to be copied to. + * @throws IllegalArgumentException If the source and destination images + * have different format, or one of the images is not copyable. + */ + public static void imageCopy(Image src, Image dst) { + if (src == null || dst == null) { + throw new IllegalArgumentException("Images should be non-null"); + } + if (src.getFormat() != dst.getFormat()) { + throw new IllegalArgumentException("Src and dst images should have the same format"); + } + if (src.isOpaque() || dst.isOpaque()) { + throw new IllegalArgumentException("Opaque image is not copyable"); + } + if (!(dst.getOwner() instanceof ImageWriter)) { + throw new IllegalArgumentException("Destination image is not from ImageWriter. Only" + + " the images from ImageWriter are writable"); + } + Size srcSize = new Size(src.getWidth(), src.getHeight()); + Size dstSize = new Size(dst.getWidth(), dst.getHeight()); + if (!srcSize.equals(dstSize)) { + throw new IllegalArgumentException("source image size " + srcSize + " is different" + + " with " + "destination image size " + dstSize); + } + + Plane[] srcPlanes = src.getPlanes(); + Plane[] dstPlanes = dst.getPlanes(); + ByteBuffer srcBuffer = null; + ByteBuffer dstBuffer = null; + for (int i = 0; i < srcPlanes.length; i++) { + srcBuffer = srcPlanes[i].getBuffer(); + int srcPos = srcBuffer.position(); + srcBuffer.rewind(); + dstBuffer = dstPlanes[i].getBuffer(); + dstBuffer.rewind(); + dstBuffer.put(srcBuffer); + srcBuffer.position(srcPos); + dstBuffer.rewind(); + } + } +} diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java new file mode 100644 index 0000000..20389a3 --- /dev/null +++ b/media/java/android/media/ImageWriter.java @@ -0,0 +1,798 @@ +/* + * Copyright 2015 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.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.Surface; + +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.NioUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * <p> + * The ImageWriter class allows an application to produce Image data into a + * {@link android.view.Surface}, and have it be consumed by another component like + * {@link android.hardware.camera2.CameraDevice CameraDevice}. + * </p> + * <p> + * Several Android API classes can provide input {@link android.view.Surface + * Surface} objects for ImageWriter to produce data into, including + * {@link MediaCodec MediaCodec} (encoder), + * {@link android.hardware.camera2.CameraDevice CameraDevice} (reprocessing + * input), {@link ImageReader}, etc. + * </p> + * <p> + * The input Image data is encapsulated in {@link Image} objects. To produce + * Image data into a destination {@link android.view.Surface Surface}, the + * application can get an input Image via {@link #dequeueInputImage} then write + * Image data into it. Multiple such {@link Image} objects can be dequeued at + * the same time and queued back in any order, up to the number specified by the + * {@code maxImages} constructor parameter. + * </p> + * <p> + * If the application already has an Image from {@link ImageReader}, the + * application can directly queue this Image into ImageWriter (via + * {@link #queueInputImage}), potentially with zero buffer copies. For the opaque + * Images produced by an opaque ImageReader (created by + * {@link ImageReader#newOpaqueInstance}), this is the only way to send Image + * data to ImageWriter, as the Image data aren't accessible by the application. + * </p> + * Once new input Images are queued into an ImageWriter, it's up to the downstream + * components (e.g. {@link ImageReader} or + * {@link android.hardware.camera2.CameraDevice}) to consume the Images. If the + * downstream components cannot consume the Images at least as fast as the + * ImageWriter production rate, the {@link #dequeueInputImage} call will eventually + * block and the application will have to drop input frames. </p> + */ +public class ImageWriter implements AutoCloseable { + private final Object mListenerLock = new Object(); + private ImageListener mListener; + private ListenerHandler mListenerHandler; + private long mNativeContext; + + // Field below is used by native code, do not access or modify. + private int mWriterFormat; + + private final int mMaxImages; + // Keep track of the currently attached Image; or an attached Image that is + // released will be removed from this list. + private List<Image> mAttachedImages = new ArrayList<Image>(); + private List<Image> mDequeuedImages = new ArrayList<Image>(); + + /** + * <p> + * Create a new ImageWriter. + * </p> + * <p> + * The {@code maxImages} parameter determines the maximum number of + * {@link Image} objects that can be be dequeued from the + * {@code ImageWriter} simultaneously. Requesting more buffers will use up + * more memory, so it is important to use only the minimum number necessary. + * </p> + * <p> + * The input Image size and format depend on the Surface that is provided by + * the downstream consumer end-point. + * </p> + * + * @param surface The destination Surface this writer produces Image data + * into. + * @param maxImages The maximum number of Images the user will want to + * access simultaneously for producing Image data. This should be + * as small as possible to limit memory use. Once maxImages + * Images are dequeued by the user, one of them has to be queued + * back before a new Image can be dequeued for access via + * {@link #dequeueInputImage()}. + * @return a new ImageWriter instance. + */ + public static ImageWriter newInstance(Surface surface, int maxImages) { + return new ImageWriter(surface, maxImages); + } + + /** + * @hide + */ + protected ImageWriter(Surface surface, int maxImages) { + if (surface == null || maxImages < 1) { + throw new IllegalArgumentException("Illegal input argument: surface " + surface + + ", maxImages: " + maxImages); + } + + mMaxImages = maxImages; + // Note that the underlying BufferQueue is working in synchronous mode + // to avoid dropping any buffers. + mNativeContext = nativeInit(new WeakReference<ImageWriter>(this), surface, maxImages); + } + + /** + * <p> + * Maximum number of Images that can be dequeued from the ImageWriter + * simultaneously (for example, with {@link #dequeueInputImage()}). + * </p> + * <p> + * An Image is considered dequeued after it's returned by + * {@link #dequeueInputImage()} from ImageWriter, and until the Image is + * sent back to ImageWriter via {@link #queueInputImage}, or + * {@link Image#close()}. + * </p> + * <p> + * Attempting to dequeue more than {@code maxImages} concurrently will + * result in the {@link #dequeueInputImage()} function throwing an + * {@link IllegalStateException}. + * </p> + * + * @return Maximum number of Images that can be dequeued from this + * ImageWriter. + * @see #dequeueInputImage + * @see #queueInputImage + * @see Image#close + */ + public int getMaxImages() { + return mMaxImages; + } + + /** + * <p> + * Dequeue the next available input Image for the application to produce + * data into. + * </p> + * <p> + * This method requests a new input Image from ImageWriter. The application + * owns this Image after this call. Once the application fills the Image + * data, it is expected to return this Image back to ImageWriter for + * downstream consumer components (e.g. + * {@link android.hardware.camera2.CameraDevice}) to consume. The Image can + * be returned to ImageWriter via {@link #queueInputImage} or + * {@link Image#close()}. + * </p> + * <p> + * This call will block if all available input images have been filled by + * the application and the downstream consumer has not yet consumed any. + * When an Image is consumed by the downstream consumer, an + * {@link ImageListener#onInputImageReleased} callback will be fired, which + * indicates that there is one input Image available. It is recommended to + * dequeue next Image only after this callback is fired, in the steady state. + * </p> + * + * @return The next available input Image from this ImageWriter. + * @throws IllegalStateException if {@code maxImages} Images are currently + * dequeued. + * @see #queueInputImage + * @see Image#close + */ + public Image dequeueInputImage() { + if (mDequeuedImages.size() >= mMaxImages) { + throw new IllegalStateException("Already dequeued max number of Images " + mMaxImages); + } + WriterSurfaceImage newImage = new WriterSurfaceImage(this); + nativeDequeueInputImage(mNativeContext, newImage); + mDequeuedImages.add(newImage); + newImage.setImageValid(true); + return newImage; + } + + /** + * <p> + * Queue an input {@link Image} back to ImageWriter for the downstream + * consumer to access. + * </p> + * <p> + * The input {@link Image} could be from ImageReader (acquired via + * {@link ImageReader#acquireNextImage} or + * {@link ImageReader#acquireLatestImage}), or from this ImageWriter + * (acquired via {@link #dequeueInputImage}). In the former case, the Image + * data will be moved to this ImageWriter. Note that the Image properties + * (size, format, strides, etc.) must be the same as the properties of the + * images dequeued from this ImageWriter, or this method will throw an + * {@link IllegalArgumentException}. In the latter case, the application has + * filled the input image with data. This method then passes the filled + * buffer to the downstream consumer. In both cases, it's up to the caller + * to ensure that the Image timestamp (in nanoseconds) is correctly set, as + * the downstream component may want to use it to indicate the Image data + * capture time. + * </p> + * <p> + * Passing in a non-opaque Image may result in a memory copy, which also + * requires a free input Image from this ImageWriter as the destination. In + * this case, this call will block, as {@link #dequeueInputImage} does, if + * there are no free Images available. To be safe, the application should ensure + * that there is at least one free Image available in this ImageWriter before calling + * this method. + * </p> + * <p> + * After this call, the input Image is no longer valid for further access, + * as if the Image is {@link Image#close closed}. Attempting to access the + * {@link ByteBuffer ByteBuffers} returned by an earlier + * {@link Image.Plane#getBuffer Plane#getBuffer} call will result in an + * {@link IllegalStateException}. + * </p> + * + * @param image The Image to be queued back to ImageWriter for future + * consumption. + * @see #dequeueInputImage() + */ + public void queueInputImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("image shouldn't be null"); + } + boolean ownedByMe = isImageOwnedByMe(image); + if (ownedByMe && !(((WriterSurfaceImage) image).isImageValid())) { + throw new IllegalStateException("Image from ImageWriter is invalid"); + } + + // For images from other components, need to detach first, then attach. + if (!ownedByMe) { + if (!(image.getOwner() instanceof ImageReader)) { + throw new IllegalArgumentException("Only images from ImageReader can be queued to" + + " ImageWriter, other image source is not supported yet!"); + } + + ImageReader prevOwner = (ImageReader) image.getOwner(); + // Only do the image attach for opaque images for now. Do the image + // copy for other formats. TODO: use attach for other formats to + // improve the performance, and fall back to copy when attach/detach fails. + if (image.isOpaque()) { + prevOwner.detachImage(image); + attachInputImage(image); + } else { + Image inputImage = dequeueInputImage(); + inputImage.setTimestamp(image.getTimestamp()); + inputImage.setCropRect(image.getCropRect()); + ImageUtils.imageCopy(image, inputImage); + image.close(); + image = inputImage; + ownedByMe = true; + } + } + + Rect crop = image.getCropRect(); + nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top, + crop.right, crop.bottom); + + /** + * Only remove and cleanup the Images that are owned by this + * ImageWriter. Images detached from other owners are only + * temporarily owned by this ImageWriter and will be detached immediately + * after they are released by downstream consumers, so there is no need to + * keep track of them in mDequeuedImages. + */ + if (ownedByMe) { + mDequeuedImages.remove(image); + WriterSurfaceImage wi = (WriterSurfaceImage) image; + wi.clearSurfacePlanes(); + wi.setImageValid(false); + } else { + // This clears the native reference held by the original owner. When + // this Image is detached later by this ImageWriter, the native + // memory won't be leaked. + image.close(); + } + } + + /** + * ImageWriter callback interface, used to to asynchronously notify the + * application of various ImageWriter events. + */ + public interface ImageListener { + /** + * <p> + * Callback that is called when an input Image is released back to + * ImageWriter after the data consumption. + * </p> + * <p> + * The client can use this callback to indicate either an input Image is + * available to fill data into, or the input Image is returned and freed + * if it was attached from other components (e.g. an + * {@link ImageReader}). For the latter case, the ownership of the Image + * will be automatically removed by ImageWriter right before this + * callback is fired. + * </p> + * + * @param writer the ImageWriter the callback is associated with. + * @see ImageWriter + * @see Image + */ + // TODO: the semantics is confusion, does't tell which buffer is + // released if an application is doing queueInputImage with a mix of + // buffers from dequeueInputImage and from an ImageReader. see b/19872821 + void onInputImageReleased(ImageWriter writer); + } + + /** + * Register a listener to be invoked when an input Image is returned to + * the ImageWriter. + * + * @param listener The listener that will be run. + * @param handler The handler on which the listener should be invoked, or + * null if the listener should be invoked on the calling thread's + * looper. + * @throws IllegalArgumentException If no handler specified and the calling + * thread has no looper. + */ + public void setImageListener(ImageListener listener, Handler handler) { + synchronized (mListenerLock) { + if (listener != null) { + Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); + if (looper == null) { + throw new IllegalArgumentException( + "handler is null but the current thread is not a looper"); + } + if (mListenerHandler == null || mListenerHandler.getLooper() != looper) { + mListenerHandler = new ListenerHandler(looper); + } + mListener = listener; + } else { + mListener = null; + mListenerHandler = null; + } + } + } + + /** + * Free up all the resources associated with this ImageWriter. + * <p> + * After calling this method, this ImageWriter cannot be used. Calling any + * methods on this ImageWriter and Images previously provided by + * {@link #dequeueInputImage()} will result in an + * {@link IllegalStateException}, and attempting to write into + * {@link ByteBuffer ByteBuffers} returned by an earlier + * {@link Image.Plane#getBuffer Plane#getBuffer} call will have undefined + * behavior. + * </p> + */ + @Override + public void close() { + setImageListener(null, null); + for (Image image : mDequeuedImages) { + image.close(); + } + mDequeuedImages.clear(); + nativeClose(mNativeContext); + mNativeContext = 0; + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Get the ImageWriter format. + * <p> + * This format may be different than the Image format returned by + * {@link Image#getFormat()} + * </p> + * + * @return The ImageWriter format. + */ + int getFormat() { + return mWriterFormat; + } + + + /** + * <p> + * Attach input Image to this ImageWriter. + * </p> + * <p> + * When an Image is from an opaque source (e.g. an opaque ImageReader created + * by {@link ImageReader#newOpaqueInstance}), or the source Image is so large + * that copying its data is too expensive, this method can be used to + * migrate the source Image into ImageWriter without a data copy. The source + * Image must be detached from its previous owner already, or this call will + * throw an {@link IllegalStateException}. + * </p> + * <p> + * After this call, the ImageWriter takes ownership of this Image. + * This ownership will be automatically removed from this writer after the + * consumer releases this Image, that is, after + * {@link ImageListener#onInputImageReleased}. The caller is + * responsible for closing this Image through {@link Image#close()} to free up + * the resources held by this Image. + * </p> + * + * @param image The source Image to be attached and queued into this + * ImageWriter for downstream consumer to use. + * @throws IllegalStateException if the Image is not detached from its + * previous owner, or the Image is already attached to this + * ImageWriter, or the source Image is invalid. + */ + private void attachInputImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("image shouldn't be null"); + } + if (isImageOwnedByMe(image)) { + throw new IllegalArgumentException( + "Can not attach an image that is owned ImageWriter already"); + } + /** + * Throw ISE if the image is not attachable, which means that it is + * either owned by other entity now, or completely non-attachable (some + * stand-alone images are not backed by native gralloc buffer, thus not + * attachable). + */ + if (!image.isAttachable()) { + throw new IllegalStateException("Image was not detached from last owner, or image " + + " is not detachable"); + } + if (mAttachedImages.contains(image)) { + throw new IllegalStateException("Image was already attached to ImageWritter"); + } + + // TODO: what if attach failed, throw RTE or detach a slot then attach? + // need do some cleanup to make sure no orphaned + // buffer caused leak. + nativeAttachImage(mNativeContext, image); + mAttachedImages.add(image); + } + + /** + * This custom handler runs asynchronously so callbacks don't get queued + * behind UI messages. + */ + private final class ListenerHandler extends Handler { + public ListenerHandler(Looper looper) { + super(looper, null, true /* async */); + } + + @Override + public void handleMessage(Message msg) { + ImageListener listener; + synchronized (mListenerLock) { + listener = mListener; + } + // TODO: detach Image from ImageWriter and remove the Image from + // mAttachedImage list. + if (listener != null) { + listener.onInputImageReleased(ImageWriter.this); + } + } + } + + /** + * Called from Native code when an Event happens. This may be called from an + * arbitrary Binder thread, so access to the ImageWriter must be + * synchronized appropriately. + */ + private static void postEventFromNative(Object selfRef) { + @SuppressWarnings("unchecked") + WeakReference<ImageWriter> weakSelf = (WeakReference<ImageWriter>) selfRef; + final ImageWriter iw = weakSelf.get(); + if (iw == null) { + return; + } + + final Handler handler; + synchronized (iw.mListenerLock) { + handler = iw.mListenerHandler; + } + if (handler != null) { + handler.sendEmptyMessage(0); + } + } + + /** + * <p> + * Abort the Images that were dequeued from this ImageWriter, and return + * them to this writer for reuse. + * </p> + * <p> + * This method is used for the cases where the application dequeued the + * Image, may have filled the data, but does not want the downstream + * component to consume it. The Image will be returned to this ImageWriter + * for reuse after this call, and the ImageWriter will immediately have an + * Image available to be dequeued. This aborted Image will be invisible to + * the downstream consumer, as if nothing happened. + * </p> + * + * @param image The Image to be aborted. + * @see #dequeueInputImage() + * @see Image#close() + */ + private void abortImage(Image image) { + if (image == null) { + throw new IllegalArgumentException("image shouldn't be null"); + } + + if (!mDequeuedImages.contains(image)) { + throw new IllegalStateException("It is illegal to abort some image that is not" + + " dequeued yet"); + } + + WriterSurfaceImage wi = (WriterSurfaceImage) image; + + if (!wi.isImageValid()) { + throw new IllegalStateException("Image is invalid"); + } + + /** + * We only need abort Images that are owned and dequeued by ImageWriter. + * For attached Images, no need to abort, as there are only two cases: + * attached + queued successfully, and attach failed. Neither of the + * cases need abort. + */ + cancelImage(mNativeContext,image); + mDequeuedImages.remove(image); + wi.clearSurfacePlanes(); + wi.setImageValid(false); + } + + private boolean isImageOwnedByMe(Image image) { + if (!(image instanceof WriterSurfaceImage)) { + return false; + } + WriterSurfaceImage wi = (WriterSurfaceImage) image; + if (wi.getOwner() != this) { + return false; + } + + return true; + } + + private static class WriterSurfaceImage extends android.media.Image { + private ImageWriter mOwner; + private AtomicBoolean mIsImageValid = new AtomicBoolean(false); + // This field is used by native code, do not access or modify. + private long mNativeBuffer; + private int mNativeFenceFd = -1; + private SurfacePlane[] mPlanes; + private int mHeight = -1; + private int mWidth = -1; + private int mFormat = -1; + // When this default timestamp is used, timestamp for the input Image + // will be generated automatically when queueInputBuffer is called. + private final long DEFAULT_TIMESTAMP = Long.MIN_VALUE; + private long mTimestamp = DEFAULT_TIMESTAMP; + + public WriterSurfaceImage(ImageWriter writer) { + mOwner = writer; + } + + @Override + public int getFormat() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + if (mFormat == -1) { + mFormat = nativeGetFormat(); + } + return mFormat; + } + + @Override + public int getWidth() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + if (mWidth == -1) { + mWidth = nativeGetWidth(); + } + + return mWidth; + } + + @Override + public int getHeight() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + if (mHeight == -1) { + mHeight = nativeGetHeight(); + } + + return mHeight; + } + + @Override + public long getTimestamp() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + return mTimestamp; + } + + @Override + public void setTimestamp(long timestamp) { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + mTimestamp = timestamp; + } + + @Override + public boolean isOpaque() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + return getFormat() == PixelFormat.OPAQUE; + } + + @Override + public Plane[] getPlanes() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + + if (mPlanes == null) { + int numPlanes = ImageUtils.getNumPlanesForFormat(getFormat()); + mPlanes = nativeCreatePlanes(numPlanes, getOwner().getFormat()); + } + + return mPlanes.clone(); + } + + @Override + boolean isAttachable() { + if (!mIsImageValid.get()) { + throw new IllegalStateException("Image is already released"); + } + // Don't allow Image to be detached from ImageWriter for now, as no + // detach API is exposed. + return false; + } + + @Override + ImageWriter getOwner() { + return mOwner; + } + + @Override + public void close() { + if (mIsImageValid.get()) { + getOwner().abortImage(this); + } + } + + @Override + protected final void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + private boolean isImageValid() { + return mIsImageValid.get(); + } + + private void setImageValid(boolean isValid) { + mIsImageValid.getAndSet(isValid); + } + + private void clearSurfacePlanes() { + if (mIsImageValid.get()) { + for (int i = 0; i < mPlanes.length; i++) { + if (mPlanes[i] != null) { + mPlanes[i].clearBuffer(); + mPlanes[i] = null; + } + } + } + } + + private class SurfacePlane extends android.media.Image.Plane { + private ByteBuffer mBuffer; + final private int mPixelStride; + final private int mRowStride; + + // SurfacePlane instance is created by native code when a new + // SurfaceImage is created + private SurfacePlane(int rowStride, int pixelStride, ByteBuffer buffer) { + mRowStride = rowStride; + mPixelStride = pixelStride; + mBuffer = buffer; + /** + * Set the byteBuffer order according to host endianness (native + * order), otherwise, the byteBuffer order defaults to + * ByteOrder.BIG_ENDIAN. + */ + mBuffer.order(ByteOrder.nativeOrder()); + } + + @Override + public int getRowStride() { + if (WriterSurfaceImage.this.isImageValid() == false) { + throw new IllegalStateException("Image is already released"); + } + return mRowStride; + } + + @Override + public int getPixelStride() { + if (WriterSurfaceImage.this.isImageValid() == false) { + throw new IllegalStateException("Image is already released"); + } + return mPixelStride; + } + + @Override + public ByteBuffer getBuffer() { + if (WriterSurfaceImage.this.isImageValid() == false) { + throw new IllegalStateException("Image is already released"); + } + + return mBuffer; + } + + private void clearBuffer() { + // Need null check first, as the getBuffer() may not be called + // before an Image is closed. + if (mBuffer == null) { + return; + } + + if (mBuffer.isDirect()) { + NioUtils.freeDirectBuffer(mBuffer); + } + mBuffer = null; + } + + } + + // this will create the SurfacePlane object and fill the information + private synchronized native SurfacePlane[] nativeCreatePlanes(int numPlanes, int writerFmt); + + private synchronized native int nativeGetWidth(); + + private synchronized native int nativeGetHeight(); + + private synchronized native int nativeGetFormat(); + } + + // Native implemented ImageWriter methods. + private synchronized native long nativeInit(Object weakSelf, Surface surface, int maxImgs); + + private synchronized native void nativeClose(long nativeCtx); + + private synchronized native void nativeAttachImage(long nativeCtx, Image image); + + private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi); + + private synchronized native void nativeQueueInputImage(long nativeCtx, Image image, + long timestampNs, int left, int top, int right, int bottom); + + private synchronized native void cancelImage(long nativeCtx, Image image); + + /** + * We use a class initializer to allow the native code to cache some field + * offsets. + */ + private static native void nativeClassInit(); + + static { + System.loadLibrary("media_jni"); + nativeClassInit(); + } +} diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index 8985b52..a7f33fa 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -19,7 +19,6 @@ package android.media; import android.graphics.ImageFormat; import android.graphics.Rect; import android.media.Image; -import android.media.Image.Plane; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaCrypto; diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 6ac854f..ebf73da 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -24,15 +24,12 @@ import android.util.Size; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Set; import static android.media.Utils.intersectSortedDistinctRanges; import static android.media.Utils.sortDistinctRanges; -import static com.android.internal.util.Preconditions.checkArgumentPositive; -import static com.android.internal.util.Preconditions.checkNotNull; /** * Provides information about a given media codec available on the device. You can @@ -1975,7 +1972,7 @@ public final class MediaCodecInfo { (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); if (complexity == null) { complexity = flacComplexity; - } else if (flacComplexity != null && complexity != flacComplexity) { + } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { throw new IllegalArgumentException( "conflicting values for complexity and " + "flac-compression-level"); @@ -1988,7 +1985,7 @@ public final class MediaCodecInfo { Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); if (profile == null) { profile = aacProfile; - } else if (aacProfile != null && aacProfile != profile) { + } else if (aacProfile != null && !aacProfile.equals(profile)) { throw new IllegalArgumentException( "conflicting values for profile and aac-profile"); } diff --git a/media/java/android/media/MediaCodecList.java b/media/java/android/media/MediaCodecList.java index bb848d9..7fd0186 100644 --- a/media/java/android/media/MediaCodecList.java +++ b/media/java/android/media/MediaCodecList.java @@ -19,8 +19,6 @@ package android.media; import android.util.Log; import android.media.MediaCodecInfo; -import android.media.MediaCodecInfo.CodecCapabilities; - import java.util.ArrayList; import java.util.Arrays; diff --git a/media/java/android/media/MediaDescription.java b/media/java/android/media/MediaDescription.java index 4399c0d..ddbffc2 100644 --- a/media/java/android/media/MediaDescription.java +++ b/media/java/android/media/MediaDescription.java @@ -1,22 +1,11 @@ package android.media; -import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.ContentResolver; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.RectF; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Parcel; import android.os.Parcelable; -import android.util.Size; /** * A simple set of metadata for a media item suitable for display. This can be diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java index 78a5abe..069f7ff 100644 --- a/media/java/android/media/MediaDrm.java +++ b/media/java/android/media/MediaDrm.java @@ -21,8 +21,6 @@ import java.util.UUID; import java.util.HashMap; import java.util.List; import android.annotation.SystemApi; -import android.os.Binder; -import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -84,6 +82,10 @@ import android.util.Log; * encrypted content, the samples returned from the extractor remain encrypted, they * are only decrypted when the samples are delivered to the decoder. * <p> + * MediaDrm methods throw {@link java.lang.IllegalStateException} + * when a method is called on a MediaDrm object that is in an invalid or inoperable + * state. This is typically due to incorrect application API usage, but may also + * be due to an unrecoverable failure in the DRM plugin or security hardware. * <a name="Callbacks"></a> * <h3>Callbacks</h3> * <p>Applications should register for informational events in order @@ -256,6 +258,9 @@ public final class MediaDrm { * This event type indicates that the app needs to request a certificate from * the provisioning server. The request message data is obtained using * {@link #getProvisionRequest} + * + * @deprecated Handle provisioning via {@link android.media.NotProvisionedException} + * instead. */ public static final int EVENT_PROVISION_REQUIRED = 1; @@ -277,6 +282,12 @@ public final class MediaDrm { */ public static final int EVENT_VENDOR_DEFINED = 4; + /** + * This event indicates that a session opened by the app has been reclaimed by the resource + * manager. + */ + public static final int EVENT_SESSION_RECLAIMED = 5; + private static final int DRM_EVENT = 200; private class EventHandler extends Handler @@ -376,11 +387,27 @@ public final class MediaDrm { public static final int KEY_TYPE_RELEASE = 3; /** + * Key request type is initial license request + */ + public static final int REQUEST_TYPE_INITIAL = 0; + + /** + * Key request type is license renewal + */ + public static final int REQUEST_TYPE_RENEWAL = 1; + + /** + * Key request type is license release + */ + public static final int REQUEST_TYPE_RELEASE = 2; + + /** * Contains the opaque data an app uses to request keys from a license server */ public final static class KeyRequest { private byte[] mData; private String mDefaultUrl; + private int mRequestType; KeyRequest() {} @@ -395,6 +422,11 @@ public final class MediaDrm { * server URL from other sources. */ public String getDefaultUrl() { return mDefaultUrl; } + + /** + * Get the type of the request + */ + public int getRequestType() { return mRequestType; } }; /** @@ -453,7 +485,6 @@ public final class MediaDrm { * reprovisioning is required * @throws DeniedByServerException if the response indicates that the * server rejected the request - * @throws ResourceBusyException if required resources are in use */ public native byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 4356a3e..0c1c7e9 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -420,6 +420,25 @@ public final class MediaFormat { public static final String KEY_QUALITY = "quality"; /** + * A key describing the desired codec priority. + * <p> + * The associated value is an integer. Higher value means lower priority. + * <p> + * Currently, only two levels are supported:<br> + * 0: realtime priority - meaning that the codec shall support the given + * performance configuration (e.g. framerate) at realtime. This should + * only be used by media playback, capture, and possibly by realtime + * communication scenarios if best effort performance is not suitable.<br> + * 1: non-realtime priority (best effort). + * <p> + * This is a hint used at codec configuration and resource planning - to understand + * the realtime requirements of the application; however, due to the nature of + * media components, performance is not guaranteed. + * + */ + public static final String KEY_PRIORITY = "priority"; + + /** * A key describing the desired profile to be used by an encoder. * Constants are declared in {@link MediaCodecInfo.CodecProfileLevel}. * This key is only supported for codecs that specify a profile. @@ -587,14 +606,14 @@ public final class MediaFormat { * Sets the value of an integer key. */ public final void setInteger(String name, int value) { - mMap.put(name, new Integer(value)); + mMap.put(name, Integer.valueOf(value)); } /** * Sets the value of a long key. */ public final void setLong(String name, long value) { - mMap.put(name, new Long(value)); + mMap.put(name, Long.valueOf(value)); } /** diff --git a/media/java/android/media/MediaHTTPService.java b/media/java/android/media/MediaHTTPService.java index 3b4703d..2348ab7 100644 --- a/media/java/android/media/MediaHTTPService.java +++ b/media/java/android/media/MediaHTTPService.java @@ -16,9 +16,7 @@ package android.media; -import android.os.Binder; import android.os.IBinder; -import android.util.Log; /** @hide */ public class MediaHTTPService extends IMediaHTTPService.Stub { diff --git a/media/java/android/media/MediaMetadata.java b/media/java/android/media/MediaMetadata.java index 754da0e..39bcef5 100644 --- a/media/java/android/media/MediaMetadata.java +++ b/media/java/android/media/MediaMetadata.java @@ -30,7 +30,6 @@ import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; -import java.util.ArrayList; import java.util.Set; /** diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index fc372eb..d77fcd8 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -16,14 +16,11 @@ package android.media; +import android.annotation.IntDef; import android.app.ActivityThread; import android.app.AppOpsManager; -import android.app.Application; -import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.Handler; @@ -64,8 +61,9 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.lang.Runnable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.InetSocketAddress; import java.util.Map; import java.util.Scanner; @@ -477,6 +475,11 @@ import java.lang.ref.WeakReference; * <td>{} </p></td> * <td>This method can be called in any state and calling it does not change * the object state. </p></td></tr> + * <tr><td>setPlaybackRate</p></td> + * <td>any </p></td> + * <td>{} </p></td> + * <td>This method can be called in any state and calling it does not change + * the object state. </p></td></tr> * <tr><td>setVolume </p></td> * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused, * PlaybackCompleted}</p></td> @@ -1324,6 +1327,59 @@ public class MediaPlayer implements SubtitleController.Listener public native boolean isPlaying(); /** + * Specifies resampling as audio mode for variable rate playback, i.e., + * resample the waveform based on the requested playback rate to get + * a new waveform, and play back the new waveform at the original sampling + * frequency. + * When rate is larger than 1.0, pitch becomes higher. + * When rate is smaller than 1.0, pitch becomes lower. + */ + public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 0; + + /** + * Specifies time stretching as audio mode for variable rate playback. + * Time stretching changes the duration of the audio samples without + * affecting its pitch. + * FIXME: implement time strectching. + * @hide + */ + public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1; + + /** @hide */ + @IntDef( + value = { + PLAYBACK_RATE_AUDIO_MODE_RESAMPLE, + PLAYBACK_RATE_AUDIO_MODE_STRETCH }) + @Retention(RetentionPolicy.SOURCE) + public @interface PlaybackRateAudioMode {} + + /** + * Sets playback rate and audio mode. + * + * <p> The supported audio modes are: + * <ul> + * <li> {@link #PLAYBACK_RATE_AUDIO_MODE_RESAMPLE} + * </ul> + * + * @param rate the ratio between desired playback rate and normal one. + * @param audioMode audio playback mode. Must be one of the supported + * audio modes. + * + * @throws IllegalStateException if the internal player engine has not been + * initialized. + * @throws IllegalArgumentException if audioMode is not supported. + */ + public void setPlaybackRate(float rate, @PlaybackRateAudioMode int audioMode) { + if (!isAudioPlaybackModeSupported(audioMode)) { + final String msg = "Audio playback mode " + audioMode + " is not supported"; + throw new IllegalArgumentException(msg); + } + _setPlaybackRate(rate); + } + + private native void _setPlaybackRate(float rate) throws IllegalStateException; + + /** * Seeks to specified time position. * * @param msec the offset in milliseconds from the start to seek to @@ -3083,6 +3139,14 @@ public class MediaPlayer implements SubtitleController.Listener mode == VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING); } + /* + * Test whether a given audio playback mode is supported. + * TODO query supported AudioPlaybackMode from player. + */ + private boolean isAudioPlaybackModeSupported(int mode) { + return (mode == PLAYBACK_RATE_AUDIO_MODE_RESAMPLE); + } + /** @hide */ static class TimeProvider implements MediaPlayer.OnSeekCompleteListener, MediaTimeProvider { diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index 97b3f63..58c86f2 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -16,6 +16,7 @@ package android.media; +import android.annotation.SystemApi; import android.app.ActivityThread; import android.hardware.Camera; import android.os.Handler; @@ -222,12 +223,11 @@ public class MediaRecorder public static final int REMOTE_SUBMIX = 8; /** - * Audio source for FM, which is used to capture current FM tuner output by FMRadio app. - * There are two use cases, one is for record FM stream for later listening, another is - * for FM indirect mode(the routing except FM to headset(headphone) device routing). + * Audio source for capturing broadcast radio tuner output. * @hide */ - public static final int FM_TUNER = 1998; + @SystemApi + public static final int RADIO_TUNER = 1998; /** * Audio source for preemptible, low-priority software hotword detection @@ -240,7 +240,8 @@ public class MediaRecorder * This is a hidden audio source. * @hide */ - protected static final int HOTWORD = 1999; + @SystemApi + public static final int HOTWORD = 1999; } /** diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index 1b6536f..c9a86d8 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -27,7 +27,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; diff --git a/media/java/android/media/SoundPool.java b/media/java/android/media/SoundPool.java index db6b38b..88d979e 100644 --- a/media/java/android/media/SoundPool.java +++ b/media/java/android/media/SoundPool.java @@ -32,7 +32,6 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.Log; @@ -112,7 +111,24 @@ import com.android.internal.app.IAppOpsService; * resumes.</p> */ public class SoundPool { - private final SoundPoolDelegate mImpl; + static { System.loadLibrary("soundpool"); } + + // SoundPool messages + // + // must match SoundPool.h + private static final int SAMPLE_LOADED = 1; + + private final static String TAG = "SoundPool"; + private final static boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private long mNativeContext; // accessed by native methods + + private EventHandler mEventHandler; + private SoundPool.OnLoadCompleteListener mOnLoadCompleteListener; + + private final Object mLock; + private final AudioAttributes mAttributes; + private final IAppOpsService mAppOps; /** * Constructor. Constructs a SoundPool object with the following @@ -135,68 +151,26 @@ public class SoundPool { } private SoundPool(int maxStreams, AudioAttributes attributes) { - if (SystemProperties.getBoolean("config.disable_media", false)) { - mImpl = new SoundPoolStub(); - } else { - mImpl = new SoundPoolImpl(this, maxStreams, attributes); + // do native setup + if (native_setup(new WeakReference<SoundPool>(this), maxStreams, attributes) != 0) { + throw new RuntimeException("Native setup failed"); } + mLock = new Object(); + mAttributes = attributes; + IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE); + mAppOps = IAppOpsService.Stub.asInterface(b); } /** - * Builder class for {@link SoundPool} objects. + * Release the SoundPool resources. + * + * Release all memory and native resources used by the SoundPool + * object. The SoundPool can no longer be used and the reference + * should be set to null. */ - public static class Builder { - private int mMaxStreams = 1; - private AudioAttributes mAudioAttributes; + public native final void release(); - /** - * Constructs a new Builder with the defaults format values. - * If not provided, the maximum number of streams is 1 (see {@link #setMaxStreams(int)} to - * change it), and the audio attributes have a usage value of - * {@link AudioAttributes#USAGE_MEDIA} (see {@link #setAudioAttributes(AudioAttributes)} to - * change them). - */ - public Builder() { - } - - /** - * Sets the maximum of number of simultaneous streams that can be played simultaneously. - * @param maxStreams a value equal to 1 or greater. - * @return the same Builder instance - * @throws IllegalArgumentException - */ - public Builder setMaxStreams(int maxStreams) throws IllegalArgumentException { - if (maxStreams <= 0) { - throw new IllegalArgumentException( - "Strictly positive value required for the maximum number of streams"); - } - mMaxStreams = maxStreams; - return this; - } - - /** - * Sets the {@link AudioAttributes}. For examples, game applications will use attributes - * built with usage information set to {@link AudioAttributes#USAGE_GAME}. - * @param attributes a non-null - * @return - */ - public Builder setAudioAttributes(AudioAttributes attributes) - throws IllegalArgumentException { - if (attributes == null) { - throw new IllegalArgumentException("Invalid null AudioAttributes"); - } - mAudioAttributes = attributes; - return this; - } - - public SoundPool build() { - if (mAudioAttributes == null) { - mAudioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA).build(); - } - return new SoundPool(mMaxStreams, mAudioAttributes); - } - } + protected void finalize() { release(); } /** * Load the sound from the specified path. @@ -207,7 +181,19 @@ public class SoundPool { * @return a sound ID. This value can be used to play or unload the sound. */ public int load(String path, int priority) { - return mImpl.load(path, priority); + int id = 0; + try { + File f = new File(path); + ParcelFileDescriptor fd = ParcelFileDescriptor.open(f, + ParcelFileDescriptor.MODE_READ_ONLY); + if (fd != null) { + id = _load(fd.getFileDescriptor(), 0, f.length(), priority); + fd.close(); + } + } catch (java.io.IOException e) { + Log.e(TAG, "error loading " + path); + } + return id; } /** @@ -226,7 +212,17 @@ public class SoundPool { * @return a sound ID. This value can be used to play or unload the sound. */ public int load(Context context, int resId, int priority) { - return mImpl.load(context, resId, priority); + AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); + int id = 0; + if (afd != null) { + id = _load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), priority); + try { + afd.close(); + } catch (java.io.IOException ex) { + //Log.d(TAG, "close failed:", ex); + } + } + return id; } /** @@ -238,7 +234,15 @@ public class SoundPool { * @return a sound ID. This value can be used to play or unload the sound. */ public int load(AssetFileDescriptor afd, int priority) { - return mImpl.load(afd, priority); + if (afd != null) { + long len = afd.getLength(); + if (len < 0) { + throw new AndroidRuntimeException("no length for fd"); + } + return _load(afd.getFileDescriptor(), afd.getStartOffset(), len, priority); + } else { + return 0; + } } /** @@ -256,7 +260,7 @@ public class SoundPool { * @return a sound ID. This value can be used to play or unload the sound. */ public int load(FileDescriptor fd, long offset, long length, int priority) { - return mImpl.load(fd, offset, length, priority); + return _load(fd, offset, length, priority); } /** @@ -269,9 +273,7 @@ public class SoundPool { * @param soundID a soundID returned by the load() function * @return true if just unloaded, false if previously unloaded */ - public final boolean unload(int soundID) { - return mImpl.unload(soundID); - } + public native final boolean unload(int soundID); /** * Play a sound from a sound ID. @@ -299,8 +301,10 @@ public class SoundPool { */ public final int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) { - return mImpl.play( - soundID, leftVolume, rightVolume, priority, loop, rate); + if (isRestricted()) { + leftVolume = rightVolume = 0; + } + return _play(soundID, leftVolume, rightVolume, priority, loop, rate); } /** @@ -314,9 +318,7 @@ public class SoundPool { * * @param streamID a streamID returned by the play() function */ - public final void pause(int streamID) { - mImpl.pause(streamID); - } + public native final void pause(int streamID); /** * Resume a playback stream. @@ -328,9 +330,7 @@ public class SoundPool { * * @param streamID a streamID returned by the play() function */ - public final void resume(int streamID) { - mImpl.resume(streamID); - } + public native final void resume(int streamID); /** * Pause all active streams. @@ -340,9 +340,7 @@ public class SoundPool { * are playing. It also sets a flag so that any streams that * are playing can be resumed by calling autoResume(). */ - public final void autoPause() { - mImpl.autoPause(); - } + public native final void autoPause(); /** * Resume all previously active streams. @@ -350,9 +348,7 @@ public class SoundPool { * Automatically resumes all streams that were paused in previous * calls to autoPause(). */ - public final void autoResume() { - mImpl.autoResume(); - } + public native final void autoResume(); /** * Stop a playback stream. @@ -365,9 +361,7 @@ public class SoundPool { * * @param streamID a streamID returned by the play() function */ - public final void stop(int streamID) { - mImpl.stop(streamID); - } + public native final void stop(int streamID); /** * Set stream volume. @@ -381,9 +375,11 @@ public class SoundPool { * @param leftVolume left volume value (range = 0.0 to 1.0) * @param rightVolume right volume value (range = 0.0 to 1.0) */ - public final void setVolume(int streamID, - float leftVolume, float rightVolume) { - mImpl.setVolume(streamID, leftVolume, rightVolume); + public final void setVolume(int streamID, float leftVolume, float rightVolume) { + if (isRestricted()) { + return; + } + _setVolume(streamID, leftVolume, rightVolume); } /** @@ -404,9 +400,7 @@ public class SoundPool { * * @param streamID a streamID returned by the play() function */ - public final void setPriority(int streamID, int priority) { - mImpl.setPriority(streamID, priority); - } + public native final void setPriority(int streamID, int priority); /** * Set loop mode. @@ -419,9 +413,7 @@ public class SoundPool { * @param streamID a streamID returned by the play() function * @param loop loop mode (0 = no loop, -1 = loop forever) */ - public final void setLoop(int streamID, int loop) { - mImpl.setLoop(streamID, loop); - } + public native final void setLoop(int streamID, int loop); /** * Change playback rate. @@ -435,9 +427,7 @@ public class SoundPool { * @param streamID a streamID returned by the play() function * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0) */ - public final void setRate(int streamID, float rate) { - mImpl.setRate(streamID, rate); - } + public native final void setRate(int streamID, float rate); public interface OnLoadCompleteListener { /** @@ -454,356 +444,137 @@ public class SoundPool { * Sets the callback hook for the OnLoadCompleteListener. */ public void setOnLoadCompleteListener(OnLoadCompleteListener listener) { - mImpl.setOnLoadCompleteListener(listener); - } - - /** - * Release the SoundPool resources. - * - * Release all memory and native resources used by the SoundPool - * object. The SoundPool can no longer be used and the reference - * should be set to null. - */ - public final void release() { - mImpl.release(); - } - - /** - * Interface for SoundPool implementations. - * SoundPool is statically referenced and unconditionally called from all - * over the framework, so we can't simply omit the class or make it throw - * runtime exceptions, as doing so would break the framework. Instead we - * now select either a real or no-op impl object based on whether media is - * enabled. - * - * @hide - */ - /* package */ interface SoundPoolDelegate { - public int load(String path, int priority); - public int load(Context context, int resId, int priority); - public int load(AssetFileDescriptor afd, int priority); - public int load( - FileDescriptor fd, long offset, long length, int priority); - public boolean unload(int soundID); - public int play( - int soundID, float leftVolume, float rightVolume, - int priority, int loop, float rate); - public void pause(int streamID); - public void resume(int streamID); - public void autoPause(); - public void autoResume(); - public void stop(int streamID); - public void setVolume(int streamID, float leftVolume, float rightVolume); - public void setVolume(int streamID, float volume); - public void setPriority(int streamID, int priority); - public void setLoop(int streamID, int loop); - public void setRate(int streamID, float rate); - public void setOnLoadCompleteListener(OnLoadCompleteListener listener); - public void release(); - } - - - /** - * Real implementation of the delegate interface. This was formerly the - * body of SoundPool itself. - */ - /* package */ static class SoundPoolImpl implements SoundPoolDelegate { - static { System.loadLibrary("soundpool"); } - - private final static String TAG = "SoundPool"; - private final static boolean DEBUG = false; - - private long mNativeContext; // accessed by native methods - - private EventHandler mEventHandler; - private SoundPool.OnLoadCompleteListener mOnLoadCompleteListener; - private SoundPool mProxy; - - private final Object mLock; - private final AudioAttributes mAttributes; - private final IAppOpsService mAppOps; - - // SoundPool messages - // - // must match SoundPool.h - private static final int SAMPLE_LOADED = 1; - - public SoundPoolImpl(SoundPool proxy, int maxStreams, AudioAttributes attr) { - - // do native setup - if (native_setup(new WeakReference(this), maxStreams, attr) != 0) { - throw new RuntimeException("Native setup failed"); - } - mLock = new Object(); - mProxy = proxy; - mAttributes = attr; - IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE); - mAppOps = IAppOpsService.Stub.asInterface(b); - } - - public int load(String path, int priority) - { - int id = 0; - try { - File f = new File(path); - ParcelFileDescriptor fd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); - if (fd != null) { - id = _load(fd.getFileDescriptor(), 0, f.length(), priority); - fd.close(); - } - } catch (java.io.IOException e) { - Log.e(TAG, "error loading " + path); - } - return id; - } - - @Override - public int load(Context context, int resId, int priority) { - AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); - int id = 0; - if (afd != null) { - id = _load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), priority); - try { - afd.close(); - } catch (java.io.IOException ex) { - //Log.d(TAG, "close failed:", ex); - } - } - return id; - } - - @Override - public int load(AssetFileDescriptor afd, int priority) { - if (afd != null) { - long len = afd.getLength(); - if (len < 0) { - throw new AndroidRuntimeException("no length for fd"); + synchronized(mLock) { + if (listener != null) { + // setup message handler + Looper looper; + if ((looper = Looper.myLooper()) != null) { + mEventHandler = new EventHandler(looper); + } else if ((looper = Looper.getMainLooper()) != null) { + mEventHandler = new EventHandler(looper); + } else { + mEventHandler = null; } - return _load(afd.getFileDescriptor(), afd.getStartOffset(), len, priority); } else { - return 0; + mEventHandler = null; } + mOnLoadCompleteListener = listener; } + } - @Override - public int load(FileDescriptor fd, long offset, long length, int priority) { - return _load(fd, offset, length, priority); - } - - private native final int _load(FileDescriptor fd, long offset, long length, int priority); - - @Override - public native final boolean unload(int soundID); - - @Override - public final int play(int soundID, float leftVolume, float rightVolume, - int priority, int loop, float rate) { - if (isRestricted()) { - leftVolume = rightVolume = 0; - } - return _play(soundID, leftVolume, rightVolume, priority, loop, rate); + private boolean isRestricted() { + if ((mAttributes.getFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) { + return false; } - - public native final int _play(int soundID, float leftVolume, float rightVolume, - int priority, int loop, float rate); - - private boolean isRestricted() { - if ((mAttributes.getFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) { - return false; - } - try { - final int mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO, - mAttributes.getUsage(), - Process.myUid(), ActivityThread.currentPackageName()); - return mode != AppOpsManager.MODE_ALLOWED; - } catch (RemoteException e) { - return false; - } + try { + final int mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO, + mAttributes.getUsage(), + Process.myUid(), ActivityThread.currentPackageName()); + return mode != AppOpsManager.MODE_ALLOWED; + } catch (RemoteException e) { + return false; } + } - @Override - public native final void pause(int streamID); + private native final int _load(FileDescriptor fd, long offset, long length, int priority); - @Override - public native final void resume(int streamID); + private native final int native_setup(Object weakRef, int maxStreams, + Object/*AudioAttributes*/ attributes); - @Override - public native final void autoPause(); + private native final int _play(int soundID, float leftVolume, float rightVolume, + int priority, int loop, float rate); - @Override - public native final void autoResume(); + private native final void _setVolume(int streamID, float leftVolume, float rightVolume); - @Override - public native final void stop(int streamID); + // post event from native code to message handler + @SuppressWarnings("unchecked") + private static void postEventFromNative(Object ref, int msg, int arg1, int arg2, Object obj) { + SoundPool soundPool = ((WeakReference<SoundPool>) ref).get(); + if (soundPool == null) + return; - @Override - public final void setVolume(int streamID, float leftVolume, float rightVolume) { - if (isRestricted()) { - return; - } - _setVolume(streamID, leftVolume, rightVolume); + if (soundPool.mEventHandler != null) { + Message m = soundPool.mEventHandler.obtainMessage(msg, arg1, arg2, obj); + soundPool.mEventHandler.sendMessage(m); } + } - private native final void _setVolume(int streamID, float leftVolume, float rightVolume); - - @Override - public void setVolume(int streamID, float volume) { - setVolume(streamID, volume, volume); + private final class EventHandler extends Handler { + public EventHandler(Looper looper) { + super(looper); } @Override - public native final void setPriority(int streamID, int priority); - - @Override - public native final void setLoop(int streamID, int loop); - - @Override - public native final void setRate(int streamID, float rate); - - @Override - public void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener) - { - synchronized(mLock) { - if (listener != null) { - // setup message handler - Looper looper; - if ((looper = Looper.myLooper()) != null) { - mEventHandler = new EventHandler(mProxy, looper); - } else if ((looper = Looper.getMainLooper()) != null) { - mEventHandler = new EventHandler(mProxy, looper); - } else { - mEventHandler = null; + public void handleMessage(Message msg) { + switch(msg.what) { + case SAMPLE_LOADED: + if (DEBUG) Log.d(TAG, "Sample " + msg.arg1 + " loaded"); + synchronized(mLock) { + if (mOnLoadCompleteListener != null) { + mOnLoadCompleteListener.onLoadComplete(SoundPool.this, msg.arg1, msg.arg2); } - } else { - mEventHandler = null; } - mOnLoadCompleteListener = listener; - } - } - - private class EventHandler extends Handler - { - private SoundPool mSoundPool; - - public EventHandler(SoundPool soundPool, Looper looper) { - super(looper); - mSoundPool = soundPool; - } - - @Override - public void handleMessage(Message msg) { - switch(msg.what) { - case SAMPLE_LOADED: - if (DEBUG) Log.d(TAG, "Sample " + msg.arg1 + " loaded"); - synchronized(mLock) { - if (mOnLoadCompleteListener != null) { - mOnLoadCompleteListener.onLoadComplete(mSoundPool, msg.arg1, msg.arg2); - } - } - break; - default: - Log.e(TAG, "Unknown message type " + msg.what); - return; - } - } - } - - // post event from native code to message handler - private static void postEventFromNative(Object weakRef, int msg, int arg1, int arg2, Object obj) - { - SoundPoolImpl soundPoolImpl = (SoundPoolImpl)((WeakReference)weakRef).get(); - if (soundPoolImpl == null) + break; + default: + Log.e(TAG, "Unknown message type " + msg.what); return; - - if (soundPoolImpl.mEventHandler != null) { - Message m = soundPoolImpl.mEventHandler.obtainMessage(msg, arg1, arg2, obj); - soundPoolImpl.mEventHandler.sendMessage(m); } } - - public native final void release(); - - private native final int native_setup(Object weakRef, int maxStreams, - Object/*AudioAttributes*/ attributes); - - protected void finalize() { release(); } } /** - * No-op implementation of SoundPool. - * Used when media is disabled by the system. - * @hide + * Builder class for {@link SoundPool} objects. */ - /* package */ static class SoundPoolStub implements SoundPoolDelegate { - public SoundPoolStub() { } - - public int load(String path, int priority) { - return 0; - } - - @Override - public int load(Context context, int resId, int priority) { - return 0; - } - - @Override - public int load(AssetFileDescriptor afd, int priority) { - return 0; - } - - @Override - public int load(FileDescriptor fd, long offset, long length, int priority) { - return 0; - } + public static class Builder { + private int mMaxStreams = 1; + private AudioAttributes mAudioAttributes; - @Override - public final boolean unload(int soundID) { - return true; + /** + * Constructs a new Builder with the defaults format values. + * If not provided, the maximum number of streams is 1 (see {@link #setMaxStreams(int)} to + * change it), and the audio attributes have a usage value of + * {@link AudioAttributes#USAGE_MEDIA} (see {@link #setAudioAttributes(AudioAttributes)} to + * change them). + */ + public Builder() { } - @Override - public final int play(int soundID, float leftVolume, float rightVolume, - int priority, int loop, float rate) { - return 0; + /** + * Sets the maximum of number of simultaneous streams that can be played simultaneously. + * @param maxStreams a value equal to 1 or greater. + * @return the same Builder instance + * @throws IllegalArgumentException + */ + public Builder setMaxStreams(int maxStreams) throws IllegalArgumentException { + if (maxStreams <= 0) { + throw new IllegalArgumentException( + "Strictly positive value required for the maximum number of streams"); + } + mMaxStreams = maxStreams; + return this; } - @Override - public final void pause(int streamID) { } - - @Override - public final void resume(int streamID) { } - - @Override - public final void autoPause() { } - - @Override - public final void autoResume() { } - - @Override - public final void stop(int streamID) { } - - @Override - public final void setVolume(int streamID, - float leftVolume, float rightVolume) { } - - @Override - public void setVolume(int streamID, float volume) { + /** + * Sets the {@link AudioAttributes}. For examples, game applications will use attributes + * built with usage information set to {@link AudioAttributes#USAGE_GAME}. + * @param attributes a non-null + * @return + */ + public Builder setAudioAttributes(AudioAttributes attributes) + throws IllegalArgumentException { + if (attributes == null) { + throw new IllegalArgumentException("Invalid null AudioAttributes"); + } + mAudioAttributes = attributes; + return this; } - @Override - public final void setPriority(int streamID, int priority) { } - - @Override - public final void setLoop(int streamID, int loop) { } - - @Override - public final void setRate(int streamID, float rate) { } - - @Override - public void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener) { + public SoundPool build() { + if (mAudioAttributes == null) { + mAudioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA).build(); + } + return new SoundPool(mMaxStreams, mAudioAttributes); } - - @Override - public final void release() { } } } diff --git a/media/java/android/media/TtmlRenderer.java b/media/java/android/media/TtmlRenderer.java index 75133c9..9d587b9 100644 --- a/media/java/android/media/TtmlRenderer.java +++ b/media/java/android/media/TtmlRenderer.java @@ -17,27 +17,15 @@ package android.media; import android.content.Context; -import android.graphics.Color; -import android.media.SubtitleTrack.RenderingWidget.OnChangedListener; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; -import android.view.View.MeasureSpec; -import android.view.ViewGroup.LayoutParams; import android.view.accessibility.CaptioningManager; -import android.view.accessibility.CaptioningManager.CaptionStyle; -import android.view.accessibility.CaptioningManager.CaptioningChangeListener; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.internal.widget.SubtitleView; - import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index df0daaf..9e01e65 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -26,8 +26,6 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Vector; -import static com.android.internal.util.Preconditions.checkNotNull; - // package private class Utils { private static final String TAG = "Utils"; diff --git a/media/java/android/media/VolumePolicy.aidl b/media/java/android/media/VolumePolicy.aidl new file mode 100644 index 0000000..371f798 --- /dev/null +++ b/media/java/android/media/VolumePolicy.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable VolumePolicy; diff --git a/media/java/android/media/VolumePolicy.java b/media/java/android/media/VolumePolicy.java new file mode 100644 index 0000000..2d3376a --- /dev/null +++ b/media/java/android/media/VolumePolicy.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public final class VolumePolicy implements Parcelable { + public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, true, 400); + + /** Allow volume adjustments lower from vibrate to enter ringer mode = silent */ + public final boolean volumeDownToEnterSilent; + + /** Allow volume adjustments higher to exit ringer mode = silent */ + public final boolean volumeUpToExitSilent; + + /** Automatically enter do not disturb when ringer mode = silent */ + public final boolean doNotDisturbWhenSilent; + + /** Only allow volume adjustment from vibrate to silent after this + number of milliseconds since an adjustment from normal to vibrate. */ + public final int vibrateToSilentDebounce; + + public VolumePolicy(boolean volumeDownToEnterSilent, boolean volumeUpToExitSilent, + boolean doNotDisturbWhenSilent, int vibrateToSilentDebounce) { + this.volumeDownToEnterSilent = volumeDownToEnterSilent; + this.volumeUpToExitSilent = volumeUpToExitSilent; + this.doNotDisturbWhenSilent = doNotDisturbWhenSilent; + this.vibrateToSilentDebounce = vibrateToSilentDebounce; + } + + @Override + public String toString() { + return "VolumePolicy[volumeDownToEnterSilent=" + volumeDownToEnterSilent + + ",volumeUpToExitSilent=" + volumeUpToExitSilent + + ",doNotDisturbWhenSilent=" + doNotDisturbWhenSilent + + ",vibrateToSilentDebounce=" + vibrateToSilentDebounce + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(volumeDownToEnterSilent ? 1 : 0); + dest.writeInt(volumeUpToExitSilent ? 1 : 0); + dest.writeInt(doNotDisturbWhenSilent ? 1 : 0); + dest.writeInt(vibrateToSilentDebounce); + } + + public static final Parcelable.Creator<VolumePolicy> CREATOR + = new Parcelable.Creator<VolumePolicy>() { + @Override + public VolumePolicy createFromParcel(Parcel p) { + return new VolumePolicy(p.readInt() != 0, + p.readInt() != 0, + p.readInt() != 0, + p.readInt()); + } + + @Override + public VolumePolicy[] newArray(int size) { + return new VolumePolicy[size]; + } + }; +}
\ No newline at end of file diff --git a/media/java/android/media/WebVttRenderer.java b/media/java/android/media/WebVttRenderer.java index 69e0ea6..91c53fa 100644 --- a/media/java/android/media/WebVttRenderer.java +++ b/media/java/android/media/WebVttRenderer.java @@ -433,7 +433,9 @@ class TextTrackCue extends SubtitleTrack.Cue { mRegionId.equals(cue.mRegionId) && mSnapToLines == cue.mSnapToLines && mAutoLinePosition == cue.mAutoLinePosition && - (mAutoLinePosition || mLinePosition == cue.mLinePosition) && + (mAutoLinePosition || + ((mLinePosition != null && mLinePosition.equals(cue.mLinePosition)) || + (mLinePosition == null && cue.mLinePosition == null))) && mTextPosition == cue.mTextPosition && mSize == cue.mSize && mAlignment == cue.mAlignment && diff --git a/media/java/android/media/audiofx/AcousticEchoCanceler.java b/media/java/android/media/audiofx/AcousticEchoCanceler.java index 4b59c88..f5f98ef 100644 --- a/media/java/android/media/audiofx/AcousticEchoCanceler.java +++ b/media/java/android/media/audiofx/AcousticEchoCanceler.java @@ -68,9 +68,8 @@ public class AcousticEchoCanceler extends AudioEffect { Log.w(TAG, "not enough resources"); } catch (RuntimeException e) { Log.w(TAG, "not enough memory"); - } finally { - return aec; } + return aec; } /** diff --git a/media/java/android/media/audiofx/AutomaticGainControl.java b/media/java/android/media/audiofx/AutomaticGainControl.java index 83eb4e9..4a6b1f3 100644 --- a/media/java/android/media/audiofx/AutomaticGainControl.java +++ b/media/java/android/media/audiofx/AutomaticGainControl.java @@ -68,9 +68,8 @@ public class AutomaticGainControl extends AudioEffect { Log.w(TAG, "not enough resources"); } catch (RuntimeException e) { Log.w(TAG, "not enough memory"); - } finally { - return agc; } + return agc; } /** diff --git a/media/java/android/media/audiofx/NoiseSuppressor.java b/media/java/android/media/audiofx/NoiseSuppressor.java index 0ea42ab..bca990f 100644 --- a/media/java/android/media/audiofx/NoiseSuppressor.java +++ b/media/java/android/media/audiofx/NoiseSuppressor.java @@ -70,9 +70,8 @@ public class NoiseSuppressor extends AudioEffect { Log.w(TAG, "not enough resources"); } catch (RuntimeException e) { Log.w(TAG, "not enough memory"); - } finally { - return ns; } + return ns; } /** diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index 019309d..917e07b 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -16,12 +16,8 @@ package android.media.audiopolicy; -import android.media.AudioAttributes; import android.media.AudioFormat; -import android.media.AudioManager; import android.media.audiopolicy.AudioMixingRule.AttributeMatchCriterion; -import android.os.Binder; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; diff --git a/media/java/android/media/midi/IMidiListener.aidl b/media/java/android/media/midi/IMidiDeviceListener.aidl index a4129e9..31c66e3 100644 --- a/media/java/android/media/midi/IMidiListener.aidl +++ b/media/java/android/media/midi/IMidiDeviceListener.aidl @@ -17,10 +17,12 @@ package android.media.midi; import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceStatus; /** @hide */ -oneway interface IMidiListener +oneway interface IMidiDeviceListener { void onDeviceAdded(in MidiDeviceInfo device); void onDeviceRemoved(in MidiDeviceInfo device); + void onDeviceStatusChanged(in MidiDeviceStatus status); } diff --git a/media/java/android/media/midi/IMidiDeviceServer.aidl b/media/java/android/media/midi/IMidiDeviceServer.aidl index 71914ad..642078a 100644 --- a/media/java/android/media/midi/IMidiDeviceServer.aidl +++ b/media/java/android/media/midi/IMidiDeviceServer.aidl @@ -21,6 +21,10 @@ import android.os.ParcelFileDescriptor; /** @hide */ interface IMidiDeviceServer { - ParcelFileDescriptor openInputPort(int portNumber); - ParcelFileDescriptor openOutputPort(int portNumber); + ParcelFileDescriptor openInputPort(IBinder token, int portNumber); + ParcelFileDescriptor openOutputPort(IBinder token, int portNumber); + void closePort(IBinder token); + + // connects the input port pfd to the specified output port + void connectPorts(IBinder token, in ParcelFileDescriptor pfd, int outputPortNumber); } diff --git a/media/java/android/media/midi/IMidiManager.aidl b/media/java/android/media/midi/IMidiManager.aidl index bba35f5..74c63b4 100644 --- a/media/java/android/media/midi/IMidiManager.aidl +++ b/media/java/android/media/midi/IMidiManager.aidl @@ -16,9 +16,10 @@ package android.media.midi; +import android.media.midi.IMidiDeviceListener; import android.media.midi.IMidiDeviceServer; -import android.media.midi.IMidiListener; import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceStatus; import android.os.Bundle; import android.os.IBinder; @@ -28,14 +29,28 @@ interface IMidiManager MidiDeviceInfo[] getDeviceList(); // for device creation & removal notifications - void registerListener(IBinder token, in IMidiListener listener); - void unregisterListener(IBinder token, in IMidiListener listener); + void registerListener(IBinder token, in IMidiDeviceListener listener); + void unregisterListener(IBinder token, in IMidiDeviceListener listener); - // for communicating with MIDI devices + // for opening built-in MIDI devices IMidiDeviceServer openDevice(IBinder token, in MidiDeviceInfo device); - // for implementing virtual MIDI devices + // for registering built-in MIDI devices MidiDeviceInfo registerDeviceServer(in IMidiDeviceServer server, int numInputPorts, - int numOutputPorts, in Bundle properties, boolean isPrivate, int type); + int numOutputPorts, in String[] inputPortNames, in String[] outputPortNames, + in Bundle properties, int type); + + // for unregistering built-in MIDI devices void unregisterDeviceServer(in IMidiDeviceServer server); + + // used by MidiDeviceService to access the MidiDeviceInfo that was created based on its + // manifest's meta-data + MidiDeviceInfo getServiceDeviceInfo(String packageName, String className); + + // used for client's to retrieve a device's MidiDeviceStatus + MidiDeviceStatus getDeviceStatus(in MidiDeviceInfo deviceInfo); + + // used by MIDI devices to report their status + // the token is used by MidiService for death notification + void setDeviceStatus(IBinder token, in MidiDeviceStatus status); } diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java index 36710fd..569f7c6 100644 --- a/media/java/android/media/midi/MidiDevice.java +++ b/media/java/android/media/midi/MidiDevice.java @@ -16,36 +16,70 @@ package android.media.midi; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; +import dalvik.system.CloseGuard; + +import libcore.io.IoUtils; + +import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; /** - * This class is used for sending and receiving data to and from an MIDI device + * This class is used for sending and receiving data to and from a MIDI device * Instances of this class are created by {@link MidiManager#openDevice}. * * CANDIDATE FOR PUBLIC API * @hide */ -public final class MidiDevice { +public final class MidiDevice implements Closeable { private static final String TAG = "MidiDevice"; private final MidiDeviceInfo mDeviceInfo; - private final IMidiDeviceServer mServer; + private final IMidiDeviceServer mDeviceServer; + private Context mContext; + private ServiceConnection mServiceConnection; - /** - * MidiDevice should only be instantiated by MidiManager - * @hide - */ - public MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) { + + private final CloseGuard mGuard = CloseGuard.get(); + + public class MidiConnection implements Closeable { + private final IBinder mToken; + private final MidiInputPort mInputPort; + + MidiConnection(IBinder token, MidiInputPort inputPort) { + mToken = token; + mInputPort = inputPort; + } + + @Override + public void close() throws IOException { + try { + mDeviceServer.closePort(mToken); + IoUtils.closeQuietly(mInputPort); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MidiConnection.close"); + } + } + } + + /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) { + this(deviceInfo, server, null, null); + } + + /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server, + Context context, ServiceConnection serviceConnection) { mDeviceInfo = deviceInfo; - mServer = server; + mDeviceServer = server; + mContext = context; + mServiceConnection = serviceConnection; + mGuard.open("close"); } /** @@ -65,11 +99,12 @@ public final class MidiDevice { */ public MidiInputPort openInputPort(int portNumber) { try { - ParcelFileDescriptor pfd = mServer.openInputPort(portNumber); + IBinder token = new Binder(); + ParcelFileDescriptor pfd = mDeviceServer.openInputPort(token, portNumber); if (pfd == null) { return null; } - return new MidiInputPort(pfd, portNumber); + return new MidiInputPort(mDeviceServer, token, pfd, portNumber); } catch (RemoteException e) { Log.e(TAG, "RemoteException in openInputPort"); return null; @@ -84,17 +119,70 @@ public final class MidiDevice { */ public MidiOutputPort openOutputPort(int portNumber) { try { - ParcelFileDescriptor pfd = mServer.openOutputPort(portNumber); + IBinder token = new Binder(); + ParcelFileDescriptor pfd = mDeviceServer.openOutputPort(token, portNumber); if (pfd == null) { return null; } - return new MidiOutputPort(pfd, portNumber); + return new MidiOutputPort(mDeviceServer, token, pfd, portNumber); } catch (RemoteException e) { Log.e(TAG, "RemoteException in openOutputPort"); return null; } } + /** + * Connects the supplied {@link MidiInputPort} to the output port of this device + * with the specified port number. Once the connection is made, the MidiInput port instance + * can no longer receive data via its {@link MidiReciever.receive} method. + * This method returns a {@link #MidiConnection} object, which can be used to close the connection + * @param inputPort the inputPort to connect + * @param outputPortNumber the port number of the output port to connect inputPort to. + * @return {@link #MidiConnection} object if the connection is successful, or null in case of failure + */ + public MidiConnection connectPorts(MidiInputPort inputPort, int outputPortNumber) { + if (outputPortNumber < 0 || outputPortNumber >= mDeviceInfo.getOutputPortCount()) { + throw new IllegalArgumentException("outputPortNumber out of range"); + } + + ParcelFileDescriptor pfd = inputPort.claimFileDescriptor(); + if (pfd == null) { + return null; + } + try { + IBinder token = new Binder(); + mDeviceServer.connectPorts(token, pfd, outputPortNumber); + // close our copy of the file descriptor + IoUtils.closeQuietly(pfd); + return new MidiConnection(token, inputPort); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in connectPorts"); + return null; + } + } + + @Override + public void close() throws IOException { + synchronized (mGuard) { + mGuard.close(); + if (mContext != null && mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + mContext = null; + mServiceConnection = null; + } + } + } + + @Override + protected void finalize() throws Throwable { + try { + mGuard.warnIfOpen(); + close(); + } finally { + super.finalize(); + } + } + @Override public String toString() { return ("MidiDevice: " + mDeviceInfo.toString()); diff --git a/media/java/android/media/midi/MidiDeviceInfo.java b/media/java/android/media/midi/MidiDeviceInfo.java index fd35052..7201e25 100644 --- a/media/java/android/media/midi/MidiDeviceInfo.java +++ b/media/java/android/media/midi/MidiDeviceInfo.java @@ -27,11 +27,8 @@ import android.os.Parcelable; * * This class is just an immutable object to encapsulate the MIDI device description. * Use the MidiDevice class to actually communicate with devices. - * - * CANDIDATE FOR PUBLIC API - * @hide */ -public class MidiDeviceInfo implements Parcelable { +public final class MidiDeviceInfo implements Parcelable { private static final String TAG = "MidiDeviceInfo"; @@ -45,11 +42,17 @@ public class MidiDeviceInfo implements Parcelable { */ public static final int TYPE_VIRTUAL = 2; - private final int mType; // USB or virtual - private final int mId; // unique ID generated by MidiService - private final int mInputPortCount; - private final int mOutputPortCount; - private final Bundle mProperties; + /** + * Constant representing Bluetooth MIDI devices for {@link #getType} + */ + public static final int TYPE_BLUETOOTH = 3; + + /** + * Bundle key for the device's user visible name property. + * Used with the {@link android.os.Bundle} returned by {@link #getProperties}. + * For USB devices, this is a concatenation of the manufacturer and product names. + */ + public static final String PROPERTY_NAME = "name"; /** * Bundle key for the device's manufacturer name property. @@ -59,11 +62,11 @@ public class MidiDeviceInfo implements Parcelable { public static final String PROPERTY_MANUFACTURER = "manufacturer"; /** - * Bundle key for the device's model name property. + * Bundle key for the device's product name property. * Used with the {@link android.os.Bundle} returned by {@link #getProperties} * Matches the USB device product name string for USB MIDI devices. */ - public static final String PROPERTY_MODEL = "model"; + public static final String PROPERTY_PRODUCT = "product"; /** * Bundle key for the device's serial number property. @@ -80,9 +83,18 @@ public class MidiDeviceInfo implements Parcelable { public static final String PROPERTY_USB_DEVICE = "usb_device"; /** + * Bundle key for the device's {@link android.bluetooth.BluetoothDevice}. + * Only set for Bluetooth MIDI devices. + * Used with the {@link android.os.Bundle} returned by {@link #getProperties} + */ + public static final String PROPERTY_BLUETOOTH_DEVICE = "bluetooth_device"; + + /** * Bundle key for the device's ALSA card number. * Only set for USB MIDI devices. * Used with the {@link android.os.Bundle} returned by {@link #getProperties} + * + * @hide */ public static final String PROPERTY_ALSA_CARD = "alsa_card"; @@ -90,20 +102,101 @@ public class MidiDeviceInfo implements Parcelable { * Bundle key for the device's ALSA device number. * Only set for USB MIDI devices. * Used with the {@link android.os.Bundle} returned by {@link #getProperties} + * + * @hide */ public static final String PROPERTY_ALSA_DEVICE = "alsa_device"; /** + * {@link android.content.pm.ServiceInfo} for the service hosting the device implementation. + * Only set for Virtual MIDI devices. + * Used with the {@link android.os.Bundle} returned by {@link #getProperties} + * + * @hide + */ + public static final String PROPERTY_SERVICE_INFO = "service_info"; + + /** + * Contains information about an input or output port. + */ + public static final class PortInfo { + /** + * Port type for input ports + */ + public static final int TYPE_INPUT = 1; + + /** + * Port type for output ports + */ + public static final int TYPE_OUTPUT = 2; + + private final int mPortType; + private final int mPortNumber; + private final String mName; + + PortInfo(int type, int portNumber, String name) { + mPortType = type; + mPortNumber = portNumber; + mName = (name == null ? "" : name); + } + + /** + * Returns the port type of the port (either {@link #TYPE_INPUT} or {@link #TYPE_OUTPUT}) + * @return the port type + */ + public int getType() { + return mPortType; + } + + /** + * Returns the port number of the port + * @return the port number + */ + public int getPortNumber() { + return mPortNumber; + } + + /** + * Returns the name of the port, or empty string if the port has no name + * @return the port name + */ + public String getName() { + return mName; + } + } + + private final int mType; // USB or virtual + private final int mId; // unique ID generated by MidiService + private final int mInputPortCount; + private final int mOutputPortCount; + private final String[] mInputPortNames; + private final String[] mOutputPortNames; + private final Bundle mProperties; + private final boolean mIsPrivate; + + /** * MidiDeviceInfo should only be instantiated by MidiService implementation * @hide */ public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts, - Bundle properties) { + String[] inputPortNames, String[] outputPortNames, Bundle properties, + boolean isPrivate) { mType = type; mId = id; mInputPortCount = numInputPorts; mOutputPortCount = numOutputPorts; + if (inputPortNames == null) { + mInputPortNames = new String[numInputPorts]; + } else { + mInputPortNames = inputPortNames; + } + if (outputPortNames == null) { + mOutputPortNames = new String[numOutputPorts]; + } else { + mOutputPortNames = outputPortNames; + } mProperties = properties; + mIsPrivate = isPrivate; } /** @@ -144,6 +237,32 @@ public class MidiDeviceInfo implements Parcelable { } /** + * Returns information about an input port. + * + * @param portNumber the number of the input port + * @return the input port's information object + */ + public PortInfo getInputPortInfo(int portNumber) { + if (portNumber < 0 || portNumber >= mInputPortCount) { + throw new IllegalArgumentException("portNumber out of range"); + } + return new PortInfo(PortInfo.TYPE_INPUT, portNumber, mInputPortNames[portNumber]); + } + + /** + * Returns information about an output port. + * + * @param portNumber the number of the output port + * @return the output port's information object + */ + public PortInfo getOutputPortInfo(int portNumber) { + if (portNumber < 0 || portNumber >= mOutputPortCount) { + throw new IllegalArgumentException("portNumber out of range"); + } + return new PortInfo(PortInfo.TYPE_OUTPUT, portNumber, mOutputPortNames[portNumber]); + } + + /** * Returns the {@link android.os.Bundle} containing the device's properties. * * @return the device's properties @@ -152,6 +271,16 @@ public class MidiDeviceInfo implements Parcelable { return mProperties; } + /** + * Returns true if the device is private. Private devices are only visible and accessible + * to clients with the same UID as the application that is hosting the device. + * + * @return true if the device is private + */ + public boolean isPrivate() { + return mIsPrivate; + } + @Override public boolean equals(Object o) { if (o instanceof MidiDeviceInfo) { @@ -171,7 +300,8 @@ public class MidiDeviceInfo implements Parcelable { return ("MidiDeviceInfo[mType=" + mType + ",mInputPortCount=" + mInputPortCount + ",mOutputPortCount=" + mOutputPortCount + - ",mProperties=" + mProperties); + ",mProperties=" + mProperties + + ",mIsPrivate=" + mIsPrivate); } public static final Parcelable.Creator<MidiDeviceInfo> CREATOR = @@ -181,8 +311,12 @@ public class MidiDeviceInfo implements Parcelable { int id = in.readInt(); int inputPorts = in.readInt(); int outputPorts = in.readInt(); + String[] inputPortNames = in.createStringArray(); + String[] outputPortNames = in.createStringArray(); Bundle properties = in.readBundle(); - return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties); + boolean isPrivate = (in.readInt() == 1); + return new MidiDeviceInfo(type, id, inputPorts, outputPorts, + inputPortNames, outputPortNames, properties, isPrivate); } public MidiDeviceInfo[] newArray(int size) { @@ -199,6 +333,9 @@ public class MidiDeviceInfo implements Parcelable { parcel.writeInt(mId); parcel.writeInt(mInputPortCount); parcel.writeInt(mOutputPortCount); + parcel.writeStringArray(mInputPortNames); + parcel.writeStringArray(mOutputPortNames); parcel.writeBundle(mProperties); + parcel.writeInt(mIsPrivate ? 1 : 0); } } diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index 3317baa..d27351f 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -16,21 +16,26 @@ package android.media.midi; +import android.os.Binder; +import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.RemoteException; import android.system.OsConstants; import android.util.Log; +import dalvik.system.CloseGuard; + +import libcore.io.IoUtils; + import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArrayList; /** - * This class is used to provide the implemention of MIDI device. - * Applications may call {@link MidiManager#createDeviceServer} - * to create an instance of this class to implement a virtual MIDI device. + * Internal class used for providing an implementation for a MIDI device. * - * CANDIDATE FOR PUBLIC API * @hide */ public final class MidiDeviceServer implements Closeable { @@ -40,64 +45,129 @@ public final class MidiDeviceServer implements Closeable { // MidiDeviceInfo for the device implemented by this server private MidiDeviceInfo mDeviceInfo; - private int mInputPortCount; - private int mOutputPortCount; + private final int mInputPortCount; + private final int mOutputPortCount; + + // MidiReceivers for receiving data on our input ports + private final MidiReceiver[] mInputPortReceivers; + + // MidiDispatchers for sending data on our output ports + private MidiDispatcher[] mOutputPortDispatchers; + + // MidiOutputPorts for clients connected to our input ports + private final MidiOutputPort[] mInputPortOutputPorts; + + // List of all MidiInputPorts we created + private final CopyOnWriteArrayList<MidiInputPort> mInputPorts + = new CopyOnWriteArrayList<MidiInputPort>(); + + + // for reporting device status + private final IBinder mDeviceStatusToken = new Binder(); + private final boolean[] mInputPortOpen; + private final int[] mOutputPortOpenCount; + + private final CloseGuard mGuard = CloseGuard.get(); + private boolean mIsClosed; + + private final Callback mCallback; + + public interface Callback { + /** + * Called to notify when an our device status has changed + * @param server the {@link MidiDeviceServer} that changed + * @param status the {@link MidiDeviceStatus} for the device + */ + public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status); + } + + abstract private class PortClient implements IBinder.DeathRecipient { + final IBinder mToken; - // output ports for receiving messages from our clients - // we can have only one per port number - private MidiOutputPort[] mInputPortSenders; + PortClient(IBinder token) { + mToken = token; - // receivers attached to our input ports - private ArrayList<MidiReceiver>[] mInputPortReceivers; + try { + token.linkToDeath(this, 0); + } catch (RemoteException e) { + close(); + } + } - // input ports for sending messages to our clients - // we can have multiple outputs per port number - private ArrayList<MidiInputPort>[] mOutputPortReceivers; + abstract void close(); - // subclass of MidiInputPort for passing to clients - // that notifies us when the connection has failed - private class ServerInputPort extends MidiInputPort { - ServerInputPort(ParcelFileDescriptor pfd, int portNumber) { - super(pfd, portNumber); + @Override + public void binderDied() { + close(); + } + } + + private class InputPortClient extends PortClient { + private final MidiOutputPort mOutputPort; + + InputPortClient(IBinder token, MidiOutputPort outputPort) { + super(token); + mOutputPort = outputPort; } @Override - public void onIOException() { - synchronized (mOutputPortReceivers) { - mOutputPortReceivers[getPortNumber()].clear(); + void close() { + mToken.unlinkToDeath(this, 0); + synchronized (mInputPortOutputPorts) { + int portNumber = mOutputPort.getPortNumber(); + mInputPortOutputPorts[portNumber] = null; + mInputPortOpen[portNumber] = false; + updateDeviceStatus(); } + IoUtils.closeQuietly(mOutputPort); } } - // subclass of MidiOutputPort for passing to clients - // that notifies us when the connection has failed - private class ServerOutputPort extends MidiOutputPort { - ServerOutputPort(ParcelFileDescriptor pfd, int portNumber) { - super(pfd, portNumber); + private class OutputPortClient extends PortClient { + private final MidiInputPort mInputPort; + + OutputPortClient(IBinder token, MidiInputPort inputPort) { + super(token); + mInputPort = inputPort; } @Override - public void onIOException() { - synchronized (mInputPortSenders) { - mInputPortSenders[getPortNumber()] = null; - } + void close() { + mToken.unlinkToDeath(this, 0); + int portNumber = mInputPort.getPortNumber(); + MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber]; + synchronized (dispatcher) { + dispatcher.getSender().disconnect(mInputPort); + int openCount = dispatcher.getReceiverCount(); + mOutputPortOpenCount[portNumber] = openCount; + updateDeviceStatus(); + } + + mInputPorts.remove(mInputPort); + IoUtils.closeQuietly(mInputPort); } } + private final HashMap<IBinder, PortClient> mPortClients = new HashMap<IBinder, PortClient>(); + // Binder interface stub for receiving connection requests from clients private final IMidiDeviceServer mServer = new IMidiDeviceServer.Stub() { @Override - public ParcelFileDescriptor openInputPort(int portNumber) { + public ParcelFileDescriptor openInputPort(IBinder token, int portNumber) { + if (mDeviceInfo.isPrivate()) { + if (Binder.getCallingUid() != Process.myUid()) { + throw new SecurityException("Can't access private device from different UID"); + } + } + if (portNumber < 0 || portNumber >= mInputPortCount) { Log.e(TAG, "portNumber out of range in openInputPort: " + portNumber); return null; } - ParcelFileDescriptor result = null; - - synchronized (mInputPortSenders) { - if (mInputPortSenders[portNumber] != null) { + synchronized (mInputPortOutputPorts) { + if (mInputPortOutputPorts[portNumber] != null) { Log.d(TAG, "port " + portNumber + " already open"); return null; } @@ -105,47 +175,102 @@ public final class MidiDeviceServer implements Closeable { try { ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair( OsConstants.SOCK_SEQPACKET); - MidiOutputPort newOutputPort = new ServerOutputPort(pair[0], portNumber); - mInputPortSenders[portNumber] = newOutputPort; - result = pair[1]; - - ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumber]; - synchronized (receivers) { - for (int i = 0; i < receivers.size(); i++) { - newOutputPort.connect(receivers.get(i)); - } + MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber); + mInputPortOutputPorts[portNumber] = outputPort; + outputPort.connect(mInputPortReceivers[portNumber]); + InputPortClient client = new InputPortClient(token, outputPort); + synchronized (mPortClients) { + mPortClients.put(token, client); } + mInputPortOpen[portNumber] = true; + updateDeviceStatus(); + return pair[1]; } catch (IOException e) { Log.e(TAG, "unable to create ParcelFileDescriptors in openInputPort"); return null; } } - - return result; } @Override - public ParcelFileDescriptor openOutputPort(int portNumber) { + public ParcelFileDescriptor openOutputPort(IBinder token, int portNumber) { + if (mDeviceInfo.isPrivate()) { + if (Binder.getCallingUid() != Process.myUid()) { + throw new SecurityException("Can't access private device from different UID"); + } + } + if (portNumber < 0 || portNumber >= mOutputPortCount) { Log.e(TAG, "portNumber out of range in openOutputPort: " + portNumber); return null; } - synchronized (mOutputPortReceivers) { - try { - ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair( - OsConstants.SOCK_SEQPACKET); - mOutputPortReceivers[portNumber].add(new ServerInputPort(pair[0], portNumber)); - return pair[1]; - } catch (IOException e) { - Log.e(TAG, "unable to create ParcelFileDescriptors in openOutputPort"); - return null; + + try { + ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair( + OsConstants.SOCK_SEQPACKET); + MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber); + MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber]; + synchronized (dispatcher) { + dispatcher.getSender().connect(inputPort); + int openCount = dispatcher.getReceiverCount(); + mOutputPortOpenCount[portNumber] = openCount; + updateDeviceStatus(); + } + + mInputPorts.add(inputPort); + OutputPortClient client = new OutputPortClient(token, inputPort); + synchronized (mPortClients) { + mPortClients.put(token, client); + } + return pair[1]; + } catch (IOException e) { + Log.e(TAG, "unable to create ParcelFileDescriptors in openOutputPort"); + return null; + } + } + + @Override + public void closePort(IBinder token) { + synchronized (mPortClients) { + PortClient client = mPortClients.remove(token); + if (client != null) { + client.close(); } } } + + @Override + public void connectPorts(IBinder token, ParcelFileDescriptor pfd, + int outputPortNumber) { + MidiInputPort inputPort = new MidiInputPort(pfd, outputPortNumber); + mOutputPortDispatchers[outputPortNumber].getSender().connect(inputPort); + mInputPorts.add(inputPort); + OutputPortClient client = new OutputPortClient(token, inputPort); + synchronized (mPortClients) { + mPortClients.put(token, client); + } + } }; - /* package */ MidiDeviceServer(IMidiManager midiManager) { + /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers, + int numOutputPorts, Callback callback) { mMidiManager = midiManager; + mInputPortReceivers = inputPortReceivers; + mInputPortCount = inputPortReceivers.length; + mOutputPortCount = numOutputPorts; + mCallback = callback; + + mInputPortOutputPorts = new MidiOutputPort[mInputPortCount]; + + mOutputPortDispatchers = new MidiDispatcher[numOutputPorts]; + for (int i = 0; i < numOutputPorts; i++) { + mOutputPortDispatchers[i] = new MidiDispatcher(); + } + + mInputPortOpen = new boolean[mInputPortCount]; + mOutputPortOpenCount = new int[numOutputPorts]; + + mGuard.open("close"); } /* package */ IMidiDeviceServer getBinderInterface() { @@ -157,112 +282,70 @@ public final class MidiDeviceServer implements Closeable { throw new IllegalStateException("setDeviceInfo should only be called once"); } mDeviceInfo = deviceInfo; - mInputPortCount = deviceInfo.getInputPortCount(); - mOutputPortCount = deviceInfo.getOutputPortCount(); - mInputPortSenders = new MidiOutputPort[mInputPortCount]; + } - mInputPortReceivers = new ArrayList[mInputPortCount]; - for (int i = 0; i < mInputPortCount; i++) { - mInputPortReceivers[i] = new ArrayList<MidiReceiver>(); - } + private void updateDeviceStatus() { + // clear calling identity, since we may be in a Binder call from one of our clients + long identityToken = Binder.clearCallingIdentity(); - mOutputPortReceivers = new ArrayList[mOutputPortCount]; - for (int i = 0; i < mOutputPortCount; i++) { - mOutputPortReceivers[i] = new ArrayList<MidiInputPort>(); + MidiDeviceStatus status = new MidiDeviceStatus(mDeviceInfo, mInputPortOpen, + mOutputPortOpenCount); + if (mCallback != null) { + mCallback.onDeviceStatusChanged(this, status); } - } - - @Override - public void close() throws IOException { try { - // FIXME - close input and output ports too? - mMidiManager.unregisterDeviceServer(mServer); + mMidiManager.setDeviceStatus(mDeviceStatusToken, status); } catch (RemoteException e) { - Log.e(TAG, "RemoteException in unregisterDeviceServer"); + Log.e(TAG, "RemoteException in updateDeviceStatus"); + } finally { + Binder.restoreCallingIdentity(identityToken); } } - /** - * Returns a {@link MidiDeviceInfo} object, which describes this device. - * - * @return the {@link MidiDeviceInfo} object - */ - public MidiDeviceInfo getInfo() { - return mDeviceInfo; - } - - /** - * Called to open a {@link MidiSender} to allow receiving MIDI messages - * on the device's input port for the specified port number. - * - * @param portNumber the number of the input port - * @return the {@link MidiSender} - */ - public MidiSender openInputPortSender(int portNumber) { - if (portNumber < 0 || portNumber >= mDeviceInfo.getInputPortCount()) { - throw new IllegalArgumentException("portNumber " + portNumber + " out of range"); - } - final int portNumberF = portNumber; - return new MidiSender() { - - @Override - public void connect(MidiReceiver receiver) { - // We always synchronize on mInputPortSenders before receivers if we need to - // synchronize on both. - synchronized (mInputPortSenders) { - ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF]; - synchronized (receivers) { - receivers.add(receiver); - MidiOutputPort outputPort = mInputPortSenders[portNumberF]; - if (outputPort != null) { - outputPort.connect(receiver); - } - } + @Override + public void close() throws IOException { + synchronized (mGuard) { + if (mIsClosed) return; + mGuard.close(); + + for (int i = 0; i < mInputPortCount; i++) { + MidiOutputPort outputPort = mInputPortOutputPorts[i]; + if (outputPort != null) { + IoUtils.closeQuietly(outputPort); + mInputPortOutputPorts[i] = null; } } - - @Override - public void disconnect(MidiReceiver receiver) { - // We always synchronize on mInputPortSenders before receivers if we need to - // synchronize on both. - synchronized (mInputPortSenders) { - ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF]; - synchronized (receivers) { - receivers.remove(receiver); - MidiOutputPort outputPort = mInputPortSenders[portNumberF]; - if (outputPort != null) { - outputPort.disconnect(receiver); - } - } - } + for (MidiInputPort inputPort : mInputPorts) { + IoUtils.closeQuietly(inputPort); + } + mInputPorts.clear(); + try { + mMidiManager.unregisterDeviceServer(mServer); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in unregisterDeviceServer"); } - }; + mIsClosed = true; + } + } + + @Override + protected void finalize() throws Throwable { + try { + mGuard.warnIfOpen(); + close(); + } finally { + super.finalize(); + } } /** - * Called to open a {@link MidiReceiver} to allow sending MIDI messages - * on the virtual device's output port for the specified port number. - * - * @param portNumber the number of the output port - * @return the {@link MidiReceiver} + * Returns an array of {@link MidiReceiver} for the device's output ports. + * Clients can use these receivers to send data out the device's output ports. + * @return array of MidiReceivers */ - public MidiReceiver openOutputPortReceiver(int portNumber) { - if (portNumber < 0 || portNumber >= mDeviceInfo.getOutputPortCount()) { - throw new IllegalArgumentException("portNumber " + portNumber + " out of range"); - } - final int portNumberF = portNumber; - return new MidiReceiver() { - - @Override - public void post(byte[] msg, int offset, int count, long timestamp) throws IOException { - ArrayList<MidiInputPort> receivers = mOutputPortReceivers[portNumberF]; - synchronized (receivers) { - for (int i = 0; i < receivers.size(); i++) { - // FIXME catch errors and remove dead ones - receivers.get(i).post(msg, offset, count, timestamp); - } - } - } - }; + public MidiReceiver[] getOutputPortReceivers() { + MidiReceiver[] receivers = new MidiReceiver[mOutputPortCount]; + System.arraycopy(mOutputPortDispatchers, 0, receivers, 0, mOutputPortCount); + return receivers; } } diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java new file mode 100644 index 0000000..8b1de3e --- /dev/null +++ b/media/java/android/media/midi/MidiDeviceService.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 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.midi; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +/** + * A service that implements a virtual MIDI device. + * Subclasses must implement the {@link #onGetInputPortReceivers} method to provide a + * list of {@link MidiReceiver}s to receive data sent to the device's input ports. + * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list + * of {@link MidiReceiver}s for sending data out the output ports. + * + * <p>To extend this class, you must declare the service in your manifest file with + * an intent filter with the {@link #SERVICE_INTERFACE} action + * and meta-data to describe the virtual device. + For example:</p> + * <pre> + * <service android:name=".VirtualDeviceService" + * android:label="@string/service_name"> + * <intent-filter> + * <action android:name="android.media.midi.MidiDeviceService" /> + * </intent-filter> + * <meta-data android:name="android.media.midi.MidiDeviceService" + android:resource="@xml/device_info" /> + * </service></pre> + */ +abstract public class MidiDeviceService extends Service { + private static final String TAG = "MidiDeviceService"; + + public static final String SERVICE_INTERFACE = "android.media.midi.MidiDeviceService"; + + private IMidiManager mMidiManager; + private MidiDeviceServer mServer; + private MidiDeviceInfo mDeviceInfo; + + private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { + @Override + public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { + MidiDeviceService.this.onDeviceStatusChanged(status); + } + }; + + @Override + public void onCreate() { + mMidiManager = IMidiManager.Stub.asInterface( + ServiceManager.getService(Context.MIDI_SERVICE)); + MidiDeviceServer server; + try { + MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(), + this.getClass().getName()); + if (deviceInfo == null) { + Log.e(TAG, "Could not find MidiDeviceInfo for MidiDeviceService " + this); + return; + } + mDeviceInfo = deviceInfo; + MidiReceiver[] inputPortReceivers = onGetInputPortReceivers(); + if (inputPortReceivers == null) { + inputPortReceivers = new MidiReceiver[0]; + } + server = new MidiDeviceServer(mMidiManager, inputPortReceivers, + deviceInfo.getOutputPortCount(), mCallback); + server.setDeviceInfo(deviceInfo); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo"); + server = null; + } + mServer = server; + } + + /** + * Returns an array of {@link MidiReceiver} for the device's input ports. + * Subclasses must override this to provide the receivers which will receive + * data sent to the device's input ports. An empty array or null should be returned if + * the device has no input ports. + * @return array of MidiReceivers + */ + abstract public MidiReceiver[] onGetInputPortReceivers(); + + /** + * Returns an array of {@link MidiReceiver} for the device's output ports. + * These can be used to send data out the device's output ports. + * @return array of MidiReceivers + */ + public final MidiReceiver[] getOutputPortReceivers() { + if (mServer == null) { + return null; + } else { + return mServer.getOutputPortReceivers(); + } + } + + /** + * returns the {@link MidiDeviceInfo} instance for this service + * @return our MidiDeviceInfo + */ + public final MidiDeviceInfo getDeviceInfo() { + return mDeviceInfo; + } + + /** + * Called to notify when an our {@link MidiDeviceStatus} has changed + * @param status the number of the port that was opened + */ + public void onDeviceStatusChanged(MidiDeviceStatus status) { + } + + @Override + public IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) { + return mServer.getBinderInterface().asBinder(); + } else { + return null; + } + } +} diff --git a/media/java/android/media/midi/MidiDeviceStatus.aidl b/media/java/android/media/midi/MidiDeviceStatus.aidl new file mode 100644 index 0000000..1a848c0 --- /dev/null +++ b/media/java/android/media/midi/MidiDeviceStatus.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015, 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.midi; + +parcelable MidiDeviceStatus; diff --git a/media/java/android/media/midi/MidiDeviceStatus.java b/media/java/android/media/midi/MidiDeviceStatus.java new file mode 100644 index 0000000..7522dcf --- /dev/null +++ b/media/java/android/media/midi/MidiDeviceStatus.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015 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.midi; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This is an immutable class that describes the current status of a MIDI device's ports. + */ +public final class MidiDeviceStatus implements Parcelable { + + private static final String TAG = "MidiDeviceStatus"; + + private final MidiDeviceInfo mDeviceInfo; + // true if input ports are open + private final boolean mInputPortOpen[]; + // open counts for output ports + private final int mOutputPortOpenCount[]; + + /** + * @hide + */ + public MidiDeviceStatus(MidiDeviceInfo deviceInfo, boolean inputPortOpen[], + int outputPortOpenCount[]) { + // MidiDeviceInfo is immutable so we can share references + mDeviceInfo = deviceInfo; + + // make copies of the arrays + mInputPortOpen = new boolean[inputPortOpen.length]; + System.arraycopy(inputPortOpen, 0, mInputPortOpen, 0, inputPortOpen.length); + mOutputPortOpenCount = new int[outputPortOpenCount.length]; + System.arraycopy(outputPortOpenCount, 0, mOutputPortOpenCount, 0, + outputPortOpenCount.length); + } + + /** + * Creates a MidiDeviceStatus with zero for all port open counts + * @hide + */ + public MidiDeviceStatus(MidiDeviceInfo deviceInfo) { + mDeviceInfo = deviceInfo; + mInputPortOpen = new boolean[deviceInfo.getInputPortCount()]; + mOutputPortOpenCount = new int[deviceInfo.getOutputPortCount()]; + } + + /** + * Returns the {@link MidiDeviceInfo} of the device. + * + * @return the device info + */ + public MidiDeviceInfo getDeviceInfo() { + return mDeviceInfo; + } + + /** + * Returns true if an input port is open. + * + * @param portNumber the input port's port number + * @return input port open status + */ + public boolean isInputPortOpen(int portNumber) { + return mInputPortOpen[portNumber]; + } + + /** + * Returns the open count for an output port. + * + * @param portNumber the output port's port number + * @return output port open count + */ + public int getOutputPortOpenCount(int portNumber) { + return mOutputPortOpenCount[portNumber]; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(mDeviceInfo.toString()); + int inputPortCount = mDeviceInfo.getInputPortCount(); + int outputPortCount = mDeviceInfo.getOutputPortCount(); + builder.append(" mInputPortOpen=["); + for (int i = 0; i < inputPortCount; i++) { + builder.append(mInputPortOpen[i]); + if (i < inputPortCount -1) { + builder.append(","); + } + } + builder.append("] mOutputPortOpenCount=["); + for (int i = 0; i < outputPortCount; i++) { + builder.append(mOutputPortOpenCount[i]); + if (i < outputPortCount -1) { + builder.append(","); + } + } + builder.append("]"); + return builder.toString(); + } + + public static final Parcelable.Creator<MidiDeviceStatus> CREATOR = + new Parcelable.Creator<MidiDeviceStatus>() { + public MidiDeviceStatus createFromParcel(Parcel in) { + ClassLoader classLoader = MidiDeviceInfo.class.getClassLoader(); + MidiDeviceInfo deviceInfo = in.readParcelable(classLoader); + boolean[] inputPortOpen = in.createBooleanArray(); + int[] outputPortOpenCount = in.createIntArray(); + return new MidiDeviceStatus(deviceInfo, inputPortOpen, outputPortOpenCount); + } + + public MidiDeviceStatus[] newArray(int size) { + return new MidiDeviceStatus[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mDeviceInfo, flags); + parcel.writeBooleanArray(mInputPortOpen); + parcel.writeIntArray(mOutputPortOpenCount); + } +} diff --git a/media/java/android/media/midi/MidiDispatcher.java b/media/java/android/media/midi/MidiDispatcher.java new file mode 100644 index 0000000..0868346 --- /dev/null +++ b/media/java/android/media/midi/MidiDispatcher.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 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.midi; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s. + * This class subclasses {@link MidiReceiver} and dispatches any data it receives + * to its receiver list. Any receivers that throw an exception upon receiving data will + * be automatically removed from the receiver list, but no IOException will be returned + * from the dispatcher's {@link #onReceive} in that case. + * + * @hide + */ +public final class MidiDispatcher extends MidiReceiver { + + private final CopyOnWriteArrayList<MidiReceiver> mReceivers + = new CopyOnWriteArrayList<MidiReceiver>(); + + private final MidiSender mSender = new MidiSender() { + /** + * Called to connect a {@link MidiReceiver} to the sender + * + * @param receiver the receiver to connect + */ + public void connect(MidiReceiver receiver) { + mReceivers.add(receiver); + } + + /** + * Called to disconnect a {@link MidiReceiver} from the sender + * + * @param receiver the receiver to disconnect + */ + public void disconnect(MidiReceiver receiver) { + mReceivers.remove(receiver); + } + }; + + /** + * Returns the number of {@link MidiReceiver}s this dispatcher contains. + * @return the number of receivers + */ + public int getReceiverCount() { + return mReceivers.size(); + } + + /** + * Returns a {@link MidiSender} which is used to add and remove {@link MidiReceiver}s + * to the dispatcher's receiver list. + * @return the dispatcher's MidiSender + */ + public MidiSender getSender() { + return mSender; + } + + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) throws IOException { + for (MidiReceiver receiver : mReceivers) { + try { + receiver.sendWithTimestamp(msg, offset, count, timestamp); + } catch (IOException e) { + // if the receiver fails we remove the receiver but do not propagate the exception + mReceivers.remove(receiver); + } + } + } +} diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java index 730d364..1d3b37a 100644 --- a/media/java/android/media/midi/MidiInputPort.java +++ b/media/java/android/media/midi/MidiInputPort.java @@ -16,65 +16,131 @@ package android.media.midi; +import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import dalvik.system.CloseGuard; import libcore.io.IoUtils; +import java.io.Closeable; import java.io.FileOutputStream; import java.io.IOException; /** * This class is used for sending data to a port on a MIDI device - * - * CANDIDATE FOR PUBLIC API - * @hide */ -public class MidiInputPort extends MidiPort implements MidiReceiver { +public final class MidiInputPort extends MidiReceiver implements Closeable { + private static final String TAG = "MidiInputPort"; + + private IMidiDeviceServer mDeviceServer; + private final IBinder mToken; + private final int mPortNumber; + private ParcelFileDescriptor mParcelFileDescriptor; + private FileOutputStream mOutputStream; + + private final CloseGuard mGuard = CloseGuard.get(); + private boolean mIsClosed; - private final FileOutputStream mOutputStream; + // buffer to use for sending data out our output stream + private final byte[] mBuffer = new byte[MidiPortImpl.MAX_PACKET_SIZE]; - // buffer to use for sending messages out our output stream - private final byte[] mBuffer = new byte[MAX_PACKET_SIZE]; + /* package */ MidiInputPort(IMidiDeviceServer server, IBinder token, + ParcelFileDescriptor pfd, int portNumber) { + mDeviceServer = server; + mToken = token; + mParcelFileDescriptor = pfd; + mPortNumber = portNumber; + mOutputStream = new FileOutputStream(pfd.getFileDescriptor()); + mGuard.open("close"); + } - /* package */ MidiInputPort(ParcelFileDescriptor pfd, int portNumber) { - super(portNumber); - mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd); + /* package */ MidiInputPort(ParcelFileDescriptor pfd, int portNumber) { + this(null, null, pfd, portNumber); } /** - * Writes a MIDI message to the input port + * Returns the port number of this port * - * @param msg byte array containing the message - * @param offset offset of first byte of the message in msg byte array - * @param count size of the message in bytes - * @param timestamp future time to post the message (based on - * {@link java.lang.System#nanoTime} + * @return the port's port number */ - public void post(byte[] msg, int offset, int count, long timestamp) throws IOException { - assert(offset >= 0 && count >= 0 && offset + count <= msg.length); + public final int getPortNumber() { + return mPortNumber; + } + + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) throws IOException { + if (offset < 0 || count < 0 || offset + count > msg.length) { + throw new IllegalArgumentException("offset or count out of range"); + } + if (count > MidiPortImpl.MAX_PACKET_DATA_SIZE) { + throw new IllegalArgumentException("count exceeds max message size"); + } synchronized (mBuffer) { - try { - while (count > 0) { - int length = packMessage(msg, offset, count, timestamp, mBuffer); - mOutputStream.write(mBuffer, 0, length); - int sent = getMessageSize(mBuffer, length); - assert(sent >= 0 && sent <= length); - - offset += sent; - count -= sent; - } - } catch (IOException e) { + if (mOutputStream == null) { + throw new IOException("MidiInputPort is closed"); + } + int length = MidiPortImpl.packMessage(msg, offset, count, timestamp, mBuffer); + mOutputStream.write(mBuffer, 0, length); + } + } + + // used by MidiDevice.connectInputPort() to connect our socket directly to another device + /* package */ ParcelFileDescriptor claimFileDescriptor() { + synchronized (mBuffer) { + ParcelFileDescriptor pfd = mParcelFileDescriptor; + if (pfd != null) { IoUtils.closeQuietly(mOutputStream); - // report I/O failure - onIOException(); - throw e; + mParcelFileDescriptor = null; + mOutputStream = null; } + return pfd; } } @Override + public int getMaxMessageSize() { + return MidiPortImpl.MAX_PACKET_DATA_SIZE; + } + + @Override public void close() throws IOException { - mOutputStream.close(); + synchronized (mGuard) { + if (mIsClosed) return; + mGuard.close(); + synchronized (mBuffer) { + if (mParcelFileDescriptor != null) { + mParcelFileDescriptor.close(); + mParcelFileDescriptor = null; + } + if (mOutputStream != null) { + mOutputStream.close(); + mOutputStream = null; + } + } + if (mDeviceServer != null) { + try { + mDeviceServer.closePort(mToken); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MidiInputPort.close()"); + } + } + mIsClosed = true; + } + } + + @Override + protected void finalize() throws Throwable { + try { + mGuard.warnIfOpen(); + // not safe to make binder calls from finalize() + mDeviceServer = null; + close(); + } finally { + super.finalize(); + } } } diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index 410120d..1b98ca5 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -16,7 +16,11 @@ package android.media.midi; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ServiceInfo; import android.os.Binder; import android.os.IBinder; import android.os.Bundle; @@ -24,7 +28,6 @@ import android.os.Handler; import android.os.RemoteException; import android.util.Log; -import java.io.IOException; import java.util.HashMap; /** @@ -39,7 +42,7 @@ import java.util.HashMap; * CANDIDATE FOR PUBLIC API * @hide */ -public class MidiManager { +public final class MidiManager { private static final String TAG = "MidiManager"; private final Context mContext; @@ -50,7 +53,7 @@ public class MidiManager { new HashMap<DeviceCallback,DeviceListener>(); // Binder stub for receiving device notifications from MidiService - private class DeviceListener extends IMidiListener.Stub { + private class DeviceListener extends IMidiDeviceListener.Stub { private final DeviceCallback mCallback; private final Handler mHandler; @@ -59,6 +62,7 @@ public class MidiManager { mHandler = handler; } + @Override public void onDeviceAdded(MidiDeviceInfo device) { if (mHandler != null) { final MidiDeviceInfo deviceF = device; @@ -72,6 +76,7 @@ public class MidiManager { } } + @Override public void onDeviceRemoved(MidiDeviceInfo device) { if (mHandler != null) { final MidiDeviceInfo deviceF = device; @@ -84,6 +89,20 @@ public class MidiManager { mCallback.onDeviceRemoved(device); } } + + @Override + public void onDeviceStatusChanged(MidiDeviceStatus status) { + if (mHandler != null) { + final MidiDeviceStatus statusF = status; + mHandler.post(new Runnable() { + @Override public void run() { + mCallback.onDeviceStatusChanged(statusF); + } + }); + } else { + mCallback.onDeviceStatusChanged(status); + } + } } /** @@ -105,6 +124,27 @@ public class MidiManager { */ public void onDeviceRemoved(MidiDeviceInfo device) { } + + /** + * Called to notify when the status of a MIDI device has changed + * + * @param device a {@link MidiDeviceStatus} for the changed device + */ + public void onDeviceStatusChanged(MidiDeviceStatus status) { + } + } + + /** + * Callback class used for receiving the results of {@link #openDevice} + */ + abstract public static class DeviceOpenCallback { + /** + * Called to respond to a {@link #openDevice} request + * + * @param deviceInfo the {@link MidiDeviceInfo} for the device to open + * @param device a {@link MidiDevice} for opened device, or null if opening failed + */ + abstract public void onDeviceOpened(MidiDeviceInfo deviceInfo, MidiDevice device); } /** @@ -164,33 +204,87 @@ public class MidiManager { } } + private void sendOpenDeviceResponse(final MidiDeviceInfo deviceInfo, final MidiDevice device, + final DeviceOpenCallback callback, Handler handler) { + if (handler != null) { + handler.post(new Runnable() { + @Override public void run() { + callback.onDeviceOpened(deviceInfo, device); + } + }); + } else { + callback.onDeviceOpened(deviceInfo, device); + } + } + /** * Opens a MIDI device for reading and writing. * * @param deviceInfo a {@link android.media.midi.MidiDeviceInfo} to open - * @return a {@link MidiDevice} object for the device + * @param callback a {@link #DeviceOpenCallback} to be called to receive the result + * @param handler the {@link android.os.Handler Handler} that will be used for delivering + * the result. If handler is null, then the thread used for the + * callback is unspecified. */ - public MidiDevice openDevice(MidiDeviceInfo deviceInfo) { + public void openDevice(MidiDeviceInfo deviceInfo, DeviceOpenCallback callback, + Handler handler) { + MidiDevice device = null; try { IMidiDeviceServer server = mService.openDevice(mToken, deviceInfo); if (server == null) { - Log.e(TAG, "could not open device " + deviceInfo); - return null; + ServiceInfo serviceInfo = (ServiceInfo)deviceInfo.getProperties().getParcelable( + MidiDeviceInfo.PROPERTY_SERVICE_INFO); + if (serviceInfo == null) { + Log.e(TAG, "no ServiceInfo for " + deviceInfo); + } else { + Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE); + intent.setComponent(new ComponentName(serviceInfo.packageName, + serviceInfo.name)); + final MidiDeviceInfo deviceInfoF = deviceInfo; + final DeviceOpenCallback callbackF = callback; + final Handler handlerF = handler; + if (mContext.bindService(intent, + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + IMidiDeviceServer server = + IMidiDeviceServer.Stub.asInterface(binder); + MidiDevice device = new MidiDevice(deviceInfoF, server, mContext, this); + sendOpenDeviceResponse(deviceInfoF, device, callbackF, handlerF); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // FIXME - anything to do here? + } + }, + Context.BIND_AUTO_CREATE)) + { + // return immediately to avoid calling sendOpenDeviceResponse below + return; + } else { + Log.e(TAG, "Unable to bind service: " + intent); + } + } + } else { + device = new MidiDevice(deviceInfo, server); } - return new MidiDevice(deviceInfo, server); } catch (RemoteException e) { Log.e(TAG, "RemoteException in openDevice"); } - return null; + sendOpenDeviceResponse(deviceInfo, device, callback, handler); } /** @hide */ - public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts, - Bundle properties, boolean isPrivate, int type) { + public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers, + int numOutputPorts, String[] inputPortNames, String[] outputPortNames, + Bundle properties, int type, MidiDeviceServer.Callback callback) { try { - MidiDeviceServer server = new MidiDeviceServer(mService); + MidiDeviceServer server = new MidiDeviceServer(mService, inputPortReceivers, + numOutputPorts, callback); MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(), - numInputPorts, numOutputPorts, properties, isPrivate, type); + inputPortReceivers.length, numOutputPorts, inputPortNames, outputPortNames, + properties, type); if (deviceInfo == null) { Log.e(TAG, "registerVirtualDevice failed"); return null; @@ -202,21 +296,4 @@ public class MidiManager { return null; } } - - /** - * Creates a new MIDI virtual device. - * - * @param numInputPorts number of input ports for the virtual device - * @param numOutputPorts number of output ports for the virtual device - * @param properties a {@link android.os.Bundle} containing properties describing the device - * @param isPrivate true if this device should only be visible and accessible to apps - * with the same UID as the caller - * @return a {@link MidiDeviceServer} object to locally represent the device - */ - public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts, - Bundle properties, boolean isPrivate) { - return createDeviceServer(numInputPorts, numOutputPorts, properties, - isPrivate, MidiDeviceInfo.TYPE_VIRTUAL); - } - } diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java index 83ddeeb..b8ed36f 100644 --- a/media/java/android/media/midi/MidiOutputPort.java +++ b/media/java/android/media/midi/MidiOutputPort.java @@ -16,38 +16,40 @@ package android.media.midi; +import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.util.Log; +import dalvik.system.CloseGuard; + import libcore.io.IoUtils; +import java.io.Closeable; import java.io.FileInputStream; import java.io.IOException; -import java.util.ArrayList; /** * This class is used for receiving data from a port on a MIDI device - * - * CANDIDATE FOR PUBLIC API - * @hide */ -public class MidiOutputPort extends MidiPort implements MidiSender { +public final class MidiOutputPort extends MidiSender implements Closeable { private static final String TAG = "MidiOutputPort"; + private IMidiDeviceServer mDeviceServer; + private final IBinder mToken; + private final int mPortNumber; private final FileInputStream mInputStream; + private final MidiDispatcher mDispatcher = new MidiDispatcher(); - // array of receiver lists, indexed by port number - private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>(); - - private int mReceiverCount; // total number of receivers for all ports + private final CloseGuard mGuard = CloseGuard.get(); + private boolean mIsClosed; // This thread reads MIDI events from a socket and distributes them to the list of // MidiReceivers attached to this device. private final Thread mThread = new Thread() { @Override public void run() { - byte[] buffer = new byte[MAX_PACKET_SIZE]; - ArrayList<MidiReceiver> deadReceivers = new ArrayList<MidiReceiver>(); + byte[] buffer = new byte[MidiPortImpl.MAX_PACKET_SIZE]; try { while (true) { @@ -55,81 +57,85 @@ public class MidiOutputPort extends MidiPort implements MidiSender { int count = mInputStream.read(buffer); if (count < 0) { break; + // FIXME - inform receivers here? } - int offset = getMessageOffset(buffer, count); - int size = getMessageSize(buffer, count); - long timestamp = getMessageTimeStamp(buffer, count); - - synchronized (mReceivers) { - for (int i = 0; i < mReceivers.size(); i++) { - MidiReceiver receiver = mReceivers.get(i); - try { - receiver.post(buffer, offset, size, timestamp); - } catch (IOException e) { - Log.e(TAG, "post failed"); - deadReceivers.add(receiver); - } - } - // remove any receivers that failed - if (deadReceivers.size() > 0) { - for (MidiReceiver receiver: deadReceivers) { - mReceivers.remove(receiver); - mReceiverCount--; - } - deadReceivers.clear(); - } - // exit if we have no receivers left - if (mReceiverCount == 0) { - break; - } - } + int offset = MidiPortImpl.getMessageOffset(buffer, count); + int size = MidiPortImpl.getMessageSize(buffer, count); + long timestamp = MidiPortImpl.getMessageTimeStamp(buffer, count); + + // dispatch to all our receivers + mDispatcher.sendWithTimestamp(buffer, offset, size, timestamp); } } catch (IOException e) { - // report I/O failure + // FIXME report I/O failure? Log.e(TAG, "read failed"); } finally { IoUtils.closeQuietly(mInputStream); - onIOException(); } } }; - /* package */ MidiOutputPort(ParcelFileDescriptor pfd, int portNumber) { - super(portNumber); + /* package */ MidiOutputPort(IMidiDeviceServer server, IBinder token, + ParcelFileDescriptor pfd, int portNumber) { + mDeviceServer = server; + mToken = token; + mPortNumber = portNumber; mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + mThread.start(); + mGuard.open("close"); + } + + /* package */ MidiOutputPort(ParcelFileDescriptor pfd, int portNumber) { + this(null, null, pfd, portNumber); } /** - * Connects a {@link MidiReceiver} to the output port to allow receiving - * MIDI messages from the port. + * Returns the port number of this port * - * @param receiver the receiver to connect + * @return the port's port number */ + public final int getPortNumber() { + return mPortNumber; + } + + @Override public void connect(MidiReceiver receiver) { - synchronized (mReceivers) { - mReceivers.add(receiver); - if (mReceiverCount++ == 0) { - mThread.start(); - } - } + mDispatcher.getSender().connect(receiver); } - /** - * Disconnects a {@link MidiReceiver} from the output port. - * - * @param receiver the receiver to connect - */ + @Override public void disconnect(MidiReceiver receiver) { - synchronized (mReceivers) { - if (mReceivers.remove(receiver)) { - mReceiverCount--; + mDispatcher.getSender().disconnect(receiver); + } + + @Override + public void close() throws IOException { + synchronized (mGuard) { + if (mIsClosed) return; + + mGuard.close(); + mInputStream.close(); + if (mDeviceServer != null) { + try { + mDeviceServer.closePort(mToken); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in MidiOutputPort.close()"); + } } + mIsClosed = true; } } @Override - public void close() throws IOException { - mInputStream.close(); + protected void finalize() throws Throwable { + try { + mGuard.warnIfOpen(); + // not safe to make binder calls from finalize() + mDeviceServer = null; + close(); + } finally { + super.finalize(); + } } } diff --git a/media/java/android/media/midi/MidiPort.java b/media/java/android/media/midi/MidiPortImpl.java index 4d3c91d..5795045 100644 --- a/media/java/android/media/midi/MidiPort.java +++ b/media/java/android/media/midi/MidiPortImpl.java @@ -16,33 +16,20 @@ package android.media.midi; -import android.util.Log; - -import java.io.Closeable; - /** - * This class represents a MIDI input or output port. - * Base class for {@link MidiInputPort} and {@link MidiOutputPort} - * - * CANDIDATE FOR PUBLIC API - * @hide + * This class contains utilities for socket communication between a + * MidiInputPort and MidiOutputPort */ -abstract public class MidiPort implements Closeable { +/* package */ class MidiPortImpl { private static final String TAG = "MidiPort"; - private final int mPortNumber; - /** * Maximum size of a packet that can pass through our ParcelFileDescriptor. - * For internal use only. Implementation details may change in the future. - * @hide */ public static final int MAX_PACKET_SIZE = 1024; /** * size of message timestamp in bytes - * For internal use only. Implementation details may change in the future. - * @hide */ private static final int TIMESTAMP_SIZE = 8; @@ -51,29 +38,6 @@ abstract public class MidiPort implements Closeable { */ public static final int MAX_PACKET_DATA_SIZE = MAX_PACKET_SIZE - TIMESTAMP_SIZE; - - /* package */ MidiPort(int portNumber) { - mPortNumber = portNumber; - } - - /** - * Returns the port number of this port - * - * @return the port's port number - */ - public final int getPortNumber() { - return mPortNumber; - } - - /** - * Called when an IOExeption occurs while sending or receiving data. - * Subclasses can override to be notified of such errors - * - * @hide - */ - public void onIOException() { - } - /** * Utility function for packing a MIDI message to be sent through our ParcelFileDescriptor * @@ -82,9 +46,6 @@ abstract public class MidiPort implements Closeable { * timestamp is message timestamp to pack * dest is buffer to pack into * returns size of packed message - * - * For internal use only. Implementation details may change in the future. - * @hide */ public static int packMessage(byte[] message, int offset, int size, long timestamp, byte[] dest) { @@ -106,9 +67,6 @@ abstract public class MidiPort implements Closeable { /** * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor * returns the offset of the MIDI message in packed buffer - * - * For internal use only. Implementation details may change in the future. - * @hide */ public static int getMessageOffset(byte[] buffer, int bufferLength) { // message is at the beginning @@ -118,9 +76,6 @@ abstract public class MidiPort implements Closeable { /** * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor * returns size of MIDI data in packed buffer - * - * For internal use only. Implementation details may change in the future. - * @hide */ public static int getMessageSize(byte[] buffer, int bufferLength) { // message length is total buffer length minus size of the timestamp @@ -130,9 +85,6 @@ abstract public class MidiPort implements Closeable { /** * Utility function for unpacking a MIDI message received from our ParcelFileDescriptor * unpacks timestamp from packed buffer - * - * For internal use only. Implementation details may change in the future. - * @hide */ public static long getMessageTimeStamp(byte[] buffer, int bufferLength) { // timestamp is at end of the packet diff --git a/media/java/android/media/midi/MidiReceiver.java b/media/java/android/media/midi/MidiReceiver.java index 64c0c07..6f4c266 100644 --- a/media/java/android/media/midi/MidiReceiver.java +++ b/media/java/android/media/midi/MidiReceiver.java @@ -20,25 +20,69 @@ import java.io.IOException; /** * Interface for sending and receiving data to and from a MIDI device. - * - * CANDIDATE FOR PUBLIC API - * @hide */ -public interface MidiReceiver { +abstract public class MidiReceiver { /** * Called to pass MIDI data to the receiver. + * May fail if count exceeds {@link #getMaxMessageSize}. * * NOTE: the msg array parameter is only valid within the context of this call. * The msg bytes should be copied by the receiver rather than retaining a reference * to this parameter. * Also, modifying the contents of the msg array parameter may result in other receivers - * in the same application receiving incorrect values in their post() method. + * in the same application receiving incorrect values in their {link #onReceive} method. + * + * @param msg a byte array containing the MIDI data + * @param offset the offset of the first byte of the data in the array to be processed + * @param count the number of bytes of MIDI data in the array to be processed + * @param timestamp the timestamp of the message (based on {@link java.lang.System#nanoTime} + * @throws IOException + */ + abstract public void onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException; + + /** + * Returns the maximum size of a message this receiver can receive. + * Defaults to {@link java.lang.Integer#MAX_VALUE} unless overridden. + * @return maximum message size + */ + public int getMaxMessageSize() { + return Integer.MAX_VALUE; + } + + /** + * Called to send MIDI data to the receiver + * Data will get split into multiple calls to {@link #onReceive} if count exceeds + * {@link #getMaxMessageSize}. + * + * @param msg a byte array containing the MIDI data + * @param offset the offset of the first byte of the data in the array to be sent + * @param count the number of bytes of MIDI data in the array to be sent + * @throws IOException + */ + public void send(byte[] msg, int offset, int count) throws IOException { + sendWithTimestamp(msg, offset, count, System.nanoTime()); + } + + /** + * Called to send MIDI data to the receiver to be handled at a specified time in the future + * Data will get split into multiple calls to {@link #onReceive} if count exceeds + * {@link #getMaxMessageSize}. * * @param msg a byte array containing the MIDI data - * @param offset the offset of the first byte of the data in the byte array - * @param count the number of bytes of MIDI data in the array + * @param offset the offset of the first byte of the data in the array to be sent + * @param count the number of bytes of MIDI data in the array to be sent * @param timestamp the timestamp of the message (based on {@link java.lang.System#nanoTime} * @throws IOException */ - public void post(byte[] msg, int offset, int count, long timestamp) throws IOException; + public void sendWithTimestamp(byte[] msg, int offset, int count, long timestamp) + throws IOException { + int messageSize = getMaxMessageSize(); + while (count > 0) { + int length = (count > messageSize ? messageSize : count); + onReceive(msg, offset, length, timestamp); + offset += length; + count -= length; + } + } } diff --git a/media/java/android/media/midi/MidiSender.java b/media/java/android/media/midi/MidiSender.java index 4550476..f64fc3c 100644 --- a/media/java/android/media/midi/MidiSender.java +++ b/media/java/android/media/midi/MidiSender.java @@ -19,22 +19,19 @@ package android.media.midi; /** * Interface provided by a device to allow attaching * MidiReceivers to a MIDI device. - * - * CANDIDATE FOR PUBLIC API - * @hide */ -public interface MidiSender { +abstract public class MidiSender { /** * Called to connect a {@link MidiReceiver} to the sender * * @param receiver the receiver to connect */ - public void connect(MidiReceiver receiver); + abstract public void connect(MidiReceiver receiver); /** * Called to disconnect a {@link MidiReceiver} from the sender * * @param receiver the receiver to disconnect */ - public void disconnect(MidiReceiver receiver); + abstract public void disconnect(MidiReceiver receiver); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index a6bde1d..e757f09 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -25,7 +25,6 @@ import android.media.AudioRecord; import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index a1cfc35..f4a548b 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -22,7 +22,6 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.media.projection.IMediaProjection; -import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; diff --git a/media/java/android/media/routing/MediaRouteSelector.java b/media/java/android/media/routing/MediaRouteSelector.java index 26a9b1c..0bfc796 100644 --- a/media/java/android/media/routing/MediaRouteSelector.java +++ b/media/java/android/media/routing/MediaRouteSelector.java @@ -19,7 +19,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.media.routing.MediaRouter.RouteFeatures; import android.os.Bundle; -import android.os.IInterface; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 57c291d..cc602c9 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -30,7 +30,6 @@ import android.media.MediaMetadata; import android.media.Rating; import android.media.VolumeProvider; import android.media.routing.MediaRouter; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java index 4ea22f9..c61d7ad 100644 --- a/media/java/android/media/session/MediaSessionLegacyHelper.java +++ b/media/java/android/media/session/MediaSessionLegacyHelper.java @@ -30,12 +30,9 @@ import android.media.MediaMetadata; import android.media.MediaMetadataEditor; import android.media.MediaMetadataRetriever; import android.media.Rating; -import android.media.RemoteControlClient; -import android.media.RemoteControlClient.MetadataEditor; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.view.KeyEvent; @@ -200,17 +197,17 @@ public class MediaSessionLegacyHelper { break; } if (down || up) { - int flags; + int flags = AudioManager.FLAG_FROM_KEY; if (musicOnly) { // This flag is used when the screen is off to only affect // active media - flags = AudioManager.FLAG_ACTIVE_MEDIA_ONLY; + flags |= AudioManager.FLAG_ACTIVE_MEDIA_ONLY; } else { // These flags are consistent with the home screen if (up) { - flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE; + flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE; } else { - flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE; + flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE; } } if (direction != 0) { diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java index b4fff8f..6ac0efb 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -29,7 +29,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.service.notification.NotificationListenerService; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.view.KeyEvent; diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 54d0acd..6807e7f 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -23,8 +23,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.text.TextUtils; -import android.util.Log; - import java.util.ArrayList; import java.util.List; diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index bc9722e..936762c 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -733,6 +733,50 @@ public final class TvContract { public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4"; + + /** * The version number of this row entry used by TV input services. * <p> * This is best used by sync adapters to identify the rows to update. The number can be @@ -1010,6 +1054,50 @@ public final class TvContract { public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3"; + + /** + * Internal integer flag used by individual TV input services. + * <p> + * This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4"; + + /** * The version number of this row entry used by TV input services. * <p> * This is best used by sync adapters to identify the rows to update. The number can be diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java index b9e99d2..5c1193f 100644 --- a/media/java/android/media/tv/TvInputInfo.java +++ b/media/java/android/media/tv/TvInputInfo.java @@ -116,12 +116,13 @@ public final class TvInputInfo implements Parcelable { private final ResolveInfo mService; private final String mId; private final String mParentId; + private final int mType; + private final boolean mIsHardwareInput; // Attributes from XML meta data. private String mSetupActivity; private String mSettingsActivity; - private int mType = TYPE_TUNER; private HdmiDeviceInfo mHdmiDeviceInfo; private String mLabel; private Uri mIconUri; @@ -153,7 +154,7 @@ public final class TvInputInfo implements Parcelable { throws XmlPullParserException, IOException { return createTvInputInfo(context, service, generateInputIdForComponentName( new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name)), - null, TYPE_TUNER, null, null, false); + null, TYPE_TUNER, false, null, null, false); } /** @@ -177,7 +178,7 @@ public final class TvInputInfo implements Parcelable { boolean isConnectedToHdmiSwitch = (hdmiDeviceInfo.getPhysicalAddress() & 0x0FFF) != 0; TvInputInfo input = createTvInputInfo(context, service, generateInputIdForHdmiDevice( new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name), - hdmiDeviceInfo), parentId, TYPE_HDMI, label, iconUri, isConnectedToHdmiSwitch); + hdmiDeviceInfo), parentId, TYPE_HDMI, true, label, iconUri, isConnectedToHdmiSwitch); input.mHdmiDeviceInfo = hdmiDeviceInfo; return input; } @@ -202,12 +203,12 @@ public final class TvInputInfo implements Parcelable { int inputType = sHardwareTypeToTvInputType.get(hardwareInfo.getType(), TYPE_TUNER); return createTvInputInfo(context, service, generateInputIdForHardware( new ComponentName(service.serviceInfo.packageName, service.serviceInfo.name), - hardwareInfo), null, inputType, label, iconUri, false); + hardwareInfo), null, inputType, true, label, iconUri, false); } private static TvInputInfo createTvInputInfo(Context context, ResolveInfo service, - String id, String parentId, int inputType, String label, Uri iconUri, - boolean isConnectedToHdmiSwitch) + String id, String parentId, int inputType, boolean isHardwareInput, String label, + Uri iconUri, boolean isConnectedToHdmiSwitch) throws XmlPullParserException, IOException { ServiceInfo si = service.serviceInfo; PackageManager pm = context.getPackageManager(); @@ -233,7 +234,7 @@ public final class TvInputInfo implements Parcelable { "Meta-data does not start with tv-input-service tag in " + si.name); } - TvInputInfo input = new TvInputInfo(service, id, parentId, inputType); + TvInputInfo input = new TvInputInfo(service, id, parentId, inputType, isHardwareInput); TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.TvInputService); input.mSetupActivity = sa.getString( @@ -272,12 +273,16 @@ public final class TvInputInfo implements Parcelable { * @param id ID of this TV input. Should be generated via generateInputId*(). * @param parentId ID of this TV input's parent input. {@code null} if none exists. * @param type The type of this TV input service. + * @param isHardwareInput {@code true} if this TV input represents a hardware device. + * {@code false} otherwise. */ - private TvInputInfo(ResolveInfo service, String id, String parentId, int type) { + private TvInputInfo(ResolveInfo service, String id, String parentId, int type, + boolean isHardwareInput) { mService = service; mId = id; mParentId = parentId; mType = type; + mIsHardwareInput = isHardwareInput; } /** @@ -381,6 +386,16 @@ public final class TvInputInfo implements Parcelable { } /** + * Returns {@code true} if this TV input represents a hardware device. (e.g. built-in tuner, + * HDMI1) {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean isHardwareInput() { + return mIsHardwareInput; + } + + /** * Returns {@code true}, if a CEC device for this TV input is connected to an HDMI switch, i.e., * the device isn't directly connected to a HDMI port. * @hide @@ -499,6 +514,7 @@ public final class TvInputInfo implements Parcelable { dest.writeString(mSetupActivity); dest.writeString(mSettingsActivity); dest.writeInt(mType); + dest.writeByte(mIsHardwareInput ? (byte) 1 : 0); dest.writeParcelable(mHdmiDeviceInfo, flags); dest.writeParcelable(mIconUri, flags); dest.writeString(mLabel); @@ -572,6 +588,7 @@ public final class TvInputInfo implements Parcelable { mSetupActivity = in.readString(); mSettingsActivity = in.readString(); mType = in.readInt(); + mIsHardwareInput = in.readByte() == 1 ? true : false; mHdmiDeviceInfo = in.readParcelable(null); mIconUri = in.readParcelable(null); mLabel = in.readString(); diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index f552a51..8ed383a 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -48,7 +48,6 @@ import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import java.util.ArrayList; @@ -242,7 +241,7 @@ public abstract class TvInputService extends Service { final Handler mHandler; private WindowManager.LayoutParams mWindowParams; private Surface mSurface; - private Context mContext; + private final Context mContext; private FrameLayout mOverlayViewContainer; private View mOverlayView; private OverlayViewCleanUpTask mOverlayViewCleanUpTask; @@ -250,11 +249,11 @@ public abstract class TvInputService extends Service { private IBinder mWindowToken; private Rect mOverlayFrame; - private Object mLock = new Object(); + private final Object mLock = new Object(); // @GuardedBy("mLock") private ITvInputSessionCallback mSessionCallback; // @GuardedBy("mLock") - private List<Runnable> mPendingActions = new ArrayList<>(); + private final List<Runnable> mPendingActions = new ArrayList<>(); /** * Creates a new Session. @@ -615,16 +614,17 @@ public abstract class TvInputService extends Service { public void onSetMain(boolean isMain) { } - /** - * Sets the {@link Surface} for the current input session on which the TV input renders video. - * <p> - * When {@code setSurface(null)} is called, the implementation should stop using the Surface - * object previously given and release any references to it. - * - * @param surface possibly {@code null} {@link Surface} an application passes to this TV input - * session. - * @return {@code true} if the surface was set, {@code false} otherwise. - */ + /** + * Sets the {@link Surface} for the current input session on which the TV input renders + * video. + * <p> + * When {@code setSurface(null)} is called, the implementation should stop using the Surface + * object previously given and release any references to it. + * + * @param surface possibly {@code null} {@link Surface} an application passes to this TV + * input session. + * @return {@code true} if the surface was set, {@code false} otherwise. + */ public abstract boolean onSetSurface(Surface surface); /** @@ -663,11 +663,11 @@ public abstract class TvInputService extends Service { /** * Tunes to a given channel. When the video is available, {@link #notifyVideoAvailable()} - * should be called. Also, {@link #notifyVideoUnavailable(int)} should be called when the - * TV input cannot continue playing the given channel. + * should be called. Also, {@link #notifyVideoUnavailable(int)} should be called when the TV + * input cannot continue playing the given channel. * * @param channelUri The URI of the channel. - * @return {@code true} the tuning was successful, {@code false} otherwise. + * @return {@code true} if the tuning was successful, {@code false} otherwise. */ public abstract boolean onTune(Uri channelUri); @@ -676,7 +676,7 @@ public abstract class TvInputService extends Service { * * @param channelUri The URI of the channel. * @param params The extra parameters from other applications. - * @return {@code true} the tuning was successful, {@code false} otherwise. + * @return {@code true} if the tuning was successful, {@code false} otherwise. * @hide */ @SystemApi @@ -712,10 +712,10 @@ public abstract class TvInputService extends Service { } /** - * Select a given track. + * Selects a given track. * <p> * If this is done successfully, the implementation should call {@link #notifyTrackSelected} - * to help applications maintain the selcted track lists. + * to help applications maintain the up-to-date list of the selected tracks. * </p> * * @param trackId The ID of the track to select. {@code null} means to unselect the current @@ -723,6 +723,7 @@ public abstract class TvInputService extends Service { * @param type The type of the track to select. The type can be * {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or * {@link TvTrackInfo#TYPE_SUBTITLE}. + * @return {@code true} if the track selection was successful, {@code false} otherwise. * @see #notifyTrackSelected */ public boolean onSelectTrack(int type, String trackId) { @@ -1230,6 +1231,8 @@ public abstract class TvInputService extends Service { args.arg2 = mProxySession; args.arg3 = mProxySessionCallback; args.arg4 = session.getToken(); + session.tune(TvContract.buildChannelUriForPassthroughInput( + getHardwareInputId())); } else { args.arg1 = null; args.arg2 = null; @@ -1239,7 +1242,6 @@ public abstract class TvInputService extends Service { } mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, args) .sendToTarget(); - session.tune(TvContract.buildChannelUriForPassthroughInput(getHardwareInputId())); } @Override diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java index e0aacd6..0284171 100644 --- a/media/java/android/media/tv/TvTrackInfo.java +++ b/media/java/android/media/tv/TvTrackInfo.java @@ -42,6 +42,7 @@ public final class TvTrackInfo implements Parcelable { private final int mType; private final String mId; private final String mLanguage; + private final String mDescription; private final int mAudioChannelCount; private final int mAudioSampleRate; private final int mVideoWidth; @@ -49,12 +50,13 @@ public final class TvTrackInfo implements Parcelable { private final float mVideoFrameRate; private final Bundle mExtra; - private TvTrackInfo(int type, String id, String language, int audioChannelCount, - int audioSampleRate, int videoWidth, int videoHeight, float videoFrameRate, - Bundle extra) { + private TvTrackInfo(int type, String id, String language, String description, + int audioChannelCount, int audioSampleRate, int videoWidth, int videoHeight, + float videoFrameRate, Bundle extra) { mType = type; mId = id; mLanguage = language; + mDescription = description; mAudioChannelCount = audioChannelCount; mAudioSampleRate = audioSampleRate; mVideoWidth = videoWidth; @@ -67,6 +69,7 @@ public final class TvTrackInfo implements Parcelable { mType = in.readInt(); mId = in.readString(); mLanguage = in.readString(); + mDescription = in.readString(); mAudioChannelCount = in.readInt(); mAudioSampleRate = in.readInt(); mVideoWidth = in.readInt(); @@ -99,6 +102,13 @@ public final class TvTrackInfo implements Parcelable { } /** + * Returns a user readable description for the current track. + */ + public final String getDescription() { + return mDescription; + } + + /** * Returns the audio channel count. Valid only for {@link #TYPE_AUDIO} tracks. */ public final int getAudioChannelCount() { @@ -174,6 +184,7 @@ public final class TvTrackInfo implements Parcelable { dest.writeInt(mType); dest.writeString(mId); dest.writeString(mLanguage); + dest.writeString(mDescription); dest.writeInt(mAudioChannelCount); dest.writeInt(mAudioSampleRate); dest.writeInt(mVideoWidth); @@ -202,6 +213,7 @@ public final class TvTrackInfo implements Parcelable { private final String mId; private final int mType; private String mLanguage; + private String mDescription; private int mAudioChannelCount; private int mAudioSampleRate; private int mVideoWidth; @@ -241,6 +253,16 @@ public final class TvTrackInfo implements Parcelable { } /** + * Sets a user readable description for the current track. + * + * @param description The user readable description. + */ + public final Builder setDescription(String description) { + mDescription = description; + return this; + } + + /** * Sets the audio channel count. Valid only for {@link #TYPE_AUDIO} tracks. * * @param audioChannelCount The audio channel count. @@ -325,8 +347,8 @@ public final class TvTrackInfo implements Parcelable { * @return The new {@link TvTrackInfo} instance */ public TvTrackInfo build() { - return new TvTrackInfo(mType, mId, mLanguage, mAudioChannelCount, mAudioSampleRate, - mVideoWidth, mVideoHeight, mVideoFrameRate, mExtra); + return new TvTrackInfo(mType, mId, mLanguage, mDescription, mAudioChannelCount, + mAudioSampleRate, mVideoWidth, mVideoHeight, mVideoFrameRate, mExtra); } } -}
\ No newline at end of file +} diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java index 5d9355a..3541fba 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -28,7 +28,6 @@ import android.database.sqlite.SQLiteDatabase; import android.media.MediaScanner; import android.net.Uri; import android.os.BatteryManager; -import android.os.BatteryStats; import android.os.RemoteException; import android.provider.MediaStore; import android.provider.MediaStore.Audio; diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java index 8287344..41156cb 100644 --- a/media/java/android/service/media/MediaBrowserService.java +++ b/media/java/android/service/media/MediaBrowserService.java @@ -16,7 +16,6 @@ package android.service.media; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 4ebbe26..dae57a8 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -2,6 +2,7 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES:= \ + android_media_ImageWriter.cpp \ android_media_ImageReader.cpp \ android_media_MediaCrypto.cpp \ android_media_MediaCodec.cpp \ diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index cf69b8f..9fc7e8e 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -95,6 +95,9 @@ public: void setBufferFormat(int format) { mFormat = format; } int getBufferFormat() { return mFormat; } + void setBufferDataspace(android_dataspace dataSpace) { mDataSpace = dataSpace; } + android_dataspace getBufferDataspace() { return mDataSpace; } + void setBufferWidth(int width) { mWidth = width; } int getBufferWidth() { return mWidth; } @@ -111,6 +114,7 @@ private: jobject mWeakThiz; jclass mClazz; int mFormat; + android_dataspace mDataSpace; int mWidth; int mHeight; }; @@ -263,29 +267,6 @@ static void Image_setBuffer(JNIEnv* env, jobject thiz, env->SetLongField(thiz, gSurfaceImageClassInfo.mLockedBuffer, reinterpret_cast<jlong>(buffer)); } -// Some formats like JPEG defined with different values between android.graphics.ImageFormat and -// graphics.h, need convert to the one defined in graphics.h here. -static int Image_getPixelFormat(JNIEnv* env, int format) -{ - int jpegFormat; - jfieldID fid; - - ALOGV("%s: format = 0x%x", __FUNCTION__, format); - - jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat"); - ALOG_ASSERT(imageFormatClazz != NULL); - - fid = env->GetStaticFieldID(imageFormatClazz, "JPEG", "I"); - jpegFormat = env->GetStaticIntField(imageFormatClazz, fid); - - // Translate the JPEG to BLOB for camera purpose. - if (format == jpegFormat) { - format = HAL_PIXEL_FORMAT_BLOB; - } - - return format; -} - static uint32_t Image_getJpegSize(CpuConsumer::LockedBuffer* buffer, bool usingRGBAOverride) { ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); @@ -483,7 +464,7 @@ static void Image_getLockedBufferInfo(JNIEnv* env, CpuConsumer::LockedBuffer* bu } static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t readerFormat) + int32_t halReaderFormat) { ALOGV("%s: buffer index: %d", __FUNCTION__, idx); ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0), "Index is out of range:%d", idx); @@ -493,7 +474,7 @@ static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* bu int32_t fmt = buffer->flexFormat; - fmt = applyFormatOverrides(fmt, readerFormat); + fmt = applyFormatOverrides(fmt, halReaderFormat); switch (fmt) { case HAL_PIXEL_FORMAT_YCbCr_420_888: @@ -543,7 +524,7 @@ static jint Image_imageGetPixelStride(JNIEnv* env, CpuConsumer::LockedBuffer* bu } static jint Image_imageGetRowStride(JNIEnv* env, CpuConsumer::LockedBuffer* buffer, int idx, - int32_t readerFormat) + int32_t halReaderFormat) { ALOGV("%s: buffer index: %d", __FUNCTION__, idx); ALOG_ASSERT((idx < IMAGE_READER_MAX_NUM_PLANES) && (idx >= 0)); @@ -553,7 +534,7 @@ static jint Image_imageGetRowStride(JNIEnv* env, CpuConsumer::LockedBuffer* buff int32_t fmt = buffer->flexFormat; - fmt = applyFormatOverrides(fmt, readerFormat); + fmt = applyFormatOverrides(fmt, halReaderFormat); switch (fmt) { case HAL_PIXEL_FORMAT_YCbCr_420_888: @@ -682,11 +663,16 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, { status_t res; int nativeFormat; + android_dataspace nativeDataspace; ALOGV("%s: width:%d, height: %d, format: 0x%x, maxImages:%d", __FUNCTION__, width, height, format, maxImages); - nativeFormat = Image_getPixelFormat(env, format); + PublicFormat publicFormat = static_cast<PublicFormat>(format); + nativeFormat = android_view_Surface_mapPublicFormatToHalFormat( + publicFormat); + nativeDataspace = android_view_Surface_mapPublicFormatToHalDataspace( + publicFormat); sp<IGraphicBufferProducer> gbProducer; sp<IGraphicBufferConsumer> gbConsumer; @@ -710,10 +696,11 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, consumer->setFrameAvailableListener(ctx); ImageReader_setNativeContext(env, thiz, ctx); ctx->setBufferFormat(nativeFormat); + ctx->setBufferDataspace(nativeDataspace); ctx->setBufferWidth(width); ctx->setBufferHeight(height); - // Set the width/height/format to the CpuConsumer + // Set the width/height/format/dataspace to the CpuConsumer res = consumer->setDefaultBufferSize(width, height); if (res != OK) { jniThrowException(env, "java/lang/IllegalStateException", @@ -725,6 +712,12 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, jniThrowException(env, "java/lang/IllegalStateException", "Failed to set CpuConsumer buffer format"); } + res = consumer->setDefaultBufferDataSpace(nativeDataspace); + if (res != OK) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to set CpuConsumer buffer dataSpace"); + } + } static void ImageReader_close(JNIEnv* env, jobject thiz) @@ -867,6 +860,25 @@ static jint ImageReader_imageSetup(JNIEnv* env, jobject thiz, return ACQUIRE_SUCCESS; } +static void ImageReader_detachImage(JNIEnv* env, jobject thiz, jobject image) { + ALOGV("%s:", __FUNCTION__); + JNIImageReaderContext* ctx = ImageReader_getContext(env, thiz); + if (ctx == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", "ImageReader was already closed"); + return; + } + + // CpuConsumer* consumer = ctx->getCpuConsumer(); + CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, image); + if (!buffer) { + ALOGW("Image already released!!!"); + return; + } + + // TODO: need implement + jniThrowRuntimeException(env, "nativeDetachImage is not implemented yet!!!"); +} + static jobject ImageReader_getSurface(JNIEnv* env, jobject thiz) { ALOGV("%s: ", __FUNCTION__); @@ -884,6 +896,8 @@ static jobject ImageReader_getSurface(JNIEnv* env, jobject thiz) static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int readerFormat) { int rowStride, pixelStride; + PublicFormat publicReaderFormat = static_cast<PublicFormat>(readerFormat); + ALOGV("%s: buffer index: %d", __FUNCTION__, idx); CpuConsumer::LockedBuffer* buffer = Image_getLockedBuffer(env, thiz); @@ -893,10 +907,11 @@ static jobject Image_createSurfacePlane(JNIEnv* env, jobject thiz, int idx, int jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); } - readerFormat = Image_getPixelFormat(env, readerFormat); + int halReaderFormat = android_view_Surface_mapPublicFormatToHalFormat( + publicReaderFormat); - rowStride = Image_imageGetRowStride(env, buffer, idx, readerFormat); - pixelStride = Image_imageGetPixelStride(env, buffer, idx, readerFormat); + rowStride = Image_imageGetRowStride(env, buffer, idx, halReaderFormat); + pixelStride = Image_imageGetPixelStride(env, buffer, idx, halReaderFormat); jobject surfPlaneObj = env->NewObject(gSurfacePlaneClassInfo.clazz, gSurfacePlaneClassInfo.ctor, thiz, idx, rowStride, pixelStride); @@ -909,6 +924,7 @@ static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int reade uint8_t *base = NULL; uint32_t size = 0; jobject byteBuffer; + PublicFormat readerPublicFormat = static_cast<PublicFormat>(readerFormat); ALOGV("%s: buffer index: %d", __FUNCTION__, idx); @@ -918,10 +934,11 @@ static jobject Image_getByteBuffer(JNIEnv* env, jobject thiz, int idx, int reade jniThrowException(env, "java/lang/IllegalStateException", "Image was released"); } - readerFormat = Image_getPixelFormat(env, readerFormat); + int readerHalFormat = android_view_Surface_mapPublicFormatToHalFormat( + readerPublicFormat); // Create byteBuffer from native buffer - Image_getLockedBufferInfo(env, buffer, idx, &base, &size, readerFormat); + Image_getLockedBufferInfo(env, buffer, idx, &base, &size, readerHalFormat); if (size > static_cast<uint32_t>(INT32_MAX)) { // Byte buffer have 'int capacity', so check the range @@ -963,6 +980,7 @@ static JNINativeMethod gImageReaderMethods[] = { {"nativeReleaseImage", "(Landroid/media/Image;)V", (void*)ImageReader_imageRelease }, {"nativeImageSetup", "(Landroid/media/Image;)I", (void*)ImageReader_imageSetup }, {"nativeGetSurface", "()Landroid/view/Surface;", (void*)ImageReader_getSurface }, + {"nativeDetachImage", "(Landroid/media/Image;)V", (void*)ImageReader_detachImage }, }; static JNINativeMethod gImageMethods[] = { diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp new file mode 100644 index 0000000..d10df3e9 --- /dev/null +++ b/media/jni/android_media_ImageWriter.cpp @@ -0,0 +1,1014 @@ +/* + * Copyright 2015 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "ImageWriter_JNI" +#include <utils/Log.h> +#include <utils/String8.h> + +#include <gui/IProducerListener.h> +#include <gui/Surface.h> +#include <gui/CpuConsumer.h> +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/android_view_Surface.h> +#include <camera3.h> + +#include <jni.h> +#include <JNIHelp.h> + +#include <stdint.h> +#include <inttypes.h> + +#define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) + +#define IMAGE_BUFFER_JNI_ID "mNativeBuffer" + +// ---------------------------------------------------------------------------- + +using namespace android; + +enum { + IMAGE_WRITER_MAX_NUM_PLANES = 3, +}; + +static struct { + jmethodID postEventFromNative; + jfieldID mWriterFormat; +} gImageWriterClassInfo; + +static struct { + jfieldID mNativeBuffer; + jfieldID mNativeFenceFd; + jfieldID mPlanes; +} gSurfaceImageClassInfo; + +static struct { + jclass clazz; + jmethodID ctor; +} gSurfacePlaneClassInfo; + +typedef CpuConsumer::LockedBuffer LockedImage; + +// ---------------------------------------------------------------------------- + +class JNIImageWriterContext : public BnProducerListener { +public: + JNIImageWriterContext(JNIEnv* env, jobject weakThiz, jclass clazz); + + virtual ~JNIImageWriterContext(); + + // Implementation of IProducerListener, used to notify the ImageWriter that the consumer + // has returned a buffer and it is ready for ImageWriter to dequeue. + virtual void onBufferReleased(); + + void setProducer(const sp<ANativeWindow>& producer) { mProducer = producer; } + ANativeWindow* getProducer() { return mProducer.get(); } + + void setBufferFormat(int format) { mFormat = format; } + int getBufferFormat() { return mFormat; } + + void setBufferWidth(int width) { mWidth = width; } + int getBufferWidth() { return mWidth; } + + void setBufferHeight(int height) { mHeight = height; } + int getBufferHeight() { return mHeight; } + +private: + static JNIEnv* getJNIEnv(bool* needsDetach); + static void detachJNI(); + + sp<ANativeWindow> mProducer; + jobject mWeakThiz; + jclass mClazz; + int mFormat; + int mWidth; + int mHeight; +}; + +JNIImageWriterContext::JNIImageWriterContext(JNIEnv* env, jobject weakThiz, jclass clazz) : + mWeakThiz(env->NewGlobalRef(weakThiz)), + mClazz((jclass)env->NewGlobalRef(clazz)), + mFormat(0), + mWidth(-1), + mHeight(-1) { +} + +JNIImageWriterContext::~JNIImageWriterContext() { + ALOGV("%s", __FUNCTION__); + bool needsDetach = false; + JNIEnv* env = getJNIEnv(&needsDetach); + if (env != NULL) { + env->DeleteGlobalRef(mWeakThiz); + env->DeleteGlobalRef(mClazz); + } else { + ALOGW("leaking JNI object references"); + } + if (needsDetach) { + detachJNI(); + } + + mProducer.clear(); +} + +JNIEnv* JNIImageWriterContext::getJNIEnv(bool* needsDetach) { + ALOGV("%s", __FUNCTION__); + LOG_ALWAYS_FATAL_IF(needsDetach == NULL, "needsDetach is null!!!"); + *needsDetach = false; + JNIEnv* env = AndroidRuntime::getJNIEnv(); + if (env == NULL) { + JavaVMAttachArgs args = {JNI_VERSION_1_4, NULL, NULL}; + JavaVM* vm = AndroidRuntime::getJavaVM(); + int result = vm->AttachCurrentThread(&env, (void*) &args); + if (result != JNI_OK) { + ALOGE("thread attach failed: %#x", result); + return NULL; + } + *needsDetach = true; + } + return env; +} + +void JNIImageWriterContext::detachJNI() { + ALOGV("%s", __FUNCTION__); + JavaVM* vm = AndroidRuntime::getJavaVM(); + int result = vm->DetachCurrentThread(); + if (result != JNI_OK) { + ALOGE("thread detach failed: %#x", result); + } +} + +void JNIImageWriterContext::onBufferReleased() { + ALOGV("%s: buffer released", __FUNCTION__); + bool needsDetach = false; + JNIEnv* env = getJNIEnv(&needsDetach); + if (env != NULL) { + env->CallStaticVoidMethod(mClazz, gImageWriterClassInfo.postEventFromNative, mWeakThiz); + } else { + ALOGW("onBufferReleased event will not posted"); + } + if (needsDetach) { + detachJNI(); + } +} + +// ---------------------------------------------------------------------------- + +extern "C" { + +// -------------------------------Private method declarations-------------- + +static bool isWritable(int format); +static bool isPossiblyYUV(PixelFormat format); +static void Image_setNativeContext(JNIEnv* env, jobject thiz, + sp<GraphicBuffer> buffer, int fenceFd); +static void Image_getNativeContext(JNIEnv* env, jobject thiz, + GraphicBuffer** buffer, int* fenceFd); +static void Image_unlockIfLocked(JNIEnv* env, jobject thiz); + +// --------------------------ImageWriter methods--------------------------------------- + +static void ImageWriter_classInit(JNIEnv* env, jclass clazz) { + ALOGV("%s:", __FUNCTION__); + jclass imageClazz = env->FindClass("android/media/ImageWriter$WriterSurfaceImage"); + LOG_ALWAYS_FATAL_IF(imageClazz == NULL, + "can't find android/media/ImageWriter$WriterSurfaceImage"); + gSurfaceImageClassInfo.mNativeBuffer = env->GetFieldID( + imageClazz, IMAGE_BUFFER_JNI_ID, "J"); + LOG_ALWAYS_FATAL_IF(gSurfaceImageClassInfo.mNativeBuffer == NULL, + "can't find android/media/ImageWriter$WriterSurfaceImage.%s", IMAGE_BUFFER_JNI_ID); + + gSurfaceImageClassInfo.mNativeFenceFd = env->GetFieldID( + imageClazz, "mNativeFenceFd", "I"); + LOG_ALWAYS_FATAL_IF(gSurfaceImageClassInfo.mNativeFenceFd == NULL, + "can't find android/media/ImageWriter$WriterSurfaceImage.mNativeFenceFd"); + + gSurfaceImageClassInfo.mPlanes = env->GetFieldID( + imageClazz, "mPlanes", "[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;"); + LOG_ALWAYS_FATAL_IF(gSurfaceImageClassInfo.mPlanes == NULL, + "can't find android/media/ImageWriter$WriterSurfaceImage.mPlanes"); + + gImageWriterClassInfo.postEventFromNative = env->GetStaticMethodID( + clazz, "postEventFromNative", "(Ljava/lang/Object;)V"); + LOG_ALWAYS_FATAL_IF(gImageWriterClassInfo.postEventFromNative == NULL, + "can't find android/media/ImageWriter.postEventFromNative"); + + gImageWriterClassInfo.mWriterFormat = env->GetFieldID( + clazz, "mWriterFormat", "I"); + LOG_ALWAYS_FATAL_IF(gImageWriterClassInfo.mWriterFormat == NULL, + "can't find android/media/ImageWriter.mWriterFormat"); + + jclass planeClazz = env->FindClass("android/media/ImageWriter$WriterSurfaceImage$SurfacePlane"); + LOG_ALWAYS_FATAL_IF(planeClazz == NULL, "Can not find SurfacePlane class"); + // FindClass only gives a local reference of jclass object. + gSurfacePlaneClassInfo.clazz = (jclass) env->NewGlobalRef(planeClazz); + gSurfacePlaneClassInfo.ctor = env->GetMethodID(gSurfacePlaneClassInfo.clazz, "<init>", + "(Landroid/media/ImageWriter$WriterSurfaceImage;IILjava/nio/ByteBuffer;)V"); + LOG_ALWAYS_FATAL_IF(gSurfacePlaneClassInfo.ctor == NULL, + "Can not find SurfacePlane constructor"); +} + +static jlong ImageWriter_init(JNIEnv* env, jobject thiz, jobject weakThiz, jobject jsurface, + jint maxImages) { + status_t res; + + ALOGV("%s: maxImages:%d", __FUNCTION__, maxImages); + + sp<Surface> surface(android_view_Surface_getSurface(env, jsurface)); + if (surface == NULL) { + jniThrowException(env, + "java/lang/IllegalArgumentException", + "The surface has been released"); + return 0; + } + sp<IGraphicBufferProducer> bufferProducer = surface->getIGraphicBufferProducer(); + + jclass clazz = env->GetObjectClass(thiz); + if (clazz == NULL) { + jniThrowRuntimeException(env, "Can't find android/graphics/ImageWriter"); + return 0; + } + sp<JNIImageWriterContext> ctx(new JNIImageWriterContext(env, weakThiz, clazz)); + + sp<Surface> producer = new Surface(bufferProducer, /*controlledByApp*/false); + ctx->setProducer(producer); + /** + * NATIVE_WINDOW_API_CPU isn't a good choice here, as it makes the bufferQueue not connectable + * after disconnect. MEDIA or CAMERA are treated the same internally. The producer listener + * will be cleared after disconnect call. + */ + producer->connect(/*api*/NATIVE_WINDOW_API_CAMERA, /*listener*/ctx); + jlong nativeCtx = reinterpret_cast<jlong>(ctx.get()); + + // Get the dimension and format of the producer. + sp<ANativeWindow> anw = producer; + int32_t width, height, format; + if ((res = anw->query(anw.get(), NATIVE_WINDOW_WIDTH, &width)) != OK) { + ALOGE("%s: Query Surface width failed: %s (%d)", __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Failed to query Surface width"); + return 0; + } + ctx->setBufferWidth(width); + + if ((res = anw->query(anw.get(), NATIVE_WINDOW_HEIGHT, &height)) != OK) { + ALOGE("%s: Query Surface height failed: %s (%d)", __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Failed to query Surface height"); + return 0; + } + ctx->setBufferHeight(height); + + if ((res = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &format)) != OK) { + ALOGE("%s: Query Surface format failed: %s (%d)", __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Failed to query Surface format"); + return 0; + } + ctx->setBufferFormat(format); + env->SetIntField(thiz, gImageWriterClassInfo.mWriterFormat, reinterpret_cast<jint>(format)); + + + if (isWritable(format)) { + res = native_window_set_usage(anw.get(), GRALLOC_USAGE_SW_WRITE_OFTEN); + if (res != OK) { + ALOGE("%s: Configure usage %08x for format %08x failed: %s (%d)", + __FUNCTION__, GRALLOC_USAGE_SW_WRITE_OFTEN, format, strerror(-res), res); + jniThrowRuntimeException(env, "Failed to SW_WRITE_OFTEN configure usage"); + return 0; + } + } + + int minUndequeuedBufferCount = 0; + res = anw->query(anw.get(), + NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS, &minUndequeuedBufferCount); + if (res != OK) { + ALOGE("%s: Query producer undequeued buffer count failed: %s (%d)", + __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Query producer undequeued buffer count failed"); + return 0; + } + + size_t totalBufferCount = maxImages + minUndequeuedBufferCount; + res = native_window_set_buffer_count(anw.get(), totalBufferCount); + if (res != OK) { + ALOGE("%s: Set buffer count failed: %s (%d)", __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Set buffer count failed"); + return 0; + } + + if (ctx != 0) { + ctx->incStrong((void*)ImageWriter_init); + } + return nativeCtx; +} + +static void ImageWriter_dequeueImage(JNIEnv* env, jobject thiz, jlong nativeCtx, jobject image) { + ALOGV("%s", __FUNCTION__); + JNIImageWriterContext* const ctx = reinterpret_cast<JNIImageWriterContext *>(nativeCtx); + if (ctx == NULL || thiz == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageWriterContext is not initialized"); + return; + } + + sp<ANativeWindow> anw = ctx->getProducer(); + android_native_buffer_t *anb = NULL; + int fenceFd = -1; + status_t res = anw->dequeueBuffer(anw.get(), &anb, &fenceFd); + if (res != OK) { + // TODO: handle different error cases here. + ALOGE("%s: Set buffer count failed: %s (%d)", __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "dequeue buffer failed"); + return; + } + // New GraphicBuffer object doesn't own the handle, thus the native buffer + // won't be freed when this object is destroyed. + sp<GraphicBuffer> buffer(new GraphicBuffer(anb, /*keepOwnership*/false)); + + // Note that: + // 1. No need to lock buffer now, will only lock it when the first getPlanes() is called. + // 2. Fence will be saved to mNativeFenceFd, and will consumed by lock/queue/cancel buffer + // later. + // 3. need use lockAsync here, as it will handle the dequeued fence for us automatically. + + // Finally, set the native info into image object. + Image_setNativeContext(env, image, buffer, fenceFd); +} + +static void ImageWriter_close(JNIEnv* env, jobject thiz, jlong nativeCtx) { + ALOGV("%s:", __FUNCTION__); + JNIImageWriterContext* const ctx = reinterpret_cast<JNIImageWriterContext *>(nativeCtx); + if (ctx == NULL || thiz == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageWriterContext is not initialized"); + return; + } + + ANativeWindow* producer = ctx->getProducer(); + if (producer != NULL) { + /** + * NATIVE_WINDOW_API_CPU isn't a good choice here, as it makes the bufferQueue not + * connectable after disconnect. MEDIA or CAMERA are treated the same internally. + * The producer listener will be cleared after disconnect call. + */ + status_t res = native_window_api_disconnect(producer, /*api*/NATIVE_WINDOW_API_CAMERA); + /** + * This is not an error. if client calling process dies, the window will + * also die and all calls to it will return DEAD_OBJECT, thus it's already + * "disconnected" + */ + if (res == DEAD_OBJECT) { + ALOGW("%s: While disconnecting ImageWriter from native window, the" + " native window died already", __FUNCTION__); + } else if (res != OK) { + ALOGE("%s: native window disconnect failed: %s (%d)", + __FUNCTION__, strerror(-res), res); + jniThrowRuntimeException(env, "Native window disconnect failed"); + return; + } + } + + ctx->decStrong((void*)ImageWriter_init); +} + +static void ImageWriter_cancelImage(JNIEnv* env, jobject thiz, jlong nativeCtx, jobject image) { + ALOGV("%s", __FUNCTION__); + JNIImageWriterContext* const ctx = reinterpret_cast<JNIImageWriterContext *>(nativeCtx); + if (ctx == NULL || thiz == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageWriterContext is not initialized"); + return; + } + + sp<ANativeWindow> anw = ctx->getProducer(); + + GraphicBuffer *buffer = NULL; + int fenceFd = -1; + Image_getNativeContext(env, image, &buffer, &fenceFd); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return; + } + + // Unlock the image if it was locked + Image_unlockIfLocked(env, image); + + anw->cancelBuffer(anw.get(), buffer, fenceFd); + + Image_setNativeContext(env, image, NULL, -1); +} + +static void ImageWriter_queueImage(JNIEnv* env, jobject thiz, jlong nativeCtx, jobject image, + jlong timestampNs, jint left, jint top, jint right, jint bottom) { + ALOGV("%s", __FUNCTION__); + JNIImageWriterContext* const ctx = reinterpret_cast<JNIImageWriterContext *>(nativeCtx); + if (ctx == NULL || thiz == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageWriterContext is not initialized"); + return; + } + + status_t res = OK; + sp<ANativeWindow> anw = ctx->getProducer(); + + GraphicBuffer *buffer = NULL; + int fenceFd = -1; + Image_getNativeContext(env, image, &buffer, &fenceFd); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return; + } + + // Unlock image if it was locked. + Image_unlockIfLocked(env, image); + + // Set timestamp + ALOGV("timestamp to be queued: %" PRId64, timestampNs); + res = native_window_set_buffers_timestamp(anw.get(), timestampNs); + if (res != OK) { + jniThrowRuntimeException(env, "Set timestamp failed"); + return; + } + + // Set crop + android_native_rect_t cropRect; + cropRect.left = left; + cropRect.top = top; + cropRect.right = right; + cropRect.bottom = bottom; + res = native_window_set_crop(anw.get(), &cropRect); + if (res != OK) { + jniThrowRuntimeException(env, "Set crop rect failed"); + return; + } + + // Finally, queue input buffer + res = anw->queueBuffer(anw.get(), buffer, fenceFd); + if (res != OK) { + jniThrowRuntimeException(env, "Queue input buffer failed"); + return; + } + + Image_setNativeContext(env, image, NULL, -1); +} + +static void ImageWriter_attachImage(JNIEnv* env, jobject thiz, jlong nativeCtx, jobject image) { + ALOGV("%s", __FUNCTION__); + JNIImageWriterContext* const ctx = reinterpret_cast<JNIImageWriterContext *>(nativeCtx); + if (ctx == NULL || thiz == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "ImageWriterContext is not initialized"); + return; + } + + sp<ANativeWindow> anw = ctx->getProducer(); + + GraphicBuffer *buffer = NULL; + int fenceFd = -1; + Image_getNativeContext(env, image, &buffer, &fenceFd); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return; + } + + // TODO: need implement + jniThrowRuntimeException(env, "nativeAttachImage is not implement yet!!!"); +} + +// --------------------------Image methods--------------------------------------- + +static void Image_getNativeContext(JNIEnv* env, jobject thiz, + GraphicBuffer** buffer, int* fenceFd) { + ALOGV("%s", __FUNCTION__); + if (buffer != NULL) { + GraphicBuffer *gb = reinterpret_cast<GraphicBuffer *> + (env->GetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer)); + *buffer = gb; + } + + if (fenceFd != NULL) { + *fenceFd = reinterpret_cast<jint>(env->GetIntField( + thiz, gSurfaceImageClassInfo.mNativeFenceFd)); + } +} + +static void Image_setNativeContext(JNIEnv* env, jobject thiz, + sp<GraphicBuffer> buffer, int fenceFd) { + ALOGV("%s:", __FUNCTION__); + GraphicBuffer* p = NULL; + Image_getNativeContext(env, thiz, &p, /*fenceFd*/NULL); + if (buffer != 0) { + buffer->incStrong((void*)Image_setNativeContext); + } + if (p) { + p->decStrong((void*)Image_setNativeContext); + } + env->SetLongField(thiz, gSurfaceImageClassInfo.mNativeBuffer, + reinterpret_cast<jlong>(buffer.get())); + + env->SetIntField(thiz, gSurfaceImageClassInfo.mNativeFenceFd, reinterpret_cast<jint>(fenceFd)); +} + +static void Image_unlockIfLocked(JNIEnv* env, jobject thiz) { + ALOGV("%s", __FUNCTION__); + GraphicBuffer* buffer; + Image_getNativeContext(env, thiz, &buffer, NULL); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return; + } + + // Is locked? + bool isLocked = false; + jobject planes = env->GetObjectField(thiz, gSurfaceImageClassInfo.mPlanes); + isLocked = (planes != NULL); + if (isLocked) { + // no need to use fence here, as we it will be consumed by either concel or queue buffer. + status_t res = buffer->unlock(); + if (res != OK) { + jniThrowRuntimeException(env, "unlock buffer failed"); + } + ALOGV("Successfully unlocked the image"); + } +} + +static jint Image_getWidth(JNIEnv* env, jobject thiz) { + ALOGV("%s", __FUNCTION__); + GraphicBuffer* buffer; + Image_getNativeContext(env, thiz, &buffer, NULL); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return -1; + } + + return buffer->getWidth(); +} + +static jint Image_getHeight(JNIEnv* env, jobject thiz) { + ALOGV("%s", __FUNCTION__); + GraphicBuffer* buffer; + Image_getNativeContext(env, thiz, &buffer, NULL); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return -1; + } + + return buffer->getHeight(); +} + +// Some formats like JPEG defined with different values between android.graphics.ImageFormat and +// graphics.h, need convert to the one defined in graphics.h here. +static int Image_getPixelFormat(JNIEnv* env, int format) { + int jpegFormat; + jfieldID fid; + + ALOGV("%s: format = 0x%x", __FUNCTION__, format); + + jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat"); + ALOG_ASSERT(imageFormatClazz != NULL); + + fid = env->GetStaticFieldID(imageFormatClazz, "JPEG", "I"); + jpegFormat = env->GetStaticIntField(imageFormatClazz, fid); + + // Translate the JPEG to BLOB for camera purpose. + if (format == jpegFormat) { + format = HAL_PIXEL_FORMAT_BLOB; + } + + return format; +} + +static jint Image_getFormat(JNIEnv* env, jobject thiz) { + ALOGV("%s", __FUNCTION__); + GraphicBuffer* buffer; + Image_getNativeContext(env, thiz, &buffer, NULL); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return 0; + } + + return Image_getPixelFormat(env, buffer->getPixelFormat()); +} + +static void Image_setFenceFd(JNIEnv* env, jobject thiz, int fenceFd) { + ALOGV("%s:", __FUNCTION__); + env->SetIntField(thiz, gSurfaceImageClassInfo.mNativeFenceFd, reinterpret_cast<jint>(fenceFd)); +} + +static void Image_getLockedImage(JNIEnv* env, jobject thiz, LockedImage *image) { + ALOGV("%s", __FUNCTION__); + GraphicBuffer* buffer; + int fenceFd = -1; + Image_getNativeContext(env, thiz, &buffer, &fenceFd); + if (buffer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Image is not initialized"); + return; + } + + void* pData = NULL; + android_ycbcr ycbcr = android_ycbcr(); + status_t res; + int format = Image_getFormat(env, thiz); + int flexFormat = format; + if (isPossiblyYUV(format)) { + // ImageWriter doesn't use crop by itself, app sets it, use the no crop version. + res = buffer->lockAsyncYCbCr(GRALLOC_USAGE_SW_WRITE_OFTEN, &ycbcr, fenceFd); + // Clear the fenceFd as it is already consumed by lock call. + Image_setFenceFd(env, thiz, /*fenceFd*/-1); + if (res != OK) { + jniThrowRuntimeException(env, "lockAsyncYCbCr failed for YUV buffer"); + return; + } + pData = ycbcr.y; + flexFormat = HAL_PIXEL_FORMAT_YCbCr_420_888; + } + + // lockAsyncYCbCr for YUV is unsuccessful. + if (pData == NULL) { + res = buffer->lockAsync(GRALLOC_USAGE_SW_WRITE_OFTEN, &pData, fenceFd); + if (res != OK) { + jniThrowRuntimeException(env, "lockAsync failed"); + return; + } + } + + image->data = reinterpret_cast<uint8_t*>(pData); + image->width = buffer->getWidth(); + image->height = buffer->getHeight(); + image->format = format; + image->flexFormat = flexFormat; + image->stride = (ycbcr.y != NULL) ? static_cast<uint32_t>(ycbcr.ystride) : buffer->getStride(); + + image->dataCb = reinterpret_cast<uint8_t*>(ycbcr.cb); + image->dataCr = reinterpret_cast<uint8_t*>(ycbcr.cr); + image->chromaStride = static_cast<uint32_t>(ycbcr.cstride); + image->chromaStep = static_cast<uint32_t>(ycbcr.chroma_step); + ALOGV("Successfully locked the image"); + // crop, transform, scalingMode, timestamp, and frameNumber should be set by producer, + // and we don't set them here. +} + +static bool usingRGBAToJpegOverride(int32_t bufferFormat, int32_t writerCtxFormat) { + return writerCtxFormat == HAL_PIXEL_FORMAT_BLOB && bufferFormat == HAL_PIXEL_FORMAT_RGBA_8888; +} + +static int32_t applyFormatOverrides(int32_t bufferFormat, int32_t writerCtxFormat) +{ + // Using HAL_PIXEL_FORMAT_RGBA_8888 gralloc buffers containing JPEGs to get around SW + // write limitations for some platforms (b/17379185). + if (usingRGBAToJpegOverride(bufferFormat, writerCtxFormat)) { + return HAL_PIXEL_FORMAT_BLOB; + } + return bufferFormat; +} + +static uint32_t Image_getJpegSize(LockedImage* buffer, bool usingRGBAOverride) { + ALOGV("%s", __FUNCTION__); + ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); + uint32_t size = 0; + uint32_t width = buffer->width; + uint8_t* jpegBuffer = buffer->data; + + if (usingRGBAOverride) { + width = (buffer->width + buffer->stride * (buffer->height - 1)) * 4; + } + + // First check for JPEG transport header at the end of the buffer + uint8_t* header = jpegBuffer + (width - sizeof(struct camera3_jpeg_blob)); + struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header); + if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID) { + size = blob->jpeg_size; + ALOGV("%s: Jpeg size = %d", __FUNCTION__, size); + } + + // failed to find size, default to whole buffer + if (size == 0) { + /* + * This is a problem because not including the JPEG header + * means that in certain rare situations a regular JPEG blob + * will be misidentified as having a header, in which case + * we will get a garbage size value. + */ + ALOGW("%s: No JPEG header detected, defaulting to size=width=%d", + __FUNCTION__, width); + size = width; + } + + return size; +} + +static void Image_getLockedImageInfo(JNIEnv* env, LockedImage* buffer, int idx, + int32_t writerFormat, uint8_t **base, uint32_t *size, int *pixelStride, int *rowStride) { + ALOGV("%s", __FUNCTION__); + ALOG_ASSERT(buffer != NULL, "Input buffer is NULL!!!"); + ALOG_ASSERT(base != NULL, "base is NULL!!!"); + ALOG_ASSERT(size != NULL, "size is NULL!!!"); + ALOG_ASSERT(pixelStride != NULL, "pixelStride is NULL!!!"); + ALOG_ASSERT(rowStride != NULL, "rowStride is NULL!!!"); + ALOG_ASSERT((idx < IMAGE_WRITER_MAX_NUM_PLANES) && (idx >= 0)); + + ALOGV("%s: buffer: %p", __FUNCTION__, buffer); + + uint32_t dataSize, ySize, cSize, cStride; + uint32_t pStride = 0, rStride = 0; + uint8_t *cb, *cr; + uint8_t *pData = NULL; + int bytesPerPixel = 0; + + dataSize = ySize = cSize = cStride = 0; + int32_t fmt = buffer->flexFormat; + + bool usingRGBAOverride = usingRGBAToJpegOverride(fmt, writerFormat); + fmt = applyFormatOverrides(fmt, writerFormat); + switch (fmt) { + case HAL_PIXEL_FORMAT_YCbCr_420_888: + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + buffer->dataCb : + buffer->dataCr; + // only map until last pixel + if (idx == 0) { + pStride = 1; + rStride = buffer->stride; + dataSize = buffer->stride * (buffer->height - 1) + buffer->width; + } else { + pStride = buffer->chromaStep; + rStride = buffer->chromaStride; + dataSize = buffer->chromaStride * (buffer->height / 2 - 1) + + buffer->chromaStep * (buffer->width / 2 - 1) + 1; + } + break; + // NV21 + case HAL_PIXEL_FORMAT_YCrCb_420_SP: + cr = buffer->data + (buffer->stride * buffer->height); + cb = cr + 1; + // only map until last pixel + ySize = buffer->width * (buffer->height - 1) + buffer->width; + cSize = buffer->width * (buffer->height / 2 - 1) + buffer->width - 1; + + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + cb: + cr; + + dataSize = (idx == 0) ? ySize : cSize; + pStride = (idx == 0) ? 1 : 2; + rStride = buffer->width; + break; + case HAL_PIXEL_FORMAT_YV12: + // Y and C stride need to be 16 pixel aligned. + LOG_ALWAYS_FATAL_IF(buffer->stride % 16, + "Stride is not 16 pixel aligned %d", buffer->stride); + + ySize = buffer->stride * buffer->height; + cStride = ALIGN(buffer->stride / 2, 16); + cr = buffer->data + ySize; + cSize = cStride * buffer->height / 2; + cb = cr + cSize; + + pData = + (idx == 0) ? + buffer->data : + (idx == 1) ? + cb : + cr; + dataSize = (idx == 0) ? ySize : cSize; + pStride = 1; + rStride = (idx == 0) ? buffer->stride : ALIGN(buffer->stride / 2, 16); + break; + case HAL_PIXEL_FORMAT_Y8: + // Single plane, 8bpp. + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + + pData = buffer->data; + dataSize = buffer->stride * buffer->height; + pStride = 1; + rStride = buffer->stride; + break; + case HAL_PIXEL_FORMAT_Y16: + bytesPerPixel = 2; + // Single plane, 16bpp, strides are specified in pixels, not in bytes + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_BLOB: + // Used for JPEG data, height must be 1, width == size, single plane. + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + ALOG_ASSERT(buffer->height == 1, "JPEG should has height value %d", buffer->height); + + pData = buffer->data; + dataSize = Image_getJpegSize(buffer, usingRGBAOverride); + pStride = bytesPerPixel; + rowStride = 0; + break; + case HAL_PIXEL_FORMAT_RAW16: + // Single plane 16bpp bayer data. + bytesPerPixel = 2; + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_RAW10: + // Single plane 10bpp bayer data. + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + LOG_ALWAYS_FATAL_IF(buffer->width % 4, + "Width is not multiple of 4 %d", buffer->width); + LOG_ALWAYS_FATAL_IF(buffer->height % 2, + "Height is not even %d", buffer->height); + LOG_ALWAYS_FATAL_IF(buffer->stride < (buffer->width * 10 / 8), + "stride (%d) should be at least %d", + buffer->stride, buffer->width * 10 / 8); + pData = buffer->data; + dataSize = buffer->stride * buffer->height; + pStride = 0; + rStride = buffer->stride; + break; + case HAL_PIXEL_FORMAT_RGBA_8888: + case HAL_PIXEL_FORMAT_RGBX_8888: + // Single plane, 32bpp. + bytesPerPixel = 4; + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 4; + break; + case HAL_PIXEL_FORMAT_RGB_565: + // Single plane, 16bpp. + bytesPerPixel = 2; + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 2; + break; + case HAL_PIXEL_FORMAT_RGB_888: + // Single plane, 24bpp. + bytesPerPixel = 3; + ALOG_ASSERT(idx == 0, "Wrong index: %d", idx); + pData = buffer->data; + dataSize = buffer->stride * buffer->height * bytesPerPixel; + pStride = bytesPerPixel; + rStride = buffer->stride * 3; + break; + default: + jniThrowExceptionFmt(env, "java/lang/UnsupportedOperationException", + "Pixel format: 0x%x is unsupported", fmt); + break; + } + + *base = pData; + *size = dataSize; + *pixelStride = pStride; + *rowStride = rStride; +} + +static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, + int numPlanes, int writerFormat) { + ALOGV("%s: create SurfacePlane array with size %d", __FUNCTION__, numPlanes); + int rowStride, pixelStride; + uint8_t *pData; + uint32_t dataSize; + jobject byteBuffer; + + int format = Image_getFormat(env, thiz); + if (!isWritable(format) && numPlanes > 0) { + String8 msg; + msg.appendFormat("Format 0x%x is opaque, thus not writable, the number of planes (%d)" + " must be 0", format, numPlanes); + jniThrowException(env, "java/lang/IllegalArgumentException", msg.string()); + return NULL; + } + + jobjectArray surfacePlanes = env->NewObjectArray(numPlanes, gSurfacePlaneClassInfo.clazz, + /*initial_element*/NULL); + if (surfacePlanes == NULL) { + jniThrowRuntimeException(env, "Failed to create SurfacePlane arrays," + " probably out of memory"); + return NULL; + } + + // Buildup buffer info: rowStride, pixelStride and byteBuffers. + LockedImage lockedImg = LockedImage(); + Image_getLockedImage(env, thiz, &lockedImg); + + // Create all SurfacePlanes + writerFormat = Image_getPixelFormat(env, writerFormat); + for (int i = 0; i < numPlanes; i++) { + Image_getLockedImageInfo(env, &lockedImg, i, writerFormat, + &pData, &dataSize, &pixelStride, &rowStride); + byteBuffer = env->NewDirectByteBuffer(pData, dataSize); + if ((byteBuffer == NULL) && (env->ExceptionCheck() == false)) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to allocate ByteBuffer"); + return NULL; + } + + // Finally, create this SurfacePlane. + jobject surfacePlane = env->NewObject(gSurfacePlaneClassInfo.clazz, + gSurfacePlaneClassInfo.ctor, thiz, rowStride, pixelStride, byteBuffer); + env->SetObjectArrayElement(surfacePlanes, i, surfacePlane); + } + + return surfacePlanes; +} + +// -------------------------------Private convenience methods-------------------- + +// Check if buffer with this format is writable. Generally speaking, the opaque formats +// like IMPLEMENTATION_DEFINED is not writable, as the actual buffer formats and layouts +// are unknown to frameworks. +static bool isWritable(int format) { + // Assume all other formats are writable. + return !(format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED || + format == HAL_PIXEL_FORMAT_RAW_OPAQUE); +} + +static bool isPossiblyYUV(PixelFormat format) { + switch (static_cast<int>(format)) { + case HAL_PIXEL_FORMAT_RGBA_8888: + case HAL_PIXEL_FORMAT_RGBX_8888: + case HAL_PIXEL_FORMAT_RGB_888: + case HAL_PIXEL_FORMAT_RGB_565: + case HAL_PIXEL_FORMAT_BGRA_8888: + case HAL_PIXEL_FORMAT_Y8: + case HAL_PIXEL_FORMAT_Y16: + case HAL_PIXEL_FORMAT_RAW16: + case HAL_PIXEL_FORMAT_RAW10: + case HAL_PIXEL_FORMAT_RAW_OPAQUE: + case HAL_PIXEL_FORMAT_BLOB: + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + return false; + + case HAL_PIXEL_FORMAT_YV12: + case HAL_PIXEL_FORMAT_YCbCr_420_888: + case HAL_PIXEL_FORMAT_YCbCr_422_SP: + case HAL_PIXEL_FORMAT_YCrCb_420_SP: + case HAL_PIXEL_FORMAT_YCbCr_422_I: + default: + return true; + } +} + +} // extern "C" + +// ---------------------------------------------------------------------------- + +static JNINativeMethod gImageWriterMethods[] = { + {"nativeClassInit", "()V", (void*)ImageWriter_classInit }, + {"nativeInit", "(Ljava/lang/Object;Landroid/view/Surface;I)J", + (void*)ImageWriter_init }, + {"nativeClose", "(J)V", (void*)ImageWriter_close }, + {"nativeAttachImage", "(JLandroid/media/Image;)V", (void*)ImageWriter_attachImage }, + {"nativeDequeueInputImage", "(JLandroid/media/Image;)V", (void*)ImageWriter_dequeueImage }, + {"nativeQueueInputImage", "(JLandroid/media/Image;JIIII)V", (void*)ImageWriter_queueImage }, + {"cancelImage", "(JLandroid/media/Image;)V", (void*)ImageWriter_cancelImage }, +}; + +static JNINativeMethod gImageMethods[] = { + {"nativeCreatePlanes", "(II)[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;", + (void*)Image_createSurfacePlanes }, + {"nativeGetWidth", "()I", (void*)Image_getWidth }, + {"nativeGetHeight", "()I", (void*)Image_getHeight }, + {"nativeGetFormat", "()I", (void*)Image_getFormat }, +}; + +int register_android_media_ImageWriter(JNIEnv *env) { + + int ret1 = AndroidRuntime::registerNativeMethods(env, + "android/media/ImageWriter", gImageWriterMethods, NELEM(gImageWriterMethods)); + + int ret2 = AndroidRuntime::registerNativeMethods(env, + "android/media/ImageWriter$WriterSurfaceImage", gImageMethods, NELEM(gImageMethods)); + + return (ret1 || ret2); +} + diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 1cf589d..16758d0 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -215,7 +215,7 @@ void JMediaCodec::deleteJavaObjects(JNIEnv *env) { status_t JMediaCodec::setCallback(jobject cb) { if (cb != NULL) { if (mCallbackNotification == NULL) { - mCallbackNotification = new AMessage(kWhatCallbackNotify, id()); + mCallbackNotification = new AMessage(kWhatCallbackNotify, this); } } else { mCallbackNotification.clear(); diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp index d9de7a9..96d7133 100644 --- a/media/jni/android_media_MediaDrm.cpp +++ b/media/jni/android_media_MediaDrm.cpp @@ -59,6 +59,7 @@ namespace android { struct RequestFields { jfieldID data; jfieldID defaultUrl; + jfieldID requestType; }; struct ArrayListFields { @@ -92,6 +93,7 @@ struct EventTypes { jint kEventKeyRequired; jint kEventKeyExpired; jint kEventVendorDefined; + jint kEventSessionReclaimed; } gEventTypes; struct KeyTypes { @@ -100,6 +102,12 @@ struct KeyTypes { jint kKeyTypeRelease; } gKeyTypes; +struct KeyRequestTypes { + jint kKeyRequestTypeInitial; + jint kKeyRequestTypeRenewal; + jint kKeyRequestTypeRelease; +} gKeyRequestTypes; + struct CertificateTypes { jint kCertificateTypeNone; jint kCertificateTypeX509; @@ -181,7 +189,7 @@ void JNIDrmListener::notify(DrmPlugin::EventType eventType, int extra, jint jeventType; // translate DrmPlugin event types into their java equivalents - switch(eventType) { + switch (eventType) { case DrmPlugin::kDrmPluginEventProvisionRequired: jeventType = gEventTypes.kEventProvisionRequired; break; @@ -194,6 +202,9 @@ void JNIDrmListener::notify(DrmPlugin::EventType eventType, int extra, case DrmPlugin::kDrmPluginEventVendorDefined: jeventType = gEventTypes.kEventVendorDefined; break; + case DrmPlugin::kDrmPluginEventSessionReclaimed: + jeventType = gEventTypes.kEventSessionReclaimed; + break; default: ALOGE("Invalid event DrmPlugin::EventType %d, ignored", (int)eventType); return; @@ -232,7 +243,7 @@ static bool throwExceptionAsNecessary( const char *drmMessage = NULL; - switch(err) { + switch (err) { case ERROR_DRM_UNKNOWN: drmMessage = "General DRM error"; break; @@ -438,9 +449,11 @@ static String8 JStringToString8(JNIEnv *env, jstring const &jstr) { Entry e = s.next(); */ -static KeyedVector<String8, String8> HashMapToKeyedVector(JNIEnv *env, jobject &hashMap) { +static KeyedVector<String8, String8> HashMapToKeyedVector( + JNIEnv *env, jobject &hashMap, bool* pIsOK) { jclass clazz = gFields.stringClassId; KeyedVector<String8, String8> keyedVector; + *pIsOK = true; jobject entrySet = env->CallObjectMethod(hashMap, gFields.hashmap.entrySet); if (entrySet) { @@ -451,16 +464,22 @@ static KeyedVector<String8, String8> HashMapToKeyedVector(JNIEnv *env, jobject & jobject entry = env->CallObjectMethod(iterator, gFields.iterator.next); if (entry) { jobject obj = env->CallObjectMethod(entry, gFields.entry.getKey); - if (!env->IsInstanceOf(obj, clazz)) { + if (obj == NULL || !env->IsInstanceOf(obj, clazz)) { jniThrowException(env, "java/lang/IllegalArgumentException", "HashMap key is not a String"); + env->DeleteLocalRef(entry); + *pIsOK = false; + break; } jstring jkey = static_cast<jstring>(obj); obj = env->CallObjectMethod(entry, gFields.entry.getValue); - if (!env->IsInstanceOf(obj, clazz)) { + if (obj == NULL || !env->IsInstanceOf(obj, clazz)) { jniThrowException(env, "java/lang/IllegalArgumentException", "HashMap value is not a String"); + env->DeleteLocalRef(entry); + *pIsOK = false; + break; } jstring jvalue = static_cast<jstring>(obj); @@ -565,6 +584,8 @@ static void android_media_MediaDrm_native_init(JNIEnv *env) { gEventTypes.kEventKeyExpired = env->GetStaticIntField(clazz, field); GET_STATIC_FIELD_ID(field, clazz, "EVENT_VENDOR_DEFINED", "I"); gEventTypes.kEventVendorDefined = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "EVENT_SESSION_RECLAIMED", "I"); + gEventTypes.kEventSessionReclaimed = env->GetStaticIntField(clazz, field); GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_STREAMING", "I"); gKeyTypes.kKeyTypeStreaming = env->GetStaticIntField(clazz, field); @@ -573,6 +594,13 @@ static void android_media_MediaDrm_native_init(JNIEnv *env) { GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_RELEASE", "I"); gKeyTypes.kKeyTypeRelease = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "REQUEST_TYPE_INITIAL", "I"); + gKeyRequestTypes.kKeyRequestTypeInitial = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "REQUEST_TYPE_RENEWAL", "I"); + gKeyRequestTypes.kKeyRequestTypeRenewal = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "REQUEST_TYPE_RELEASE", "I"); + gKeyRequestTypes.kKeyRequestTypeRelease = env->GetStaticIntField(clazz, field); + GET_STATIC_FIELD_ID(field, clazz, "CERTIFICATE_TYPE_NONE", "I"); gCertificateTypes.kCertificateTypeNone = env->GetStaticIntField(clazz, field); GET_STATIC_FIELD_ID(field, clazz, "CERTIFICATE_TYPE_X509", "I"); @@ -581,6 +609,7 @@ static void android_media_MediaDrm_native_init(JNIEnv *env) { FIND_CLASS(clazz, "android/media/MediaDrm$KeyRequest"); GET_FIELD_ID(gFields.keyRequest.data, clazz, "mData", "[B"); GET_FIELD_ID(gFields.keyRequest.defaultUrl, clazz, "mDefaultUrl", "Ljava/lang/String;"); + GET_FIELD_ID(gFields.keyRequest.requestType, clazz, "mRequestType", "I"); FIND_CLASS(clazz, "android/media/MediaDrm$ProvisionRequest"); GET_FIELD_ID(gFields.provisionRequest.data, clazz, "mData", "[B"); @@ -763,14 +792,19 @@ static jobject android_media_MediaDrm_getKeyRequest( KeyedVector<String8, String8> optParams; if (joptParams != NULL) { - optParams = HashMapToKeyedVector(env, joptParams); + bool isOK; + optParams = HashMapToKeyedVector(env, joptParams, &isOK); + if (!isOK) { + return NULL; + } } Vector<uint8_t> request; String8 defaultUrl; + DrmPlugin::KeyRequestType keyRequestType; status_t err = drm->getKeyRequest(sessionId, initData, mimeType, - keyType, optParams, request, defaultUrl); + keyType, optParams, request, defaultUrl, &keyRequestType); if (throwExceptionAsNecessary(env, err, "Failed to get key request")) { return NULL; @@ -789,6 +823,25 @@ static jobject android_media_MediaDrm_getKeyRequest( jstring jdefaultUrl = env->NewStringUTF(defaultUrl.string()); env->SetObjectField(keyObj, gFields.keyRequest.defaultUrl, jdefaultUrl); + + switch (keyRequestType) { + case DrmPlugin::kKeyRequestType_Initial: + env->SetIntField(keyObj, gFields.keyRequest.requestType, + gKeyRequestTypes.kKeyRequestTypeInitial); + break; + case DrmPlugin::kKeyRequestType_Renewal: + env->SetIntField(keyObj, gFields.keyRequest.requestType, + gKeyRequestTypes.kKeyRequestTypeRenewal); + break; + case DrmPlugin::kKeyRequestType_Release: + env->SetIntField(keyObj, gFields.keyRequest.requestType, + gKeyRequestTypes.kKeyRequestTypeRelease); + break; + case DrmPlugin::kKeyRequestType_Unknown: + throwStateException(env, "DRM plugin failure: unknown key request type", + ERROR_DRM_UNKNOWN); + break; + } } return keyObj; diff --git a/media/jni/android_media_MediaMetadataRetriever.cpp b/media/jni/android_media_MediaMetadataRetriever.cpp index 93138fa..2f6bbf4 100644 --- a/media/jni/android_media_MediaMetadataRetriever.cpp +++ b/media/jni/android_media_MediaMetadataRetriever.cpp @@ -40,7 +40,6 @@ using namespace android; struct fields_t { jfieldID context; jclass bitmapClazz; // Must be a global ref - jfieldID nativeBitmap; jmethodID createBitmapMethod; jmethodID createScaledBitmapMethod; jclass configClazz; // Must be a global ref @@ -282,8 +281,7 @@ static jobject android_media_MediaMetadataRetriever_getFrameAtTime(JNIEnv *env, return NULL; } - SkBitmap *bitmap = - (SkBitmap *) env->GetLongField(jBitmap, fields.nativeBitmap); + SkBitmap *bitmap = GraphicsJNI::getSkBitmap(env, jBitmap); bitmap->lockPixels(); rotate((uint16_t*)bitmap->getPixels(), @@ -421,10 +419,6 @@ static void android_media_MediaMetadataRetriever_native_init(JNIEnv *env) if (fields.createScaledBitmapMethod == NULL) { return; } - fields.nativeBitmap = env->GetFieldID(fields.bitmapClazz, "mNativeBitmap", "J"); - if (fields.nativeBitmap == NULL) { - return; - } jclass configClazz = env->FindClass("android/graphics/Bitmap$Config"); if (configClazz == NULL) { diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index 820de5b..b748f3a 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -402,6 +402,18 @@ android_media_MediaPlayer_isPlaying(JNIEnv *env, jobject thiz) } static void +android_media_MediaPlayer_setPlaybackRate(JNIEnv *env, jobject thiz, jfloat rate) +{ + sp<MediaPlayer> mp = getMediaPlayer(env, thiz); + if (mp == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return; + } + ALOGV("setPlaybackRate: %f", rate); + process_media_player_call(env, thiz, mp->setPlaybackRate(rate), NULL, NULL); +} + +static void android_media_MediaPlayer_seekTo(JNIEnv *env, jobject thiz, jint msec) { sp<MediaPlayer> mp = getMediaPlayer(env, thiz); @@ -867,6 +879,7 @@ static JNINativeMethod gMethods[] = { {"_stop", "()V", (void *)android_media_MediaPlayer_stop}, {"getVideoWidth", "()I", (void *)android_media_MediaPlayer_getVideoWidth}, {"getVideoHeight", "()I", (void *)android_media_MediaPlayer_getVideoHeight}, + {"_setPlaybackRate", "(F)V", (void *)android_media_MediaPlayer_setPlaybackRate}, {"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo}, {"_pause", "()V", (void *)android_media_MediaPlayer_pause}, {"isPlaying", "()Z", (void *)android_media_MediaPlayer_isPlaying}, @@ -901,8 +914,8 @@ static int register_android_media_MediaPlayer(JNIEnv *env) return AndroidRuntime::registerNativeMethods(env, "android/media/MediaPlayer", gMethods, NELEM(gMethods)); } - extern int register_android_media_ImageReader(JNIEnv *env); +extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_Crypto(JNIEnv *env); extern int register_android_media_Drm(JNIEnv *env); extern int register_android_media_MediaCodec(JNIEnv *env); @@ -931,6 +944,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) } assert(env != NULL); + if (register_android_media_ImageWriter(env) != JNI_OK) { + ALOGE("ERROR: ImageWriter native registration failed"); + goto bail; + } + if (register_android_media_ImageReader(env) < 0) { ALOGE("ERROR: ImageReader native registration failed"); goto bail; diff --git a/media/jni/soundpool/Android.mk b/media/jni/soundpool/Android.mk index 71ab013..2476056 100644 --- a/media/jni/soundpool/Android.mk +++ b/media/jni/soundpool/Android.mk @@ -2,7 +2,7 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES:= \ - android_media_SoundPool_SoundPoolImpl.cpp \ + android_media_SoundPool.cpp \ SoundPool.cpp \ SoundPoolThread.cpp diff --git a/media/jni/soundpool/SoundPool.cpp b/media/jni/soundpool/SoundPool.cpp index a73209b..10233f3 100644 --- a/media/jni/soundpool/SoundPool.cpp +++ b/media/jni/soundpool/SoundPool.cpp @@ -256,7 +256,7 @@ int SoundPool::play(int sampleID, float leftVolume, float rightVolume, dump(); // allocate a channel - channel = allocateChannel_l(priority); + channel = allocateChannel_l(priority, sampleID); // no channel allocated - return 0 if (!channel) { @@ -271,13 +271,25 @@ int SoundPool::play(int sampleID, float leftVolume, float rightVolume, return channelID; } -SoundChannel* SoundPool::allocateChannel_l(int priority) +SoundChannel* SoundPool::allocateChannel_l(int priority, int sampleID) { List<SoundChannel*>::iterator iter; SoundChannel* channel = NULL; - // allocate a channel + // check if channel for given sampleID still available if (!mChannels.empty()) { + for (iter = mChannels.begin(); iter != mChannels.end(); ++iter) { + if (sampleID == (*iter)->getPrevSampleID() && (*iter)->state() == SoundChannel::IDLE) { + channel = *iter; + mChannels.erase(iter); + ALOGV("Allocated recycled channel for same sampleID"); + break; + } + } + } + + // allocate any channel + if (!channel && !mChannels.empty()) { iter = mChannels.begin(); if (priority >= (*iter)->priority()) { channel = *iter; @@ -626,7 +638,7 @@ status_t Sample::doLoad() goto error; } - if ((numChannels < 1) || (numChannels > 2)) { + if ((numChannels < 1) || (numChannels > 8)) { ALOGE("Sample channel count (%d) out of range", numChannels); status = BAD_VALUE; goto error; @@ -648,6 +660,7 @@ error: void SoundChannel::init(SoundPool* soundPool) { mSoundPool = soundPool; + mPrevSampleID = -1; } // call with sound pool lock held @@ -656,7 +669,7 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV { sp<AudioTrack> oldTrack; sp<AudioTrack> newTrack; - status_t status; + status_t status = NO_ERROR; { // scope for the lock Mutex::Autolock lock(&mLock); @@ -689,8 +702,10 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV size_t frameCount = 0; if (loop) { - frameCount = sample->size()/numChannels/ - ((sample->format() == AUDIO_FORMAT_PCM_16_BIT) ? sizeof(int16_t) : sizeof(uint8_t)); + const audio_format_t format = sample->format(); + const size_t frameSize = audio_is_linear_pcm(format) + ? numChannels * audio_bytes_per_sample(format) : 1; + frameCount = sample->size() / frameSize; } #ifndef USE_SHARED_MEM_BUFFER @@ -701,38 +716,43 @@ void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftV } #endif - // mToggle toggles each time a track is started on a given channel. - // The toggle is concatenated with the SoundChannel address and passed to AudioTrack - // as callback user data. This enables the detection of callbacks received from the old - // audio track while the new one is being started and avoids processing them with - // wrong audio audio buffer size (mAudioBufferSize) - unsigned long toggle = mToggle ^ 1; - void *userData = (void *)((unsigned long)this | toggle); - audio_channel_mask_t channelMask = audio_channel_out_mask_from_count(numChannels); - - // do not create a new audio track if current track is compatible with sample parameters -#ifdef USE_SHARED_MEM_BUFFER - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, sample->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, callback, userData); -#else - uint32_t bufferFrames = (totalFrames + (kDefaultBufferCount - 1)) / kDefaultBufferCount; - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData, - bufferFrames); -#endif - oldTrack = mAudioTrack; - status = newTrack->initCheck(); - if (status != NO_ERROR) { - ALOGE("Error creating AudioTrack"); - goto exit; + if (!mAudioTrack.get() || mPrevSampleID != sample->sampleID()) { + // mToggle toggles each time a track is started on a given channel. + // The toggle is concatenated with the SoundChannel address and passed to AudioTrack + // as callback user data. This enables the detection of callbacks received from the old + // audio track while the new one is being started and avoids processing them with + // wrong audio audio buffer size (mAudioBufferSize) + unsigned long toggle = mToggle ^ 1; + void *userData = (void *)((unsigned long)this | toggle); + audio_channel_mask_t channelMask = audio_channel_out_mask_from_count(numChannels); + + // do not create a new audio track if current track is compatible with sample parameters + #ifdef USE_SHARED_MEM_BUFFER + newTrack = new AudioTrack(streamType, sampleRate, sample->format(), + channelMask, sample->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, callback, userData); + #else + uint32_t bufferFrames = (totalFrames + (kDefaultBufferCount - 1)) / kDefaultBufferCount; + newTrack = new AudioTrack(streamType, sampleRate, sample->format(), + channelMask, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData, + bufferFrames); + #endif + oldTrack = mAudioTrack; + status = newTrack->initCheck(); + if (status != NO_ERROR) { + ALOGE("Error creating AudioTrack"); + goto exit; + } + // From now on, AudioTrack callbacks received with previous toggle value will be ignored. + mToggle = toggle; + mAudioTrack = newTrack; + ALOGV("using new track %p for sample %d", newTrack.get(), sample->sampleID()); + } else { + newTrack = mAudioTrack; + newTrack->setSampleRate(sampleRate); + ALOGV("reusing track %p for sample %d", mAudioTrack.get(), sample->sampleID()); } - ALOGV("setVolume %p", newTrack.get()); newTrack->setVolume(leftVolume, rightVolume); newTrack->setLoop(0, frameCount, loop); - - // From now on, AudioTrack callbacks received with previous toggle value will be ignored. - mToggle = toggle; - mAudioTrack = newTrack; mPos = 0; mSample = sample; mChannelID = nextChannelID; @@ -875,6 +895,7 @@ bool SoundChannel::doStop_l() setVolume_l(0, 0); ALOGV("stop"); mAudioTrack->stop(); + mPrevSampleID = mSample->sampleID(); mSample.clear(); mState = IDLE; mPriority = IDLE_PRIORITY; diff --git a/media/jni/soundpool/SoundPool.h b/media/jni/soundpool/SoundPool.h index 9d9cbdf..4aacf53 100644 --- a/media/jni/soundpool/SoundPool.h +++ b/media/jni/soundpool/SoundPool.h @@ -72,8 +72,8 @@ private: volatile int32_t mRefCount; uint16_t mSampleID; uint16_t mSampleRate; - uint8_t mState : 3; - uint8_t mNumChannels : 2; + uint8_t mState; + uint8_t mNumChannels; audio_format_t mFormat; int mFd; int64_t mOffset; @@ -136,6 +136,7 @@ public: void nextEvent(); int nextChannelID() { return mNextEvent.channelID(); } void dump(); + int getPrevSampleID(void) { return mPrevSampleID; } private: static void callback(int event, void* user, void *info); @@ -152,6 +153,7 @@ private: int mAudioBufferSize; unsigned long mToggle; bool mAutoPaused; + int mPrevSampleID; }; // application object for managing a pool of sounds @@ -193,7 +195,7 @@ private: sp<Sample> findSample(int sampleID) { return mSamples.valueFor(sampleID); } SoundChannel* findChannel (int channelID); SoundChannel* findNextChannel (int channelID); - SoundChannel* allocateChannel_l(int priority); + SoundChannel* allocateChannel_l(int priority, int sampleID); void moveToFront_l(SoundChannel* channel); void notify(SoundPoolEvent event); void dump(); diff --git a/media/jni/soundpool/android_media_SoundPool_SoundPoolImpl.cpp b/media/jni/soundpool/android_media_SoundPool.cpp index b2333f8..fc4cf05 100644 --- a/media/jni/soundpool/android_media_SoundPool_SoundPoolImpl.cpp +++ b/media/jni/soundpool/android_media_SoundPool.cpp @@ -47,10 +47,10 @@ static audio_attributes_fields_t javaAudioAttrFields; // ---------------------------------------------------------------------------- static jint -android_media_SoundPool_SoundPoolImpl_load_FD(JNIEnv *env, jobject thiz, jobject fileDescriptor, +android_media_SoundPool_load_FD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length, jint priority) { - ALOGV("android_media_SoundPool_SoundPoolImpl_load_FD"); + ALOGV("android_media_SoundPool_load_FD"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return 0; return (jint) ap->load(jniGetFDFromFileDescriptor(env, fileDescriptor), @@ -58,104 +58,104 @@ android_media_SoundPool_SoundPoolImpl_load_FD(JNIEnv *env, jobject thiz, jobject } static jboolean -android_media_SoundPool_SoundPoolImpl_unload(JNIEnv *env, jobject thiz, jint sampleID) { - ALOGV("android_media_SoundPool_SoundPoolImpl_unload\n"); +android_media_SoundPool_unload(JNIEnv *env, jobject thiz, jint sampleID) { + ALOGV("android_media_SoundPool_unload\n"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return JNI_FALSE; return ap->unload(sampleID) ? JNI_TRUE : JNI_FALSE; } static jint -android_media_SoundPool_SoundPoolImpl_play(JNIEnv *env, jobject thiz, jint sampleID, +android_media_SoundPool_play(JNIEnv *env, jobject thiz, jint sampleID, jfloat leftVolume, jfloat rightVolume, jint priority, jint loop, jfloat rate) { - ALOGV("android_media_SoundPool_SoundPoolImpl_play\n"); + ALOGV("android_media_SoundPool_play\n"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return 0; return (jint) ap->play(sampleID, leftVolume, rightVolume, priority, loop, rate); } static void -android_media_SoundPool_SoundPoolImpl_pause(JNIEnv *env, jobject thiz, jint channelID) +android_media_SoundPool_pause(JNIEnv *env, jobject thiz, jint channelID) { - ALOGV("android_media_SoundPool_SoundPoolImpl_pause"); + ALOGV("android_media_SoundPool_pause"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->pause(channelID); } static void -android_media_SoundPool_SoundPoolImpl_resume(JNIEnv *env, jobject thiz, jint channelID) +android_media_SoundPool_resume(JNIEnv *env, jobject thiz, jint channelID) { - ALOGV("android_media_SoundPool_SoundPoolImpl_resume"); + ALOGV("android_media_SoundPool_resume"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->resume(channelID); } static void -android_media_SoundPool_SoundPoolImpl_autoPause(JNIEnv *env, jobject thiz) +android_media_SoundPool_autoPause(JNIEnv *env, jobject thiz) { - ALOGV("android_media_SoundPool_SoundPoolImpl_autoPause"); + ALOGV("android_media_SoundPool_autoPause"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->autoPause(); } static void -android_media_SoundPool_SoundPoolImpl_autoResume(JNIEnv *env, jobject thiz) +android_media_SoundPool_autoResume(JNIEnv *env, jobject thiz) { - ALOGV("android_media_SoundPool_SoundPoolImpl_autoResume"); + ALOGV("android_media_SoundPool_autoResume"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->autoResume(); } static void -android_media_SoundPool_SoundPoolImpl_stop(JNIEnv *env, jobject thiz, jint channelID) +android_media_SoundPool_stop(JNIEnv *env, jobject thiz, jint channelID) { - ALOGV("android_media_SoundPool_SoundPoolImpl_stop"); + ALOGV("android_media_SoundPool_stop"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->stop(channelID); } static void -android_media_SoundPool_SoundPoolImpl_setVolume(JNIEnv *env, jobject thiz, jint channelID, +android_media_SoundPool_setVolume(JNIEnv *env, jobject thiz, jint channelID, jfloat leftVolume, jfloat rightVolume) { - ALOGV("android_media_SoundPool_SoundPoolImpl_setVolume"); + ALOGV("android_media_SoundPool_setVolume"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->setVolume(channelID, (float) leftVolume, (float) rightVolume); } static void -android_media_SoundPool_SoundPoolImpl_setPriority(JNIEnv *env, jobject thiz, jint channelID, +android_media_SoundPool_setPriority(JNIEnv *env, jobject thiz, jint channelID, jint priority) { - ALOGV("android_media_SoundPool_SoundPoolImpl_setPriority"); + ALOGV("android_media_SoundPool_setPriority"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->setPriority(channelID, (int) priority); } static void -android_media_SoundPool_SoundPoolImpl_setLoop(JNIEnv *env, jobject thiz, jint channelID, +android_media_SoundPool_setLoop(JNIEnv *env, jobject thiz, jint channelID, int loop) { - ALOGV("android_media_SoundPool_SoundPoolImpl_setLoop"); + ALOGV("android_media_SoundPool_setLoop"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->setLoop(channelID, loop); } static void -android_media_SoundPool_SoundPoolImpl_setRate(JNIEnv *env, jobject thiz, jint channelID, +android_media_SoundPool_setRate(JNIEnv *env, jobject thiz, jint channelID, jfloat rate) { - ALOGV("android_media_SoundPool_SoundPoolImpl_setRate"); + ALOGV("android_media_SoundPool_setRate"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap == NULL) return; ap->setRate(channelID, (float) rate); @@ -169,7 +169,7 @@ static void android_media_callback(SoundPoolEvent event, SoundPool* soundPool, v } static jint -android_media_SoundPool_SoundPoolImpl_native_setup(JNIEnv *env, jobject thiz, jobject weakRef, +android_media_SoundPool_native_setup(JNIEnv *env, jobject thiz, jobject weakRef, jint maxChannels, jobject jaa) { if (jaa == 0) { @@ -191,7 +191,7 @@ android_media_SoundPool_SoundPoolImpl_native_setup(JNIEnv *env, jobject thiz, jo (audio_content_type_t) env->GetIntField(jaa, javaAudioAttrFields.fieldContentType); paa->flags = env->GetIntField(jaa, javaAudioAttrFields.fieldFlags); - ALOGV("android_media_SoundPool_SoundPoolImpl_native_setup"); + ALOGV("android_media_SoundPool_native_setup"); SoundPool *ap = new SoundPool(maxChannels, paa); if (ap == NULL) { return -1; @@ -211,9 +211,9 @@ android_media_SoundPool_SoundPoolImpl_native_setup(JNIEnv *env, jobject thiz, jo } static void -android_media_SoundPool_SoundPoolImpl_release(JNIEnv *env, jobject thiz) +android_media_SoundPool_release(JNIEnv *env, jobject thiz) { - ALOGV("android_media_SoundPool_SoundPoolImpl_release"); + ALOGV("android_media_SoundPool_release"); SoundPool *ap = MusterSoundPool(env, thiz); if (ap != NULL) { @@ -236,63 +236,63 @@ android_media_SoundPool_SoundPoolImpl_release(JNIEnv *env, jobject thiz) static JNINativeMethod gMethods[] = { { "_load", "(Ljava/io/FileDescriptor;JJI)I", - (void *)android_media_SoundPool_SoundPoolImpl_load_FD + (void *)android_media_SoundPool_load_FD }, { "unload", "(I)Z", - (void *)android_media_SoundPool_SoundPoolImpl_unload + (void *)android_media_SoundPool_unload }, { "_play", "(IFFIIF)I", - (void *)android_media_SoundPool_SoundPoolImpl_play + (void *)android_media_SoundPool_play }, { "pause", "(I)V", - (void *)android_media_SoundPool_SoundPoolImpl_pause + (void *)android_media_SoundPool_pause }, { "resume", "(I)V", - (void *)android_media_SoundPool_SoundPoolImpl_resume + (void *)android_media_SoundPool_resume }, { "autoPause", "()V", - (void *)android_media_SoundPool_SoundPoolImpl_autoPause + (void *)android_media_SoundPool_autoPause }, { "autoResume", "()V", - (void *)android_media_SoundPool_SoundPoolImpl_autoResume + (void *)android_media_SoundPool_autoResume }, { "stop", "(I)V", - (void *)android_media_SoundPool_SoundPoolImpl_stop + (void *)android_media_SoundPool_stop }, { "_setVolume", "(IFF)V", - (void *)android_media_SoundPool_SoundPoolImpl_setVolume + (void *)android_media_SoundPool_setVolume }, { "setPriority", "(II)V", - (void *)android_media_SoundPool_SoundPoolImpl_setPriority + (void *)android_media_SoundPool_setPriority }, { "setLoop", "(II)V", - (void *)android_media_SoundPool_SoundPoolImpl_setLoop + (void *)android_media_SoundPool_setLoop }, { "setRate", "(IF)V", - (void *)android_media_SoundPool_SoundPoolImpl_setRate + (void *)android_media_SoundPool_setRate }, { "native_setup", "(Ljava/lang/Object;ILjava/lang/Object;)I", - (void*)android_media_SoundPool_SoundPoolImpl_native_setup + (void*)android_media_SoundPool_native_setup }, { "release", "()V", - (void*)android_media_SoundPool_SoundPoolImpl_release + (void*)android_media_SoundPool_release } }; -static const char* const kClassPathName = "android/media/SoundPool$SoundPoolImpl"; +static const char* const kClassPathName = "android/media/SoundPool"; jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { @@ -314,14 +314,14 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) fields.mNativeContext = env->GetFieldID(clazz, "mNativeContext", "J"); if (fields.mNativeContext == NULL) { - ALOGE("Can't find SoundPoolImpl.mNativeContext"); + ALOGE("Can't find SoundPool.mNativeContext"); return result; } fields.mPostEvent = env->GetStaticMethodID(clazz, "postEventFromNative", "(Ljava/lang/Object;IIILjava/lang/Object;)V"); if (fields.mPostEvent == NULL) { - ALOGE("Can't find android/media/SoundPoolImpl.postEventFromNative"); + ALOGE("Can't find android/media/SoundPool.postEventFromNative"); return result; } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index cc50c43..3bb5f01 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -20,8 +20,6 @@ import android.hardware.CameraInfo; import android.hardware.ICamera; import android.hardware.ICameraClient; import android.hardware.ICameraServiceListener; -import android.hardware.IProCameraCallbacks; -import android.hardware.IProCameraUser; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.impl.CameraMetadataNative; @@ -181,30 +179,6 @@ public class CameraBinderTest extends AndroidTestCase { } } - static class DummyProCameraCallbacks extends DummyBase implements IProCameraCallbacks { - } - - @SmallTest - public void testConnectPro() throws Exception { - for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) { - - IProCameraCallbacks dummyCallbacks = new DummyProCameraCallbacks(); - - String clientPackageName = getContext().getPackageName(); - - BinderHolder holder = new BinderHolder(); - CameraBinderDecorator.newInstance(mUtils.getCameraService()) - .connectPro(dummyCallbacks, cameraId, - clientPackageName, CameraBinderTestUtils.USE_CALLING_UID, holder); - IProCameraUser cameraUser = IProCameraUser.Stub.asInterface(holder.getBinder()); - assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); - - Log.v(TAG, String.format("Camera %s connected", cameraId)); - - cameraUser.disconnect(); - } - } - @SmallTest public void testConnectLegacy() throws Exception { final int CAMERA_HAL_API_VERSION_1_0 = 0x100; @@ -316,6 +290,11 @@ public class CameraBinderTest extends AndroidTestCase { throws RemoteException { Log.v(TAG, String.format("Camera %d has status changed to 0x%x", cameraId, status)); } + public void onTorchStatusChanged(int status, String cameraId) + throws RemoteException { + Log.v(TAG, String.format("Camera %s has torch status changed to 0x%x", + cameraId, status)); + } } /** diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java index 3cae19d..e05e1fc 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java @@ -25,6 +25,7 @@ import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; +import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.BinderHolder; import android.media.Image; import android.media.ImageReader; @@ -67,6 +68,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { private CameraBinderTestUtils mUtils; private ICameraDeviceCallbacks.Stub mMockCb; private Surface mSurface; + private OutputConfiguration mOutputConfiguration; private HandlerThread mHandlerThread; private Handler mHandler; ImageReader mImageReader; @@ -147,6 +149,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { MAX_NUM_IMAGES); mImageReader.setOnImageAvailableListener(new ImageDropperListener(), mHandler); mSurface = mImageReader.getSurface(); + mOutputConfiguration = new OutputConfiguration(mSurface); } private CaptureRequest.Builder createDefaultBuilder(boolean needStream) throws Exception { @@ -161,8 +164,7 @@ public class CameraDeviceBinderTest extends AndroidTestCase { assertFalse(request.isEmpty()); assertFalse(metadata.isEmpty()); if (needStream) { - int streamId = mCameraUser.createStream(/* ignored */10, /* ignored */20, - /* ignored */30, mSurface); + int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); request.addTarget(mSurface); } @@ -235,12 +237,11 @@ public class CameraDeviceBinderTest extends AndroidTestCase { @SmallTest public void testCreateStream() throws Exception { - int streamId = mCameraUser.createStream(/* ignored */10, /* ignored */20, /* ignored */30, - mSurface); + int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); assertEquals(CameraBinderTestUtils.ALREADY_EXISTS, - mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, mSurface)); + mCameraUser.createStream(mOutputConfiguration)); assertEquals(CameraBinderTestUtils.NO_ERROR, mCameraUser.deleteStream(streamId)); } @@ -257,20 +258,19 @@ public class CameraDeviceBinderTest extends AndroidTestCase { public void testCreateStreamTwo() throws Exception { // Create first stream - int streamId = mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, - mSurface); + int streamId = mCameraUser.createStream(mOutputConfiguration); assertEquals(0, streamId); assertEquals(CameraBinderTestUtils.ALREADY_EXISTS, - mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, mSurface)); + mCameraUser.createStream(mOutputConfiguration)); // Create second stream with a different surface. SurfaceTexture surfaceTexture = new SurfaceTexture(/* ignored */0); surfaceTexture.setDefaultBufferSize(640, 480); Surface surface2 = new Surface(surfaceTexture); + OutputConfiguration output2 = new OutputConfiguration(surface2); - int streamId2 = mCameraUser.createStream(/* ignored */0, /* ignored */0, /* ignored */0, - surface2); + int streamId2 = mCameraUser.createStream(output2); assertEquals(1, streamId2); // Clean up streams |
