diff options
Diffstat (limited to 'tests/input/src/com/android')
5 files changed, 728 insertions, 0 deletions
diff --git a/tests/input/src/com/android/tv/testinput/TestInputControl.java b/tests/input/src/com/android/tv/testinput/TestInputControl.java new file mode 100644 index 00000000..cd85c86e --- /dev/null +++ b/tests/input/src/com/android/tv/testinput/TestInputControl.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.testinput; + +import android.content.ContentUris; +import android.content.Context; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; +import android.util.LongSparseArray; + +import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.ChannelUtils; +import com.android.tv.testing.testinput.ChannelState; +import com.android.tv.testing.testinput.ChannelStateData; +import com.android.tv.testing.testinput.ITestInputControl; + +import java.util.Map; + +/** + * Maintains state for the {@link TestTvInputService}. + * + * <p>Maintains the current state for every channel. A default is sent if the state is not + * explicitly set. The state is versioned so TestTvInputService can tell if onNotifyXXX events need + * to be sent. + * + * <p> Test update the state using @{link ITestInputControl} via {@link TestInputControlService}. + */ +class TestInputControl extends ITestInputControl.Stub { + + private final static String TAG = "TestInputControl"; + private final static TestInputControl INSTANCE = new TestInputControl(); + + private final LongSparseArray<ChannelInfo> mId2ChannelInfoMap = new LongSparseArray<>(); + private final LongSparseArray<ChannelState> mOrigId2StateMap = new LongSparseArray<>(); + + private java.lang.String mInputId; + private boolean initialized; + + private TestInputControl() { + } + + public static TestInputControl getInstance() { + return INSTANCE; + } + + public synchronized void init(Context context, String inputId) { + if (!initialized) { + // TODO run initialization in a separate thread. + mInputId = inputId; + updateChannelMap(context); + initialized = true; + } + } + + private void updateChannelMap(Context context) { + mId2ChannelInfoMap.clear(); + Map<Long, ChannelInfo> channelIdToInfoMap = + ChannelUtils.queryChannelInfoMapForTvInput(context, mInputId); + for (Long channelId : channelIdToInfoMap.keySet()) { + mId2ChannelInfoMap.put(channelId, channelIdToInfoMap.get(channelId)); + } + Log.i(TAG, "Initialized channel map for " + mInputId + " with " + mId2ChannelInfoMap.size() + + " channels"); + } + + public ChannelInfo getChannelInfo(Uri channelUri) { + return mId2ChannelInfoMap.get(ContentUris.parseId(channelUri)); + } + + public ChannelState getChannelState(int originalNetworkId) { + return mOrigId2StateMap.get(originalNetworkId, ChannelState.DEFAULT); + } + + @Override + public synchronized void updateChannelState(int origId, ChannelStateData data) + throws RemoteException { + ChannelState state; + ChannelState orig = getChannelState(origId); + state = orig.next(data); + mOrigId2StateMap.put(origId, state); + + Log.i(TAG, "Setting channel " + origId + " state to " + state); + } +} diff --git a/tests/input/src/com/android/tv/testinput/TestInputControlService.java b/tests/input/src/com/android/tv/testinput/TestInputControlService.java new file mode 100644 index 00000000..4a5668cc --- /dev/null +++ b/tests/input/src/com/android/tv/testinput/TestInputControlService.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.testinput; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * Testcases communicate to the {@link TestInputControl} via + * {@link com.android.tv.testing.testinput.ITestInputControl}. + */ +public class TestInputControlService extends Service { + + @Override + public IBinder onBind(Intent intent) { + return TestInputControl.getInstance(); + } +} diff --git a/tests/input/src/com/android/tv/testinput/TestTvInputService.java b/tests/input/src/com/android/tv/testinput/TestTvInputService.java new file mode 100644 index 00000000..98ac9438 --- /dev/null +++ b/tests/input/src/com/android/tv/testinput/TestTvInputService.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.testinput; + +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.media.PlaybackParams; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputService; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Surface; + +import com.android.tv.common.TvCommonConstants; +import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.testinput.ChannelState; + +import java.util.Date; + +/** + * Simple TV input service which provides test channels. + */ +public class TestTvInputService extends TvInputService { + private static final String TAG = "TestTvInputServices"; + private static final int REFRESH_DELAY_MS = 1000 / 5; + private static final boolean DEBUG = false; + private TestInputControl mBackend = TestInputControl.getInstance(); + + public static String buildInputId(Context context) { + return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class)); + } + + @Override + public void onCreate() { + super.onCreate(); + mBackend.init(this, buildInputId(this)); + } + + @Override + public Session onCreateSession(String inputId) { + Log.v(TAG, "Creating session for " + inputId); + return new SimpleSessionImpl(this); + } + + /** + * Simple session implementation that just display some text. + */ + private class SimpleSessionImpl extends Session { + private static final int MSG_SEEK = 1000; + private static final int SEEK_DELAY_MS = 300; + + private final Paint mTextPaint = new Paint(); + private final DrawRunnable mDrawRunnable = new DrawRunnable(); + private Surface mSurface = null; + private ChannelInfo mChannel = null; + private ChannelState mCurrentState = null; + private String mCurrentVideoTrackId = null; + private String mCurrentAudioTrackId = null; + + private long mRecordStartTimeMs; + private long mPausedTimeMs; + // The time in milliseconds when the current position is lastly updated. + private long mLastCurrentPositionUpdateTimeMs; + // The current playback position. + private long mCurrentPositionMs; + // The current playback speed rate. + private float mSpeed; + + private final Handler mHandler = new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_SEEK) { + // Actually, this input doesn't play any videos, it just shows the image. + // So we should simulate the playback here by changing the current playback + // position periodically in order to test the time shift. + // If the playback is paused, the current playback position doesn't need to be + // changed. + if (mPausedTimeMs == 0) { + long currentTimeMs = System.currentTimeMillis(); + mCurrentPositionMs += (long) ((currentTimeMs + - mLastCurrentPositionUpdateTimeMs) * mSpeed); + mCurrentPositionMs = Math.max(mRecordStartTimeMs, + Math.min(mCurrentPositionMs, currentTimeMs)); + mLastCurrentPositionUpdateTimeMs = currentTimeMs; + } + sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); + } + super.handleMessage(msg); + } + }; + + SimpleSessionImpl(Context context) { + super(context); + mTextPaint.setColor(Color.BLACK); + mTextPaint.setTextSize(150); + mHandler.post(mDrawRunnable); + if (DEBUG) { + Log.v(TAG, "Created session " + this); + } + } + + private void setAudioTrack(String selectedAudioTrackId) { + Log.i(TAG, "Set audio track to " + selectedAudioTrackId); + mCurrentAudioTrackId = selectedAudioTrackId; + notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId); + } + + private void setVideoTrack(String selectedVideoTrackId) { + Log.i(TAG, "Set video track to " + selectedVideoTrackId); + mCurrentVideoTrackId = selectedVideoTrackId; + notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId); + } + + @Override + public void onRelease() { + if (DEBUG) { + Log.v(TAG, "Releasing session " + this); + } + mDrawRunnable.cancel(); + mHandler.removeCallbacks(mDrawRunnable); + mSurface = null; + mChannel = null; + mCurrentState = null; + } + + @Override + public boolean onSetSurface(Surface surface) { + synchronized (mDrawRunnable) { + mSurface = surface; + } + if (surface != null) { + if (DEBUG) { + Log.v(TAG, "Surface set"); + } + } else { + if (DEBUG) { + Log.v(TAG, "Surface unset"); + } + } + + return true; + } + + @Override + public void onSurfaceChanged(int format, int width, int height) { + super.onSurfaceChanged(format, width, height); + Log.d(TAG, "format=" + format + " width=" + width + " height=" + height); + } + + @Override + public void onSetStreamVolume(float volume) { + // No-op + } + + @Override + public boolean onTune(Uri channelUri) { + Log.i(TAG, "Tune to " + channelUri); + ChannelInfo info = mBackend.getChannelInfo(channelUri); + synchronized (mDrawRunnable) { + if (info == null || mChannel == null + || mChannel.originalNetworkId != info.originalNetworkId) { + mCurrentState = null; + } + mChannel = info; + mCurrentVideoTrackId = null; + mCurrentAudioTrackId = null; + } + if (mChannel == null) { + Log.i(TAG, "Channel not found for " + channelUri); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + } else { + Log.i(TAG, "Tuning to " + mChannel); + } + if (TvCommonConstants.HAS_TIME_SHIFT_API) { + notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); + mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs + = System.currentTimeMillis(); + mPausedTimeMs = 0; + mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); + mSpeed = 1; + } + return true; + } + + @Override + public void onSetCaptionEnabled(boolean enabled) { + // No-op + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")"); + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")"); + return true; + } + + @Override + public long onTimeShiftGetCurrentPosition() { + Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs); + return mCurrentPositionMs; + } + + @Override + public long onTimeShiftGetStartPosition() { + return mRecordStartTimeMs; + } + + @Override + public void onTimeShiftPause() { + mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs + = System.currentTimeMillis(); + } + + @Override + public void onTimeShiftResume() { + mSpeed = 1; + mPausedTimeMs = 0; + mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); + } + + @Override + public void onTimeShiftSeekTo(long timeMs) { + mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); + mCurrentPositionMs = Math.max(mRecordStartTimeMs, + Math.min(timeMs, mLastCurrentPositionUpdateTimeMs)); + } + + @Override + public void onTimeShiftSetPlaybackParams(PlaybackParams params) { + mSpeed = params.getSpeed(); + } + + private final class DrawRunnable implements Runnable { + private volatile boolean mIsCanceled = false; + + @Override + public void run() { + if (mIsCanceled) { + return; + } + if (DEBUG) { + Log.v(TAG, "Draw task running"); + } + boolean updatedState = false; + ChannelState oldState; + ChannelState newState = null; + Surface currentSurface; + ChannelInfo currentChannel; + + synchronized (this) { + oldState = mCurrentState; + currentSurface = mSurface; + currentChannel = mChannel; + if (currentChannel != null) { + newState = mBackend.getChannelState(currentChannel.originalNetworkId); + if (oldState == null || newState.getVersion() > oldState.getVersion()) { + mCurrentState = newState; + updatedState = true; + } + } else { + mCurrentState = null; + } + } + + draw(currentSurface, currentChannel); + if (updatedState) { + update(oldState, newState, currentChannel); + } + + if (!mIsCanceled) { + mHandler.postDelayed(this, REFRESH_DELAY_MS); + } + } + + private void update(ChannelState oldState, ChannelState newState, + ChannelInfo currentChannel) { + Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState); + notifyTracksChanged(newState.getTrackInfoList()); + if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) { + if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) { + notifyVideoAvailable(); + //TODO handle parental controls. + notifyContentAllowed(); + setAudioTrack(newState.getSelectedAudioTrackId()); + setVideoTrack(newState.getSelectedVideoTrackId()); + } else { + notifyVideoUnavailable(newState.getTuneStatus()); + } + } + } + + private void draw(Surface surface, ChannelInfo currentChannel) { + if (surface != null) { + String now = TvCommonConstants.HAS_TIME_SHIFT_API + ? new Date(mCurrentPositionMs).toString() : new Date().toString(); + String name = currentChannel == null ? "Null" : currentChannel.name; + Canvas c = surface.lockCanvas(null); + c.drawColor(0xFF888888); + c.drawText(name, 100f, 200f, mTextPaint); + c.drawText(now, 100f, 400f, mTextPaint); + surface.unlockCanvasAndPost(c); + if (DEBUG) { + Log.v(TAG, "Post to canvas"); + } + } else { + if (DEBUG) { + Log.v(TAG, "No surface"); + } + } + } + + public void cancel() { + mIsCanceled = true; + } + } + } +} diff --git a/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java b/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java new file mode 100644 index 00000000..732972cc --- /dev/null +++ b/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.testinput; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.ChannelUtils; +import com.android.tv.testing.Constants; +import com.android.tv.testing.ProgramInfo; +import com.android.tv.testing.ProgramUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The setup activity for {@link TestTvInputService}. + */ +public class TestTvInputSetupActivity extends Activity { + private static final String TAG = "TestTvInputSetup"; + private String mInputId; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mInputId = getIntent().getStringExtra(TvInputInfo.EXTRA_INPUT_ID); + + DialogFragment newFragment = new MyAlertDialogFragment(); + newFragment.show(getFragmentManager(), "dialog"); + } + + private void registerChannels(int channelCount) { + TestTvInputSetupActivity context = this; + registerChannels(context, mInputId, false, channelCount); + } + + public static void registerChannels(Context context, String inputId, boolean updateBrowsable, + int channelCount) { + Log.i(TAG, "Registering " + channelCount + " channels"); + List<ChannelInfo> channels = new ArrayList<>(); + for (int i = 1; i <= channelCount; i++) { + channels.add(ChannelInfo.create(context, i)); + } + ChannelUtils.updateChannels(context, inputId, channels); + if (updateBrowsable) { + updateChannelsBrowsable(context.getContentResolver(), inputId); + } + + // Reload channels so we have the ids. + Map<Long, ChannelInfo> channelIdToInfoMap = + ChannelUtils.queryChannelInfoMapForTvInput(context, inputId); + for (Long channelId : channelIdToInfoMap.keySet()) { + // TODO: http://b/21705569 Create better program info for tests + ProgramInfo programInfo = ProgramInfo.create(); + ProgramUtils.populatePrograms(context, TvContract.buildChannelUri(channelId), + programInfo); + } + } + + private static void updateChannelsBrowsable(ContentResolver contentResolver, String inputId) { + Uri uri = TvContract.buildChannelsUriForInput(inputId); + ContentValues values = new ContentValues(); + values.put(TvContract.Channels.COLUMN_BROWSABLE, 1); + contentResolver.update(uri, values, null, null); + } + + public static class MyAlertDialogFragment extends DialogFragment { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()).setTitle(R.string.simple_setup_title) + .setMessage(R.string.simple_setup_message) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + // TODO: add UI to ask how many channels + ((TestTvInputSetupActivity) getActivity()) + .registerChannels(Constants.UNIT_TEST_CHANNEL_COUNT); + // Sets the results so that the application can process the + // registered channels properly. + getActivity().setResult(Activity.RESULT_OK); + getActivity().finish(); + } + }).setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + getActivity().finish(); + } + }).create(); + } + } +} diff --git a/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java b/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java new file mode 100644 index 00000000..379bce86 --- /dev/null +++ b/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.testinput.instrument; + +import android.app.Activity; +import android.app.Instrumentation; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.testing.Constants; +import com.android.tv.testinput.TestTvInputService; +import com.android.tv.testinput.TestTvInputSetupActivity; + +/** + * An instrumentation utility to set up the needed inputs, channels, programs and other settings + * for automated unit tests. + * + * <p><pre>{@code + * adb shell am instrument \ + * -e testSetupMode {func,jank,unit} \ + * -w com.android.tv.testinput/.instrument.TestSetupInstrumentation + * }</pre> + * + * <p>Optional arguments are: + * <pre> + * -e channelCount number + * </pre> + */ +public class TestSetupInstrumentation extends Instrumentation { + private static final String TAG = "TestSetupInstrument"; + private static final String TEST_SETUP_MODE_ARG = "testSetupMode"; + private static final String CHANNEL_COUNT_ARG = "channelCount"; + private Bundle mArguments; + private String mInputId; + + /** + * Fails an instrumentation request. + * + * @param errMsg an error message + */ + protected void fail(String errMsg) { + Log.e(TAG, errMsg); + Bundle result = new Bundle(); + result.putString("error", errMsg); + finish(Activity.RESULT_CANCELED, result); + } + + @Override + public void onCreate(Bundle arguments) { + super.onCreate(arguments); + mArguments = arguments; + start(); + } + + @Override + public void onStart() { + super.onStart(); + try { + mInputId = TestTvInputService.buildInputId(getContext()); + setup(); + finish(Activity.RESULT_OK, new Bundle()); + } catch (TestSetupException e) { + fail(e.getMessage()); + } + } + + private void setup() throws TestSetupException { + final String testSetupMode = mArguments.getString(TEST_SETUP_MODE_ARG); + if (TextUtils.isEmpty(testSetupMode)) { + Log.i(TAG, "Performing no setup actions because " + TEST_SETUP_MODE_ARG + + " was not passed as an argument"); + } else { + Log.i(TAG, "Running setup for " + testSetupMode + " tests."); + int channelCount; + switch (testSetupMode) { + case "func": + channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG, + Constants.FUNC_TEST_CHANNEL_COUNT); + break; + case "jank": + channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG, + Constants.JANK_TEST_CHANNEL_COUNT); + break; + case "unit": + channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG, + Constants.UNIT_TEST_CHANNEL_COUNT); + break; + default: + throw new TestSetupException( + "Unknown " + TEST_SETUP_MODE_ARG + " of " + testSetupMode); + } + TestTvInputSetupActivity.registerChannels(getContext(), mInputId, true, channelCount); + } + } + + private int getArgumentAsInt(String arg, int defaultValue) { + String stringValue = mArguments.getString(arg); + if (stringValue != null) { + try { + return Integer.parseInt(stringValue); + } catch (NumberFormatException e) { + Log.w(TAG, "Unable to parse arg " + arg + " with value " + stringValue + + " to a integer.", e); + } + } + return defaultValue; + } + + static class TestSetupException extends Exception { + public TestSetupException(String msg) { + super(msg); + } + + public static TestSetupException fromMissingArg(String arg) { + return new TestSetupException( + String.format("Error: missing mandatory argument '%s'", arg)); + } + } +} |