Add Clear Key test.

bug: 12035506
Change-Id: I22d54e8d0361d0419710aac3659f71f558923dee
diff --git a/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java b/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java
new file mode 100644
index 0000000..c05a605
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java
@@ -0,0 +1,454 @@
+/*
+ * 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.cts;
+
+import android.content.Context;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecList;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.CamcorderProfile;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Base64;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.Vector;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests of MediaPlayer streaming capabilities.
+ */
+public class ClearKeySystemTest extends MediaPlayerTestBase {
+    private static final String TAG = ClearKeySystemTest.class.getSimpleName();
+
+    // Add additional keys here if the content has more keys.
+    private static final byte[] CLEAR_KEY =
+        { 0x1a, (byte)0x8a, 0x20, (byte)0x95, (byte)0xe4, (byte)0xde, (byte)0xb2, (byte)0xd2,
+          (byte)0x9e, (byte)0xc8, 0x16, (byte)0xac, 0x7b, (byte)0xae, 0x20, (byte)0x82 };
+
+    private static final int SLEEP_TIME_MS = 1000;
+    private static final int VIDEO_WIDTH = 1280;
+    private static final int VIDEO_HEIGHT = 720;
+    private static final long PLAY_TIME_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
+    private static final String MIME_VIDEO_AVC = "video/avc";
+
+    private static final Uri AUDIO_URL = Uri.parse(
+            "http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car_cenc-20120827-8c.mp4");
+    private static final Uri VIDEO_URL = Uri.parse(
+            "http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car_cenc-20120827-88.mp4");
+
+    private static final UUID CLEARKEY_SCHEME_UUID =
+            new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
+
+    private byte[] mDrmInitData;
+    private byte[] mSessionId;
+    private Context mContext;
+    private final List<byte[]> mClearKeys = new ArrayList<byte[]>() {
+        {
+            add(CLEAR_KEY);
+            // add additional keys here
+        }
+    };
+    private Looper mLooper;
+    private MediaCodecCencPlayer mMediaCodecPlayer;
+    private MediaDrm mDrm;
+    private Object mLock = new Object();
+    private SurfaceHolder mSurfaceHolder;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        if (false == deviceHasMediaDrm()) {
+            tearDown();
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private boolean deviceHasMediaDrm() {
+        // ClearKey is introduced after KitKat.
+        if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.KITKAT) {
+            Log.i(TAG, "This test is designed to work after Android KitKat.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Extracts key ids from the pssh blob returned by getKeyRequest() and
+     * places it in keyIds.
+     * keyRequestBlob format (section 5.1.3.1):
+     * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#clear-key
+     *
+     * @return size of keyIds vector that contains the key ids, 0 for error
+     */
+    private int getKeyIds(byte[] keyRequestBlob, Vector<String> keyIds) {
+        if (0 == keyRequestBlob.length || keyIds == null)
+            return 0;
+
+        String jsonLicenseRequest = new String(keyRequestBlob);
+        keyIds.clear();
+
+        try {
+            JSONObject license = new JSONObject(jsonLicenseRequest);
+            final JSONArray ids = license.getJSONArray("kids");
+            for (int i = 0; i < ids.length(); ++i) {
+                keyIds.add(ids.getString(i));
+            }
+        } catch (JSONException e) {
+            Log.e(TAG, "Invalid JSON license = " + jsonLicenseRequest);
+            return 0;
+        }
+        return keyIds.size();
+    }
+
+    /**
+     * Creates the JSON Web Key string.
+     *
+     * @return JSON Web Key string.
+     */
+    private String createJsonWebKeySet(Vector<String> keyIds, Vector<String> keys) {
+        String jwkSet = "{\"keys\":[";
+        for (int i = 0; i < keyIds.size(); ++i) {
+            String id = new String(keyIds.get(i).getBytes(Charset.forName("UTF-8")));
+            String key = new String(keys.get(i).getBytes(Charset.forName("UTF-8")));
+
+            jwkSet += "{\"kty\":\"oct\",\"kid\":\"" + id +
+                    "\",\"k\":\"" + key + "\"}";
+        }
+        jwkSet += "]}";
+        return jwkSet;
+    }
+
+    /**
+     * Retrieves clear key ids from getKeyRequest(), create JSON Web Key
+     * set and send it to the CDM via provideKeyResponse().
+     */
+    private void getKeys(MediaDrm drm, byte[] sessionId, byte[] drmInitData) {
+        MediaDrm.KeyRequest drmRequest = null;;
+        try {
+            drmRequest = drm.getKeyRequest(sessionId, drmInitData, "cenc",
+                    MediaDrm.KEY_TYPE_STREAMING, null);
+        } catch (Exception e) {
+            e.printStackTrace();
+            Log.i(TAG, "Failed to get key request: " + e.toString());
+        }
+        if (drmRequest == null) {
+            Log.e(TAG, "Failed getKeyRequest");
+            return;
+        }
+
+        Vector<String> keyIds = new Vector<String>();
+        if (0 == getKeyIds(drmRequest.getData(), keyIds)) {
+            Log.e(TAG, "No key ids found in initData");
+            return;
+        }
+
+        if (mClearKeys.size() != keyIds.size()) {
+            Log.e(TAG, "Mismatch number of key ids and keys: ids=" +
+                    keyIds.size() + ", keys=" + mClearKeys.size());
+            return;
+        }
+
+        // Base64 encodes clearkeys. Keys are known to the application.
+        Vector<String> keys = new Vector<String>();
+        for (int i = 0; i < mClearKeys.size(); ++i) {
+            String clearKey = Base64.encodeToString(mClearKeys.get(i),
+                    Base64.NO_PADDING | Base64.NO_WRAP);
+            keys.add(clearKey);
+        }
+
+        String jwkSet = createJsonWebKeySet(keyIds, keys);
+        byte[] jsonResponse = jwkSet.getBytes(Charset.forName("UTF-8"));
+
+        try {
+            try {
+                drm.provideKeyResponse(sessionId, jsonResponse);
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to provide key response: " + e.toString());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            Log.e(TAG, "Failed to provide key response: " + e.toString());
+        }
+    }
+
+    private MediaDrm startDrm() {
+        new Thread() {
+            @Override
+            public void run() {
+                // Set up a looper to handle events
+                Looper.prepare();
+
+                // Save the looper so that we can terminate this thread
+                // after we are done with it.
+                mLooper = Looper.myLooper();
+
+                try {
+                    mDrm = new MediaDrm(CLEARKEY_SCHEME_UUID);
+                } catch (MediaDrmException e) {
+                    Log.e(TAG, "Failed to create MediaDrm: " + e.getMessage());
+                    return;
+                }
+
+                synchronized(mLock) {
+                    mDrm.setOnEventListener(new MediaDrm.OnEventListener() {
+                            @Override
+                            public void onEvent(MediaDrm md, byte[] sessionId, int event,
+                                    int extra, byte[] data) {
+                                if (event == MediaDrm.EVENT_KEY_REQUIRED) {
+                                    Log.i(TAG, "MediaDrm event: Key required");
+                                    getKeys(mDrm, mSessionId, mDrmInitData);
+                                } else if (event == MediaDrm.EVENT_KEY_EXPIRED) {
+                                    Log.i(TAG, "MediaDrm event: Key expired");
+                                    getKeys(mDrm, mSessionId, mDrmInitData);
+                                } else {
+                                    Log.e(TAG, "Events not supported" + event);
+                                }
+                            }
+                        });
+                    mLock.notify();
+                }
+                Looper.loop();  // Blocks forever until Looper.quit() is called.
+            }
+        }.start();
+
+        // wait for mDrm to be created
+        synchronized(mLock) {
+            try {
+                mLock.wait(1000);
+            } catch (Exception e) {
+            }
+        }
+        return mDrm;
+    }
+
+    private void stopDrm(MediaDrm drm) {
+        if (drm != mDrm) {
+            Log.e(TAG, "invalid drm specified in stopDrm");
+        }
+        mLooper.quit();
+    }
+
+    private byte[] openSession(MediaDrm drm) {
+        byte[] mSessionId = null;
+        boolean mRetryOpen;
+        do {
+            try {
+                mRetryOpen = false;
+                mSessionId = drm.openSession();
+            } catch (Exception e) {
+                mRetryOpen = true;
+            }
+        } while (mRetryOpen);
+        return mSessionId;
+    }
+
+    private void closeSession(MediaDrm drm, byte[] sessionId) {
+        drm.closeSession(sessionId);
+    }
+
+    public boolean isResolutionSupported(int videoWidth, int videoHeight) {
+        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+            if  (videoHeight <= 144) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_QCIF);
+            } else if (videoHeight <= 240) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_QVGA);
+            } else if (videoHeight <= 288) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_CIF);
+            } else if (videoHeight <= 480) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_480P);
+            } else if (videoHeight <= 720) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P);
+            } else if (videoHeight <= 1080) {
+                return CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_1080P);
+            } else {
+                return false;
+            }
+        }
+
+        CodecCapabilities cap;
+        int highestProfileLevel = 0;
+        MediaCodecInfo codecInfo;
+
+        for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
+            codecInfo = MediaCodecList.getCodecInfoAt(i);
+            if (codecInfo.isEncoder()) {
+                continue;
+            }
+
+            String[] types = codecInfo.getSupportedTypes();
+            for (int j = 0; j < types.length; ++j) {
+                if (!types[j].equalsIgnoreCase(MIME_VIDEO_AVC)) {
+                    continue;
+                }
+
+                Log.d(TAG, "codec: " + codecInfo.getName() + "types: " + types[j]);
+                cap = codecInfo.getCapabilitiesForType(types[j]);
+                for (CodecProfileLevel profileLevel : cap.profileLevels) {
+                    Log.i(TAG, "codec " + codecInfo.getName() + ", level " + profileLevel.level);
+                    if (profileLevel.level > highestProfileLevel) {
+                        highestProfileLevel = profileLevel.level;
+                    }
+                }
+                Log.i(TAG, "codec " + codecInfo.getName() + ", highest level " + highestProfileLevel);
+            }
+        }
+
+        // AVCLevel and its resolution is taken from http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
+        switch(highestProfileLevel) {
+        case CodecProfileLevel.AVCLevel1:
+        case CodecProfileLevel.AVCLevel1b:
+            return (videoWidth <= 176 && videoHeight <= 144);
+        case CodecProfileLevel.AVCLevel11:
+        case CodecProfileLevel.AVCLevel12:
+        case CodecProfileLevel.AVCLevel13:
+        case CodecProfileLevel.AVCLevel2:
+            return (videoWidth <= 352 && videoHeight <= 288);
+        case CodecProfileLevel.AVCLevel21:
+            return (videoWidth <= 352 && videoHeight <= 576);
+        case CodecProfileLevel.AVCLevel22:
+        case CodecProfileLevel.AVCLevel3:
+            return (videoWidth <= 720 && videoHeight <= 576);
+        case CodecProfileLevel.AVCLevel31:
+            return (videoWidth <= 1280 && videoHeight <= 720);
+        case CodecProfileLevel.AVCLevel32:
+            return (videoWidth <= 1280 && videoHeight <= 1024);
+        case CodecProfileLevel.AVCLevel4:
+        case CodecProfileLevel.AVCLevel41:
+            // 1280 x 720
+            // 1920 x 1080
+            // 2048 x 1024
+            if (videoWidth <= 1920) {
+                return (videoHeight <= 1080);
+            } else if (videoWidth <= 2048) {
+                return (videoHeight <= 1024);
+            } else {
+                return false;
+            }
+        case CodecProfileLevel.AVCLevel42:
+            return (videoWidth <= 2048 && videoHeight <= 1080);
+        case CodecProfileLevel.AVCLevel5:
+            // 1920 x 1080
+            // 2048 x 1024
+            // 2048 x 1080
+            // 2560 x 1920
+            // 3672 x 1536
+            if (videoWidth <= 1920) {
+                return (videoHeight <= 1080);
+            } else if (videoWidth <= 2048) {
+                return (videoHeight <= 1080);
+            } else if (videoWidth <= 2560) {
+                return (videoHeight <= 1920);
+            } else if (videoWidth <= 3672) {
+                return (videoHeight <= 1536);
+            } else {
+                return false;
+            }
+        case CodecProfileLevel.AVCLevel51:
+        default:  // any future extension will cap at level 5.1
+            // 1920 x 1080
+            // 2560 x 1920
+            // 3840 x 2160
+            // 4096 x 2048
+            // 4096 x 2160
+            // 4096 x 2304
+            if (videoWidth <= 1920) {
+                return (videoHeight <= 1080);
+            } else if (videoWidth <= 2560) {
+                return (videoHeight <= 1920);
+            } else if (videoWidth <= 3840) {
+                return (videoHeight <= 2160);
+            } else if (videoWidth <= 4096) {
+                return (videoHeight <= 2304);
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Tests clear key system playback.
+     */
+    public void testClearKeyPlayback() throws Exception {
+        MediaDrm drm = startDrm();
+        if (null == drm) {
+            throw new Error("Failed to create drm.");
+        }
+
+        if (!drm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID)) {
+            stopDrm(drm);
+            throw new Error("Crypto scheme is not supported.");
+        }
+
+        if (!isResolutionSupported(VIDEO_WIDTH, VIDEO_HEIGHT)) {
+            Log.i(TAG, "Device does not support " +
+                    VIDEO_WIDTH + "x" + VIDEO_HEIGHT + "resolution.");
+            return;
+        }
+
+        mSessionId = openSession(drm);
+        mMediaCodecPlayer = new MediaCodecCencPlayer(
+                getActivity().getSurfaceHolder(), mSessionId);
+
+        mMediaCodecPlayer.setAudioDataSource(AUDIO_URL, null, false);
+        mMediaCodecPlayer.setVideoDataSource(VIDEO_URL, null, true);
+        mMediaCodecPlayer.start();
+        mMediaCodecPlayer.prepare();
+        mDrmInitData = mMediaCodecPlayer.getPsshInfo().get(CLEARKEY_SCHEME_UUID);
+
+        getKeys(mDrm, mSessionId, mDrmInitData);
+        // starts video playback
+        mMediaCodecPlayer.startThread();
+
+        long timeOut = System.currentTimeMillis() + PLAY_TIME_MS * 4;
+        while (timeOut > System.currentTimeMillis() && !mMediaCodecPlayer.isEnded()) {
+            Thread.sleep(SLEEP_TIME_MS);
+            if (mMediaCodecPlayer.getCurrentPosition() >= mMediaCodecPlayer.getDuration() ) {
+                Log.d(TAG, "current pos = " + mMediaCodecPlayer.getCurrentPosition() +
+                        ">= duration = " + mMediaCodecPlayer.getDuration());
+                break;
+            }
+        }
+
+        Log.d(TAG, "playVideo player.reset()");
+        mMediaCodecPlayer.reset();
+        closeSession(drm, mSessionId);
+        stopDrm(drm);
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/CodecState.java b/tests/tests/media/src/android/media/cts/CodecState.java
new file mode 100644
index 0000000..cd6b68f
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/CodecState.java
@@ -0,0 +1,347 @@
+/*
+ * 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.cts;
+
+import android.media.AudioTrack;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+
+/**
+ * Class for directly managing both audio and video playback by
+ * using {@link MediaCodec} and {@link AudioTrack}.
+ */
+public class CodecState {
+    private static final String TAG = CodecState.class.getSimpleName();
+
+    private boolean mSawInputEOS, mSawOutputEOS;
+    private boolean mLimitQueueDepth;
+    private ByteBuffer[] mCodecInputBuffers;
+    private ByteBuffer[] mCodecOutputBuffers;
+    private int mTrackIndex;
+    private LinkedList<Integer> mAvailableInputBufferIndices;
+    private LinkedList<Integer> mAvailableOutputBufferIndices;
+    private LinkedList<MediaCodec.BufferInfo> mAvailableOutputBufferInfos;
+    private long mPresentationTimeUs;
+    private MediaCodec mCodec;
+    private MediaCodecCencPlayer mMediaCodecPlayer;
+    private MediaExtractor mExtractor;
+    private MediaFormat mFormat;
+    private MediaFormat mOutputFormat;
+    private NonBlockingAudioTrack mAudioTrack;
+
+    /**
+     * Manages audio and video playback using MediaCodec and AudioTrack.
+     */
+    public CodecState(
+            MediaCodecCencPlayer mediaCodecPlayer,
+            MediaExtractor extractor,
+            int trackIndex,
+            MediaFormat format,
+            MediaCodec codec,
+            boolean limitQueueDepth) {
+
+        mMediaCodecPlayer = mediaCodecPlayer;
+        mExtractor = extractor;
+        mTrackIndex = trackIndex;
+        mFormat = format;
+        mSawInputEOS = mSawOutputEOS = false;
+        mLimitQueueDepth = limitQueueDepth;
+
+        mCodec = codec;
+
+        mAvailableInputBufferIndices = new LinkedList<Integer>();
+        mAvailableOutputBufferIndices = new LinkedList<Integer>();
+        mAvailableOutputBufferInfos = new LinkedList<MediaCodec.BufferInfo>();
+
+        mPresentationTimeUs = 0;
+    }
+
+    public void release() {
+        mCodec.stop();
+        mCodecInputBuffers = null;
+        mCodecOutputBuffers = null;
+        mOutputFormat = null;
+
+        mAvailableInputBufferIndices.clear();
+        mAvailableOutputBufferIndices.clear();
+        mAvailableOutputBufferInfos.clear();
+
+        mAvailableInputBufferIndices = null;
+        mAvailableOutputBufferIndices = null;
+        mAvailableOutputBufferInfos = null;
+
+        mCodec.release();
+        mCodec = null;
+
+        if (mAudioTrack != null) {
+            mAudioTrack.release();
+            mAudioTrack = null;
+        }
+    }
+
+    public void start() {
+        mCodec.start();
+        mCodecInputBuffers = mCodec.getInputBuffers();
+        mCodecOutputBuffers = mCodec.getOutputBuffers();
+
+        if (mAudioTrack != null) {
+            mAudioTrack.play();
+        }
+    }
+
+    public void pause() {
+        if (mAudioTrack != null) {
+            mAudioTrack.pause();
+        }
+    }
+
+    public long getCurrentPositionUs() {
+        return mPresentationTimeUs;
+    }
+
+    public void flush() {
+        mAvailableInputBufferIndices.clear();
+        mAvailableOutputBufferIndices.clear();
+        mAvailableOutputBufferInfos.clear();
+
+        mSawInputEOS = false;
+        mSawOutputEOS = false;
+
+        if (mAudioTrack != null
+                && mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) {
+            mAudioTrack.play();
+        }
+
+        mCodec.flush();
+    }
+
+    public boolean isEnded() {
+        return mSawInputEOS && mSawOutputEOS;
+    }
+
+    /**
+     * doSomeWork() is the worker function that does all buffer handling and decoding works.
+     * It first reads data from {@link MediaExtractor} and pushes it into {@link MediaCodec};
+     * it then dequeues buffer from {@link MediaCodec}, consumes it and pushes back to its own
+     * buffer queue for next round reading data from {@link MediaExtractor}.
+     */
+    public void doSomeWork() {
+        int indexInput = mCodec.dequeueInputBuffer(0 /* timeoutUs */);
+
+        if (indexInput != MediaCodec.INFO_TRY_AGAIN_LATER) {
+            mAvailableInputBufferIndices.add(indexInput);
+        }
+
+        while (feedInputBuffer()) {
+        }
+
+        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+        int indexOutput = mCodec.dequeueOutputBuffer(info, 0 /* timeoutUs */);
+
+        if (indexOutput == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+            mOutputFormat = mCodec.getOutputFormat();
+            onOutputFormatChanged();
+        } else if (indexOutput == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+            mCodecOutputBuffers = mCodec.getOutputBuffers();
+        } else if (indexOutput != MediaCodec.INFO_TRY_AGAIN_LATER) {
+            mAvailableOutputBufferIndices.add(indexOutput);
+            mAvailableOutputBufferInfos.add(info);
+        }
+
+        while (drainOutputBuffer()) {
+        }
+    }
+
+    /** Returns true if more input data could be fed. */
+    private boolean feedInputBuffer() throws MediaCodec.CryptoException, IllegalStateException {
+        if (mSawInputEOS || mAvailableInputBufferIndices.isEmpty()) {
+            return false;
+        }
+
+        // stalls read if audio queue is larger than 2MB full so we will not occupy too much heap
+        if (mLimitQueueDepth && mAudioTrack != null &&
+                mAudioTrack.getNumBytesQueued() > 2 * 1024 * 1024) {
+            return false;
+        }
+
+        int index = mAvailableInputBufferIndices.peekFirst().intValue();
+
+        ByteBuffer codecData = mCodecInputBuffers[index];
+
+        int trackIndex = mExtractor.getSampleTrackIndex();
+
+        if (trackIndex == mTrackIndex) {
+            int sampleSize =
+                mExtractor.readSampleData(codecData, 0 /* offset */);
+
+            long sampleTime = mExtractor.getSampleTime();
+
+            int sampleFlags = mExtractor.getSampleFlags();
+
+            if (sampleSize <= 0) {
+                Log.d(TAG, "sampleSize: " + sampleSize + " trackIndex:" + trackIndex +
+                        " sampleTime:" + sampleTime + " sampleFlags:" + sampleFlags);
+                mSawInputEOS = true;
+                return false;
+            }
+
+            if ((sampleFlags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
+                MediaCodec.CryptoInfo info = new MediaCodec.CryptoInfo();
+                mExtractor.getSampleCryptoInfo(info);
+
+                mCodec.queueSecureInputBuffer(
+                        index, 0 /* offset */, info, sampleTime, 0 /* flags */);
+            } else {
+                mCodec.queueInputBuffer(
+                        index, 0 /* offset */, sampleSize, sampleTime, 0 /* flags */);
+            }
+
+            mAvailableInputBufferIndices.removeFirst();
+            mExtractor.advance();
+
+            return true;
+        } else if (trackIndex < 0) {
+            Log.d(TAG, "saw input EOS on track " + mTrackIndex);
+
+            mSawInputEOS = true;
+
+            mCodec.queueInputBuffer(
+                    index, 0 /* offset */, 0 /* sampleSize */,
+                    0 /* sampleTime */, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+
+            mAvailableInputBufferIndices.removeFirst();
+        }
+
+        return false;
+    }
+
+    private void onOutputFormatChanged() {
+        String mime = mOutputFormat.getString(MediaFormat.KEY_MIME);
+        // b/9250789
+        Log.d(TAG, "CodecState::onOutputFormatChanged " + mime);
+
+        if (mime.startsWith("audio/")) {
+            int sampleRate =
+                mOutputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+
+            int channelCount =
+                mOutputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+
+            Log.d(TAG, "CodecState::onOutputFormatChanged Audio" +
+                    " sampleRate:" + sampleRate + " channels:" + channelCount);
+            // We do sanity check here after we receive data from MediaExtractor and before
+            // we pass them down to AudioTrack. If MediaExtractor works properly, this
+            // sanity-check is not necessary, however, in our tests, we found that there
+            // are a few cases where ch=0 and samplerate=0 were returned by MediaExtractor.
+            if (channelCount < 1 || channelCount > 8 ||
+                    sampleRate < 8000 || sampleRate > 128000) {
+                return;
+            }
+            mAudioTrack = new NonBlockingAudioTrack(sampleRate, channelCount);
+            mAudioTrack.play();
+        }
+
+        if (mime.startsWith("video/")) {
+            int width = mOutputFormat.getInteger(MediaFormat.KEY_WIDTH);
+            int height = mOutputFormat.getInteger(MediaFormat.KEY_HEIGHT);
+            Log.d(TAG, "CodecState::onOutputFormatChanged Video" +
+                    " width:" + width + " height:" + height);
+        }
+    }
+
+    /** Returns true if more output data could be drained. */
+    private boolean drainOutputBuffer() {
+        if (mSawOutputEOS || mAvailableOutputBufferIndices.isEmpty()) {
+            return false;
+        }
+
+        int index = mAvailableOutputBufferIndices.peekFirst().intValue();
+        MediaCodec.BufferInfo info = mAvailableOutputBufferInfos.peekFirst();
+
+        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+            Log.d(TAG, "saw output EOS on track " + mTrackIndex);
+
+            mSawOutputEOS = true;
+
+            return false;
+        }
+
+        long realTimeUs =
+            mMediaCodecPlayer.getRealTimeUsForMediaTime(info.presentationTimeUs);
+
+        long nowUs = mMediaCodecPlayer.getNowUs();
+
+        long lateUs = nowUs - realTimeUs;
+
+        if (mAudioTrack != null) {
+            ByteBuffer buffer = mCodecOutputBuffers[index];
+            buffer.clear();
+            buffer.position(0 /* offset */);
+
+            byte[] audioCopy = new byte[info.size];
+            buffer.get(audioCopy, 0, info.size);
+
+            mAudioTrack.write(audioCopy, info.size);
+
+            mCodec.releaseOutputBuffer(index, false /* render */);
+
+            mPresentationTimeUs = info.presentationTimeUs;
+
+            mAvailableOutputBufferIndices.removeFirst();
+            mAvailableOutputBufferInfos.removeFirst();
+            return true;
+        } else {
+            // video
+            boolean render;
+
+            if (lateUs < -45000) {
+                // too early;
+                return false;
+            } else if (lateUs > 30000) {
+                Log.d(TAG, "video late by " + lateUs + " us.");
+                render = false;
+            } else {
+                render = true;
+                mPresentationTimeUs = info.presentationTimeUs;
+            }
+
+            mCodec.releaseOutputBuffer(index, render);
+
+            mAvailableOutputBufferIndices.removeFirst();
+            mAvailableOutputBufferInfos.removeFirst();
+            return true;
+        }
+    }
+
+    public long getAudioTimeUs() {
+        if (mAudioTrack == null) {
+            return 0;
+        }
+
+        return mAudioTrack.getAudioTimeUs();
+    }
+
+    public void process() {
+        if (mAudioTrack != null) {
+            mAudioTrack.process();
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/MediaCodecCencPlayer.java b/tests/tests/media/src/android/media/cts/MediaCodecCencPlayer.java
new file mode 100644
index 0000000..90696ff
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaCodecCencPlayer.java
@@ -0,0 +1,532 @@
+/*
+ * 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.cts;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.net.Uri;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * JB(API 16) introduces {@link MediaCodec} API.  It allows apps have more control over
+ * media playback, pushes individual frames to decoder and supports decryption via
+ * {@link MediaCrypto} API.
+ *
+ * {@link MediaDrm} can be used to obtain keys for decrypting protected media streams,
+ * in conjunction with MediaCrypto.
+ */
+public class MediaCodecCencPlayer {
+    private static final String TAG = MediaCodecCencPlayer.class.getSimpleName();
+
+    private static final int STATE_IDLE = 1;
+    private static final int STATE_PREPARING = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+
+    private static final UUID CLEARKEY_SCHEME_UUID =
+            new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
+
+    private boolean mEncryptedAudio;
+    private boolean mEncryptedVideo;
+    private boolean mThreadStarted = false;
+    private byte[] mSessionId;
+    private CodecState mAudioTrackState;
+    private int mMediaFormatHeight;
+    private int mMediaFormatWidth;
+    private int mState;
+    private long mDeltaTimeUs;
+    private long mDurationUs;
+    private Map<Integer, CodecState> mAudioCodecStates;
+    private Map<Integer, CodecState> mVideoCodecStates;
+    private Map<String, String> mAudioHeaders;
+    private Map<String, String> mVideoHeaders;
+    private Map<UUID, byte[]> mPsshInitData;
+    private MediaCrypto mCrypto;
+    private MediaExtractor mAudioExtractor;
+    private MediaExtractor mVideoExtractor;
+    private SurfaceHolder mSurfaceHolder;
+    private Thread mThread;
+    private Uri mAudioUri;
+    private Uri mVideoUri;
+
+    private static final byte[] PSSH = hexStringToByteArray(
+            "0000003470737368" +  // BMFF box header (4 bytes size + 'pssh')
+            "01000000" +          // Full box header (version = 1 flags = 0)
+            "1077efecc0b24d02" +  // SystemID
+            "ace33c1e52e2fb4b" +
+            "00000001" +          // Number of key ids
+            "60061e017e477e87" +  // Key id
+            "7e57d00d1ed00d1e" +
+            "00000000"            // Size of Data, must be zero
+            );
+
+    /**
+     * Convert a hex string into byte array.
+     */
+    private static byte[] hexStringToByteArray(String s) {
+        int len = s.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+                    + Character.digit(s.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
+    /*
+     * Media player class to stream CENC content using MediaCodec class.
+     */
+    public MediaCodecCencPlayer(SurfaceHolder holder, byte[] sessionId) {
+        mSessionId = sessionId;
+        mSurfaceHolder = holder;
+        mState = STATE_IDLE;
+        mThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                while (mThreadStarted == true) {
+                    doSomeWork();
+                    if (mAudioTrackState != null) {
+                        mAudioTrackState.process();
+                    }
+                    try {
+                        Thread.sleep(5);
+                    } catch (InterruptedException ex) {
+                        Log.d(TAG, "Thread interrupted");
+                    }
+                }
+            }
+        });
+    }
+
+    public void setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
+        mAudioUri = uri;
+        mAudioHeaders = headers;
+        mEncryptedAudio = encrypted;
+    }
+
+    public void setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
+        mVideoUri = uri;
+        mVideoHeaders = headers;
+        mEncryptedVideo = encrypted;
+    }
+
+    public final int getMediaFormatHeight() {
+        return mMediaFormatHeight;
+    }
+
+    public final int getMediaFormatWidth() {
+        return mMediaFormatWidth;
+    }
+
+    public final Map<UUID, byte[]> getPsshInfo() {
+        // TODO (edwinwong@)
+        // Remove the if statement when we get content that has the clear key system id.
+        if (mPsshInitData == null ||
+                (mPsshInitData != null && !mPsshInitData.containsKey(CLEARKEY_SCHEME_UUID))) {
+            mPsshInitData = new HashMap<UUID, byte[]>();
+            mPsshInitData.put(CLEARKEY_SCHEME_UUID, PSSH);
+        }
+        return mPsshInitData;
+    }
+
+    private void prepareAudio() throws IOException {
+        boolean hasAudio = false;
+        for (int i = mAudioExtractor.getTrackCount(); i-- > 0;) {
+            MediaFormat format = mAudioExtractor.getTrackFormat(i);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+
+            Log.d(TAG, "audio track #" + i + " " + format + " " + mime +
+                  " Is ADTS:" + getMediaFormatInteger(format, MediaFormat.KEY_IS_ADTS) +
+                  " Sample rate:" + getMediaFormatInteger(format, MediaFormat.KEY_SAMPLE_RATE) +
+                  " Channel count:" +
+                  getMediaFormatInteger(format, MediaFormat.KEY_CHANNEL_COUNT));
+
+            if (!hasAudio) {
+                mAudioExtractor.selectTrack(i);
+                addTrack(i, format, mEncryptedAudio);
+                hasAudio = true;
+
+                if (format.containsKey(MediaFormat.KEY_DURATION)) {
+                    long durationUs = format.getLong(MediaFormat.KEY_DURATION);
+
+                    if (durationUs > mDurationUs) {
+                        mDurationUs = durationUs;
+                    }
+                    Log.d(TAG, "audio track format #" + i +
+                            " Duration:" + mDurationUs + " microseconds");
+                }
+
+                if (hasAudio) {
+                    break;
+                }
+            }
+        }
+    }
+
+    private void prepareVideo() throws IOException {
+        boolean hasVideo = false;
+
+        for (int i = mVideoExtractor.getTrackCount(); i-- > 0;) {
+            MediaFormat format = mVideoExtractor.getTrackFormat(i);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+
+            mMediaFormatHeight = getMediaFormatInteger(format, MediaFormat.KEY_HEIGHT);
+            mMediaFormatWidth = getMediaFormatInteger(format, MediaFormat.KEY_WIDTH);
+            Log.d(TAG, "video track #" + i + " " + format + " " + mime +
+                  " Width:" + mMediaFormatWidth + ", Height:" + mMediaFormatHeight);
+
+            if (!hasVideo) {
+                mVideoExtractor.selectTrack(i);
+                addTrack(i, format, mEncryptedVideo);
+
+                hasVideo = true;
+
+                if (format.containsKey(MediaFormat.KEY_DURATION)) {
+                    long durationUs = format.getLong(MediaFormat.KEY_DURATION);
+
+                    if (durationUs > mDurationUs) {
+                        mDurationUs = durationUs;
+                    }
+                    Log.d(TAG, "track format #" + i + " Duration:" +
+                            mDurationUs + " microseconds");
+                }
+
+                if (hasVideo) {
+                    break;
+                }
+            }
+        }
+        return;
+    }
+
+    public void prepare() throws IOException, MediaCryptoException {
+        if (null == mAudioExtractor) {
+            mAudioExtractor = new MediaExtractor();
+            if (null == mAudioExtractor) {
+                Log.e(TAG, "Cannot create Audio extractor.");
+                return;
+            }
+        }
+
+        if (null == mVideoExtractor){
+            mVideoExtractor = new MediaExtractor();
+            if (null == mVideoExtractor) {
+                Log.e(TAG, "Cannot create Video extractor.");
+                return;
+            }
+        }
+
+        mAudioExtractor.setDataSource(mAudioUri.toString(), mAudioHeaders);
+        mVideoExtractor.setDataSource(mVideoUri.toString(), mVideoHeaders);
+        mPsshInitData = mVideoExtractor.getPsshInfo();
+
+        if (null == mCrypto && (mEncryptedVideo || mEncryptedAudio)) {
+            try {
+                mCrypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, mSessionId);
+            } catch (MediaCryptoException e) {
+                reset();
+                Log.e(TAG, "Failed to create MediaCrypto instance.");
+                throw e;
+            }
+        } else {
+            reset();
+            mCrypto.release();
+            mCrypto = null;
+        }
+
+        if (null == mVideoCodecStates) {
+            mVideoCodecStates = new HashMap<Integer, CodecState>();
+        } else {
+            mVideoCodecStates.clear();
+        }
+
+        if (null == mAudioCodecStates) {
+            mAudioCodecStates = new HashMap<Integer, CodecState>();
+        } else {
+            mAudioCodecStates.clear();
+        }
+
+        prepareVideo();
+        prepareAudio();
+
+        mState = STATE_PAUSED;
+    }
+
+    private void addTrack(int trackIndex, MediaFormat format,
+            boolean encrypted) throws IOException {
+        String mime = format.getString(MediaFormat.KEY_MIME);
+        boolean isVideo = mime.startsWith("video/");
+        boolean isAudio = mime.startsWith("audio/");
+
+        MediaCodec codec;
+
+        if (encrypted && mCrypto.requiresSecureDecoderComponent(mime)) {
+            codec = MediaCodec.createByCodecName(
+                    getSecureDecoderNameForMime(mime));
+        } else {
+            codec = MediaCodec.createDecoderByType(mime);
+        }
+
+        codec.configure(
+                format,
+                isVideo ? mSurfaceHolder.getSurface() : null,
+                mCrypto,
+                0);
+
+        CodecState state;
+        if (isVideo) {
+            state = new CodecState(this, mVideoExtractor, trackIndex, format, codec, true);
+            mVideoCodecStates.put(Integer.valueOf(trackIndex), state);
+        } else {
+            state = new CodecState(this, mAudioExtractor, trackIndex, format, codec, true);
+            mAudioCodecStates.put(Integer.valueOf(trackIndex), state);
+        }
+
+        if (isAudio) {
+            mAudioTrackState = state;
+        }
+    }
+
+    protected int getMediaFormatInteger(MediaFormat format, String key) {
+        return format.containsKey(key) ? format.getInteger(key) : 0;
+    }
+
+    protected String getSecureDecoderNameForMime(String mime) {
+        int n = MediaCodecList.getCodecCount();
+        for (int i = 0; i < n; ++i) {
+            MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+
+            if (info.isEncoder()) {
+                continue;
+            }
+
+            String[] supportedTypes = info.getSupportedTypes();
+
+            for (int j = 0; j < supportedTypes.length; ++j) {
+                if (supportedTypes[j].equalsIgnoreCase(mime)) {
+                    return info.getName() + ".secure";
+                }
+            }
+        }
+        return null;
+    }
+
+    public void start() {
+        Log.d(TAG, "start");
+
+        if (mState == STATE_PLAYING || mState == STATE_PREPARING) {
+            return;
+        } else if (mState == STATE_IDLE) {
+            mState = STATE_PREPARING;
+            return;
+        } else if (mState != STATE_PAUSED) {
+            throw new IllegalStateException();
+        }
+
+        for (CodecState state : mVideoCodecStates.values()) {
+            state.start();
+        }
+
+        for (CodecState state : mAudioCodecStates.values()) {
+            state.start();
+        }
+
+        mDeltaTimeUs = -1;
+        mState = STATE_PLAYING;
+    }
+
+    public void startWork() throws IOException, MediaCryptoException, Exception {
+        try {
+            // Just change state from STATE_IDLE to STATE_PREPARING.
+            start();
+            // Extract media information from uri asset, and change state to STATE_PAUSED.
+            prepare();
+            // Start CodecState, and change from STATE_PAUSED to STATE_PLAYING.
+            start();
+        } catch (IOException e) {
+            throw e;
+        } catch (MediaCryptoException e) {
+            throw e;
+        }
+
+        mThreadStarted = true;
+        mThread.start();
+    }
+
+    public void startThread() {
+        start();
+        mThreadStarted = true;
+        mThread.start();
+    }
+
+    public void pause() {
+        Log.d(TAG, "pause");
+
+        if (mState == STATE_PAUSED) {
+            return;
+        } else if (mState != STATE_PLAYING) {
+            throw new IllegalStateException();
+        }
+
+        for (CodecState state : mVideoCodecStates.values()) {
+            state.pause();
+        }
+
+        for (CodecState state : mAudioCodecStates.values()) {
+            state.pause();
+        }
+
+        mState = STATE_PAUSED;
+    }
+
+    public void reset() {
+        if (mState == STATE_PLAYING) {
+            mThreadStarted = false;
+
+            try {
+                mThread.join();
+            } catch (InterruptedException ex) {
+                Log.d(TAG, "mThread.join " + ex);
+            }
+
+            pause();
+        }
+
+        if (mVideoCodecStates != null) {
+            for (CodecState state : mVideoCodecStates.values()) {
+                state.release();
+            }
+            mVideoCodecStates = null;
+        }
+
+        if (mAudioCodecStates != null) {
+            for (CodecState state : mAudioCodecStates.values()) {
+                state.release();
+            }
+            mAudioCodecStates = null;
+        }
+
+        if (mAudioExtractor != null) {
+            mAudioExtractor.release();
+            mAudioExtractor = null;
+        }
+
+        if (mVideoExtractor != null) {
+            mVideoExtractor.release();
+            mVideoExtractor = null;
+        }
+
+        if (mCrypto != null) {
+            mCrypto.release();
+            mCrypto = null;
+        }
+
+        mDurationUs = -1;
+        mState = STATE_IDLE;
+    }
+
+    public boolean isEnded() {
+        for (CodecState state : mVideoCodecStates.values()) {
+          if (!state.isEnded()) {
+            return false;
+          }
+        }
+
+        for (CodecState state : mAudioCodecStates.values()) {
+            if (!state.isEnded()) {
+              return false;
+            }
+        }
+
+        return true;
+    }
+
+    private void doSomeWork() {
+        try {
+            for (CodecState state : mVideoCodecStates.values()) {
+                state.doSomeWork();
+            }
+        } catch (MediaCodec.CryptoException e) {
+            throw new Error("Video CryptoException w/ errorCode "
+                    + e.getErrorCode() + ", '" + e.getMessage() + "'");
+        } catch (IllegalStateException e) {
+            throw new Error("Video CodecState.feedInputBuffer IllegalStateException " + e);
+        }
+
+        try {
+            for (CodecState state : mAudioCodecStates.values()) {
+                state.doSomeWork();
+            }
+        } catch (MediaCodec.CryptoException e) {
+            throw new Error("Audio CryptoException w/ errorCode "
+                    + e.getErrorCode() + ", '" + e.getMessage() + "'");
+        } catch (IllegalStateException e) {
+            throw new Error("Aduio CodecState.feedInputBuffer IllegalStateException " + e);
+        }
+
+    }
+
+    public long getNowUs() {
+        if (mAudioTrackState == null) {
+            return System.currentTimeMillis() * 1000;
+        }
+
+        return mAudioTrackState.getAudioTimeUs();
+    }
+
+    public long getRealTimeUsForMediaTime(long mediaTimeUs) {
+        if (mDeltaTimeUs == -1) {
+            long nowUs = getNowUs();
+            mDeltaTimeUs = nowUs - mediaTimeUs;
+        }
+
+        return mDeltaTimeUs + mediaTimeUs;
+    }
+
+    public int getDuration() {
+        return (int)((mDurationUs + 500) / 1000);
+    }
+
+    public int getCurrentPosition() {
+        if (mVideoCodecStates == null) {
+                return 0;
+        }
+
+        long positionUs = 0;
+
+        for (CodecState state : mVideoCodecStates.values()) {
+            long trackPositionUs = state.getCurrentPositionUs();
+
+            if (trackPositionUs > positionUs) {
+                positionUs = trackPositionUs;
+            }
+        }
+        return (int)((positionUs + 500) / 1000);
+    }
+
+}
diff --git a/tests/tests/media/src/android/media/cts/NonBlockingAudioTrack.java b/tests/tests/media/src/android/media/cts/NonBlockingAudioTrack.java
new file mode 100644
index 0000000..3ba1ce8
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/NonBlockingAudioTrack.java
@@ -0,0 +1,202 @@
+/*
+ * 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.cts;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.util.Log;
+
+import java.util.LinkedList;
+
+/**
+ * Class for playing audio by using audio track.
+ * {@link #write(byte[], int, int)} and {@link #write(short[], int, int)} methods will
+ * block until all data has been written to system. In order to avoid blocking, this class
+ * caculates available buffer size first then writes to audio sink.
+ */
+public class NonBlockingAudioTrack {
+    private static final String TAG = NonBlockingAudioTrack.class.getSimpleName();
+
+    class QueueElem {
+        byte[] data;
+        int offset;
+        int size;
+    }
+
+    private AudioTrack mAudioTrack;
+    private boolean mWriteMorePending = false;
+    private int mSampleRate;
+    private int mFrameSize;
+    private int mBufferSizeInFrames;
+    private int mNumFramesSubmitted = 0;
+    private int mNumBytesQueued = 0;
+    private LinkedList<QueueElem> mQueue = new LinkedList<QueueElem>();
+
+    public NonBlockingAudioTrack(int sampleRate, int channelCount) {
+        int channelConfig;
+        switch (channelCount) {
+            case 1:
+                channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+                break;
+            case 2:
+                channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+                break;
+            case 6:
+                channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+                break;
+            default:
+                throw new IllegalArgumentException();
+        }
+
+        int minBufferSize =
+            AudioTrack.getMinBufferSize(
+                    sampleRate,
+                    channelConfig,
+                    AudioFormat.ENCODING_PCM_16BIT);
+
+        int bufferSize = 2 * minBufferSize;
+
+        mAudioTrack = new AudioTrack(
+                AudioManager.STREAM_MUSIC,
+                sampleRate,
+                channelConfig,
+                AudioFormat.ENCODING_PCM_16BIT,
+                bufferSize,
+                AudioTrack.MODE_STREAM);
+
+        mSampleRate = sampleRate;
+        mFrameSize = 2 * channelCount;
+        mBufferSizeInFrames = bufferSize / mFrameSize;
+    }
+
+    public long getAudioTimeUs() {
+        int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition();
+
+        return (numFramesPlayed * 1000000L) / mSampleRate;
+    }
+
+    public int getNumBytesQueued() {
+        return mNumBytesQueued;
+    }
+
+    public void play() {
+        mAudioTrack.play();
+    }
+
+    public void stop() {
+        cancelWriteMore();
+
+        mAudioTrack.stop();
+
+        mNumFramesSubmitted = 0;
+        mQueue.clear();
+        mNumBytesQueued = 0;
+    }
+
+    public void pause() {
+        cancelWriteMore();
+
+        mAudioTrack.pause();
+    }
+
+    public void release() {
+        cancelWriteMore();
+
+        mAudioTrack.release();
+        mAudioTrack = null;
+    }
+
+    public void process() {
+        mWriteMorePending = false;
+        writeMore();
+    }
+
+    public int getPlayState() {
+        return mAudioTrack.getPlayState();
+    }
+
+    private void writeMore() {
+        if (mQueue.isEmpty()) {
+            return;
+        }
+
+        int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition();
+        int numFramesPending = mNumFramesSubmitted - numFramesPlayed;
+        int numFramesAvailableToWrite = mBufferSizeInFrames - numFramesPending;
+        int numBytesAvailableToWrite = numFramesAvailableToWrite * mFrameSize;
+
+        while (numBytesAvailableToWrite > 0) {
+            QueueElem elem = mQueue.peekFirst();
+
+            int numBytes = elem.size;
+            if (numBytes > numBytesAvailableToWrite) {
+                numBytes = numBytesAvailableToWrite;
+            }
+
+            int written = mAudioTrack.write(elem.data, elem.offset, numBytes);
+            assert(written == numBytes);
+
+            mNumFramesSubmitted += written / mFrameSize;
+
+            elem.size -= numBytes;
+            numBytesAvailableToWrite -= numBytes;
+            mNumBytesQueued -= numBytes;
+
+            if (elem.size == 0) {
+                mQueue.removeFirst();
+
+                if (mQueue.isEmpty()) {
+                    break;
+                }
+            } else {
+                elem.offset += numBytes;
+            }
+        }
+
+        if (!mQueue.isEmpty()) {
+            scheduleWriteMore();
+        }
+    }
+
+    private void scheduleWriteMore() {
+        if (mWriteMorePending) {
+            return;
+        }
+
+        int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition();
+        int numFramesPending = mNumFramesSubmitted - numFramesPlayed;
+        int pendingDurationMs = 1000 * numFramesPending / mSampleRate;
+
+        mWriteMorePending = true;
+    }
+
+    private void cancelWriteMore() {
+        mWriteMorePending = false;
+    }
+
+    public void write(byte[] data, int size) {
+        QueueElem elem = new QueueElem();
+        elem.data = data;
+        elem.offset = 0;
+        elem.size = size;
+
+        // accumulate size written to queue
+        mNumBytesQueued += size;
+        mQueue.add(elem);
+    }
+}
+