diff options
Diffstat (limited to 'media/java')
23 files changed, 4020 insertions, 9 deletions
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index b07d2c5..4b4be1b 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -16,6 +16,11 @@ package android.media; +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * The AudioFormat class is used to access a number of audio format and * channel configuration constants. They are for instance used @@ -162,4 +167,151 @@ public class AudioFormat { throw new UnsupportedOperationException("There is no valid usage of this constructor"); } + /** + * Private constructor with an ignored argument to differentiate from the removed default ctor + * @param ignoredArgument + */ + private AudioFormat(int ignoredArgument) { + } + + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_NONE = 0x0; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_ENCODING = 0x1 << 0; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE = 0x1 << 1; + /** @hide */ + public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK = 0x1 << 2; + + private int mEncoding; + private int mSampleRate; + private int mChannelMask; + private int mPropertySetMask; + + /** + * @hide CANDIDATE FOR PUBLIC API + * Builder class for {@link AudioFormat} objects. + */ + public static class Builder { + private int mEncoding = ENCODING_DEFAULT; + private int mSampleRate = 0; + private int mChannelMask = CHANNEL_INVALID; + private int mPropertySetMask = AUDIO_FORMAT_HAS_PROPERTY_NONE; + + /** + * Constructs a new Builder with the defaults. + */ + public Builder() { + } + + /** + * Constructs a new Builder from a given {@link AudioFormat}. + * @param af the {@link AudioFormat} object whose data will be reused in the new Builder. + */ + public Builder(AudioFormat af) { + mEncoding = af.mEncoding; + mSampleRate = af.mSampleRate; + mChannelMask = af.mChannelMask; + mPropertySetMask = af.mPropertySetMask; + } + + /** + * Combines all of the format characteristics that have been set and return a new + * {@link AudioFormat} object. + * @return a new {@link AudioFormat} object + */ + public AudioFormat build() { + AudioFormat af = new AudioFormat(1980/*ignored*/); + af.mEncoding = mEncoding; + af.mSampleRate = mSampleRate; + af.mChannelMask = mChannelMask; + af.mPropertySetMask = mPropertySetMask; + return af; + } + + /** + * Sets the data encoding format. + * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT}, + * {@link AudioFormat#ENCODING_PCM_8BIT}, + * {@link AudioFormat#ENCODING_PCM_16BIT}, + * {@link AudioFormat#ENCODING_PCM_FLOAT}. + * @return the same Builder instance. + * @throws java.lang.IllegalArgumentException + */ + public Builder setEncoding(@Encoding int encoding) throws IllegalArgumentException { + switch (encoding) { + case ENCODING_DEFAULT: + mEncoding = ENCODING_PCM_16BIT; + break; + case ENCODING_PCM_8BIT: + case ENCODING_PCM_16BIT: + case ENCODING_PCM_FLOAT: + mEncoding = encoding; + break; + case ENCODING_INVALID: + default: + throw new IllegalArgumentException("Invalid encoding " + encoding); + } + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_ENCODING; + return this; + } + + /** + * Sets the channel mask. + * @param channelMask describes the configuration of the audio channels. + * <p>For output, the mask should be a combination of + * {@link AudioFormat#CHANNEL_OUT_FRONT_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_FRONT_CENTER}, + * {@link AudioFormat#CHANNEL_OUT_FRONT_RIGHT}, + * {@link AudioFormat#CHANNEL_OUT_SIDE_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_SIDE_RIGHT}, + * {@link AudioFormat#CHANNEL_OUT_BACK_LEFT}, + * {@link AudioFormat#CHANNEL_OUT_BACK_RIGHT}. + * <p>for input, the mask should be {@link AudioFormat#CHANNEL_IN_MONO} or + * {@link AudioFormat#CHANNEL_IN_STEREO}. {@link AudioFormat#CHANNEL_IN_MONO} is + * guaranteed to work on all devices. + * @return the same Builder instance. + */ + public Builder setChannelMask(int channelMask) { + // only validated when used, with input or output context + mChannelMask = channelMask; + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK; + return this; + } + + /** + * Sets the sample rate. + * @param sampleRate the sample rate expressed in Hz + * @return the same Builder instance. + * @throws java.lang.IllegalArgumentException + */ + public Builder setSampleRate(int sampleRate) throws IllegalArgumentException { + if ((sampleRate <= 0) || (sampleRate > 192000)) { + throw new IllegalArgumentException("Invalid sample rate " + sampleRate); + } + mSampleRate = sampleRate; + mPropertySetMask |= AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE; + return this; + } + } + + @Override + public String toString () { + return new String("AudioFormat:" + + " props=" + mPropertySetMask + + " enc=" + mEncoding + + " chan=0x" + Integer.toHexString(mChannelMask) + + " rate=" + mSampleRate); + } + + /** @hide */ + @IntDef({ + ENCODING_DEFAULT, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_FLOAT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Encoding {} + } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 84d4ab6..88756d7 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -3188,15 +3188,11 @@ public class AudioManager { do { newPorts.clear(); status = AudioSystem.listAudioPorts(newPorts, portGeneration); - Log.i(TAG, "updateAudioPortCache AudioSystem.listAudioPorts() status: "+ - status+" num ports: "+ newPorts.size() +" portGeneration: "+portGeneration[0]); if (status != SUCCESS) { return status; } newPatches.clear(); status = AudioSystem.listAudioPatches(newPatches, patchGeneration); - Log.i(TAG, "updateAudioPortCache AudioSystem.listAudioPatches() status: "+ - status+" num patches: "+ newPatches.size() +" patchGeneration: "+patchGeneration[0]); if (status != SUCCESS) { return status; } @@ -3204,14 +3200,16 @@ public class AudioManager { for (int i = 0; i < newPatches.size(); i++) { for (int j = 0; j < newPatches.get(i).sources().length; j++) { - AudioPortConfig portCfg = updatePortConfig(newPatches.get(i).sources()[j], newPorts); + AudioPortConfig portCfg = updatePortConfig(newPatches.get(i).sources()[j], + newPorts); if (portCfg == null) { return ERROR; } newPatches.get(i).sources()[j] = portCfg; } for (int j = 0; j < newPatches.get(i).sinks().length; j++) { - AudioPortConfig portCfg = updatePortConfig(newPatches.get(i).sinks()[j], newPorts); + AudioPortConfig portCfg = updatePortConfig(newPatches.get(i).sinks()[j], + newPorts); if (portCfg == null) { return ERROR; } @@ -3242,8 +3240,6 @@ public class AudioManager { // compare handles because the port returned by JNI is not of the correct // subclass if (ports.get(k).handle().equals(port.handle())) { - Log.i(TAG, "updatePortConfig match found for port handle: "+ - port.handle().id()+" port: "+ k); port = ports.get(k); break; } diff --git a/media/java/android/media/AudioPort.java b/media/java/android/media/AudioPort.java index fbd5022..8b74842 100644 --- a/media/java/android/media/AudioPort.java +++ b/media/java/android/media/AudioPort.java @@ -133,7 +133,7 @@ public class AudioPort { * Get the gain descriptor at a given index */ AudioGain gain(int index) { - if (index < mGains.length) { + if (index < 0 || index >= mGains.length) { return null; } return mGains[index]; diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 8eb83e4..cfd9c3b 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -816,6 +816,8 @@ public class AudioTrack * with the estimated time when that frame was presented or is committed to * be presented. * In the case that no timestamp is available, any supplied instance is left unaltered. + * A timestamp may be temporarily unavailable while the audio clock is stabilizing, + * or during and immediately after a route change. */ // Add this text when the "on new timestamp" API is added: // Use if you need to get the most recent timestamp outside of the event callback handler. diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl new file mode 100644 index 0000000..011da35 --- /dev/null +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.media.tv.ITvInputSession; +import android.os.Bundle; +import android.view.InputChannel; + +/** + * Interface a client of the ITvInputManager implements, to identify itself and receive information + * about changes to the state of each TV input service. + * @hide + */ +oneway interface ITvInputClient { + void onSessionCreated(in String inputId, IBinder token, in InputChannel channel, int seq); + void onAvailabilityChanged(in String inputId, boolean isAvailable); + void onSessionReleased(int seq); + void onSessionEvent(in String name, in Bundle args, int seq); + void onVideoStreamChanged(int width, int height, boolean interlaced, int seq); + void onAudioStreamChanged(int channelCount, int seq); + void onClosedCaptionStreamChanged(boolean hasClosedCaption, int seq); +} diff --git a/media/java/android/media/tv/ITvInputHardware.aidl b/media/java/android/media/tv/ITvInputHardware.aidl new file mode 100644 index 0000000..f35e8f3 --- /dev/null +++ b/media/java/android/media/tv/ITvInputHardware.aidl @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.media.tv.TvStreamConfig; +import android.view.KeyEvent; +import android.view.Surface; + +/** + * TvInputService representing a physical port should connect to HAL through this interface. + * Framework will take care of communication among system services including TvInputManagerService, + * HdmiControlService, AudioService, etc. + * + * @hide + */ +interface ITvInputHardware { + /** + * Make the input render on the surface according to the config. In case of HDMI, this will + * trigger CEC commands for adjusting active HDMI source. Returns true on success. + */ + boolean setSurface(in Surface surface, in TvStreamConfig config); + /** + * Set volume for this stream via AudioGain. (TBD) + */ + void setVolume(float volume); + + /** + * Dispatch key event to HDMI service. The events would be automatically converted to + * HDMI CEC commands. If the hardware is not representing an HDMI port, this method will fail. + */ + boolean dispatchKeyEventToHdmi(in KeyEvent event); +} diff --git a/media/java/android/media/tv/ITvInputHardwareCallback.aidl b/media/java/android/media/tv/ITvInputHardwareCallback.aidl new file mode 100644 index 0000000..870883b --- /dev/null +++ b/media/java/android/media/tv/ITvInputHardwareCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.media.tv.TvStreamConfig; + +/** + * @hide + */ +oneway interface ITvInputHardwareCallback { + void onReleased(); + void onStreamConfigChanged(in TvStreamConfig[] configs); +} diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl new file mode 100644 index 0000000..6db5a18 --- /dev/null +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.graphics.Rect; +import android.media.tv.ITvInputHardware; +import android.media.tv.ITvInputHardwareCallback; +import android.media.tv.ITvInputClient; +import android.media.tv.TvInputHardwareInfo; +import android.media.tv.TvInputInfo; +import android.net.Uri; +import android.view.Surface; + +/** + * Interface to the TV input manager service. + * @hide + */ +interface ITvInputManager { + List<TvInputInfo> getTvInputList(int userId); + + boolean getAvailability(in ITvInputClient client, in String inputId, int userId); + + void registerCallback(in ITvInputClient client, in String inputId, int userId); + void unregisterCallback(in ITvInputClient client, in String inputId, int userId); + + void createSession(in ITvInputClient client, in String inputId, int seq, int userId); + void releaseSession(in IBinder sessionToken, int userId); + + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void setVolume(in IBinder sessionToken, float volume, int userId); + void tune(in IBinder sessionToken, in Uri channelUri, int userId); + + void createOverlayView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, + int userId); + void relayoutOverlayView(in IBinder sessionToken, in Rect frame, int userId); + void removeOverlayView(in IBinder sessionToken, int userId); + + // For TV input hardware binding + List<TvInputHardwareInfo> getHardwareList(); + ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback, + int userId); + void releaseTvInputHardware(int deviceId, in ITvInputHardware hardware, int userId); +} diff --git a/media/java/android/media/tv/ITvInputService.aidl b/media/java/android/media/tv/ITvInputService.aidl new file mode 100644 index 0000000..992e424 --- /dev/null +++ b/media/java/android/media/tv/ITvInputService.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.media.tv.ITvInputServiceCallback; +import android.media.tv.ITvInputSessionCallback; +import android.view.InputChannel; + +/** + * Top-level interface to a TV input component (implemented in a Service). + * @hide + */ +oneway interface ITvInputService { + void registerCallback(ITvInputServiceCallback callback); + void unregisterCallback(in ITvInputServiceCallback callback); + void createSession(in InputChannel channel, ITvInputSessionCallback callback); +} diff --git a/media/java/android/media/tv/ITvInputServiceCallback.aidl b/media/java/android/media/tv/ITvInputServiceCallback.aidl new file mode 100644 index 0000000..c9484dd --- /dev/null +++ b/media/java/android/media/tv/ITvInputServiceCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; + +/** + * Helper interface for ITvInputService to allow the TV input to notify the client when its status + * has been changed. + * @hide + */ +oneway interface ITvInputServiceCallback { + void onAvailabilityChanged(in String inputId, boolean isAvailable); +} diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl new file mode 100644 index 0000000..fb2e251 --- /dev/null +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.graphics.Rect; +import android.net.Uri; +import android.view.Surface; + +/** + * Sub-interface of ITvInputService which is created per session and has its own context. + * @hide + */ +oneway interface ITvInputSession { + void release(); + + void setSurface(in Surface surface); + // TODO: Remove this once it becomes irrelevant for applications to handle audio focus. The plan + // is to introduce some new concepts that will solve a number of problems in audio policy today. + void setVolume(float volume); + void tune(in Uri channelUri); + + void createOverlayView(in IBinder windowToken, in Rect frame); + void relayoutOverlayView(in Rect frame); + void removeOverlayView(); +} diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl new file mode 100644 index 0000000..00f2922 --- /dev/null +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.media.tv.ITvInputSession; +import android.os.Bundle; + +/** + * Helper interface for ITvInputSession to allow the TV input to notify the system service when a + * new session has been created. + * @hide + */ +oneway interface ITvInputSessionCallback { + void onSessionCreated(ITvInputSession session); + void onSessionEvent(in String name, in Bundle args); + void onVideoStreamChanged(int width, int height, boolean interlaced); + void onAudioStreamChanged(int channelCount); + void onClosedCaptionStreamChanged(boolean hasClosedCaption); +} diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java new file mode 100644 index 0000000..975e391 --- /dev/null +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.Context; +import android.graphics.Rect; +import android.media.tv.TvInputManager.Session; +import android.media.tv.TvInputService.TvInputSessionImpl; +import android.net.Uri; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +/** + * Implements the internal ITvInputSession interface to convert incoming calls on to it back to + * calls on the public TvInputSession interface, scheduling them on the main thread of the process. + * + * @hide + */ +public class ITvInputSessionWrapper extends ITvInputSession.Stub implements HandlerCaller.Callback { + private static final String TAG = "TvInputSessionWrapper"; + + private static final int DO_RELEASE = 1; + private static final int DO_SET_SURFACE = 2; + private static final int DO_SET_VOLUME = 3; + private static final int DO_TUNE = 4; + private static final int DO_CREATE_OVERLAY_VIEW = 5; + private static final int DO_RELAYOUT_OVERLAY_VIEW = 6; + private static final int DO_REMOVE_OVERLAY_VIEW = 7; + + private final HandlerCaller mCaller; + + private TvInputSessionImpl mTvInputSessionImpl; + private InputChannel mChannel; + private TvInputEventReceiver mReceiver; + + public ITvInputSessionWrapper(Context context, TvInputSessionImpl sessionImpl, + InputChannel channel) { + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mTvInputSessionImpl = sessionImpl; + mChannel = channel; + if (channel != null) { + mReceiver = new TvInputEventReceiver(channel, context.getMainLooper()); + } + } + + @Override + public void executeMessage(Message msg) { + if (mTvInputSessionImpl == null) { + return; + } + + switch (msg.what) { + case DO_RELEASE: { + mTvInputSessionImpl.release(); + mTvInputSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + return; + } + case DO_SET_SURFACE: { + mTvInputSessionImpl.setSurface((Surface) msg.obj); + return; + } + case DO_SET_VOLUME: { + mTvInputSessionImpl.setVolume((Float) msg.obj); + return; + } + case DO_TUNE: { + mTvInputSessionImpl.tune((Uri) msg.obj); + return; + } + case DO_CREATE_OVERLAY_VIEW: { + SomeArgs args = (SomeArgs) msg.obj; + mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2); + args.recycle(); + return; + } + case DO_RELAYOUT_OVERLAY_VIEW: { + mTvInputSessionImpl.relayoutOverlayView((Rect) msg.obj); + return; + } + case DO_REMOVE_OVERLAY_VIEW: { + mTvInputSessionImpl.removeOverlayView(true); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public final void setVolume(float volume) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VOLUME, volume)); + } + + @Override + public void tune(Uri channelUri) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TUNE, channelUri)); + } + + @Override + public void createOverlayView(IBinder windowToken, Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_OVERLAY_VIEW, windowToken, + frame)); + } + + @Override + public void relayoutOverlayView(Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_OVERLAY_VIEW, frame)); + } + + @Override + public void removeOverlayView() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_OVERLAY_VIEW)); + } + + private final class TvInputEventReceiver extends InputEventReceiver { + public TvInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mTvInputSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mTvInputSessionImpl.dispatchInputEvent(event, this); + if (handled != Session.DISPATCH_IN_PROGRESS) { + finishInputEvent(event, handled == Session.DISPATCH_HANDLED); + } + } + } +} diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java new file mode 100644 index 0000000..6e0586e --- /dev/null +++ b/media/java/android/media/tv/TvContract.java @@ -0,0 +1,805 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.net.Uri; +import android.provider.BaseColumns; + +import java.util.List; + +/** + * <p> + * The contract between the TV provider and applications. Contains definitions for the supported + * URIs and columns. + * </p> + * <h3>Overview</h3> + * <p> + * TvContract defines a basic database of TV content metadata such as channel and program + * information. The information is stored in {@link Channels} and {@link Programs} tables. + * </p> + * <ul> + * <li>A row in the {@link Channels} table represents information about a TV channel. The data + * format can vary greatly from standard to standard or according to service provider, thus + * the columns here are mostly comprised of basic entities that are usually seen to users + * regardless of standard such as channel number and name.</li> + * <li>A row in the {@link Programs} table represents a set of data describing a TV program such + * as program title and start time.</li> + * </ul> + */ +public final class TvContract { + /** The authority for the TV provider. */ + public static final String AUTHORITY = "android.media.tv"; + + private static final String PATH_CHANNEL = "channel"; + private static final String PATH_PROGRAM = "program"; + private static final String PATH_INPUT = "input"; + + /** + * An optional query, update or delete URI parameter that allows the caller to specify start + * time (in milliseconds since the epoch) to filter programs. + * + * @hide + */ + public static final String PARAM_START_TIME = "start_time"; + + /** + * An optional query, update or delete URI parameter that allows the caller to specify end time + * (in milliseconds since the epoch) to filter programs. + * + * @hide + */ + public static final String PARAM_END_TIME = "end_time"; + + /** + * A query, update or delete URI parameter that allows the caller to operate on all or + * browsable-only channels. If set to "true", the rows that contain non-browsable channels are + * not affected. + * + * @hide + */ + public static final String PARAM_BROWSABLE_ONLY = "browsable_only"; + + /** + * Builds a URI that points to a specific channel. + * + * @param channelId The ID of the channel to point to. + */ + public static final Uri buildChannelUri(long channelId) { + return ContentUris.withAppendedId(Channels.CONTENT_URI, channelId); + } + + /** + * Builds a URI that points to all browsable channels from a given TV input. + * + * @param name {@link ComponentName} of the {@link android.media.tv.TvInputService} that + * implements the given TV input. + */ + public static final Uri buildChannelsUriForInput(ComponentName name) { + return buildChannelsUriForInput(name, true); + } + + /** + * Builds a URI that points to all or browsable-only channels from a given TV input. + * + * @param name {@link ComponentName} of the {@link android.media.tv.TvInputService} that + * implements the given TV input. + * @param browsableOnly If set to {@code true} the URI points to only browsable channels. If set + * to {@code false} the URI points to all channels regardless of whether they are + * browsable or not. + */ + public static final Uri buildChannelsUriForInput(ComponentName name, boolean browsableOnly) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) + .appendPath(PATH_INPUT).appendPath(name.getPackageName()) + .appendPath(name.getClassName()).appendPath(PATH_CHANNEL) + .appendQueryParameter(PARAM_BROWSABLE_ONLY, String.valueOf(browsableOnly)).build(); + } + + /** + * Builds a URI that points to a specific program. + * + * @param programId The ID of the program to point to. + */ + public static final Uri buildProgramUri(long programId) { + return ContentUris.withAppendedId(Programs.CONTENT_URI, programId); + } + + /** + * Builds a URI that points to all programs on a given channel. + * + * @param channelUri The URI of the channel to return programs for. + */ + public static final Uri buildProgramsUriForChannel(Uri channelUri) { + if (!PATH_CHANNEL.equals(channelUri.getPathSegments().get(0))) { + throw new IllegalArgumentException("Not a channel: " + channelUri); + } + String channelId = String.valueOf(ContentUris.parseId(channelUri)); + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) + .appendPath(PATH_CHANNEL).appendPath(channelId).appendPath(PATH_PROGRAM).build(); + } + + /** + * Builds a URI that points to programs on a specific channel whose schedules overlap with the + * given time frame. + * + * @param channelUri The URI of the channel to return programs for. + * @param startTime The start time used to filter programs. The returned programs should have + * {@link Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this time. + * @param endTime The end time used to filter programs. The returned programs should have + * {@link Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time. + */ + public static final Uri buildProgramsUriForChannel(Uri channelUri, long startTime, + long endTime) { + Uri uri = buildProgramsUriForChannel(channelUri); + return uri.buildUpon().appendQueryParameter(PARAM_START_TIME, String.valueOf(startTime)) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(endTime)).build(); + } + + /** + * Builds a URI that points to a specific program the user watched. + * + * @param watchedProgramId The ID of the watched program to point to. + * @hide + */ + public static final Uri buildWatchedProgramUri(long watchedProgramId) { + return ContentUris.withAppendedId(WatchedPrograms.CONTENT_URI, watchedProgramId); + } + + /** + * Extracts the {@link Channels#COLUMN_PACKAGE_NAME} from a given URI. + * + * @param channelsUri A URI constructed by {@link #buildChannelsUriForInput(ComponentName)} or + * {@link #buildChannelsUriForInput(ComponentName, boolean)}. + * @hide + */ + public static final String getPackageName(Uri channelsUri) { + final List<String> paths = channelsUri.getPathSegments(); + if (paths.size() < 4) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + if (!PATH_INPUT.equals(paths.get(0)) || !PATH_CHANNEL.equals(paths.get(3))) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + return paths.get(1); + } + + /** + * Extracts the {@link Channels#COLUMN_SERVICE_NAME} from a given URI. + * + * @param channelsUri A URI constructed by {@link #buildChannelsUriForInput(ComponentName)} or + * {@link #buildChannelsUriForInput(ComponentName, boolean)}. + * @hide + */ + public static final String getServiceName(Uri channelsUri) { + final List<String> paths = channelsUri.getPathSegments(); + if (paths.size() < 4) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + if (!PATH_INPUT.equals(paths.get(0)) || !PATH_CHANNEL.equals(paths.get(3))) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + return paths.get(2); + } + + /** + * Extracts the {@link Channels#_ID} from a given URI. + * + * @param programsUri A URI constructed by {@link #buildProgramsUriForChannel(Uri)} or + * {@link #buildProgramsUriForChannel(Uri, long, long)}. + * @hide + */ + public static final String getChannelId(Uri programsUri) { + final List<String> paths = programsUri.getPathSegments(); + if (paths.size() < 3) { + throw new IllegalArgumentException("Not programs: " + programsUri); + } + if (!PATH_CHANNEL.equals(paths.get(0)) || !PATH_PROGRAM.equals(paths.get(2))) { + throw new IllegalArgumentException("Not programs: " + programsUri); + } + return paths.get(1); + } + + + private TvContract() {} + + /** + * Common base for the tables of TV channels/programs. + */ + public interface BaseTvColumns extends BaseColumns { + /** + * The name of the package that owns a row in each table. + * <p> + * The TV provider fills it in with the name of the package that provides the initial data + * of that row. If the package is later uninstalled, the rows it owns are automatically + * removed from the tables. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_PACKAGE_NAME = "package_name"; + } + + /** Column definitions for the TV channels table. */ + public static final class Channels implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + + PATH_CHANNEL); + + /** The MIME type of a directory of TV channels. */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/channel"; + + /** The MIME type of a single TV channel. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel"; + + /** A generic channel type. */ + public static final int TYPE_OTHER = 0x0; + + /** The special channel type used for pass-through inputs such as HDMI. */ + public static final int TYPE_PASSTHROUGH = 0x00010000; + + /** The channel type for DVB-T (terrestrial). */ + public static final int TYPE_DVB_T = 0x00020000; + + /** The channel type for DVB-T2 (terrestrial). */ + public static final int TYPE_DVB_T2 = 0x00020001; + + /** The channel type for DVB-S (satellite). */ + public static final int TYPE_DVB_S = 0x00020100; + + /** The channel type for DVB-S2 (satellite). */ + public static final int TYPE_DVB_S2 = 0x00020101; + + /** The channel type for DVB-C (cable). */ + public static final int TYPE_DVB_C = 0x00020200; + + /** The channel type for DVB-C2 (cable). */ + public static final int TYPE_DVB_C2 = 0x00020201; + + /** The channel type for DVB-H (handheld). */ + public static final int TYPE_DVB_H = 0x00020300; + + /** The channel type for DVB-SH (satellite). */ + public static final int TYPE_DVB_SH = 0x00020400; + + /** The channel type for ATSC (terrestrial). */ + public static final int TYPE_ATSC_T = 0x00030000; + + /** The channel type for ATSC (cable). */ + public static final int TYPE_ATSC_C = 0x00030200; + + /** The channel type for ATSC-M/H (mobile/handheld). */ + public static final int TYPE_ATSC_M_H = 0x00030200; + + /** The channel type for ISDB-T (terrestrial). */ + public static final int TYPE_ISDB_T = 0x00040000; + + /** The channel type for ISDB-Tb (Brazil). */ + public static final int TYPE_ISDB_TB = 0x00040100; + + /** The channel type for ISDB-S (satellite). */ + public static final int TYPE_ISDB_S = 0x00040200; + + /** The channel type for ISDB-C (cable). */ + public static final int TYPE_ISDB_C = 0x00040300; + + /** The channel type for 1seg (handheld). */ + public static final int TYPE_1SEG = 0x00040400; + + /** The channel type for DTMB (terrestrial). */ + public static final int TYPE_DTMB = 0x00050000; + + /** The channel type for CMMB (handheld). */ + public static final int TYPE_CMMB = 0x00050100; + + /** The channel type for T-DMB (terrestrial). */ + public static final int TYPE_T_DMB = 0x00060000; + + /** The channel type for S-DMB (satellite). */ + public static final int TYPE_S_DMB = 0x00060100; + + /** A generic service type. */ + public static final int SERVICE_TYPE_OTHER = 0x0; + + /** The service type for regular TV channels that have both audio and video. */ + public static final int SERVICE_TYPE_AUDIO_VIDEO = 0x1; + + /** The service type for radio channels that have audio only. */ + public static final int SERVICE_TYPE_AUDIO = 0x2; + + /** + * The name of the {@link TvInputService} subclass that provides this TV channel. This + * should be a fully qualified class name (such as, "com.example.project.TvInputService"). + * <p> + * This is a required field. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_SERVICE_NAME = "service_name"; + + /** + * The predefined type of this TV channel. + * <p> + * This is primarily used to indicate which broadcast standard (e.g. ATSC, DVB or ISDB) the + * current channel conforms to, with an exception being {@link #TYPE_PASSTHROUGH}, which is + * a special channel type used only by pass-through inputs such as HDMI. The value should + * match to one of the followings: {@link #TYPE_OTHER}, {@link #TYPE_PASSTHROUGH}, + * {@link #TYPE_DVB_T}, {@link #TYPE_DVB_T2}, {@link #TYPE_DVB_S}, {@link #TYPE_DVB_S2}, + * {@link #TYPE_DVB_C}, {@link #TYPE_DVB_C2}, {@link #TYPE_DVB_H}, {@link #TYPE_DVB_SH}, + * {@link #TYPE_ATSC_T}, {@link #TYPE_ATSC_C}, {@link #TYPE_ATSC_M_H}, {@link #TYPE_ISDB_T}, + * {@link #TYPE_ISDB_TB}, {@link #TYPE_ISDB_S}, {@link #TYPE_ISDB_C} {@link #TYPE_1SEG}, + * {@link #TYPE_DTMB}, {@link #TYPE_CMMB}, {@link #TYPE_T_DMB}, {@link #TYPE_S_DMB} + * </p><p> + * This is a required field. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_TYPE = "type"; + + /** + * The predefined service type of this TV channel. + * <p> + * This is primarily used to indicate whether the current channel is a regular TV channel or + * a radio-like channel. Use the same coding for {@code service_type} in the underlying + * broadcast standard if it is defined there (e.g. ATSC A/53, ETSI EN 300 468 and ARIB + * STD-B10). Otherwise use one of the followings: {@link #SERVICE_TYPE_OTHER}, + * {@link #SERVICE_TYPE_AUDIO_VIDEO}, {@link #SERVICE_TYPE_AUDIO} + * </p><p> + * This is a required field. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_SERVICE_TYPE = "service_type"; + + /** + * The original network ID of this TV channel. + * <p> + * This is used to identify the originating delivery system, if applicable. Use the same + * coding for {@code original_network_id} in the underlying broadcast standard if it is + * defined there (e.g. ETSI EN 300 468/TR 101 211 and ARIB STD-B10). If channels cannot be + * globally identified by 2-tuple {{@link #COLUMN_TRANSPORT_STREAM_ID}, + * {@link #COLUMN_SERVICE_ID}}, one must carefully assign a value to this field to form a + * unique 3-tuple identification {{@link #COLUMN_ORIGINAL_NETWORK_ID}, + * {@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}} for its channels. + * </p><p> + * This is a required field if the channel cannot be uniquely identified by a 2-tuple + * {{@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}}. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_ORIGINAL_NETWORK_ID = "original_network_id"; + + /** + * The transport stream ID of this channel. + * <p> + * This is used to identify the Transport Stream that contains the current channel from any + * other multiplex within a network, if applicable. Use the same coding for + * {@code transport_stream_id} defined in ISO/IEC 13818-1 if the channel is transmitted via + * the MPEG Transport Stream as is the case for many digital broadcast standards. + * </p><p> + * This is a required field if the current channel is transmitted via the MPEG Transport + * Stream. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_TRANSPORT_STREAM_ID = "transport_stream_id"; + + /** + * The service ID of this channel. + * <p> + * This is used to identify the current service (roughly equivalent to channel) from any + * other service within the Transport Stream, if applicable. Use the same coding for + * {@code service_id} in the underlying broadcast standard if it is defined there (e.g. ETSI + * EN 300 468 and ARIB STD-B10) or {@code program_number} (which usually has the same value + * as {@code service_id}) in ISO/IEC 13818-1 if the channel is transmitted via the MPEG + * Transport Stream. + * </p><p> + * This is a required field if the current channel is transmitted via the MPEG Transport + * Stream. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_SERVICE_ID = "service_id"; + + /** + * The channel number that is displayed to the user. + * <p> + * The format can vary depending on broadcast standard and product specification. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_DISPLAY_NUMBER = "display_number"; + + /** + * The channel name that is displayed to the user. + * <p> + * A call sign is a good candidate to use for this purpose but any name that helps the user + * recognize the current channel will be enough. Can also be empty depending on broadcast + * standard. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_DISPLAY_NAME = "display_name"; + + /** + * The description of this TV channel. + * <p> + * Can be empty initially. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_DESCRIPTION = "description"; + + /** + * The flag indicating whether this TV channel is browsable or not. + * <p> + * A value of 1 indicates the channel is included in the channel list that applications use + * to browse channels, a value of 0 indicates the channel is not included in the list. If + * not specified, this value is set to 1 (browsable) by default. + * </p><p> + * Type: INTEGER (boolean) + * </p> + */ + public static final String COLUMN_BROWSABLE = "browsable"; + + /** + * The flag indicating whether this TV channel is searchable or not. + * <p> + * In some regions, it is not allowed to surface search results for a given channel without + * broadcaster's consent. This is used to impose such restriction. A value of 1 indicates + * the channel is searchable and can be included in search results, a value of 0 indicates + * the channel and its TV programs are hidden from search. If not specified, this value is + * set to 1 (searchable) by default. + * </p> + * <p> + * Type: INTEGER (boolean) + * </p> + */ + public static final String COLUMN_SEARCHABLE = "searchable"; + + /** + * The flag indicating whether this TV channel is locked or not. + * <p> + * This is primarily used for alternative parental control to prevent unauthorized users + * from watching the current channel regardless of the content rating. A value of 1 + * indicates the channel is locked and the user is required to enter passcode to unlock it + * in order to watch the current program from the channel, a value of 0 indicates the + * channel is not locked thus the user is not prompted to enter passcode If not specified, + * this value is set to 0 (not locked) by default. + * </p><p> + * Type: INTEGER (boolean) + * </p> + * @hide + */ + public static final String COLUMN_LOCKED = "locked"; + + /** + * Internal data 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: BLOB + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; + + /** + * 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 + * defined by individual TV input services. One may assign the same value as + * {@code version_number} that appears in ETSI EN 300 468 or ATSC A/65, if the data are + * coming from a TV broadcast. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_VERSION_NUMBER = "version_number"; + + private Channels() {} + } + + /** Column definitions for the TV programs table. */ + public static final class Programs implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + + PATH_PROGRAM); + + /** The MIME type of a directory of TV programs. */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/program"; + + /** The MIME type of a single TV program. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program"; + + /** + * The ID of the TV channel that contains this TV program. + * <p> + * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. + * </p><p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_CHANNEL_ID = "channel_id"; + + /** + * The title of this TV program. + * <p> + * Type: TEXT + * </p> + **/ + public static final String COLUMN_TITLE = "title"; + + /** + * The start time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The comma-separated genre string of this TV program. + * <p> + * Use the same language appeared in the underlying broadcast standard, if applicable. (For + * example, one can refer to the genre strings used in Genre Descriptor of ATSC A/65 or + * Content Descriptor of ETSI EN 300 468, if appropriate.) Otherwise, leave empty. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + + /** + * The comma-separated canonical genre string of this TV program. + * <p> + * Canonical genres are defined in {@link Genres}. Use {@link Genres#encode Genres.encode()} + * to create a text that can be stored in this column. Use {@link Genres#decode + * Genres.decode()} to get the canonical genre strings from the text stored in this column. + * </p><p> + * Type: TEXT + * </p> + * @see Genres + */ + public static final String COLUMN_CANONICAL_GENRE = "canonical_genre"; + + /** + * The short description of this TV program that is displayed to the user by default. + * <p> + * It is recommended to limit the length of the descriptions to 256 characters. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_SHORT_DESCRIPTION = "short_description"; + + /** + * The detailed, lengthy description of this TV program that is displayed only when the user + * wants to see more information. + * <p> + * TV input services should leave this field empty if they have no additional details beyond + * {@link #COLUMN_SHORT_DESCRIPTION}. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_LONG_DESCRIPTION = "long_description"; + + /** + * The comma-separated audio languages of this TV program. + * <p> + * This is used to describe available audio languages included in the program. Use + * 3-character language code as specified by ISO 639-2. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_AUDIO_LANGUAGE = "audio_language"; + + /** + * Internal data 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: BLOB + * </p> + */ + public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; + + /** + * 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 + * defined by individual TV input services. One may assign the same value as + * {@code version_number} in ETSI EN 300 468 or ATSC A/65, if the data are coming from a TV + * broadcast. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String COLUMN_VERSION_NUMBER = "version_number"; + + private Programs() {} + + /** Canonical genres for TV programs. */ + public static final class Genres { + /** The genre for Family/Kids. */ + public static final String FAMILY_KIDS = "Family/Kids"; + + /** The genre for Sports. */ + public static final String SPORTS = "Sports"; + + /** The genre for Shopping. */ + public static final String SHOPPING = "Shopping"; + + /** The genre for Movies. */ + public static final String MOVIES = "Movies"; + + /** The genre for Comedy. */ + public static final String COMEDY = "Comedy"; + + /** The genre for Travel. */ + public static final String TRAVEL = "Travel"; + + /** The genre for Drama. */ + public static final String DRAMA = "Drama"; + + /** The genre for Education. */ + public static final String EDUCATION = "Education"; + + /** The genre for Animal/Wildlife. */ + public static final String ANIMAL_WILDLIFE = "Animal/Wildlife"; + + /** The genre for News. */ + public static final String NEWS = "News"; + + /** The genre for Gaming. */ + public static final String GAMING = "Gaming"; + + private Genres() {} + + /** + * Encodes canonical genre strings to a text that can be put into the database. + * + * @param genres Canonical genre strings. Use the strings defined in this class. + * @return an encoded genre string that can be inserted into the + * {@link #COLUMN_CANONICAL_GENRE} column. + */ + public static String encode(String... genres) { + StringBuilder sb = new StringBuilder(); + String separator = ""; + for (String genre : genres) { + sb.append(separator).append(genre); + separator = ","; + } + return sb.toString(); + } + + /** + * Decodes the canonical genre strings from the text stored in the database. + * + * @param genres The encoded genre string retrieved from the + * {@link #COLUMN_CANONICAL_GENRE} column. + * @return canonical genre strings. + */ + public static String[] decode(String genres) { + return genres.split("\\s*,\\s*"); + } + } + } + + /** + * Column definitions for the TV programs that the user watched. Applications do not have access + * to this table. + * + * @hide + */ + public static final class WatchedPrograms implements BaseColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/watched_program"); + + /** The MIME type of a directory of watched programs. */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/watched_program"; + + /** The MIME type of a single item in this table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watched_program"; + + /** + * The UTC time that the user started watching this TV program, in milliseconds since the + * epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_WATCH_START_TIME_UTC_MILLIS = + "watch_start_time_utc_millis"; + + /** + * The UTC time that the user stopped watching this TV program, in milliseconds since the + * epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis"; + + /** + * The channel ID that contains this TV program. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_CHANNEL_ID = "channel_id"; + + /** + * The title of this TV program. + * <p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_TITLE = "title"; + + /** + * The start time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The description of this TV program. + * <p> + * Type: TEXT + * </p> + */ + public static final String COLUMN_DESCRIPTION = "description"; + + private WatchedPrograms() {} + } +} diff --git a/media/java/android/media/tv/TvInputHardwareInfo.aidl b/media/java/android/media/tv/TvInputHardwareInfo.aidl new file mode 100644 index 0000000..a4c38bb --- /dev/null +++ b/media/java/android/media/tv/TvInputHardwareInfo.aidl @@ -0,0 +1,20 @@ +/* + * + * Copyright 2014, 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.tv; + +parcelable TvInputHardwareInfo; diff --git a/media/java/android/media/tv/TvInputHardwareInfo.java b/media/java/android/media/tv/TvInputHardwareInfo.java new file mode 100644 index 0000000..4beb960 --- /dev/null +++ b/media/java/android/media/tv/TvInputHardwareInfo.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * Simple container for information about TV input hardware. + * Not for third-party developers. + * + * @hide + */ +public final class TvInputHardwareInfo implements Parcelable { + static final String TAG = "TvInputHardwareInfo"; + + // Match hardware/libhardware/include/hardware/tv_input.h + public static final int TV_INPUT_TYPE_HDMI = 1; + public static final int TV_INPUT_TYPE_BUILT_IN_TUNER = 2; + public static final int TV_INPUT_TYPE_PASSTHROUGH = 3; + + public static final Parcelable.Creator<TvInputHardwareInfo> CREATOR = + new Parcelable.Creator<TvInputHardwareInfo>() { + @Override + public TvInputHardwareInfo createFromParcel(Parcel source) { + try { + TvInputHardwareInfo info = new TvInputHardwareInfo(); + info.readFromParcel(source); + return info; + } catch (Exception e) { + Log.e(TAG, "Exception creating TvInputHardwareInfo from parcel", e); + return null; + } + } + + @Override + public TvInputHardwareInfo[] newArray(int size) { + return new TvInputHardwareInfo[size]; + } + }; + + private int mDeviceId; + private int mType; + // TODO: Add audio port & audio address for audio service. + // TODO: Add HDMI handle for HDMI service. + + public TvInputHardwareInfo() { } + + public TvInputHardwareInfo(int deviceId, int type) { + mDeviceId = deviceId; + mType = type; + } + + public int getDeviceId() { + return mDeviceId; + } + + public int getType() { + return mType; + } + + // Parcelable + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mDeviceId); + dest.writeInt(mType); + } + + public void readFromParcel(Parcel source) { + mDeviceId = source.readInt(); + mType = source.readInt(); + } +} diff --git a/media/java/android/media/tv/TvInputInfo.aidl b/media/java/android/media/tv/TvInputInfo.aidl new file mode 100644 index 0000000..ba139a2 --- /dev/null +++ b/media/java/android/media/tv/TvInputInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2014 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.tv; + +parcelable TvInputInfo; diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java new file mode 100644 index 0000000..ed599ed --- /dev/null +++ b/media/java/android/media/tv/TvInputInfo.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * This class is used to specify meta information of a TV input. + */ +public final class TvInputInfo implements Parcelable { + private static final boolean DEBUG = false; + private static final String TAG = "TvInputInfo"; + + /** + * The name of the TV input service to provide to the setup activity and settings activity. + */ + public static final String EXTRA_SERVICE_NAME = "serviceName"; + + private static final String XML_START_TAG_NAME = "tv-input"; + + private final ResolveInfo mService; + private final String mId; + + // Attributes from XML meta data. + private String mSetupActivity; + private String mSettingsActivity; + + /** + * Create a new instance of the TvInputInfo class, + * instantiating it from the given Context and ResolveInfo. + * + * @param service The ResolveInfo returned from the package manager about this TV input service. + * @hide */ + public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service) + throws XmlPullParserException, IOException { + ServiceInfo si = service.serviceInfo; + PackageManager pm = context.getPackageManager(); + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + TvInputService.SERVICE_META_DATA + + " meta-data for " + si.name); + } + + Resources res = pm.getResourcesForApplication(si.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!XML_START_TAG_NAME.equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with tv-input-service tag in " + si.name); + } + + TvInputInfo input = new TvInputInfo(context, service); + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.TvInputService); + input.mSetupActivity = sa.getString( + com.android.internal.R.styleable.TvInputService_setupActivity); + if (DEBUG) { + Log.d(TAG, "Setup activity loaded. [" + input.mSetupActivity + "] for " + si.name); + } + input.mSettingsActivity = sa.getString( + com.android.internal.R.styleable.TvInputService_settingsActivity); + if (DEBUG) { + Log.d(TAG, "Settings activity loaded. [" + input.mSettingsActivity + "] for " + + si.name); + } + sa.recycle(); + + return input; + } catch (NameNotFoundException e) { + throw new XmlPullParserException("Unable to create context for: " + si.packageName); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + /** + * Constructor. + * + * @param service The ResolveInfo returned from the package manager about this TV input service. + * @hide + */ + private TvInputInfo(Context context, ResolveInfo service) { + mService = service; + ServiceInfo si = service.serviceInfo; + mId = generateInputIdForComponentName(new ComponentName(si.packageName, si.name)); + } + + /** + * Returns a unique ID for this TV input. The ID is generated from the package and class name + * implementing the TV input service. + */ + public String getId() { + return mId; + } + + /** + * Returns the .apk package that implements this TV input service. + */ + public String getPackageName() { + return mService.serviceInfo.packageName; + } + + /** + * Returns the class name of the service component that implements this TV input service. + */ + public String getServiceName() { + return mService.serviceInfo.name; + } + + /** + * Returns the component of the service that implements this TV input. + */ + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name); + } + + /** + * Returns an intent to start the setup activity for this TV input service. + */ + public Intent getIntentForSetupActivity() { + if (!TextUtils.isEmpty(mSetupActivity)) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getPackageName(), mSetupActivity); + intent.putExtra(EXTRA_SERVICE_NAME, getServiceName()); + return intent; + } + return null; + } + + /** + * Returns an intent to start the settings activity for this TV input service. + */ + public Intent getIntentForSettingsActivity() { + if (!TextUtils.isEmpty(mSettingsActivity)) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getPackageName(), mSettingsActivity); + intent.putExtra(EXTRA_SERVICE_NAME, getServiceName()); + return intent; + } + return null; + } + + /** + * Loads the user-displayed label for this TV input service. + * + * @param pm Supplies a PackageManager used to load the TV input's resources. + * @return a CharSequence containing the TV input's label. If the TV input does not have + * a label, its name is returned. + */ + public CharSequence loadLabel(PackageManager pm) { + return mService.loadLabel(pm); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return mId.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof TvInputInfo)) { + return false; + } + + TvInputInfo obj = (TvInputInfo) o; + return mId.equals(obj.mId) + && mService.serviceInfo.packageName.equals(obj.mService.serviceInfo.packageName) + && mService.serviceInfo.name.equals(obj.mService.serviceInfo.name); + } + + @Override + public String toString() { + return "TvInputInfo{id=" + mId + + ", pkg=" + mService.serviceInfo.packageName + + ", service=" + mService.serviceInfo.name + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + mService.writeToParcel(dest, flags); + dest.writeString(mSetupActivity); + dest.writeString(mSettingsActivity); + } + + /** + * Used to generate an input id from a ComponentName. + * + * @param name the component name for generating an input id. + * @return the generated input id for the given {@code name}. + * @hide + */ + public static final String generateInputIdForComponentName(ComponentName name) { + return name.flattenToShortString(); + } + + /** + * Used to make this class parcelable. + * + * @hide + */ + public static final Parcelable.Creator<TvInputInfo> CREATOR = + new Parcelable.Creator<TvInputInfo>() { + @Override + public TvInputInfo createFromParcel(Parcel in) { + return new TvInputInfo(in); + } + + @Override + public TvInputInfo[] newArray(int size) { + return new TvInputInfo[size]; + } + }; + + private TvInputInfo(Parcel in) { + mId = in.readString(); + mService = ResolveInfo.CREATOR.createFromParcel(in); + mSetupActivity = in.readString(); + mSettingsActivity = in.readString(); + } +} diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java new file mode 100644 index 0000000..698a861 --- /dev/null +++ b/media/java/android/media/tv/TvInputManager.java @@ -0,0 +1,917 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.graphics.Rect; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pools.Pool; +import android.util.Pools.SimplePool; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; +import android.view.Surface; +import android.view.View; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Central system API to the overall TV input framework (TIF) architecture, which arbitrates + * interaction between applications and the selected TV inputs. + */ +public final class TvInputManager { + private static final String TAG = "TvInputManager"; + + private final ITvInputManager mService; + + // A mapping from an input to the list of its TvInputListenerRecords. + private final Map<String, List<TvInputListenerRecord>> mTvInputListenerRecordsMap = + new HashMap<String, List<TvInputListenerRecord>>(); + + // A mapping from the sequence number of a session to its SessionCallbackRecord. + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = + new SparseArray<SessionCallbackRecord>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCallbackRecordMap}. + private int mNextSeq; + + private final ITvInputClient mClient; + + private final int mUserId; + + /** + * Interface used to receive the created session. + */ + public abstract static class SessionCallback { + /** + * This is called after {@link TvInputManager#createSession} has been processed. + * + * @param session A {@link TvInputManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + public void onSessionCreated(Session session) { + } + + /** + * This is called when {@link TvInputManager.Session} is released. + * This typically happens when the process hosting the session has crashed or been killed. + * + * @param session A {@link TvInputManager.Session} instance released. + */ + public void onSessionReleased(Session session) { + } + + /** + * This is called at the beginning of the playback of a channel and later when the format of + * the video stream has been changed. + * + * @param session A {@link TvInputManager.Session} associated with this callback + * @param width The width of the video. + * @param height The height of the video. + * @param interlaced whether the video is interlaced mode or planer mode. + * @hide + */ + public void onVideoStreamChanged(Session session, int width, int height, + boolean interlaced) { + } + + /** + * This is called at the beginning of the playback of a channel and later when the format of + * the audio stream has been changed. + * + * @param session A {@link TvInputManager.Session} associated with this callback + * @param channelCount The number of channels in the audio stream. + * @hide + */ + public void onAudioStreamChanged(Session session, int channelCount) { + } + + /** + * This is called at the beginning of the playback of a channel and later when the closed + * caption stream has been changed. + * + * @param session A {@link TvInputManager.Session} associated with this callback + * @param hasClosedCaption Whether the stream has closed caption or not. + * @hide + */ + public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) { + } + + /** + * This is called when a custom event has been sent from this session. + * + * @param session A {@link TvInputManager.Session} associated with this callback + * @param eventType The type of the event. + * @param eventArgs Optional arguments of the event. + * @hide + */ + public void onSessionEvent(Session session, String eventType, Bundle eventArgs) { + } + } + + private static final class SessionCallbackRecord { + private final SessionCallback mSessionCallback; + private final Handler mHandler; + private Session mSession; + + public SessionCallbackRecord(SessionCallback sessionCallback, + Handler handler) { + mSessionCallback = sessionCallback; + mHandler = handler; + } + + public void postSessionCreated(final Session session) { + mSession = session; + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionCreated(session); + } + }); + } + + public void postSessionReleased() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionReleased(mSession); + } + }); + } + + public void postVideoStreamChanged(final int width, final int height, + final boolean interlaced) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onVideoStreamChanged(mSession, width, height, interlaced); + } + }); + } + + public void postAudioStreamChanged(final int channelCount) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onAudioStreamChanged(mSession, channelCount); + } + }); + } + + public void postClosedCaptionStreamChanged(final boolean hasClosedCaption) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onClosedCaptionStreamChanged(mSession, hasClosedCaption); + } + }); + } + + public void postSessionEvent(final String eventType, final Bundle eventArgs) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionEvent(mSession, eventType, eventArgs); + } + }); + } + } + + /** + * Interface used to monitor status of the TV input. + */ + public abstract static class TvInputListener { + /** + * This is called when the availability status of a given TV input is changed. + * + * @param inputId the id of the TV input. + * @param isAvailable {@code true} if the given TV input is available to show TV programs. + * {@code false} otherwise. + */ + public void onAvailabilityChanged(String inputId, boolean isAvailable) { + } + } + + private static final class TvInputListenerRecord { + private final TvInputListener mListener; + private final Handler mHandler; + + public TvInputListenerRecord(TvInputListener listener, Handler handler) { + mListener = listener; + mHandler = handler; + } + + public TvInputListener getListener() { + return mListener; + } + + public void postAvailabilityChanged(final String inputId, final boolean isAvailable) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onAvailabilityChanged(inputId, isAvailable); + } + }); + } + } + + /** + * @hide + */ + public TvInputManager(ITvInputManager service, int userId) { + mService = service; + mUserId = userId; + mClient = new ITvInputClient.Stub() { + @Override + public void onSessionCreated(String inputId, IBinder token, InputChannel channel, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(token, channel, mService, mUserId, seq, + mSessionCallbackRecordMap); + } + record.postSessionCreated(session); + } + } + + @Override + public void onSessionReleased(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + mSessionCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq:" + seq); + return; + } + record.mSession.releaseInternal(); + record.postSessionReleased(); + } + } + + @Override + public void onVideoStreamChanged(int width, int height, boolean interlaced, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postVideoStreamChanged(width, height, interlaced); + } + } + + @Override + public void onAudioStreamChanged(int channelCount, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postAudioStreamChanged(channelCount); + } + } + + @Override + public void onClosedCaptionStreamChanged(boolean hasClosedCaption, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postClosedCaptionStreamChanged(hasClosedCaption); + } + } + + @Override + public void onSessionEvent(String eventType, Bundle eventArgs, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postSessionEvent(eventType, eventArgs); + } + } + + @Override + public void onAvailabilityChanged(String inputId, boolean isAvailable) { + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); + if (records == null) { + // Silently ignore - no listener is registered yet. + return; + } + int recordsCount = records.size(); + for (int i = 0; i < recordsCount; i++) { + records.get(i).postAvailabilityChanged(inputId, isAvailable); + } + } + } + }; + } + + /** + * Returns the complete list of TV inputs on the system. + * + * @return List of {@link TvInputInfo} for each TV input that describes its meta information. + */ + public List<TvInputInfo> getTvInputList() { + try { + return mService.getTvInputList(mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the availability of a given TV input. + * + * @param inputId the id of the TV input. + * @throws IllegalArgumentException if the argument is {@code null}. + * @throws IllegalStateException If there is no {@link TvInputListener} registered on the given + * TV input. + */ + public boolean getAvailability(String inputId) { + if (inputId == null) { + throw new IllegalArgumentException("id cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); + if (records == null || records.size() == 0) { + throw new IllegalStateException("At least one listener should be registered."); + } + } + try { + return mService.getAvailability(mClient, inputId, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Registers a {@link TvInputListener} for a given TV input. + * + * @param inputId the id of the TV input. + * @param listener a listener used to monitor status of the given TV input. + * @param handler a {@link Handler} that the status change will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void registerListener(String inputId, TvInputListener listener, Handler handler) { + if (inputId == null) { + throw new IllegalArgumentException("id cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); + if (records == null) { + records = new ArrayList<TvInputListenerRecord>(); + mTvInputListenerRecordsMap.put(inputId, records); + try { + mService.registerCallback(mClient, inputId, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + records.add(new TvInputListenerRecord(listener, handler)); + } + } + + /** + * Unregisters the existing {@link TvInputListener} for a given TV input. + * + * @param inputId the id of the TV input. + * @param listener the existing listener to remove for the given TV input. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void unregisterListener(String inputId, final TvInputListener listener) { + if (inputId == null) { + throw new IllegalArgumentException("id cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); + if (records == null) { + Log.e(TAG, "No listener found for " + inputId); + return; + } + for (Iterator<TvInputListenerRecord> it = records.iterator(); it.hasNext();) { + TvInputListenerRecord record = it.next(); + if (record.getListener() == listener) { + it.remove(); + } + } + if (records.isEmpty()) { + try { + mService.unregisterCallback(mClient, inputId, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + mTvInputListenerRecordsMap.remove(inputId); + } + } + } + } + + /** + * Creates a {@link Session} for a given TV input. + * <p> + * The number of sessions that can be created at the same time is limited by the capability of + * the given TV input. + * </p> + * + * @param inputId the id of the TV input. + * @param callback a callback used to receive the created session. + * @param handler a {@link Handler} that the session creation will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void createSession(String inputId, final SessionCallback callback, + Handler handler) { + if (inputId == null) { + throw new IllegalArgumentException("id cannot be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + SessionCallbackRecord record = new SessionCallbackRecord(callback, handler); + synchronized (mSessionCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, inputId, seq, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + + /** The Session provides the per-session functionality of TV inputs. */ + public static final class Session { + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; + + private final ITvInputManager mService; + private final int mUserId; + private final int mSeq; + + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20); + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; + + private IBinder mToken; + private TvInputEventSender mSender; + private InputChannel mChannel; + + /** @hide */ + private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId, + int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { + mToken = token; + mChannel = channel; + mService = service; + mUserId = userId; + mSeq = seq; + mSessionCallbackRecordMap = sessionCallbackRecordMap; + } + + /** + * Releases this session. + * + * @throws IllegalStateException if the session has been already released. + */ + public void release() { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + releaseInternal(); + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render video. + * @throws IllegalStateException if the session has been already released. + * @hide + */ + public void setSurface(Surface surface) { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the relative volume of this session to handle a change of audio focus. + * + * @param volume A volume value between 0.0f to 1.0f. + * @throws IllegalArgumentException if the volume value is out of range. + * @throws IllegalStateException if the session has been already released. + */ + public void setVolume(float volume) { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + if (volume < 0.0f || volume > 1.0f) { + throw new IllegalArgumentException("volume should be between 0.0f and 1.0f"); + } + mService.setVolume(mToken, volume, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of a channel. + * @throws IllegalArgumentException if the argument is {@code null}. + * @throws IllegalStateException if the session has been already released. + */ + public void tune(Uri channelUri) { + if (channelUri == null) { + throw new IllegalArgumentException("channelUri cannot be null"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.tune(mToken, channelUri, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView} + * should be called whenever the layout of its containing view is changed. + * {@link #removeOverlayView()} should be called to remove the overlay view. + * Since a session can have only one overlay view, this method should be called only once + * or it can be called again after calling {@link #removeOverlayView()}. + * + * @param view A view playing TV. + * @param frame A position of the overlay view. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + * @throws IllegalStateException if {@code view} is not attached to a window or + * if the session has been already released. + */ + void createOverlayView(View view, Rect frame) { + if (view == null) { + throw new IllegalArgumentException("view cannot be null"); + } + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + if (view.getWindowToken() == null) { + throw new IllegalStateException("view must be attached to a window"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Relayouts the current overlay view. + * + * @param frame A new position of the overlay view. + * @throws IllegalArgumentException if the arguments is {@code null}. + * @throws IllegalStateException if the session has been already released. + */ + void relayoutOverlayView(Rect frame) { + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.relayoutOverlayView(mToken, frame, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Removes the current overlay view. + * + * @throws IllegalStateException if the session has been already released. + */ + void removeOverlayView() { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.removeOverlayView(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Dispatches an input event to this session. + * + * @param event {@link InputEvent} to dispatch. + * @param token A token used to identify the input event later in the callback. + * @param callback A callback used to receive the dispatch result. + * @param handler {@link Handler} that the dispatch result will be delivered to. + * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns + * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns + * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will + * be invoked later. + * @throws IllegalArgumentException if any of the necessary arguments is {@code null}. + * @hide + */ + public int dispatchInputEvent(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + if (event == null) { + throw new IllegalArgumentException("event cannot be null"); + } + if (callback != null && handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mHandler) { + if (mChannel == null) { + return DISPATCH_NOT_HANDLED; + } + PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); + if (Looper.myLooper() == Looper.getMainLooper()) { + // Already running on the main thread so we can send the event immediately. + return sendInputEventOnMainLooperLocked(p); + } + + // Post the event to the main thread. + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + return DISPATCH_IN_PROGRESS; + } + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token a token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + public void onFinishedInputEvent(Object token, boolean handled); + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for seesion to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + PendingEvent p = mPendingEventPool.acquire(); + if (p == null) { + p = new PendingEvent(); + } + p.mEvent = event; + p.mToken = token; + p.mCallback = callback; + p.mHandler = handler; + return p; + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + private void releaseInternal() { + mToken = null; + synchronized (mHandler) { + if (mChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mChannel.dispose(); + mChannel = null; + } + } + synchronized (mSessionCallbackRecordMap) { + mSessionCallbackRecordMap.remove(mSeq); + } + } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + private final class TvInputEventSender extends InputEventSender { + public TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mToken; + public FinishedInputEventCallback mCallback; + public Handler mHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mToken = null; + mCallback = null; + mHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mToken, mHandled); + + synchronized (mHandler) { + recyclePendingEventLocked(this); + } + } + } + } +} diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java new file mode 100644 index 0000000..8ba0e20 --- /dev/null +++ b/media/java/android/media/tv/TvInputService.java @@ -0,0 +1,651 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.media.tv.ITvInputService; +import android.media.tv.TvInputManager.Session; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.Gravity; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.SomeArgs; + +/** + * A base class for implementing television input service. + */ +public abstract class TvInputService extends Service { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvInputService"; + + /** + * This is the interface name that a service implementing a TV input should say that it support + * -- that is, this is the action it uses for its intent filter. To be supported, the service + * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that + * other applications cannot abuse it. + */ + public static final String SERVICE_INTERFACE = "android.media.tv.TvInputService"; + + /** + * Name under which a TvInputService component publishes information about itself. + * This meta-data must reference an XML resource containing an + * <code><{@link android.R.styleable#TvInputService tv-input}></code> + * tag. + */ + public static final String SERVICE_META_DATA = "android.media.tv.input"; + + private String mId; + private final Handler mHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks = + new RemoteCallbackList<ITvInputServiceCallback>(); + private boolean mAvailable; + + @Override + public void onCreate() { + super.onCreate(); + mId = TvInputInfo.generateInputIdForComponentName( + new ComponentName(getPackageName(), getClass().getName())); + } + + @Override + public final IBinder onBind(Intent intent) { + return new ITvInputService.Stub() { + @Override + public void registerCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + // The first time a callback is registered, the service needs to report its + // availability status so that the system can know its initial value. + try { + cb.onAvailabilityChanged(mId, mAvailable); + } catch (RemoteException e) { + Log.e(TAG, "error in onAvailabilityChanged", e); + } + } + } + + @Override + public void unregisterCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(InputChannel channel, ITvInputSessionCallback cb) { + if (channel == null) { + Log.w(TAG, "Creating session without input channel"); + } + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget(); + } + }; + } + + /** + * Convenience method to notify an availability change of this TV input service. + * + * @param available {@code true} if the input service is available to show TV programs. + */ + public final void setAvailable(boolean available) { + if (available != mAvailable) { + mAvailable = available; + mHandler.obtainMessage(ServiceHandler.DO_BROADCAST_AVAILABILITY_CHANGE, available) + .sendToTarget(); + } + } + + /** + * Get the number of callbacks that are registered. + * + * @hide + */ + @VisibleForTesting + public final int getRegisteredCallbackCount() { + return mCallbacks.getRegisteredCallbackCount(); + } + + /** + * Returns a concrete implementation of {@link TvInputSessionImpl}. + * <p> + * May return {@code null} if this TV input service fails to create a session for some reason. + * </p> + */ + public abstract TvInputSessionImpl onCreateSession(); + + /** + * Base class for derived classes to implement to provide {@link TvInputManager.Session}. + */ + public abstract class TvInputSessionImpl implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + private final WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; + private Surface mSurface; + private View mOverlayView; + private boolean mOverlayViewEnabled; + private IBinder mWindowToken; + private Rect mOverlayFrame; + private ITvInputSessionCallback mSessionCallback; + + public TvInputSessionImpl() { + mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + } + + /** + * Enables or disables the overlay view. By default, the overlay view is disabled. Must be + * called explicitly after the session is created to enable the overlay view. + * + * @param enable {@code true} if you want to enable the overlay view. {@code false} + * otherwise. + */ + public void setOverlayViewEnabled(final boolean enable) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (enable == mOverlayViewEnabled) { + return; + } + mOverlayViewEnabled = enable; + if (enable) { + if (mWindowToken != null) { + createOverlayView(mWindowToken, mOverlayFrame); + } + } else { + removeOverlayView(false); + } + } + }); + } + + /** + * Dispatches an event to the application using this session. + * + * @param eventType The type of the event. + * @param eventArgs Optional arguments of the event. + * @hide + */ + public void dispatchSessionEvent(final String eventType, final Bundle eventArgs) { + if (eventType == null) { + throw new IllegalArgumentException("eventType should not be null."); + } + mHandler.post(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "dispatchSessionEvent(" + eventType + ")"); + mSessionCallback.onSessionEvent(eventType, eventArgs); + } catch (RemoteException e) { + Log.w(TAG, "error in sending event (event=" + eventType + ")"); + } + } + }); + } + + /** + * Sends the change on the format of the video stream. This is expected to be called at the + * beginning of the playback and later when the format has been changed. + * + * @param width The width of the video. + * @param height The height of the video. + * @param interlaced Whether the video is interlaced mode or planer mode. + * @hide + */ + public void dispatchVideoStreamChanged(final int width, final int height, + final boolean interlaced) { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "dispatchVideoSizeChanged"); + mSessionCallback.onVideoStreamChanged(width, height, interlaced); + } catch (RemoteException e) { + Log.w(TAG, "error in dispatchVideoSizeChanged"); + } + } + }); + } + + /** + * Sends the change on the format of the audio stream. This is expected to be called at the + * beginning of the playback and later when the format has been changed. + * + * @param channelNumber The number of channels in the audio stream. + * @hide + */ + public void dispatchAudioStreamChanged(final int channelNumber) { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "dispatchAudioStreamChanged"); + mSessionCallback.onAudioStreamChanged(channelNumber); + } catch (RemoteException e) { + Log.w(TAG, "error in dispatchAudioStreamChanged"); + } + } + }); + } + + /** + * Sends the change on the closed caption stream. This is expected to be called at the + * beginning of the playback and later when the stream has been changed. + * + * @param hasClosedCaption Whether the stream has closed caption or not. + * @hide + */ + public void dispatchClosedCaptionStreamChanged(final boolean hasClosedCaption) { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "dispatchClosedCaptionStreamChanged"); + mSessionCallback.onClosedCaptionStreamChanged(hasClosedCaption); + } catch (RemoteException e) { + Log.w(TAG, "error in dispatchClosedCaptionStreamChanged"); + } + } + }); + } + + /** + * Called when the session is released. + */ + public abstract void onRelease(); + + /** + * Sets the {@link Surface} for the current input session on which the TV input renders + * video. + * + * @param surface {@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); + + /** + * Sets the relative volume of the current TV input session to handle the change of audio + * focus by setting. + * + * @param volume Volume scale from 0.0 to 1.0. + */ + public abstract void onSetVolume(float volume); + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of the channel. + * @return {@code true} the tuning was successful, {@code false} otherwise. + */ + public abstract boolean onTune(Uri channelUri); + + /** + * Called when an application requests to create an overlay view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the overlay window + */ + public View onCreateOverlayView() { + return null; + } + + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent) + * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key down events before they are processed by the application. + * If you return true, the application will not process the event itself. If you return + * false, the normal application processing will occur as if the TV input had not seen the + * event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key long press events before they are processed by the + * application. If you return true, the application will not process the event itself. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) + * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept special key multiple events before they are processed by the + * application. If you return true, the application will not itself process the event. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param count The number of times the action was made. + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent) + * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key up events before they are processed by the application. If + * you return true, the application will not itself process the event. If you return false, + * the normal application processing will occur as if the TV input had not seen the event at + * all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + /** + * Implement this method to handle touch screen motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return false; + } + + /** + * This method is called when the application would like to stop using the current input + * session. + */ + void release() { + onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + removeOverlayView(true); + } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = surface; + // TODO: Handle failure. + } + + /** + * Calls {@link #onSetVolume}. + */ + void setVolume(float volume) { + onSetVolume(volume); + } + + /** + * Calls {@link #onTune}. + */ + void tune(Uri channelUri) { + onTune(channelUri); + // TODO: Handle failure. + } + + /** + * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach + * to the overlay window. + * + * @param windowToken A window token of an application. + * @param frame A position of the overlay view. + */ + void createOverlayView(IBinder windowToken, Rect frame) { + if (mOverlayView != null) { + mWindowManager.removeView(mOverlayView); + mOverlayView = null; + } + if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")"); + mWindowToken = windowToken; + mOverlayFrame = frame; + if (!mOverlayViewEnabled) { + return; + } + mOverlayView = onCreateOverlayView(); + if (mOverlayView == null) { + return; + } + // 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; + // We make the overlay view non-focusable and non-touchable so that + // the application that owns the window token can decide whether to consume or + // dispatch the input events. + int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + mWindowParams = new WindowManager.LayoutParams( + frame.right - frame.left, frame.bottom - frame.top, + frame.left, frame.top, type, flag, PixelFormat.TRANSPARENT); + mWindowParams.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowParams.gravity = Gravity.START | Gravity.TOP; + mWindowParams.token = windowToken; + mWindowManager.addView(mOverlayView, mWindowParams); + } + + /** + * Relayouts the current overlay view. + * + * @param frame A new position of the overlay view. + */ + void relayoutOverlayView(Rect frame) { + if (DEBUG) Log.d(TAG, "relayoutOverlayView(" + frame + ")"); + mOverlayFrame = frame; + if (!mOverlayViewEnabled || mOverlayView == 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); + } + + /** + * Removes the current overlay view. + */ + void removeOverlayView(boolean clearWindowToken) { + if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayView + ")"); + if (clearWindowToken) { + mWindowToken = null; + mOverlayFrame = null; + } + if (mOverlayView != null) { + mWindowManager.removeView(mOverlayView); + mOverlayView = null; + mWindowParams = null; + } + } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + if (((KeyEvent) event).dispatch(this, mDispatcherState, this)) { + return Session.DISPATCH_HANDLED; + } + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } + } + if (mOverlayView == null || !mOverlayView.isAttachedToWindow()) { + return Session.DISPATCH_NOT_HANDLED; + } + if (!mOverlayView.hasWindowFocus()) { + mOverlayView.getViewRootImpl().windowFocusChanged(true, true); + } + mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver); + return Session.DISPATCH_IN_PROGRESS; + } + + private void setSessionCallback(ITvInputSessionCallback callback) { + mSessionCallback = callback; + } + } + + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_BROADCAST_AVAILABILITY_CHANGE = 2; + + @Override + public final void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2; + try { + TvInputSessionImpl sessionImpl = onCreateSession(); + if (sessionImpl == null) { + // Failed to create a session. + cb.onSessionCreated(null); + } else { + sessionImpl.setSessionCallback(cb); + ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, + sessionImpl, channel); + cb.onSessionCreated(stub); + } + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated"); + } + args.recycle(); + return; + } + case DO_BROADCAST_AVAILABILITY_CHANGE: { + boolean isAvailable = (Boolean) msg.obj; + int n = mCallbacks.beginBroadcast(); + try { + for (int i = 0; i < n; i++) { + mCallbacks.getBroadcastItem(i).onAvailabilityChanged(mId, isAvailable); + } + } catch (RemoteException e) { + Log.e(TAG, "Unexpected exception", e); + } finally { + mCallbacks.finishBroadcast(); + } + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } +} diff --git a/media/java/android/media/tv/TvStreamConfig.aidl b/media/java/android/media/tv/TvStreamConfig.aidl new file mode 100644 index 0000000..569fcc0 --- /dev/null +++ b/media/java/android/media/tv/TvStreamConfig.aidl @@ -0,0 +1,20 @@ +/* + * + * Copyright 2014, 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.tv; + +parcelable TvStreamConfig;
\ No newline at end of file diff --git a/media/java/android/media/tv/TvStreamConfig.java b/media/java/android/media/tv/TvStreamConfig.java new file mode 100644 index 0000000..7f0c92f --- /dev/null +++ b/media/java/android/media/tv/TvStreamConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * @hide + */ +public class TvStreamConfig implements Parcelable { + static final String TAG = TvStreamConfig.class.getSimpleName(); + + public final static int STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE = 1; + public final static int STREAM_TYPE_BUFFER_PRODUCER = 2; + + private int mStreamId; + private int mType; + // TODO: Revisit if max widht/height really make sense. + private int mMaxWidth; + private int mMaxHeight; + /** + * Generations are incremented once framework receives STREAM_CONFIGURATION_CHANGED event from + * HAL module. Framework should throw away outdated configurations and get new configurations + * via tv_input_device::get_stream_configurations(). + */ + private int mGeneration; + + public static final Parcelable.Creator<TvStreamConfig> CREATOR = + new Parcelable.Creator<TvStreamConfig>() { + @Override + public TvStreamConfig createFromParcel(Parcel source) { + try { + return new Builder(). + streamId(source.readInt()). + type(source.readInt()). + maxWidth(source.readInt()). + maxHeight(source.readInt()). + generation(source.readInt()).build(); + } catch (Exception e) { + Log.e(TAG, "Exception creating TvStreamConfig from parcel", e); + return null; + } + } + + @Override + public TvStreamConfig[] newArray(int size) { + return new TvStreamConfig[size]; + } + }; + + private TvStreamConfig() {} + + public int getStreamId() { + return mStreamId; + } + + public int getType() { + return mType; + } + + public int getMaxWidth() { + return mMaxWidth; + } + + public int getMaxHeight() { + return mMaxHeight; + } + + public int getGeneration() { + return mGeneration; + } + + // Parcelable + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mStreamId); + dest.writeInt(mType); + dest.writeInt(mMaxWidth); + dest.writeInt(mMaxHeight); + dest.writeInt(mGeneration); + } + + /** + * A helper class for creating a TvStreamConfig object. + */ + public static final class Builder { + private Integer mStreamId; + private Integer mType; + private Integer mMaxWidth; + private Integer mMaxHeight; + private Integer mGeneration; + + public Builder() { + } + + public Builder streamId(int streamId) { + mStreamId = streamId; + return this; + } + + public Builder type(int type) { + mType = type; + return this; + } + + public Builder maxWidth(int maxWidth) { + mMaxWidth = maxWidth; + return this; + } + + public Builder maxHeight(int maxHeight) { + mMaxHeight = maxHeight; + return this; + } + + public Builder generation(int generation) { + mGeneration = generation; + return this; + } + + public TvStreamConfig build() { + if (mStreamId == null || mType == null || mMaxWidth == null || mMaxHeight == null + || mGeneration == null) { + throw new UnsupportedOperationException(); + } + + TvStreamConfig config = new TvStreamConfig(); + config.mStreamId = mStreamId; + config.mType = mType; + config.mMaxWidth = mMaxWidth; + config.mMaxHeight = mMaxHeight; + config.mGeneration = mGeneration; + return config; + } + } +}
\ No newline at end of file diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java new file mode 100644 index 0000000..d8b362d --- /dev/null +++ b/media/java/android/media/tv/TvView.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.Context; +import android.graphics.Rect; +import android.media.tv.TvInputManager.Session; +import android.media.tv.TvInputManager.Session.FinishedInputEventCallback; +import android.media.tv.TvInputManager.SessionCallback; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewRootImpl; + +/** + * View playing TV + */ +public class TvView extends SurfaceView { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvView"; + + private final Handler mHandler = new Handler(); + private TvInputManager.Session mSession; + private Surface mSurface; + private boolean mOverlayViewCreated; + private Rect mOverlayViewFrame; + private final TvInputManager mTvInputManager; + private SessionCallback mSessionCallback; + private OnUnhandledInputEventListener mOnUnhandledInputEventListener; + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width + + ", height=" + height + ")"); + if (holder.getSurface() == mSurface) { + return; + } + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + setSessionSurface(null); + } + }; + + private final FinishedInputEventCallback mFinishedInputEventCallback = + new FinishedInputEventCallback() { + @Override + public void onFinishedInputEvent(Object token, boolean handled) { + if (DEBUG) { + Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); + } + if (handled) { + return; + } + // TODO: Re-order unhandled events. + InputEvent event = (InputEvent) token; + if (dispatchUnhandledInputEvent(event)) { + return; + } + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.dispatchUnhandledInputEvent(event); + } + } + }; + + public TvView(Context context) { + this(context, null, 0); + } + + public TvView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + getHolder().addCallback(mSurfaceHolderCallback); + mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE); + } + + /** + * Binds a TV input to this view. {@link SessionCallback#onSessionCreated} will be + * called to send the result of this binding with {@link TvInputManager.Session}. + * If a TV input is already bound, the input will be unbound from this view and its session + * will be released. + * + * @param inputId the id of TV input which will be bound to this view. + * @param callback called when TV input is bound. The callback sends + * {@link TvInputManager.Session} + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void bindTvInput(String inputId, SessionCallback callback) { + if (TextUtils.isEmpty(inputId)) { + throw new IllegalArgumentException("inputId cannot be null or an empty string"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (mSession != null) { + release(); + } + // When bindTvInput is called multiple times before the callback is called, + // only the callback of the last bindTvInput call will be actually called back. + // The previous callbacks will be ignored. For the logic, mSessionCallback + // is newly assigned for every bindTvInput call and compared with + // MySessionCreateCallback.this. + mSessionCallback = new MySessionCallback(callback); + mTvInputManager.createSession(inputId, mSessionCallback, mHandler); + } + + /** + * Unbinds a TV input currently bound. Its corresponding {@link TvInputManager.Session} + * is released. + */ + public void unbindTvInput() { + if (mSession != null) { + release(); + } + mSessionCallback = null; + } + + /** + * Dispatches an unhandled input event to the next receiver. + * <p> + * Except system keys, TvView always consumes input events in the normal flow. This is called + * asynchronously from where the event is dispatched. It gives the host application a chance to + * dispatch the unhandled input events. + * + * @param event The input event. + * @return {@code true} if the event was handled by the view, {@code false} otherwise. + */ + public boolean dispatchUnhandledInputEvent(InputEvent event) { + if (mOnUnhandledInputEventListener != null) { + if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { + return true; + } + } + return onUnhandledInputEvent(event); + } + + /** + * Called when an unhandled input event was also not handled by the user provided callback. This + * is the last chance to handle the unhandled input event in the TvView. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to be + * handled by the next receiver, return {@code false}. + */ + public boolean onUnhandledInputEvent(InputEvent event) { + return false; + } + + /** + * Registers a callback to be invoked when an input event was not handled by the bound TV input. + * + * @param listener The callback to invoke when the unhandled input event was received. + */ + public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { + mOnUnhandledInputEventListener = listener; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (super.dispatchKeyEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); + if (mSession == null) { + return false; + } + InputEvent copiedEvent = event.copy(); + int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, + mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (super.dispatchTouchEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); + if (mSession == null) { + return false; + } + InputEvent copiedEvent = event.copy(); + int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, + mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + if (super.dispatchTrackballEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); + if (mSession == null) { + return false; + } + InputEvent copiedEvent = event.copy(); + int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, + mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (super.dispatchGenericMotionEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); + if (mSession == null) { + return false; + } + InputEvent copiedEvent = event.copy(); + int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, + mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionOverlayView(); + } + + @Override + protected void onDetachedFromWindow() { + removeSessionOverlayView(); + super.onDetachedFromWindow(); + } + + /** @hide */ + @Override + protected void updateWindow(boolean force, boolean redrawNeeded) { + super.updateWindow(force, redrawNeeded); + relayoutSessionOverlayView(); + } + + private void release() { + setSessionSurface(null); + removeSessionOverlayView(); + mSession.release(); + mSession = null; + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void createSessionOverlayView() { + if (mSession == null || !isAttachedToWindow() + || mOverlayViewCreated) { + return; + } + mOverlayViewFrame = getViewFrameOnScreen(); + mSession.createOverlayView(this, mOverlayViewFrame); + mOverlayViewCreated = true; + } + + private void removeSessionOverlayView() { + if (mSession == null || !mOverlayViewCreated) { + return; + } + mSession.removeOverlayView(); + mOverlayViewCreated = false; + mOverlayViewFrame = null; + } + + private void relayoutSessionOverlayView() { + if (mSession == null || !isAttachedToWindow() + || !mOverlayViewCreated) { + return; + } + Rect viewFrame = getViewFrameOnScreen(); + if (viewFrame.equals(mOverlayViewFrame)) { + return; + } + mSession.relayoutOverlayView(viewFrame); + mOverlayViewFrame = viewFrame; + } + + private Rect getViewFrameOnScreen() { + int[] location = new int[2]; + getLocationOnScreen(location); + return new Rect(location[0], location[1], + location[0] + getWidth(), location[1] + getHeight()); + } + + /** + * Interface definition for a callback to be invoked when the unhandled input event is received. + */ + public interface OnUnhandledInputEventListener { + /** + * Called when an input event was not handled by the bound TV input. + * <p> + * This is called asynchronously from where the event is dispatched. It gives the host + * application a chance to handle the unhandled input events. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + boolean onUnhandledInputEvent(InputEvent event); + } + + private class MySessionCallback extends SessionCallback { + final SessionCallback mExternalCallback; + + MySessionCallback(SessionCallback externalCallback) { + mExternalCallback = externalCallback; + } + + @Override + public void onSessionCreated(Session session) { + if (this != mSessionCallback) { + // This callback is obsolete. + if (session != null) { + session.release(); + } + return; + } + mSession = session; + if (session != null) { + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + } + createSessionOverlayView(); + } + if (mExternalCallback != null) { + mExternalCallback.onSessionCreated(session); + } + } + + @Override + public void onSessionReleased(Session session) { + mSession = null; + if (mExternalCallback != null) { + mExternalCallback.onSessionReleased(session); + } + } + + @Override + public void onVideoStreamChanged(Session session, int width, int height, + boolean interlaced) { + if (DEBUG) { + Log.d(TAG, "onVideoSizeChanged(" + width + ", " + height + ")"); + } + if (mExternalCallback != null) { + mExternalCallback.onVideoStreamChanged(session, width, height, interlaced); + } + } + + @Override + public void onAudioStreamChanged(Session session, int channelCount) { + if (DEBUG) { + Log.d(TAG, "onAudioStreamChanged(" + channelCount + ")"); + } + if (mExternalCallback != null) { + mExternalCallback.onAudioStreamChanged(session, channelCount); + } + } + + @Override + public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) { + if (DEBUG) { + Log.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")"); + } + if (mExternalCallback != null) { + mExternalCallback.onClosedCaptionStreamChanged(session, hasClosedCaption); + } + } + + @Override + public void onSessionEvent(TvInputManager.Session session, String eventType, + Bundle eventArgs) { + if (mExternalCallback != null) { + mExternalCallback.onSessionEvent(session, eventType, eventArgs); + } + } + } +} |