summaryrefslogtreecommitdiffstats
path: root/media/java
diff options
context:
space:
mode:
Diffstat (limited to 'media/java')
-rw-r--r--media/java/android/media/AudioAttributes.java7
-rw-r--r--media/java/android/media/AudioDevicePort.java7
-rw-r--r--media/java/android/media/AudioManager.java36
-rw-r--r--media/java/android/media/AudioPatch.java21
-rw-r--r--media/java/android/media/AudioPort.java18
-rw-r--r--media/java/android/media/AudioService.java130
-rw-r--r--media/java/android/media/AudioSystem.java73
-rw-r--r--media/java/android/media/IAudioService.aidl4
-rw-r--r--media/java/android/media/Image.java6
-rw-r--r--media/java/android/media/MediaCodec.java10
-rw-r--r--media/java/android/media/audiopolicy/AudioMix.java13
-rw-r--r--media/java/android/media/audiopolicy/AudioMixingRule.java2
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicy.java134
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicyConfig.java22
-rw-r--r--media/java/android/media/tv/ITvInputSessionWrapper.java1
-rw-r--r--media/java/android/media/tv/TvContract.java24
-rw-r--r--media/java/android/media/tv/TvInputInfo.java3
-rw-r--r--media/java/android/media/tv/TvInputManager.java272
-rw-r--r--media/java/android/media/tv/TvInputService.java211
-rw-r--r--media/java/android/media/tv/TvStreamConfig.java14
-rw-r--r--media/java/android/media/tv/TvView.java149
21 files changed, 890 insertions, 267 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 17d3251..20c4978 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -161,6 +161,12 @@ public final class AudioAttributes implements Parcelable {
* Usage value to use when the usage is for game audio.
*/
public final static int USAGE_GAME = 14;
+ /**
+ * @hide
+ * Usage value to use when feeding audio to the platform and replacing "traditional" audio
+ * source, such as audio capture devices.
+ */
+ public final static int USAGE_VIRTUAL_SOURCE = 15;
/**
* Flag defining a behavior where the audibility of the sound will be ensured by the system.
@@ -374,6 +380,7 @@ public final class AudioAttributes implements Parcelable {
case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
case USAGE_ASSISTANCE_SONIFICATION:
case USAGE_GAME:
+ case USAGE_VIRTUAL_SOURCE:
mUsage = usage;
break;
default:
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 7975e04..b10736b 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -16,6 +16,8 @@
package android.media;
+import android.media.AudioSystem;
+
/**
* The AudioDevicePort is a specialized type of AudioPort
* describing an input (e.g microphone) or output device (e.g speaker)
@@ -85,8 +87,11 @@ public class AudioDevicePort extends AudioPort {
@Override
public String toString() {
+ String type = (mRole == ROLE_SOURCE ?
+ AudioSystem.getInputDeviceName(mType) :
+ AudioSystem.getOutputDeviceName(mType));
return "{" + super.toString()
- + ", mType:" + mType
+ + ", mType: " + type
+ ", mAddress: " + mAddress
+ "}";
}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 69c1142..8fc0b8e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -353,21 +353,6 @@ public class AudioManager {
*/
@Deprecated public static final int NUM_STREAMS = AudioSystem.NUM_STREAMS;
-
- /** @hide Default volume index values for audio streams */
- public static final int[] DEFAULT_STREAM_VOLUME = new int[] {
- 4, // STREAM_VOICE_CALL
- 7, // STREAM_SYSTEM
- 5, // STREAM_RING
- 11, // STREAM_MUSIC
- 6, // STREAM_ALARM
- 5, // STREAM_NOTIFICATION
- 7, // STREAM_BLUETOOTH_SCO
- 7, // STREAM_SYSTEM_ENFORCED
- 11, // STREAM_DTMF
- 11 // STREAM_TTS
- };
-
/**
* Increase the ringer volume.
*
@@ -512,8 +497,11 @@ public class AudioManager {
*/
public static final int RINGER_MODE_NORMAL = 2;
- // maximum valid ringer mode value. Values must start from 0 and be contiguous.
- private static final int RINGER_MODE_MAX = RINGER_MODE_NORMAL;
+ /**
+ * Maximum valid ringer mode value. Values must start from 0 and be contiguous.
+ * @hide
+ */
+ public static final int RINGER_MODE_MAX = RINGER_MODE_NORMAL;
/**
* Vibrate type that corresponds to the ringer.
@@ -887,7 +875,13 @@ public class AudioManager {
if (ringerMode < 0 || ringerMode > RINGER_MODE_MAX) {
return false;
}
- return true;
+ IAudioService service = getService();
+ try {
+ return service.isValidRingerMode(ringerMode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in isValidRingerMode", e);
+ return false;
+ }
}
/**
@@ -2669,9 +2663,13 @@ public class AudioManager {
}
IAudioService service = getService();
try {
- if (!service.registerAudioPolicy(policy.getConfig(), policy.token())) {
+ String regId = service.registerAudioPolicy(policy.getConfig(), policy.token());
+ if (regId == null) {
return ERROR;
+ } else {
+ policy.setRegistration(regId);
}
+ // successful registration
} catch (RemoteException e) {
Log.e(TAG, "Dead object in registerAudioPolicyAsync()", e);
return ERROR;
diff --git a/media/java/android/media/AudioPatch.java b/media/java/android/media/AudioPatch.java
index 81eceb1..acadb41 100644
--- a/media/java/android/media/AudioPatch.java
+++ b/media/java/android/media/AudioPatch.java
@@ -52,4 +52,25 @@ public class AudioPatch {
public AudioPortConfig[] sinks() {
return mSinks;
}
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ s.append("mHandle: ");
+ s.append(mHandle.toString());
+
+ s.append(" mSources: {");
+ for (AudioPortConfig source : mSources) {
+ s.append(source.toString());
+ s.append(", ");
+ }
+ s.append("} mSinks: {");
+ for (AudioPortConfig sink : mSinks) {
+ s.append(sink.toString());
+ s.append(", ");
+ }
+ s.append("}");
+
+ return s.toString();
+ }
}
diff --git a/media/java/android/media/AudioPort.java b/media/java/android/media/AudioPort.java
index 53212aa..1ab7e89 100644
--- a/media/java/android/media/AudioPort.java
+++ b/media/java/android/media/AudioPort.java
@@ -67,7 +67,7 @@ public class AudioPort {
AudioHandle mHandle;
- private final int mRole;
+ protected final int mRole;
private final int[] mSamplingRates;
private final int[] mChannelMasks;
private final int[] mFormats;
@@ -176,8 +176,20 @@ public class AudioPort {
@Override
public String toString() {
- return "{mHandle:" + mHandle
- + ", mRole:" + mRole
+ String role = Integer.toString(mRole);
+ switch (mRole) {
+ case ROLE_NONE:
+ role = "NONE";
+ break;
+ case ROLE_SOURCE:
+ role = "SOURCE";
+ break;
+ case ROLE_SINK:
+ role = "SINK";
+ break;
+ }
+ return "{mHandle: " + mHandle
+ + ", mRole: " + role
+ "}";
}
}
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index b0bf4a1..2f68382 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -48,6 +48,7 @@ import android.hardware.hdmi.HdmiTvClient;
import android.hardware.usb.UsbManager;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
+import android.media.audiopolicy.AudioMix;
import android.media.audiopolicy.AudioPolicyConfig;
import android.media.session.MediaSessionLegacyHelper;
import android.os.Binder;
@@ -118,6 +119,10 @@ public class AudioService extends IAudioService.Stub {
/** Debug audio mode */
protected static final boolean DEBUG_MODE = Log.isLoggable(TAG + ".MOD", Log.DEBUG);
+
+ /** Debug audio policy feature */
+ protected static final boolean DEBUG_AP = Log.isLoggable(TAG + ".AP", Log.DEBUG);
+
/** Debug volumes */
protected static final boolean DEBUG_VOL = Log.isLoggable(TAG + ".VOL", Log.DEBUG);
@@ -248,7 +253,7 @@ public class AudioService extends IAudioService.Stub {
private final int[][] SOUND_EFFECT_FILES_MAP = new int[AudioManager.NUM_SOUND_EFFECTS][2];
/** @hide Maximum volume index values for audio streams */
- private static final int[] MAX_STREAM_VOLUME = new int[] {
+ private static int[] MAX_STREAM_VOLUME = new int[] {
5, // STREAM_VOICE_CALL
7, // STREAM_SYSTEM
7, // STREAM_RING
@@ -260,6 +265,20 @@ public class AudioService extends IAudioService.Stub {
15, // STREAM_DTMF
15 // STREAM_TTS
};
+
+ private static int[] DEFAULT_STREAM_VOLUME = new int[] {
+ 4, // STREAM_VOICE_CALL
+ 7, // STREAM_SYSTEM
+ 5, // STREAM_RING
+ 11, // STREAM_MUSIC
+ 6, // STREAM_ALARM
+ 5, // STREAM_NOTIFICATION
+ 7, // STREAM_BLUETOOTH_SCO
+ 7, // STREAM_SYSTEM_ENFORCED
+ 11, // STREAM_DTMF
+ 11 // STREAM_TTS
+ };
+
/* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
* of another stream: This avoids multiplying the volume settings for hidden
* stream types that follow other stream behavior for volume settings
@@ -541,12 +560,18 @@ public class AudioService extends IAudioService.Stub {
mHasVibrator = vibrator == null ? false : vibrator.hasVibrator();
// Intialized volume
- MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = SystemProperties.getInt(
- "ro.config.vc_call_vol_steps",
- MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL]);
- MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] = SystemProperties.getInt(
- "ro.config.media_vol_steps",
- MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]);
+ int maxVolume = SystemProperties.getInt("ro.config.vc_call_vol_steps",
+ MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL]);
+ if (maxVolume != MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL]) {
+ MAX_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = maxVolume;
+ DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] = (maxVolume * 3) / 4;
+ }
+ maxVolume = SystemProperties.getInt("ro.config.media_vol_steps",
+ MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]);
+ if (maxVolume != MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]) {
+ MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] = maxVolume;
+ DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] = (maxVolume * 3) / 4;
+ }
sSoundEffectVolumeDb = context.getResources().getInteger(
com.android.internal.R.integer.config_soundEffectVolumeDb);
@@ -843,7 +868,7 @@ public class AudioService extends IAudioService.Stub {
int ringerMode = ringerModeFromSettings;
// sanity check in case the settings are restored from a device with incompatible
// ringer modes
- if (!AudioManager.isValidRingerMode(ringerMode)) {
+ if (!isValidRingerMode(ringerMode)) {
ringerMode = AudioManager.RINGER_MODE_NORMAL;
}
if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
@@ -1625,6 +1650,10 @@ public class AudioService extends IAudioService.Stub {
return MAX_STREAM_VOLUME[streamType];
}
+ public static int getDefaultStreamVolume(int streamType) {
+ return DEFAULT_STREAM_VOLUME[streamType];
+ }
+
/** @see AudioManager#getStreamVolume(int) */
public int getStreamVolume(int streamType) {
ensureValidStreamType(streamType);
@@ -1733,17 +1762,22 @@ public class AudioService extends IAudioService.Stub {
}
private void ensureValidRingerMode(int ringerMode) {
- if (!AudioManager.isValidRingerMode(ringerMode)) {
+ if (!isValidRingerMode(ringerMode)) {
throw new IllegalArgumentException("Bad ringer mode " + ringerMode);
}
}
+ /** @see AudioManager#isValidRingerMode(int) */
+ public boolean isValidRingerMode(int ringerMode) {
+ return ringerMode >= 0 && ringerMode <= AudioManager.RINGER_MODE_MAX;
+ }
+
/** @see AudioManager#setRingerMode(int) */
public void setRingerMode(int ringerMode, boolean checkZen) {
if (mUseFixedVolume || isPlatformTelevision()) {
return;
}
-
+ ensureValidRingerMode(ringerMode);
if ((ringerMode == AudioManager.RINGER_MODE_VIBRATE) && !mHasVibrator) {
ringerMode = AudioManager.RINGER_MODE_SILENT;
}
@@ -2545,13 +2579,17 @@ public class AudioService extends IAudioService.Stub {
if (mScoAudioState == SCO_STATE_INACTIVE) {
mScoAudioMode = scoAudioMode;
if (scoAudioMode == SCO_MODE_UNDEFINED) {
- mScoAudioMode = new Integer(Settings.Global.getInt(
- mContentResolver,
- "bluetooth_sco_channel_"+
- mBluetoothHeadsetDevice.getAddress(),
- SCO_MODE_VIRTUAL_CALL));
- if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) {
- mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
+ if (mBluetoothHeadsetDevice != null) {
+ mScoAudioMode = new Integer(Settings.Global.getInt(
+ mContentResolver,
+ "bluetooth_sco_channel_"+
+ mBluetoothHeadsetDevice.getAddress(),
+ SCO_MODE_VIRTUAL_CALL));
+ if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) {
+ mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
+ }
+ } else {
+ mScoAudioMode = SCO_MODE_RAW;
}
}
if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null) {
@@ -3351,7 +3389,7 @@ public class AudioService extends IAudioService.Stub {
// only be stale values
if ((mStreamType == AudioSystem.STREAM_SYSTEM) ||
(mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED)) {
- int index = 10 * AudioManager.DEFAULT_STREAM_VOLUME[mStreamType];
+ int index = 10 * DEFAULT_STREAM_VOLUME[mStreamType];
synchronized (mCameraSoundForced) {
if (mCameraSoundForced) {
index = mIndexMax;
@@ -3375,7 +3413,7 @@ public class AudioService extends IAudioService.Stub {
// if no volume stored for current stream and device, use default volume if default
// device, continue otherwise
int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ?
- AudioManager.DEFAULT_STREAM_VOLUME[mStreamType] : -1;
+ DEFAULT_STREAM_VOLUME[mStreamType] : -1;
int index = Settings.System.getIntForUser(
mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
if (index == -1) {
@@ -5601,31 +5639,33 @@ public class AudioService extends IAudioService.Stub {
//==========================================================================================
// Audio policy management
//==========================================================================================
- public boolean registerAudioPolicy(AudioPolicyConfig policyConfig, IBinder cb) {
+ public String registerAudioPolicy(AudioPolicyConfig policyConfig, IBinder cb) {
//Log.v(TAG, "registerAudioPolicy for " + cb + " got policy:" + policyConfig);
+ String regId = null;
boolean hasPermissionForPolicy =
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_AUDIO_ROUTING));
if (!hasPermissionForPolicy) {
Slog.w(TAG, "Can't register audio policy for pid " + Binder.getCallingPid() + " / uid "
+ Binder.getCallingUid() + ", need MODIFY_AUDIO_ROUTING");
- return false;
+ return null;
}
synchronized (mAudioPolicies) {
- AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, cb);
try {
+ AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, cb);
cb.linkToDeath(app, 0/*flags*/);
+ regId = app.connectMixes();
mAudioPolicies.put(cb, app);
} catch (RemoteException e) {
// audio policy owner has already died!
Slog.w(TAG, "Audio policy registration failed, could not link to " + cb +
" binder death", e);
- return false;
+ return null;
}
}
- // TODO implement registration with native audio policy (including permission check)
- return true;
+ return regId;
}
+
public void unregisterAudioPolicyAsync(IBinder cb) {
synchronized (mAudioPolicies) {
AudioPolicyProxy app = mAudioPolicies.remove(cb);
@@ -5635,27 +5675,59 @@ public class AudioService extends IAudioService.Stub {
} else {
cb.unlinkToDeath(app, 0/*flags*/);
}
+ app.disconnectMixes();
}
- // TODO implement registration with native audio policy
+ // TODO implement clearing mix attribute matching info in native audio policy
}
- public class AudioPolicyProxy implements IBinder.DeathRecipient {
+ /**
+ * This internal class inherits from AudioPolicyConfig which contains all the mixes and
+ * their configurations.
+ */
+ public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
private static final String TAG = "AudioPolicyProxy";
AudioPolicyConfig mConfig;
IBinder mToken;
AudioPolicyProxy(AudioPolicyConfig config, IBinder token) {
- mConfig = config;
+ super(config);
+ setRegistration(new String(config.toString() + ":ap:" + mAudioPolicyCounter++));
mToken = token;
}
public void binderDied() {
synchronized (mAudioPolicies) {
- Log.v(TAG, "audio policy " + mToken + " died");
+ Log.i(TAG, "audio policy " + mToken + " died");
mAudioPolicies.remove(mToken);
+ disconnectMixes();
+ }
+ }
+
+ String connectMixes() {
+ updateMixes(AudioSystem.DEVICE_STATE_AVAILABLE);
+ return mRegistrationId;
+ }
+
+ void disconnectMixes() {
+ updateMixes(AudioSystem.DEVICE_STATE_UNAVAILABLE);
+ }
+
+ void updateMixes(int connectionState) {
+ for (AudioMix mix : mMixes) {
+ // TODO implement sending the mix attribute matching info to native audio policy
+ if (DEBUG_AP) {
+ Log.v(TAG, "AudioPolicyProxy connect mix state=" + connectionState
+ + " addr=" + mix.getRegistration()); }
+ AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_REMOTE_SUBMIX,
+ connectionState,
+ mix.getRegistration());
+ AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX,
+ connectionState,
+ mix.getRegistration());
}
}
};
private HashMap<IBinder, AudioPolicyProxy> mAudioPolicies =
new HashMap<IBinder, AudioPolicyProxy>();
+ private int mAudioPolicyCounter = 0; // always accessed synchronized on mAudioPolicies
}
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 9a76f94..e795fa7 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -255,6 +255,7 @@ public class AudioSystem
public static final int DEVICE_OUT_SPDIF = 0x80000;
public static final int DEVICE_OUT_FM = 0x100000;
public static final int DEVICE_OUT_AUX_LINE = 0x200000;
+ public static final int DEVICE_OUT_SPEAKER_SAFE = 0x400000;
public static final int DEVICE_OUT_DEFAULT = DEVICE_BIT_DEFAULT;
@@ -280,6 +281,7 @@ public class AudioSystem
DEVICE_OUT_SPDIF |
DEVICE_OUT_FM |
DEVICE_OUT_AUX_LINE |
+ DEVICE_OUT_SPEAKER_SAFE |
DEVICE_OUT_DEFAULT);
public static final int DEVICE_OUT_ALL_A2DP = (DEVICE_OUT_BLUETOOTH_A2DP |
DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
@@ -372,6 +374,27 @@ public class AudioSystem
public static final String DEVICE_OUT_SPDIF_NAME = "spdif";
public static final String DEVICE_OUT_FM_NAME = "fm_transmitter";
public static final String DEVICE_OUT_AUX_LINE_NAME = "aux_line";
+ public static final String DEVICE_OUT_SPEAKER_SAFE_NAME = "speaker_safe";
+
+ public static final String DEVICE_IN_COMMUNICATION_NAME = "communication";
+ public static final String DEVICE_IN_AMBIENT_NAME = "ambient";
+ public static final String DEVICE_IN_BUILTIN_MIC_NAME = "mic";
+ public static final String DEVICE_IN_BLUETOOTH_SCO_HEADSET_NAME = "bt_sco_hs";
+ public static final String DEVICE_IN_WIRED_HEADSET_NAME = "headset";
+ public static final String DEVICE_IN_AUX_DIGITAL_NAME = "aux_digital";
+ public static final String DEVICE_IN_TELEPHONY_RX_NAME = "telephony_rx";
+ public static final String DEVICE_IN_BACK_MIC_NAME = "back_mic";
+ public static final String DEVICE_IN_REMOTE_SUBMIX_NAME = "remote_submix";
+ public static final String DEVICE_IN_ANLG_DOCK_HEADSET_NAME = "analog_dock";
+ public static final String DEVICE_IN_DGTL_DOCK_HEADSET_NAME = "digital_dock";
+ public static final String DEVICE_IN_USB_ACCESSORY_NAME = "usb_accessory";
+ public static final String DEVICE_IN_USB_DEVICE_NAME = "usb_device";
+ public static final String DEVICE_IN_FM_TUNER_NAME = "fm_tuner";
+ public static final String DEVICE_IN_TV_TUNER_NAME = "tv_tuner";
+ public static final String DEVICE_IN_LINE_NAME = "line";
+ public static final String DEVICE_IN_SPDIF_NAME = "spdif";
+ public static final String DEVICE_IN_BLUETOOTH_A2DP_NAME = "bt_a2dp";
+ public static final String DEVICE_IN_LOOPBACK_NAME = "loopback";
public static String getOutputDeviceName(int device)
{
@@ -420,12 +443,60 @@ public class AudioSystem
return DEVICE_OUT_FM_NAME;
case DEVICE_OUT_AUX_LINE:
return DEVICE_OUT_AUX_LINE_NAME;
+ case DEVICE_OUT_SPEAKER_SAFE:
+ return DEVICE_OUT_SPEAKER_SAFE_NAME;
case DEVICE_OUT_DEFAULT:
default:
- return "";
+ return Integer.toString(device);
}
}
+ public static String getInputDeviceName(int device)
+ {
+ switch(device) {
+ case DEVICE_IN_COMMUNICATION:
+ return DEVICE_IN_COMMUNICATION_NAME;
+ case DEVICE_IN_AMBIENT:
+ return DEVICE_IN_AMBIENT_NAME;
+ case DEVICE_IN_BUILTIN_MIC:
+ return DEVICE_IN_BUILTIN_MIC_NAME;
+ case DEVICE_IN_BLUETOOTH_SCO_HEADSET:
+ return DEVICE_IN_BLUETOOTH_SCO_HEADSET_NAME;
+ case DEVICE_IN_WIRED_HEADSET:
+ return DEVICE_IN_WIRED_HEADSET_NAME;
+ case DEVICE_IN_AUX_DIGITAL:
+ return DEVICE_IN_AUX_DIGITAL_NAME;
+ case DEVICE_IN_TELEPHONY_RX:
+ return DEVICE_IN_TELEPHONY_RX_NAME;
+ case DEVICE_IN_BACK_MIC:
+ return DEVICE_IN_BACK_MIC_NAME;
+ case DEVICE_IN_REMOTE_SUBMIX:
+ return DEVICE_IN_REMOTE_SUBMIX_NAME;
+ case DEVICE_IN_ANLG_DOCK_HEADSET:
+ return DEVICE_IN_ANLG_DOCK_HEADSET_NAME;
+ case DEVICE_IN_DGTL_DOCK_HEADSET:
+ return DEVICE_IN_DGTL_DOCK_HEADSET_NAME;
+ case DEVICE_IN_USB_ACCESSORY:
+ return DEVICE_IN_USB_ACCESSORY_NAME;
+ case DEVICE_IN_USB_DEVICE:
+ return DEVICE_IN_USB_DEVICE_NAME;
+ case DEVICE_IN_FM_TUNER:
+ return DEVICE_IN_FM_TUNER_NAME;
+ case DEVICE_IN_TV_TUNER:
+ return DEVICE_IN_TV_TUNER_NAME;
+ case DEVICE_IN_LINE:
+ return DEVICE_IN_LINE_NAME;
+ case DEVICE_IN_SPDIF:
+ return DEVICE_IN_SPDIF_NAME;
+ case DEVICE_IN_BLUETOOTH_A2DP:
+ return DEVICE_IN_BLUETOOTH_A2DP_NAME;
+ case DEVICE_IN_LOOPBACK:
+ return DEVICE_IN_LOOPBACK_NAME;
+ case DEVICE_IN_DEFAULT:
+ default:
+ return Integer.toString(device);
+ }
+ }
// phone state, match audio_mode???
public static final int PHONE_STATE_OFFCALL = 0;
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 39b074e..317cc21 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -80,6 +80,8 @@ interface IAudioService {
int getRingerMode();
+ boolean isValidRingerMode(int ringerMode);
+
void setVibrateSetting(int vibrateType, int vibrateSetting);
int getVibrateSetting(int vibrateType);
@@ -205,6 +207,6 @@ interface IAudioService {
boolean isHdmiSystemAudioSupported();
- boolean registerAudioPolicy(in AudioPolicyConfig policyConfig, IBinder cb);
+ String registerAudioPolicy(in AudioPolicyConfig policyConfig, IBinder cb);
oneway void unregisterAudioPolicyAsync(in IBinder cb);
}
diff --git a/media/java/android/media/Image.java b/media/java/android/media/Image.java
index 522e45d..0d6b91a 100644
--- a/media/java/android/media/Image.java
+++ b/media/java/android/media/Image.java
@@ -146,8 +146,10 @@ public abstract class Image implements AutoCloseable {
* using coordinates in the largest-resolution plane.
*/
public void setCropRect(Rect cropRect) {
- cropRect = new Rect(cropRect); // make a copy
- cropRect.intersect(0, 0, getWidth(), getHeight());
+ if (cropRect != null) {
+ cropRect = new Rect(cropRect); // make a copy
+ cropRect.intersect(0, 0, getWidth(), getHeight());
+ }
mCropRect = cropRect;
}
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 420510a..bdd62f2 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -1778,10 +1778,6 @@ final public class MediaCodec {
mIsValid = true;
mIsReadOnly = buffer.isReadOnly();
mBuffer = buffer.duplicate();
- if (cropRect != null) {
- cropRect.offset(-xOffset, -yOffset);
- }
- super.setCropRect(cropRect);
// save offsets and info
mXOffset = xOffset;
@@ -1833,6 +1829,12 @@ final public class MediaCodec {
throw new UnsupportedOperationException(
"unsupported info length: " + info.remaining());
}
+
+ if (cropRect == null) {
+ cropRect = new Rect(0, 0, mWidth, mHeight);
+ }
+ cropRect.offset(-xOffset, -yOffset);
+ super.setCropRect(cropRect);
}
private class MediaPlane extends Plane {
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index f7967f1..bb52682 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -24,13 +24,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
- * @hide CANDIDATE FOR PUBLIC API
+ * @hide
*/
public class AudioMix {
private AudioMixingRule mRule;
private AudioFormat mFormat;
private int mRouteFlags;
+ private String mRegistrationId;
/**
* All parameters are guaranteed valid through the Builder.
@@ -39,6 +40,7 @@ public class AudioMix {
mRule = rule;
mFormat = format;
mRouteFlags = routeFlags;
+ mRegistrationId = null;
}
/**
@@ -65,6 +67,15 @@ public class AudioMix {
return mRule;
}
+ void setRegistration(String regId) {
+ mRegistrationId = regId;
+ }
+
+ /** @hide */
+ public String getRegistration() {
+ return mRegistrationId;
+ }
+
/** @hide */
@IntDef(flag = true,
value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index ced7881..2e06a80 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -23,7 +23,7 @@ import java.util.Iterator;
/**
- * @hide CANDIDATE FOR PUBLIC API
+ * @hide
*
* Here's an example of creating a mixing rule for all media playback:
* <pre>
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 314eb88..255d828 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -17,18 +17,26 @@
package android.media.audiopolicy;
import android.annotation.IntDef;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioSystem;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
+import android.util.Slog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
/**
- * @hide CANDIDATE FOR PUBLIC API
+ * @hide
* AudioPolicy provides access to the management of audio routing and audio focus.
*/
public class AudioPolicy {
@@ -49,11 +57,13 @@ public class AudioPolicy {
public static final int POLICY_STATUS_REGISTERED = 2;
private int mStatus;
- private AudioPolicyStatusListener mStatusListener = null;
+ private String mRegistrationId;
+ private AudioPolicyStatusListener mStatusListener;
private final IBinder mToken = new Binder();
/** @hide */
public IBinder token() { return mToken; }
+ private Context mContext;
private AudioPolicyConfig mConfig;
/** @hide */
@@ -62,13 +72,14 @@ public class AudioPolicy {
/**
* The parameter is guaranteed non-null through the Builder
*/
- private AudioPolicy(AudioPolicyConfig config) {
+ private AudioPolicy(AudioPolicyConfig config, Context context) {
mConfig = config;
if (mConfig.mMixes.isEmpty()) {
mStatus = POLICY_STATUS_INVALID;
} else {
mStatus = POLICY_STATUS_UNREGISTERED;
}
+ mContext = context;
}
/**
@@ -76,12 +87,15 @@ public class AudioPolicy {
*/
public static class Builder {
private ArrayList<AudioMix> mMixes;
+ private Context mContext;
/**
* Constructs a new Builder with no audio mixes.
+ * @param context the context for the policy
*/
- public Builder() {
+ public Builder(Context context) {
mMixes = new ArrayList<AudioMix>();
+ mContext = context;
}
/**
@@ -99,10 +113,115 @@ public class AudioPolicy {
}
public AudioPolicy build() {
- return new AudioPolicy(new AudioPolicyConfig(mMixes));
+ return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext);
}
}
+ /** @hide */
+ public void setRegistration(String regId) {
+ mRegistrationId = regId;
+ mConfig.setRegistration(regId);
+ }
+
+ private boolean policyReadyToUse() {
+ if (mContext == null) {
+ Log.e(TAG, "Cannot use AudioPolicy without context");
+ return false;
+ }
+ if (mRegistrationId == null) {
+ Log.e(TAG, "Cannot use unregistered AudioPolicy");
+ return false;
+ }
+ if (!(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.MODIFY_AUDIO_ROUTING))) {
+ Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid "
+ + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING");
+ return false;
+ }
+ return true;
+ }
+
+ private void checkMixReadyToUse(AudioMix mix, boolean forTrack)
+ throws IllegalArgumentException{
+ if (mix == null) {
+ String msg = forTrack ? "Invalid null AudioMix for AudioTrack creation"
+ : "Invalid null AudioMix for AudioRecord creation";
+ throw new IllegalArgumentException(msg);
+ }
+ if (!mConfig.mMixes.contains(mix)) {
+ throw new IllegalArgumentException("Invalid mix: not part of this policy");
+ }
+ if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) != AudioMix.ROUTE_FLAG_LOOP_BACK)
+ {
+ throw new IllegalArgumentException("Invalid AudioMix: not defined for loop back");
+ }
+ }
+
+ /**
+ * @hide
+ * Create an {@link AudioRecord} instance that is associated with the given {@link AudioMix}.
+ * Audio buffers recorded through the created instance will contain the mix of the audio
+ * streams that fed the given mixer.
+ * @param mix a non-null {@link AudioMix} instance whose routing flags was defined with
+ * {@link AudioMix#ROUTE_FLAG_LOOP_BACK}, previously added to this policy.
+ * @return a new {@link AudioRecord} instance whose data format is the one defined in the
+ * {@link AudioMix}, or null if this policy was not successfully registered
+ * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}.
+ * @throws IllegalArgumentException
+ */
+ public AudioRecord createAudioRecordSink(AudioMix mix) throws IllegalArgumentException {
+ if (!policyReadyToUse()) {
+ Log.e(TAG, "Cannot create AudioRecord sink for AudioMix");
+ return null;
+ }
+ checkMixReadyToUse(mix, false/*not for an AudioTrack*/);
+ // create the AudioRecord, configured for loop back, using the same format as the mix
+ AudioRecord ar = new AudioRecord(
+ new AudioAttributes.Builder()
+ .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
+ .addTag(mix.getRegistration())
+ .build(),
+ mix.getFormat(),
+ AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
+ // using stereo for buffer size to avoid the current poor support for masks
+ AudioFormat.CHANNEL_IN_STEREO, mix.getFormat().getEncoding()),
+ AudioManager.AUDIO_SESSION_ID_GENERATE
+ );
+ return ar;
+ }
+
+ /**
+ * @hide
+ * Create an {@link AudioTrack} instance that is associated with the given {@link AudioMix}.
+ * Audio buffers played through the created instance will be sent to the given mix
+ * to be recorded through the recording APIs.
+ * @param mix a non-null {@link AudioMix} instance whose routing flags was defined with
+ * {@link AudioMix#ROUTE_FLAG_LOOP_BACK}, previously added to this policy.
+ * @returna new {@link AudioTrack} instance whose data format is the one defined in the
+ * {@link AudioMix}, or null if this policy was not successfully registered
+ * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}.
+ * @throws IllegalArgumentException
+ */
+ public AudioTrack createAudioTrackSource(AudioMix mix) throws IllegalArgumentException {
+ if (!policyReadyToUse()) {
+ Log.e(TAG, "Cannot create AudioTrack source for AudioMix");
+ return null;
+ }
+ checkMixReadyToUse(mix, true/*for an AudioTrack*/);
+ // create the AudioTrack, configured for loop back, using the same format as the mix
+ AudioTrack at = new AudioTrack(
+ new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
+ .addTag(mix.getRegistration())
+ .build(),
+ mix.getFormat(),
+ AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(),
+ mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()),
+ AudioTrack.MODE_STREAM,
+ AudioManager.AUDIO_SESSION_ID_GENERATE
+ );
+ return at;
+ }
public int getStatus() {
return mStatus;
@@ -118,10 +237,9 @@ public class AudioPolicy {
}
/** @hide */
- @Override
- public String toString () {
+ public String toLogFriendlyString() {
String textDump = new String("android.media.audiopolicy.AudioPolicy:\n");
- textDump += "config=" + mConfig.toString();
+ textDump += "config=" + mConfig.toLogFriendlyString();
return (textDump);
}
diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
index 2fc6d58..a9a4175 100644
--- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
@@ -36,7 +36,13 @@ public class AudioPolicyConfig implements Parcelable {
private static final String TAG = "AudioPolicyConfig";
- ArrayList<AudioMix> mMixes;
+ protected ArrayList<AudioMix> mMixes;
+
+ protected String mRegistrationId = null;
+
+ protected AudioPolicyConfig(AudioPolicyConfig conf) {
+ mMixes = conf.mMixes;
+ }
AudioPolicyConfig(ArrayList<AudioMix> mixes) {
mMixes = mixes;
@@ -117,7 +123,6 @@ public class AudioPolicyConfig implements Parcelable {
}
}
- /** @hide */
public static final Parcelable.Creator<AudioPolicyConfig> CREATOR
= new Parcelable.Creator<AudioPolicyConfig>() {
/**
@@ -133,9 +138,7 @@ public class AudioPolicyConfig implements Parcelable {
}
};
- /** @hide */
- @Override
- public String toString () {
+ public String toLogFriendlyString () {
String textDump = new String("android.media.audiopolicy.AudioPolicyConfig:\n");
textDump += mMixes.size() + " AudioMix:\n";
for(AudioMix mix : mMixes) {
@@ -166,4 +169,13 @@ public class AudioPolicyConfig implements Parcelable {
}
return textDump;
}
+
+ public void setRegistration(String regId) {
+ mRegistrationId = regId;
+ int mixIndex = 0;
+ for (AudioMix mix : mMixes) {
+ mix.setRegistration(mRegistrationId + "mix:" + mixIndex++);
+ }
+ }
+
}
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index b8cdc4b..1ac80c1 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -166,6 +166,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
@Override
public void release() {
+ mTvInputSessionImpl.scheduleOverlayViewCleanup();
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
}
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index b3890d4..691df77 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -1052,6 +1052,24 @@ public final class TvContract {
/** The genre for Gaming. */
public static final String GAMING = "GAMING";
+ /** The genre for Arts. */
+ public static final String ARTS = "ARTS";
+
+ /** The genre for Entertainment. */
+ public static final String ENTERTAINMENT = "ENTERTAINMENT";
+
+ /** The genre for Life Style. */
+ public static final String LIFE_STYLE = "LIFE_STYLE";
+
+ /** The genre for Music. */
+ public static final String MUSIC = "MUSIC";
+
+ /** The genre for Premier. */
+ public static final String PREMIER = "PREMIER";
+
+ /** The genre for Tech/Science. */
+ public static final String TECH_SCIENCE = "TECH_SCIENCE";
+
private static final ArraySet<String> CANONICAL_GENRES = new ArraySet<String>();
static {
CANONICAL_GENRES.add(FAMILY_KIDS);
@@ -1065,6 +1083,12 @@ public final class TvContract {
CANONICAL_GENRES.add(ANIMAL_WILDLIFE);
CANONICAL_GENRES.add(NEWS);
CANONICAL_GENRES.add(GAMING);
+ CANONICAL_GENRES.add(ARTS);
+ CANONICAL_GENRES.add(ENTERTAINMENT);
+ CANONICAL_GENRES.add(LIFE_STYLE);
+ CANONICAL_GENRES.add(MUSIC);
+ CANONICAL_GENRES.add(PREMIER);
+ CANONICAL_GENRES.add(TECH_SCIENCE);
}
private Genres() {}
diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java
index 54ebc6a..b9e99d2 100644
--- a/media/java/android/media/tv/TvInputInfo.java
+++ b/media/java/android/media/tv/TvInputInfo.java
@@ -241,6 +241,9 @@ public final class TvInputInfo implements Parcelable {
if (DEBUG) {
Log.d(TAG, "Setup activity loaded. [" + input.mSetupActivity + "] for " + si.name);
}
+ if (inputType == TYPE_TUNER && TextUtils.isEmpty(input.mSetupActivity)) {
+ throw new XmlPullParserException("Setup activity not found in " + si.name);
+ }
input.mSettingsActivity = sa.getString(
com.android.internal.R.styleable.TvInputService_settingsActivity);
if (DEBUG) {
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 78714d2..51bd205 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -159,12 +159,12 @@ public final class TvInputManager {
private final Object mLock = new Object();
- // @GuardedBy(mLock)
+ // @GuardedBy("mLock")
private final List<TvInputCallbackRecord> mCallbackRecords =
new LinkedList<TvInputCallbackRecord>();
// A mapping from TV input ID to the state of corresponding input.
- // @GuardedBy(mLock)
+ // @GuardedBy("mLock")
private final Map<String, Integer> mStateMap = new ArrayMap<String, Integer>();
// A mapping from the sequence number of a session to its SessionCallbackRecord.
@@ -207,7 +207,7 @@ public final class TvInputManager {
/**
* This is called when the channel of this session is changed by the underlying TV input
- * with out any {@link TvInputManager.Session#tune(Uri)} request.
+ * without any {@link TvInputManager.Session#tune(Uri)} request.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param channelUri The URI of a channel.
@@ -227,7 +227,7 @@ public final class TvInputManager {
/**
* This is called when a track for a given type is selected.
*
- * @param session A {@link TvInputManager.Session} associated with this callback
+ * @param session A {@link TvInputManager.Session} associated with this callback.
* @param type The type of the selected track. The type can be
* {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
* {@link TvTrackInfo#TYPE_SUBTITLE}.
@@ -238,6 +238,18 @@ public final class TvInputManager {
}
/**
+ * This is invoked when the video size has been changed. It is also called when the first
+ * time video size information becomes available after the session is tuned to a specific
+ * channel.
+ *
+ * @param session A {@link TvInputManager.Session} associated with this callback.
+ * @param width The width of the video.
+ * @param height The height of the video.
+ */
+ public void onVideoSizeChanged(Session session, int width, int height) {
+ }
+
+ /**
* This is called when the video is available, so the TV input starts the playback.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
@@ -312,13 +324,13 @@ public final class TvInputManager {
private final Handler mHandler;
private Session mSession;
- public SessionCallbackRecord(SessionCallback sessionCallback,
+ SessionCallbackRecord(SessionCallback sessionCallback,
Handler handler) {
mSessionCallback = sessionCallback;
mHandler = handler;
}
- public void postSessionCreated(final Session session) {
+ void postSessionCreated(final Session session) {
mSession = session;
mHandler.post(new Runnable() {
@Override
@@ -328,7 +340,7 @@ public final class TvInputManager {
});
}
- public void postSessionReleased() {
+ void postSessionReleased() {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -337,7 +349,7 @@ public final class TvInputManager {
});
}
- public void postChannelRetuned(final Uri channelUri) {
+ void postChannelRetuned(final Uri channelUri) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -346,49 +358,34 @@ public final class TvInputManager {
});
}
- public void postTracksChanged(final List<TvTrackInfo> tracks) {
+ void postTracksChanged(final List<TvTrackInfo> tracks) {
mHandler.post(new Runnable() {
@Override
public void run() {
- mSession.mAudioTracks.clear();
- mSession.mVideoTracks.clear();
- mSession.mSubtitleTracks.clear();
- for (TvTrackInfo track : tracks) {
- if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
- mSession.mAudioTracks.add(track);
- } else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
- mSession.mVideoTracks.add(track);
- } else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
- mSession.mSubtitleTracks.add(track);
- } else {
- // Silently ignore.
- }
- }
mSessionCallback.onTracksChanged(mSession, tracks);
}
});
}
- public void postTrackSelected(final int type, final String trackId) {
+ void postTrackSelected(final int type, final String trackId) {
mHandler.post(new Runnable() {
@Override
public void run() {
- if (type == TvTrackInfo.TYPE_AUDIO) {
- mSession.mSelectedAudioTrackId = trackId;
- } else if (type == TvTrackInfo.TYPE_VIDEO) {
- mSession.mSelectedVideoTrackId = trackId;
- } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
- mSession.mSelectedSubtitleTrackId = trackId;
- } else {
- // Silently ignore.
- return;
- }
mSessionCallback.onTrackSelected(mSession, type, trackId);
}
});
}
- public void postVideoAvailable() {
+ void postVideoSizeChanged(final int width, final int height) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onVideoSizeChanged(mSession, width, height);
+ }
+ });
+ }
+
+ void postVideoAvailable() {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -397,7 +394,7 @@ public final class TvInputManager {
});
}
- public void postVideoUnavailable(final int reason) {
+ void postVideoUnavailable(final int reason) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -406,7 +403,7 @@ public final class TvInputManager {
});
}
- public void postContentAllowed() {
+ void postContentAllowed() {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -415,7 +412,7 @@ public final class TvInputManager {
});
}
- public void postContentBlocked(final TvContentRating rating) {
+ void postContentBlocked(final TvContentRating rating) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -424,7 +421,7 @@ public final class TvInputManager {
});
}
- public void postLayoutSurface(final int left, final int top, final int right,
+ void postLayoutSurface(final int left, final int top, final int right,
final int bottom) {
mHandler.post(new Runnable() {
@Override
@@ -434,7 +431,7 @@ public final class TvInputManager {
});
}
- public void postSessionEvent(final String eventType, final Bundle eventArgs) {
+ void postSessionEvent(final String eventType, final Bundle eventArgs) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -610,7 +607,10 @@ public final class TvInputManager {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
- record.postTracksChanged(tracks);
+ if (record.mSession.updateTracks(tracks)) {
+ record.postTracksChanged(tracks);
+ postVideoSizeChangedIfNeededLocked(record);
+ }
}
}
@@ -622,7 +622,17 @@ public final class TvInputManager {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
- record.postTrackSelected(type, trackId);
+ if (record.mSession.updateTrackSelection(type, trackId)) {
+ record.postTrackSelected(type, trackId);
+ postVideoSizeChangedIfNeededLocked(record);
+ }
+ }
+ }
+
+ private void postVideoSizeChangedIfNeededLocked(SessionCallbackRecord record) {
+ TvTrackInfo track = record.mSession.getVideoTrackToNotify();
+ if (track != null) {
+ record.postVideoSizeChanged(track.getVideoWidth(), track.getVideoHeight());
}
}
@@ -778,7 +788,7 @@ public final class TvInputManager {
}
/**
- * Returns the state of a given TV input. It retuns one of the following:
+ * Returns the state of a given TV input. It returns one of the following:
* <ul>
* <li>{@link #INPUT_STATE_CONNECTED}
* <li>{@link #INPUT_STATE_CONNECTED_STANDBY}
@@ -1133,12 +1143,24 @@ public final class TvInputManager {
private IBinder mToken;
private TvInputEventSender mSender;
private InputChannel mChannel;
+
+ private final Object mTrackLock = new Object();
+ // @GuardedBy("mTrackLock")
private final List<TvTrackInfo> mAudioTracks = new ArrayList<TvTrackInfo>();
+ // @GuardedBy("mTrackLock")
private final List<TvTrackInfo> mVideoTracks = new ArrayList<TvTrackInfo>();
+ // @GuardedBy("mTrackLock")
private final List<TvTrackInfo> mSubtitleTracks = new ArrayList<TvTrackInfo>();
+ // @GuardedBy("mTrackLock")
private String mSelectedAudioTrackId;
+ // @GuardedBy("mTrackLock")
private String mSelectedVideoTrackId;
+ // @GuardedBy("mTrackLock")
private String mSelectedSubtitleTrackId;
+ // @GuardedBy("mTrackLock")
+ private int mVideoWidth;
+ // @GuardedBy("mTrackLock")
+ private int mVideoHeight;
private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
@@ -1273,12 +1295,16 @@ public final class TvInputManager {
Log.w(TAG, "The session has been already released");
return;
}
- mAudioTracks.clear();
- mVideoTracks.clear();
- mSubtitleTracks.clear();
- mSelectedAudioTrackId = null;
- mSelectedVideoTrackId = null;
- mSelectedSubtitleTrackId = null;
+ synchronized (mTrackLock) {
+ mAudioTracks.clear();
+ mVideoTracks.clear();
+ mSubtitleTracks.clear();
+ mSelectedAudioTrackId = null;
+ mSelectedVideoTrackId = null;
+ mSelectedSubtitleTrackId = null;
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
try {
mService.tune(mToken, channelUri, params, mUserId);
} catch (RemoteException e) {
@@ -1314,23 +1340,25 @@ public final class TvInputManager {
* @see #getTracks
*/
public void selectTrack(int type, String trackId) {
- if (type == TvTrackInfo.TYPE_AUDIO) {
- if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
- Log.w(TAG, "Invalid audio trackId: " + trackId);
- return;
- }
- } else if (type == TvTrackInfo.TYPE_VIDEO) {
- if (trackId != null && !containsTrack(mVideoTracks, trackId)) {
- Log.w(TAG, "Invalid video trackId: " + trackId);
- return;
- }
- } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
- if (trackId != null && !containsTrack(mSubtitleTracks, trackId)) {
- Log.w(TAG, "Invalid subtitle trackId: " + trackId);
- return;
+ synchronized (mTrackLock) {
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
+ Log.w(TAG, "Invalid audio trackId: " + trackId);
+ return;
+ }
+ } else if (type == TvTrackInfo.TYPE_VIDEO) {
+ if (trackId != null && !containsTrack(mVideoTracks, trackId)) {
+ Log.w(TAG, "Invalid video trackId: " + trackId);
+ return;
+ }
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+ if (trackId != null && !containsTrack(mSubtitleTracks, trackId)) {
+ Log.w(TAG, "Invalid subtitle trackId: " + trackId);
+ return;
+ }
+ } else {
+ throw new IllegalArgumentException("invalid type: " + type);
}
- } else {
- throw new IllegalArgumentException("invalid type: " + type);
}
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1361,21 +1389,23 @@ public final class TvInputManager {
* @return the list of tracks for the given type.
*/
public List<TvTrackInfo> getTracks(int type) {
- if (type == TvTrackInfo.TYPE_AUDIO) {
- if (mAudioTracks == null) {
- return null;
- }
- return mAudioTracks;
- } else if (type == TvTrackInfo.TYPE_VIDEO) {
- if (mVideoTracks == null) {
- return null;
- }
- return mVideoTracks;
- } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
- if (mSubtitleTracks == null) {
- return null;
+ synchronized (mTrackLock) {
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ if (mAudioTracks == null) {
+ return null;
+ }
+ return new ArrayList<TvTrackInfo>(mAudioTracks);
+ } else if (type == TvTrackInfo.TYPE_VIDEO) {
+ if (mVideoTracks == null) {
+ return null;
+ }
+ return new ArrayList<TvTrackInfo>(mVideoTracks);
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+ if (mSubtitleTracks == null) {
+ return null;
+ }
+ return new ArrayList<TvTrackInfo>(mSubtitleTracks);
}
- return mSubtitleTracks;
}
throw new IllegalArgumentException("invalid type: " + type);
}
@@ -1388,17 +1418,89 @@ public final class TvInputManager {
* @see #selectTrack
*/
public String getSelectedTrack(int type) {
- if (type == TvTrackInfo.TYPE_AUDIO) {
- return mSelectedAudioTrackId;
- } else if (type == TvTrackInfo.TYPE_VIDEO) {
- return mSelectedVideoTrackId;
- } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
- return mSelectedSubtitleTrackId;
+ synchronized (mTrackLock) {
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ return mSelectedAudioTrackId;
+ } else if (type == TvTrackInfo.TYPE_VIDEO) {
+ return mSelectedVideoTrackId;
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+ return mSelectedSubtitleTrackId;
+ }
}
throw new IllegalArgumentException("invalid type: " + type);
}
/**
+ * Responds to onTracksChanged() and updates the internal track information. Returns true if
+ * there is an update.
+ */
+ boolean updateTracks(List<TvTrackInfo> tracks) {
+ synchronized (mTrackLock) {
+ mAudioTracks.clear();
+ mVideoTracks.clear();
+ mSubtitleTracks.clear();
+ for (TvTrackInfo track : tracks) {
+ if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
+ mAudioTracks.add(track);
+ } else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
+ mVideoTracks.add(track);
+ } else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
+ mSubtitleTracks.add(track);
+ }
+ }
+ return !mAudioTracks.isEmpty() || !mVideoTracks.isEmpty()
+ || !mSubtitleTracks.isEmpty();
+ }
+ }
+
+ /**
+ * Responds to onTrackSelected() and updates the internal track selection information.
+ * Returns true if there is an update.
+ */
+ boolean updateTrackSelection(int type, String trackId) {
+ synchronized (mTrackLock) {
+ if (type == TvTrackInfo.TYPE_AUDIO && trackId != mSelectedAudioTrackId) {
+ mSelectedAudioTrackId = trackId;
+ return true;
+ } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != mSelectedVideoTrackId) {
+ mSelectedVideoTrackId = trackId;
+ return true;
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE
+ && trackId != mSelectedSubtitleTrackId) {
+ mSelectedSubtitleTrackId = trackId;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the new/updated video track that contains new video size information. Returns
+ * null if there is no video track to notify. Subsequent calls of this method results in a
+ * non-null video track returned only by the first call and null returned by following
+ * calls. The caller should immediately notify of the video size change upon receiving the
+ * track.
+ */
+ TvTrackInfo getVideoTrackToNotify() {
+ synchronized (mTrackLock) {
+ if (!mVideoTracks.isEmpty() && mSelectedVideoTrackId != null) {
+ for (TvTrackInfo track : mVideoTracks) {
+ if (track.getId().equals(mSelectedVideoTrackId)) {
+ int videoWidth = track.getVideoWidth();
+ int videoHeight = track.getVideoHeight();
+ if (mVideoWidth != videoWidth || mVideoHeight != videoHeight) {
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+ return track;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
* Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
* TvInputService.Session.appPrivateCommand()} on the current TvView.
*
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 4f8facb..0ca5810 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -25,10 +25,12 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.text.TextUtils;
@@ -44,10 +46,12 @@ import android.view.Surface;
import android.view.View;
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;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -241,16 +245,25 @@ public abstract class TvInputService extends Service {
* Base class for derived classes to implement to provide a TV input session.
*/
public abstract static class Session implements KeyEvent.Callback {
+ private static final int DETACH_OVERLAY_VIEW_TIMEOUT = 5000;
private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
private final WindowManager mWindowManager;
final Handler mHandler;
private WindowManager.LayoutParams mWindowParams;
private Surface mSurface;
+ private Context mContext;
+ private FrameLayout mOverlayViewContainer;
private View mOverlayView;
+ private OverlayViewCleanUpTask mOverlayViewCleanUpTask;
private boolean mOverlayViewEnabled;
private IBinder mWindowToken;
private Rect mOverlayFrame;
+
+ private Object mLock = new Object();
+ // @GuardedBy("mLock")
private ITvInputSessionCallback mSessionCallback;
+ // @GuardedBy("mLock")
+ private List<Runnable> mPendingActions = new ArrayList<>();
/**
* Creates a new Session.
@@ -258,6 +271,7 @@ public abstract class TvInputService extends Service {
* @param context The context of the application
*/
public Session(Context context) {
+ mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mHandler = new Handler(context.getMainLooper());
}
@@ -295,11 +309,12 @@ public abstract class TvInputService extends Service {
* @param eventArgs Optional arguments of the event.
* @hide
*/
+ @SystemApi
public void notifySessionEvent(final String eventType, final Bundle eventArgs) {
if (eventType == null) {
throw new IllegalArgumentException("eventType should not be null.");
}
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -318,7 +333,7 @@ public abstract class TvInputService extends Service {
* @param channelUri The URI of a channel.
*/
public void notifyChannelRetuned(final Uri channelUri) {
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -332,8 +347,13 @@ public abstract class TvInputService extends Service {
}
/**
- * Sends the change on the track information. This is expected to be called whenever a track
- * is added/removed and the metadata of a track is modified.
+ * Sends the list of all audio/video/subtitle tracks. The is used by the framework to
+ * maintain the track information for a given session, which in turn is used by
+ * {@link TvView#getTracks} for the application to retrieve metadata for a given track type.
+ * The TV input service must call this method as soon as the track information becomes
+ * available or is updated. Note that in a case where a part of the information for a
+ * certain track is updated, it is not necessary to create a new {@link TvTrackInfo} object
+ * with a different track ID.
*
* @param tracks A list which includes track information.
* @throws IllegalArgumentException if {@code tracks} contains redundant tracks.
@@ -350,7 +370,7 @@ public abstract class TvInputService extends Service {
trackIdSet.clear();
// TODO: Validate the track list.
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -364,8 +384,12 @@ public abstract class TvInputService extends Service {
}
/**
- * Sends the ID of the selected track for a given track type. This is expected to be called
- * whenever there is a change on track selection.
+ * Sends the type and ID of a selected track. This is used to inform the application that a
+ * specific track is selected. The TV input service must call this method as soon as a track
+ * is selected either by default or in response to a call to {@link #onSelectTrack}. The
+ * selected track ID for a given type is maintained in the framework until the next call to
+ * this method even after the entire track list is updated (but is reset when the session is
+ * tuned to a new channel), so care must be taken not to result in an obsolete track ID.
*
* @param type The type of the selected track. The type can be
* {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
@@ -374,7 +398,7 @@ public abstract class TvInputService extends Service {
* @see #onSelectTrack
*/
public void notifyTrackSelected(final int type, final String trackId) {
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -395,7 +419,7 @@ public abstract class TvInputService extends Service {
* @see #notifyVideoUnavailable
*/
public void notifyVideoAvailable() {
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -427,7 +451,7 @@ public abstract class TvInputService extends Service {
|| reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) {
throw new IllegalArgumentException("Unknown reason: " + reason);
}
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -466,7 +490,7 @@ public abstract class TvInputService extends Service {
* @see TvInputManager
*/
public void notifyContentAllowed() {
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -506,7 +530,7 @@ public abstract class TvInputService extends Service {
* @see TvInputManager
*/
public void notifyContentBlocked(final TvContentRating rating) {
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -535,7 +559,7 @@ public abstract class TvInputService extends Service {
if (left > right || top > bottm) {
throw new IllegalArgumentException("Invalid parameter");
}
- runOnMainThread(new Runnable() {
+ executeOrPostRunnable(new Runnable() {
@Override
public void run() {
try {
@@ -837,12 +861,18 @@ public abstract class TvInputService extends Service {
* session.
*/
void release() {
- removeOverlayView(true);
onRelease();
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
+ synchronized(mLock) {
+ mSessionCallback = null;
+ mPendingActions.clear();
+ }
+ // Removes the overlay view lastly so that any hanging on the main thread can be handled
+ // in {@link #scheduleOverlayViewCleanup}.
+ removeOverlayView(true);
}
/**
@@ -927,9 +957,8 @@ public abstract class TvInputService extends Service {
* @param frame A position of the overlay view.
*/
void createOverlayView(IBinder windowToken, Rect frame) {
- if (mOverlayView != null) {
- mWindowManager.removeView(mOverlayView);
- mOverlayView = null;
+ if (mOverlayViewContainer != null) {
+ removeOverlayView(false);
}
if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")");
mWindowToken = windowToken;
@@ -942,6 +971,15 @@ public abstract class TvInputService extends Service {
if (mOverlayView == null) {
return;
}
+ if (mOverlayViewCleanUpTask != null) {
+ mOverlayViewCleanUpTask.cancel(true);
+ mOverlayViewCleanUpTask = null;
+ }
+ // Creates a container view to check hanging on the overlay view detaching.
+ // Adding/removing the overlay view to/from the container make the view attach/detach
+ // logic run on the main thread.
+ mOverlayViewContainer = new FrameLayout(mContext);
+ mOverlayViewContainer.addView(mOverlayView);
// TvView's window type is TYPE_APPLICATION_MEDIA and we want to create
// an overlay window above the media window but below the application window.
int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
@@ -958,7 +996,7 @@ public abstract class TvInputService extends Service {
WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
mWindowParams.gravity = Gravity.START | Gravity.TOP;
mWindowParams.token = windowToken;
- mWindowManager.addView(mOverlayView, mWindowParams);
+ mWindowManager.addView(mOverlayViewContainer, mWindowParams);
}
/**
@@ -975,33 +1013,51 @@ public abstract class TvInputService extends Service {
onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
}
mOverlayFrame = frame;
- if (!mOverlayViewEnabled || mOverlayView == null) {
+ if (!mOverlayViewEnabled || mOverlayViewContainer == null) {
return;
}
mWindowParams.x = frame.left;
mWindowParams.y = frame.top;
mWindowParams.width = frame.right - frame.left;
mWindowParams.height = frame.bottom - frame.top;
- mWindowManager.updateViewLayout(mOverlayView, mWindowParams);
+ mWindowManager.updateViewLayout(mOverlayViewContainer, mWindowParams);
}
/**
* Removes the current overlay view.
*/
void removeOverlayView(boolean clearWindowToken) {
- if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayView + ")");
+ if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayViewContainer + ")");
if (clearWindowToken) {
mWindowToken = null;
mOverlayFrame = null;
}
- if (mOverlayView != null) {
- mWindowManager.removeView(mOverlayView);
+ if (mOverlayViewContainer != null) {
+ // Removes the overlay view from the view hierarchy in advance so that it can be
+ // cleaned up in the {@link OverlayViewCleanUpTask} if the remove process is
+ // hanging.
+ mOverlayViewContainer.removeView(mOverlayView);
mOverlayView = null;
+ mWindowManager.removeView(mOverlayViewContainer);
+ mOverlayViewContainer = null;
mWindowParams = null;
}
}
/**
+ * Schedules a task which checks whether the overlay view is detached and kills the process
+ * if it is not. Note that this method is expected to be called in a non-main thread.
+ */
+ void scheduleOverlayViewCleanup() {
+ View overlayViewParent = mOverlayViewContainer;
+ if (overlayViewParent != null) {
+ mOverlayViewCleanUpTask = new OverlayViewCleanUpTask();
+ mOverlayViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ overlayViewParent);
+ }
+ }
+
+ /**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
@@ -1030,46 +1086,89 @@ public abstract class TvInputService extends Service {
}
}
}
- if (mOverlayView == null || !mOverlayView.isAttachedToWindow()) {
+ if (mOverlayViewContainer == null || !mOverlayViewContainer.isAttachedToWindow()) {
return TvInputManager.Session.DISPATCH_NOT_HANDLED;
}
- if (!mOverlayView.hasWindowFocus()) {
- mOverlayView.getViewRootImpl().windowFocusChanged(true, true);
+ if (!mOverlayViewContainer.hasWindowFocus()) {
+ mOverlayViewContainer.getViewRootImpl().windowFocusChanged(true, true);
}
- if (isNavigationKey && mOverlayView.hasFocusable()) {
+ if (isNavigationKey && mOverlayViewContainer.hasFocusable()) {
// If mOverlayView has focusable views, navigation key events should be always
// handled. If not, it can make the application UI navigation messed up.
// For example, in the case that the left-most view is focused, a left key event
// will not be handled in ViewRootImpl. Then, the left key event will be handled in
// the application during the UI navigation of the TV input.
- mOverlayView.getViewRootImpl().dispatchInputEvent(event);
+ mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event);
return TvInputManager.Session.DISPATCH_HANDLED;
} else {
- mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver);
+ mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event, receiver);
return TvInputManager.Session.DISPATCH_IN_PROGRESS;
}
}
- private void setSessionCallback(ITvInputSessionCallback callback) {
- mSessionCallback = callback;
+ private void initialize(ITvInputSessionCallback callback) {
+ synchronized(mLock) {
+ mSessionCallback = callback;
+ for (Runnable runnable : mPendingActions) {
+ runnable.run();
+ }
+ mPendingActions.clear();
+ }
}
- private final void runOnMainThread(Runnable action) {
- if (mHandler.getLooper().isCurrentThread() && mSessionCallback != null) {
- action.run();
- } else {
- // Posts the runnable if this is not called from the main thread or the session
- // is not initialized yet.
- mHandler.post(action);
+ private final void executeOrPostRunnable(Runnable action) {
+ synchronized(mLock) {
+ if (mSessionCallback == null) {
+ // The session is not initialized yet.
+ mPendingActions.add(action);
+ } else {
+ if (mHandler.getLooper().isCurrentThread()) {
+ action.run();
+ } else {
+ // Posts the runnable if this is not called from the main thread
+ mHandler.post(action);
+ }
+ }
+ }
+ }
+
+ private final class OverlayViewCleanUpTask extends AsyncTask<View, Void, Void> {
+ @Override
+ protected Void doInBackground(View... views) {
+ View overlayViewParent = views[0];
+ try {
+ Thread.sleep(DETACH_OVERLAY_VIEW_TIMEOUT);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ if (isCancelled()) {
+ return null;
+ }
+ if (overlayViewParent.isAttachedToWindow()) {
+ Log.e(TAG, "Time out on releasing overlay view. Killing "
+ + overlayViewParent.getContext().getPackageName());
+ Process.killProcess(Process.myPid());
+ }
+ return null;
}
}
}
/**
* Base class for a TV input session which represents an external device connected to a
- * hardware TV input. Once TV input returns an implementation of this class on
- * {@link #onCreateSession(String)}, the framework will create a hardware session and forward
- * the application's surface to the hardware TV input.
+ * hardware TV input.
+ * <p>
+ * This class is for an input which provides channels for the external set-top box to the
+ * application. Once a TV input returns an implementation of this class on
+ * {@link #onCreateSession(String)}, the framework will create a separate session for
+ * a hardware TV Input (e.g. HDMI 1) and forward the application's surface to the session so
+ * that the user can see the screen of the hardware TV Input when she tunes to a channel from
+ * this TV input. The implementation of this class is expected to change the channel of the
+ * external set-top box via a proprietary protocol when {@link HardwareSession#onTune(Uri)} is
+ * requested by the application.
+ * </p><p>
+ * Note that this class is not for inputs for internal hardware like built-in tuner and HDMI 1.
+ * </p>
* @see #onCreateSession(String)
*/
public abstract static class HardwareSession extends Session {
@@ -1106,13 +1205,15 @@ public abstract class TvInputService extends Service {
mHardwareSession = session;
SomeArgs args = SomeArgs.obtain();
if (session != null) {
- args.arg1 = mProxySession;
- args.arg2 = mProxySessionCallback;
- args.arg3 = session.getToken();
+ args.arg1 = HardwareSession.this;
+ args.arg2 = mProxySession;
+ args.arg3 = mProxySessionCallback;
+ args.arg4 = session.getToken();
} else {
args.arg1 = null;
- args.arg2 = mProxySessionCallback;
- args.arg3 = null;
+ args.arg2 = null;
+ args.arg3 = mProxySessionCallback;
+ args.arg4 = null;
onRelease();
}
mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, args)
@@ -1250,7 +1351,6 @@ public abstract class TvInputService extends Service {
}
return;
}
- sessionImpl.setSessionCallback(cb);
ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
sessionImpl, channel);
if (sessionImpl instanceof HardwareSession) {
@@ -1281,9 +1381,10 @@ public abstract class TvInputService extends Service {
proxySession.mHardwareSessionCallback, mServiceHandler);
} else {
SomeArgs someArgs = SomeArgs.obtain();
- someArgs.arg1 = stub;
- someArgs.arg2 = cb;
- someArgs.arg3 = null;
+ someArgs.arg1 = sessionImpl;
+ someArgs.arg2 = stub;
+ someArgs.arg3 = cb;
+ someArgs.arg4 = null;
mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED,
someArgs).sendToTarget();
}
@@ -1291,14 +1392,18 @@ public abstract class TvInputService extends Service {
}
case DO_NOTIFY_SESSION_CREATED: {
SomeArgs args = (SomeArgs) msg.obj;
- ITvInputSession stub = (ITvInputSession) args.arg1;
- ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
- IBinder hardwareSessionToken = (IBinder) args.arg3;
+ Session sessionImpl = (Session) args.arg1;
+ ITvInputSession stub = (ITvInputSession) args.arg2;
+ ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg3;
+ IBinder hardwareSessionToken = (IBinder) args.arg4;
try {
cb.onSessionCreated(stub, hardwareSessionToken);
} catch (RemoteException e) {
Log.e(TAG, "error in onSessionCreated");
}
+ if (sessionImpl != null) {
+ sessionImpl.initialize(cb);
+ }
args.recycle();
return;
}
diff --git a/media/java/android/media/tv/TvStreamConfig.java b/media/java/android/media/tv/TvStreamConfig.java
index a7e7e44..1bdc63e 100644
--- a/media/java/android/media/tv/TvStreamConfig.java
+++ b/media/java/android/media/tv/TvStreamConfig.java
@@ -33,7 +33,6 @@ public class TvStreamConfig implements Parcelable {
private int mStreamId;
private int mType;
- // TODO: Revisit if max widht/height really make sense.
private int mMaxWidth;
private int mMaxHeight;
/**
@@ -166,4 +165,17 @@ public class TvStreamConfig implements Parcelable {
return config;
}
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (!(obj instanceof TvStreamConfig)) return false;
+
+ TvStreamConfig config = (TvStreamConfig) obj;
+ return config.mGeneration == mGeneration
+ && config.mStreamId == mStreamId
+ && config.mType == mType
+ && config.mMaxWidth == mMaxWidth
+ && config.mMaxHeight == mMaxHeight;
+ }
}
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index 0949b1a..f9d84c1 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -59,8 +59,6 @@ public class TvView extends ViewGroup {
private static final String TAG = "TvView";
private static final boolean DEBUG = false;
- private static final int VIDEO_SIZE_VALUE_UNKNOWN = 0;
-
private static final int ZORDER_MEDIA = 0;
private static final int ZORDER_MEDIA_OVERLAY = 1;
private static final int ZORDER_ON_TOP = 2;
@@ -69,7 +67,7 @@ public class TvView extends ViewGroup {
private static final int CAPTION_ENABLED = 1;
private static final int CAPTION_DISABLED = 2;
- private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference(null);
+ private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null);
private static final Object sMainTvViewLock = new Object();
private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
@@ -86,8 +84,10 @@ public class TvView extends ViewGroup {
private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
private boolean mHasStreamVolume;
private float mStreamVolume;
- private int mVideoWidth = VIDEO_SIZE_VALUE_UNKNOWN;
- private int mVideoHeight = VIDEO_SIZE_VALUE_UNKNOWN;
+ private int mCaptionEnabled;
+ private String mAppPrivateCommandAction;
+ private Bundle mAppPrivateCommandData;
+
private boolean mSurfaceChanged;
private int mSurfaceFormat;
private int mSurfaceWidth;
@@ -100,7 +100,6 @@ public class TvView extends ViewGroup {
private int mSurfaceViewRight;
private int mSurfaceViewTop;
private int mSurfaceViewBottom;
- private int mCaptionEnabled;
private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
@@ -197,7 +196,7 @@ public class TvView extends ViewGroup {
@SystemApi
public void setMain() {
synchronized (sMainTvViewLock) {
- sMainTvView = new WeakReference(this);
+ sMainTvView = new WeakReference<>(this);
if (hasWindowFocus() && mSession != null) {
mSession.setMain();
}
@@ -291,7 +290,7 @@ public class TvView extends ViewGroup {
}
synchronized (sMainTvViewLock) {
if (sMainTvView.get() == null) {
- sMainTvView = new WeakReference(this);
+ sMainTvView = new WeakReference<>(this);
}
}
if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) {
@@ -421,10 +420,10 @@ public class TvView extends ViewGroup {
* Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
* TvInputService.Session.appPrivateCommand()} on the current TvView.
*
- * @param action Name of the command to be performed. This <em>must</em> be a scoped name, i.e.
- * prefixed with a package name you own, so that different developers will not create
- * conflicting commands.
- * @param data Any data to include with the command.
+ * @param action The name of the private command to send. This <em>must</em> be a scoped name,
+ * i.e. prefixed with a package name you own, so that different developers will not
+ * create conflicting commands.
+ * @param data An optional bundle to send with the command.
* @hide
*/
@SystemApi
@@ -434,6 +433,13 @@ public class TvView extends ViewGroup {
}
if (mSession != null) {
mSession.sendAppPrivateCommand(action, data);
+ } else {
+ Log.w(TAG, "sendAppPrivateCommand - session not created (action " + action + " cached)");
+ if (mAppPrivateCommandAction != null) {
+ Log.w(TAG, "previous cached action " + action + " removed");
+ }
+ mAppPrivateCommandAction = action;
+ mAppPrivateCommandData = data;
}
}
@@ -619,6 +625,9 @@ public class TvView extends ViewGroup {
}
private void release() {
+ mAppPrivateCommandAction = null;
+ mAppPrivateCommandData = null;
+
setSessionSurface(null);
removeSessionOverlayView();
mUseRequestedSurfaceLayout = false;
@@ -703,19 +712,8 @@ public class TvView extends ViewGroup {
}
/**
- * This is invoked when the view is tuned to a specific channel and starts decoding video
- * stream from there. It is also called later when the video size is changed.
- *
- * @param inputId The ID of the TV input bound to this view.
- * @param width The width of the video.
- * @param height The height of the video.
- */
- public void onVideoSizeChanged(String inputId, int width, int height) {
- }
-
- /**
* This is invoked when the channel of this TvView is changed by the underlying TV input
- * with out any {@link TvView#tune(String, Uri)} request.
+ * without any {@link TvView#tune(String, Uri)} request.
*
* @param inputId The ID of the TV input bound to this view.
* @param channelUri The URI of a channel.
@@ -745,6 +743,18 @@ public class TvView extends ViewGroup {
}
/**
+ * This is invoked when the video size has been changed. It is also called when the first
+ * time video size information becomes available after this view is tuned to a specific
+ * channel.
+ *
+ * @param inputId The ID of the TV input bound to this view.
+ * @param width The width of the video.
+ * @param height The height of the video.
+ */
+ public void onVideoSizeChanged(String inputId, int width, int height) {
+ }
+
+ /**
* This is called when the video is available, so the TV input starts the playback.
*
* @param inputId The ID of the TV input bound to this view.
@@ -828,16 +838,17 @@ public class TvView extends ViewGroup {
@Override
public void onSessionCreated(Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onSessionCreated()");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onSessionCreated - session already created");
// This callback is obsolete.
if (session != null) {
session.release();
}
return;
}
- if (DEBUG) {
- Log.d(TAG, "onSessionCreated()");
- }
mSession = session;
if (session != null) {
synchronized (sMainTvViewLock) {
@@ -862,6 +873,12 @@ public class TvView extends ViewGroup {
if (mHasStreamVolume) {
mSession.setStreamVolume(mStreamVolume);
}
+ if (mAppPrivateCommandAction != null) {
+ mSession.sendAppPrivateCommand(
+ mAppPrivateCommandAction, mAppPrivateCommandData);
+ mAppPrivateCommandAction = null;
+ mAppPrivateCommandData = null;
+ }
} else {
mSessionCallback = null;
if (mCallback != null) {
@@ -872,7 +889,11 @@ public class TvView extends ViewGroup {
@Override
public void onSessionReleased(Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onSessionReleased()");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onSessionReleased - session not created");
return;
}
mOverlayViewCreated = false;
@@ -886,12 +907,13 @@ public class TvView extends ViewGroup {
@Override
public void onChannelRetuned(Session session, Uri channelUri) {
- if (this != mSessionCallback) {
- return;
- }
if (DEBUG) {
Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
}
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onChannelRetuned - session not created");
+ return;
+ }
if (mCallback != null) {
mCallback.onChannelRetuned(mInputId, channelUri);
}
@@ -899,12 +921,13 @@ public class TvView extends ViewGroup {
@Override
public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "onTracksChanged(" + tracks + ")");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onTracksChanged - session not created");
return;
}
- if (DEBUG) {
- Log.d(TAG, "onTracksChanged()");
- }
if (mCallback != null) {
mCallback.onTracksChanged(mInputId, tracks);
}
@@ -912,26 +935,41 @@ public class TvView extends ViewGroup {
@Override
public void onTrackSelected(Session session, int type, String trackId) {
+ if (DEBUG) {
+ Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onTrackSelected - session not created");
return;
}
- if (DEBUG) {
- Log.d(TAG, "onTrackSelected()");
- }
- // TODO: Update the video size when the type is TYPE_VIDEO.
if (mCallback != null) {
mCallback.onTrackSelected(mInputId, type, trackId);
}
}
@Override
- public void onVideoAvailable(Session session) {
+ public void onVideoSizeChanged(Session session, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged()");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onVideoSizeChanged - session not created");
return;
}
+ if (mCallback != null) {
+ mCallback.onVideoSizeChanged(mInputId, width, height);
+ }
+ }
+
+ @Override
+ public void onVideoAvailable(Session session) {
if (DEBUG) {
Log.d(TAG, "onVideoAvailable()");
}
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onVideoAvailable - session not created");
+ return;
+ }
if (mCallback != null) {
mCallback.onVideoAvailable(mInputId);
}
@@ -939,12 +977,13 @@ public class TvView extends ViewGroup {
@Override
public void onVideoUnavailable(Session session, int reason) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onVideoUnavailable - session not created");
return;
}
- if (DEBUG) {
- Log.d(TAG, "onVideoUnavailable(" + reason + ")");
- }
if (mCallback != null) {
mCallback.onVideoUnavailable(mInputId, reason);
}
@@ -952,12 +991,13 @@ public class TvView extends ViewGroup {
@Override
public void onContentAllowed(Session session) {
- if (this != mSessionCallback) {
- return;
- }
if (DEBUG) {
Log.d(TAG, "onContentAllowed()");
}
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onContentAllowed - session not created");
+ return;
+ }
if (mCallback != null) {
mCallback.onContentAllowed(mInputId);
}
@@ -965,12 +1005,13 @@ public class TvView extends ViewGroup {
@Override
public void onContentBlocked(Session session, TvContentRating rating) {
+ if (DEBUG) {
+ Log.d(TAG, "onContentBlocked(rating=" + rating + ")");
+ }
if (this != mSessionCallback) {
+ Log.w(TAG, "onContentBlocked - session not created");
return;
}
- if (DEBUG) {
- Log.d(TAG, "onContentBlocked()");
- }
if (mCallback != null) {
mCallback.onContentBlocked(mInputId, rating);
}
@@ -978,13 +1019,14 @@ public class TvView extends ViewGroup {
@Override
public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
- if (this != mSessionCallback) {
- return;
- }
if (DEBUG) {
Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
+ right + ", bottom=" + bottom + ",)");
}
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onLayoutSurface - session not created");
+ return;
+ }
mSurfaceViewLeft = left;
mSurfaceViewTop = top;
mSurfaceViewRight = right;
@@ -995,12 +1037,13 @@ public class TvView extends ViewGroup {
@Override
public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
- if (this != mSessionCallback) {
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSessionEvent(" + eventType + ")");
}
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onSessionEvent - session not created");
+ return;
+ }
if (mCallback != null) {
mCallback.onEvent(mInputId, eventType, eventArgs);
}