Merge "Merge "Federate platform docs against support library API file" into stage-aosp-master am: 4ede13e1f5  -s ours am: e8bb602327"
diff --git a/core/proto/android/server/windowmanagertrace.proto b/core/proto/android/server/windowmanagertrace.proto
new file mode 100644
index 0000000..0c65bb2
--- /dev/null
+++ b/core/proto/android/server/windowmanagertrace.proto
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+syntax = "proto2";
+
+import "frameworks/base/core/proto/android/content/configuration.proto";
+import "frameworks/base/core/proto/android/graphics/rect.proto";
+import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
+import "frameworks/base/core/proto/android/view/displayinfo.proto";
+import "frameworks/base/core/proto/android/view/windowlayoutparams.proto";
+
+package com.android.server.wm.proto;
+
+option java_multiple_files = true;
+
+/* represents a file full of window manager trace entries.
+   Encoded, it should start with 0x9 0x57 0x49 0x4e 0x54 0x52 0x41 0x43 0x45 (.WINTRACE), such
+   that they can be easily identified. */
+message WindowManagerTraceFileProto {
+
+    /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+       (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+        constants into .proto files. */
+    enum MagicNumber {
+        INVALID = 0;
+        MAGIC_NUMBER_L = 0x544e4957;  /* WINT (little-endian ASCII) */
+        MAGIC_NUMBER_H = 0x45434152;  /* RACE (little-endian ASCII) */
+    }
+
+    optional fixed64 magic_number = 1;  /* Must be the first field, set to value in MagicNumber */
+    repeated WindowManagerTraceProto entry = 2;
+}
+
+/* one window manager trace entry. */
+message WindowManagerTraceProto {
+    /* required: elapsed realtime in nanos since boot of when this entry was logged */
+    optional fixed64 elapsed_realtime_nanos = 1;
+
+    /* where the trace originated */
+    optional string where = 2;
+
+    optional WindowManagerServiceProto window_manager_service = 3;
+}
diff --git a/services/core/java/com/android/server/media/AudioPlaybackMonitor.java b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
new file mode 100644
index 0000000..791ee82
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 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.server.media;
+
+import android.content.Context;
+import android.media.AudioManager.AudioPlaybackCallback;
+import android.media.AudioPlaybackConfiguration;
+import android.media.IAudioService;
+import android.media.IPlaybackConfigDispatcher;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Monitors changes in audio playback, and notify the newly started audio playback through the
+ * {@link OnAudioPlaybackStartedListener} and the activeness change through the
+ * {@link OnAudioPlaybackActiveStateListener}.
+ */
+class AudioPlaybackMonitor extends IPlaybackConfigDispatcher.Stub {
+    private static boolean DEBUG = MediaSessionService.DEBUG;
+    private static String TAG = "AudioPlaybackMonitor";
+
+    private static AudioPlaybackMonitor sInstance;
+
+    /**
+     * Called when audio playback is started for a given UID.
+     */
+    interface OnAudioPlaybackStartedListener {
+        void onAudioPlaybackStarted(int uid);
+    }
+
+    /**
+     * Called when audio player state is changed.
+     */
+    interface OnAudioPlayerActiveStateChangedListener {
+        void onAudioPlayerActiveStateChanged(int uid, boolean active);
+    }
+
+    private final Object mLock = new Object();
+    private final Context mContext;
+    private final List<OnAudioPlaybackStartedListener> mAudioPlaybackStartedListeners
+            = new ArrayList<>();
+    private final List<OnAudioPlayerActiveStateChangedListener>
+            mAudioPlayerActiveStateChangedListeners = new ArrayList<>();
+    private final Map<Integer, Integer> mAudioPlaybackStates = new HashMap<>();
+    private final Set<Integer> mActiveAudioPlaybackClientUids = new HashSet<>();
+
+    // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
+    // The UID whose audio playback becomes active at the last comes first.
+    // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
+    private final IntArray mSortedAudioPlaybackClientUids = new IntArray();
+
+    static AudioPlaybackMonitor getInstance(Context context, IAudioService audioService) {
+        if (sInstance == null) {
+            sInstance = new AudioPlaybackMonitor(context, audioService);
+        }
+        return sInstance;
+    }
+
+    private AudioPlaybackMonitor(Context context, IAudioService audioService) {
+        mContext = context;
+        try {
+            audioService.registerPlaybackCallback(this);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failed to register playback callback", e);
+        }
+    }
+
+    /**
+     * Called when the {@link AudioPlaybackConfiguration} is updated.
+     * <p>If an app starts audio playback, the app's local media session will be the media button
+     * session. If the app has multiple media sessions, the playback active local session will be
+     * picked.
+     *
+     * @param configs List of the current audio playback configuration
+     */
+    @Override
+    public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
+            boolean flush) {
+        if (flush) {
+            Binder.flushPendingCommands();
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            List<Integer> newActiveAudioPlaybackClientUids = new ArrayList<>();
+            List<OnAudioPlayerActiveStateChangedListener> audioPlayerActiveStateChangedListeners;
+            List<OnAudioPlaybackStartedListener> audioPlaybackStartedListeners;
+            synchronized (mLock) {
+                // Update mActiveAudioPlaybackClientUids and mSortedAudioPlaybackClientUids,
+                // and find newly activated audio playbacks.
+                mActiveAudioPlaybackClientUids.clear();
+                for (AudioPlaybackConfiguration config : configs) {
+                    // Ignore inactive (i.e. not playing) or PLAYER_TYPE_JAM_SOUNDPOOL
+                    // (i.e. playback from the SoundPool class which is only for sound effects)
+                    // playback.
+                    // Note that we shouldn't ignore PLAYER_TYPE_UNKNOWN because it might be OEM
+                    // specific audio/video players.
+                    if (!config.isActive() || config.getPlayerType()
+                            == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+                        continue;
+                    }
+
+                    mActiveAudioPlaybackClientUids.add(config.getClientUid());
+                    Integer oldState = mAudioPlaybackStates.get(config.getPlayerInterfaceId());
+                    if (!isActiveState(oldState)) {
+                        if (DEBUG) {
+                            Log.d(TAG, "Found a new active media playback. " +
+                                    AudioPlaybackConfiguration.toLogFriendlyString(config));
+                        }
+                        // New active audio playback.
+                        newActiveAudioPlaybackClientUids.add(config.getClientUid());
+                        int index = mSortedAudioPlaybackClientUids.indexOf(config.getClientUid());
+                        if (index == 0) {
+                            // It's the lastly played music app already. Skip updating.
+                            continue;
+                        } else if (index > 0) {
+                            mSortedAudioPlaybackClientUids.remove(index);
+                        }
+                        mSortedAudioPlaybackClientUids.add(0, config.getClientUid());
+                    }
+                }
+                audioPlayerActiveStateChangedListeners = new ArrayList<>(
+                        mAudioPlayerActiveStateChangedListeners);
+                audioPlaybackStartedListeners = new ArrayList<>(mAudioPlaybackStartedListeners);
+            }
+            // Notify the change of audio playback states.
+            for (AudioPlaybackConfiguration config : configs) {
+                boolean wasActive = isActiveState(
+                        mAudioPlaybackStates.get(config.getPlayerInterfaceId()));
+                boolean isActive = config.isActive();
+                if (wasActive != isActive) {
+                    for (OnAudioPlayerActiveStateChangedListener listener
+                            : audioPlayerActiveStateChangedListeners) {
+                        listener.onAudioPlayerActiveStateChanged(config.getClientUid(),
+                                isActive);
+                    }
+                }
+            }
+            // Notify the start of audio playback
+            for (int uid : newActiveAudioPlaybackClientUids) {
+                for (OnAudioPlaybackStartedListener listener : audioPlaybackStartedListeners) {
+                    listener.onAudioPlaybackStarted(uid);
+                }
+            }
+            mAudioPlaybackStates.clear();
+            for (AudioPlaybackConfiguration config : configs) {
+                mAudioPlaybackStates.put(config.getPlayerInterfaceId(), config.getPlayerState());
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Registers OnAudioPlaybackStartedListener.
+     */
+    public void registerOnAudioPlaybackStartedListener(OnAudioPlaybackStartedListener listener) {
+        synchronized (mLock) {
+            mAudioPlaybackStartedListeners.add(listener);
+        }
+    }
+
+    /**
+     * Unregisters OnAudioPlaybackStartedListener.
+     */
+    public void unregisterOnAudioPlaybackStartedListener(OnAudioPlaybackStartedListener listener) {
+        synchronized (mLock) {
+            mAudioPlaybackStartedListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Registers OnAudioPlayerActiveStateChangedListener.
+     */
+    public void registerOnAudioPlayerActiveStateChangedListener(
+            OnAudioPlayerActiveStateChangedListener listener) {
+        synchronized (mLock) {
+            mAudioPlayerActiveStateChangedListeners.add(listener);
+        }
+    }
+
+    /**
+     * Unregisters OnAudioPlayerActiveStateChangedListener.
+     */
+    public void unregisterOnAudioPlayerActiveStateChangedListener(
+            OnAudioPlayerActiveStateChangedListener listener) {
+        synchronized (mLock) {
+            mAudioPlayerActiveStateChangedListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
+     * audio/video) The UID whose audio playback becomes active at the last comes first.
+     */
+    public IntArray getSortedAudioPlaybackClientUids() {
+        IntArray sortedAudioPlaybackClientUids = new IntArray();
+        synchronized (mLock) {
+            sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
+        }
+        return sortedAudioPlaybackClientUids;
+    }
+
+    /**
+     * Returns if the audio playback is active for the uid.
+     */
+    public boolean isPlaybackActive(int uid) {
+        synchronized (mLock) {
+            return mActiveAudioPlaybackClientUids.contains(uid);
+        }
+    }
+
+    /**
+     * Cleans up the sorted list of audio playback client UIDs with given {@param
+     * mediaButtonSessionUid}.
+     * <p>UIDs whose audio playback started after the media button session's audio playback
+     * cannot be the lastly played media app. So they won't needed anymore.
+     *
+     * @param mediaButtonSessionUid UID of the media button session.
+     */
+    public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
+        synchronized (mLock) {
+            int userId = UserHandle.getUserId(mediaButtonSessionUid);
+            for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
+                if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
+                    break;
+                }
+                int uid = mSortedAudioPlaybackClientUids.get(i);
+                if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) {
+                    // Clean up unnecessary UIDs.
+                    // It doesn't need to be managed profile aware because it's just to prevent
+                    // the list from increasing indefinitely. The media button session updating
+                    // shouldn't be affected by cleaning up.
+                    mSortedAudioPlaybackClientUids.remove(i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Dumps {@link AudioPlaybackMonitor}.
+     */
+    public void dump(PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            pw.println(prefix + "Audio playback (lastly played comes first)");
+            String indent = prefix + "  ";
+            for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
+                int uid = mSortedAudioPlaybackClientUids.get(i);
+                pw.print(indent + "uid=" + uid + " packages=");
+                String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+                if (packages != null && packages.length > 0) {
+                    for (int j = 0; j < packages.length; j++) {
+                        pw.print(packages[j] + " ");
+                    }
+                }
+                pw.println();
+            }
+        }
+    }
+
+    private boolean isActiveState(Integer state) {
+        return state != null && state.equals(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
+    }
+}
diff --git a/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java b/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
deleted file mode 100644
index 110f26d..0000000
--- a/services/core/java/com/android/server/media/AudioPlayerStateMonitor.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/*
- * Copyright (C) 2017 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.server.media;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.media.AudioPlaybackConfiguration;
-import android.media.IAudioService;
-import android.media.IPlaybackConfigDispatcher;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.IntArray;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Monitors the state changes of audio players.
- */
-class AudioPlayerStateMonitor extends IPlaybackConfigDispatcher.Stub {
-    private static boolean DEBUG = MediaSessionService.DEBUG;
-    private static String TAG = "AudioPlayerStateMonitor";
-
-    private static AudioPlayerStateMonitor sInstance = new AudioPlayerStateMonitor();
-
-    /**
-     * Called when the state of audio player is changed.
-     */
-    interface OnAudioPlayerStateChangedListener {
-        void onAudioPlayerStateChanged(
-                int uid, int prevState, @Nullable AudioPlaybackConfiguration config);
-    }
-
-    private final static class MessageHandler extends Handler {
-        private static final int MSG_AUDIO_PLAYER_STATE_CHANGED = 1;
-
-        private final OnAudioPlayerStateChangedListener mListsner;
-
-        public MessageHandler(Looper looper, OnAudioPlayerStateChangedListener listener) {
-            super(looper);
-            mListsner = listener;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_AUDIO_PLAYER_STATE_CHANGED:
-                    mListsner.onAudioPlayerStateChanged(
-                            msg.arg1, msg.arg2, (AudioPlaybackConfiguration) msg.obj);
-                    break;
-            }
-        }
-
-        public void sendAudioPlayerStateChangedMessage(int uid, int prevState,
-                AudioPlaybackConfiguration config) {
-            obtainMessage(MSG_AUDIO_PLAYER_STATE_CHANGED, uid, prevState, config).sendToTarget();
-        }
-    }
-
-    private final Object mLock = new Object();
-    @GuardedBy("mLock")
-    private final Map<OnAudioPlayerStateChangedListener, MessageHandler> mListenerMap =
-            new HashMap<>();
-    @GuardedBy("mLock")
-    private final Map<Integer, Integer> mAudioPlayerStates = new HashMap<>();
-    @GuardedBy("mLock")
-    private final Map<Integer, HashSet<Integer>> mAudioPlayersForUid = new HashMap<>();
-    // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
-    // The UID whose audio playback becomes active at the last comes first.
-    // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
-    @GuardedBy("mLock")
-    private final IntArray mSortedAudioPlaybackClientUids = new IntArray();
-
-    @GuardedBy("mLock")
-    private boolean mRegisteredToAudioService;
-
-    static AudioPlayerStateMonitor getInstance() {
-        return sInstance;
-    }
-
-    private AudioPlayerStateMonitor() {
-    }
-
-    /**
-     * Called when the {@link AudioPlaybackConfiguration} is updated.
-     * <p>If an app starts audio playback, the app's local media session will be the media button
-     * session. If the app has multiple media sessions, the playback active local session will be
-     * picked.
-     *
-     * @param configs List of the current audio playback configuration
-     */
-    @Override
-    public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
-            boolean flush) {
-        if (flush) {
-            Binder.flushPendingCommands();
-        }
-        final long token = Binder.clearCallingIdentity();
-        try {
-            final Map<Integer, Integer> prevAudioPlayerStates = new HashMap<>(mAudioPlayerStates);
-            final Map<Integer, HashSet<Integer>> prevAudioPlayersForUid =
-                    new HashMap<>(mAudioPlayersForUid);
-            synchronized (mLock) {
-                mAudioPlayerStates.clear();
-                mAudioPlayersForUid.clear();
-                for (AudioPlaybackConfiguration config : configs) {
-                    int pii = config.getPlayerInterfaceId();
-                    int uid = config.getClientUid();
-                    mAudioPlayerStates.put(pii, config.getPlayerState());
-                    HashSet<Integer> players = mAudioPlayersForUid.get(uid);
-                    if (players == null) {
-                        players = new HashSet<Integer>();
-                        players.add(pii);
-                        mAudioPlayersForUid.put(uid, players);
-                    } else {
-                        players.add(pii);
-                    }
-                }
-                for (AudioPlaybackConfiguration config : configs) {
-                    if (!config.isActive()) {
-                        continue;
-                    }
-
-                    int uid = config.getClientUid();
-                    if (!isActiveState(prevAudioPlayerStates.get(config.getPlayerInterfaceId()))) {
-                        if (DEBUG) {
-                            Log.d(TAG, "Found a new active media playback. " +
-                                    AudioPlaybackConfiguration.toLogFriendlyString(config));
-                        }
-                        // New active audio playback.
-                        int index = mSortedAudioPlaybackClientUids.indexOf(uid);
-                        if (index == 0) {
-                            // It's the lastly played music app already. Skip updating.
-                            continue;
-                        } else if (index > 0) {
-                            mSortedAudioPlaybackClientUids.remove(index);
-                        }
-                        mSortedAudioPlaybackClientUids.add(0, uid);
-                    }
-                }
-                // Notify the change of audio player states.
-                for (AudioPlaybackConfiguration config : configs) {
-                    Integer prevState = prevAudioPlayerStates.get(config.getPlayerInterfaceId());
-                    if (prevState == null || prevState != config.getPlayerState()) {
-                        sendAudioPlayerStateChangedMessageLocked(
-                                config.getClientUid(), prevState, config);
-                    }
-                }
-                for (Integer prevUid : prevAudioPlayersForUid.keySet()) {
-                    // If all players for prevUid is removed, notify the prev state was
-                    // PLAYER_STATE_STARTED only when there were a player whose state was
-                    // PLAYER_STATE_STARTED, otherwise any inactive state is okay to notify.
-                    if (!mAudioPlayersForUid.containsKey(prevUid)) {
-                        Set<Integer> players = mAudioPlayersForUid.get(prevUid);
-                        int prevState = AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN;
-                        for (int pii : players) {
-                            Integer state = prevAudioPlayerStates.get(pii);
-                            if (state == null) {
-                                continue;
-                            }
-                            if (state == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                                prevState = state;
-                                break;
-                            } else if (prevState
-                                    == AudioPlaybackConfiguration.PLAYER_STATE_UNKNOWN) {
-                                prevState = state;
-                            }
-                        }
-                        sendAudioPlayerStateChangedMessageLocked(prevUid, prevState, null);
-                    }
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    /**
-     * Registers OnAudioPlayerStateChangedListener.
-     */
-    public void registerListener(OnAudioPlayerStateChangedListener listener, Handler handler) {
-        synchronized (mLock) {
-            mListenerMap.put(listener, new MessageHandler((handler == null) ?
-                    Looper.myLooper() : handler.getLooper(), listener));
-        }
-    }
-
-    /**
-     * Unregisters OnAudioPlayerStateChangedListener.
-     */
-    public void unregisterListener(OnAudioPlayerStateChangedListener listener) {
-        synchronized (mLock) {
-            mListenerMap.remove(listener);
-        }
-    }
-
-    /**
-     * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
-     * audio/video) The UID whose audio playback becomes active at the last comes first.
-     */
-    public IntArray getSortedAudioPlaybackClientUids() {
-        IntArray sortedAudioPlaybackClientUids = new IntArray();
-        synchronized (mLock) {
-            sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
-        }
-        return sortedAudioPlaybackClientUids;
-    }
-
-    /**
-     * Returns if the audio playback is active for the uid.
-     */
-    public boolean isPlaybackActive(int uid) {
-        synchronized (mLock) {
-            Set<Integer> players = mAudioPlayersForUid.get(uid);
-            if (players == null) {
-                return false;
-            }
-            for (Integer pii : players) {
-                if (isActiveState(mAudioPlayerStates.get(pii))) {
-                    return true;
-                }
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Cleans up the sorted list of audio playback client UIDs with given {@param
-     * mediaButtonSessionUid}.
-     * <p>UIDs whose audio playback are inactive and have started before the media button session's
-     * audio playback cannot be the lastly played media app. So they won't needed anymore.
-     *
-     * @param mediaButtonSessionUid UID of the media button session.
-     */
-    public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
-        synchronized (mLock) {
-            int userId = UserHandle.getUserId(mediaButtonSessionUid);
-            for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
-                if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
-                    break;
-                }
-                int uid = mSortedAudioPlaybackClientUids.get(i);
-                if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) {
-                    // Clean up unnecessary UIDs.
-                    // It doesn't need to be managed profile aware because it's just to prevent
-                    // the list from increasing indefinitely. The media button session updating
-                    // shouldn't be affected by cleaning up.
-                    mSortedAudioPlaybackClientUids.remove(i);
-                }
-            }
-        }
-    }
-
-    /**
-     * Dumps {@link AudioPlayerStateMonitor}.
-     */
-    public void dump(Context context, PrintWriter pw, String prefix) {
-        synchronized (mLock) {
-            pw.println(prefix + "Audio playback (lastly played comes first)");
-            String indent = prefix + "  ";
-            for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
-                int uid = mSortedAudioPlaybackClientUids.get(i);
-                pw.print(indent + "uid=" + uid + " packages=");
-                String[] packages = context.getPackageManager().getPackagesForUid(uid);
-                if (packages != null && packages.length > 0) {
-                    for (int j = 0; j < packages.length; j++) {
-                        pw.print(packages[j] + " ");
-                    }
-                }
-                pw.println();
-            }
-        }
-    }
-
-    public void registerSelfIntoAudioServiceIfNeeded(IAudioService audioService) {
-        synchronized (mLock) {
-            try {
-                if (!mRegisteredToAudioService) {
-                    audioService.registerPlaybackCallback(this);
-                    mRegisteredToAudioService = true;
-                }
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Failed to register playback callback", e);
-                mRegisteredToAudioService = false;
-            }
-        }
-    }
-
-    private void sendAudioPlayerStateChangedMessageLocked(
-            final int uid, final int prevState, final AudioPlaybackConfiguration config) {
-        for (MessageHandler messageHandler : mListenerMap.values()) {
-            messageHandler.sendAudioPlayerStateChangedMessage(uid, prevState, config);
-        }
-    }
-
-    private static boolean isActiveState(Integer state) {
-        return state != null && state.equals(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
-    }
-}
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 3c9e1d4..1cfd5f0 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -19,14 +19,12 @@
 import com.android.internal.util.DumpUtils;
 import com.android.server.Watchdog;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
@@ -98,8 +96,7 @@
     private int mCurrentUserId = -1;
     private boolean mGlobalBluetoothA2dpOn = false;
     private final IAudioService mAudioService;
-    private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
-    private final Handler mHandler = new Handler();
+    private final AudioPlaybackMonitor mAudioPlaybackMonitor;
     private final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
 
     public MediaRouterService(Context context) {
@@ -109,57 +106,31 @@
         mAudioService = IAudioService.Stub.asInterface(
                 ServiceManager.getService(Context.AUDIO_SERVICE));
 
-        mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
-        mAudioPlayerStateMonitor.registerListener(
-                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
-            static final long WAIT_MS = 500;
-            final Runnable mRestoreBluetoothA2dpRunnable = new Runnable() {
-                @Override
-                public void run() {
-                    restoreBluetoothA2dp();
-                }
-            };
-
+        mAudioPlaybackMonitor = AudioPlaybackMonitor.getInstance(context, mAudioService);
+        mAudioPlaybackMonitor.registerOnAudioPlayerActiveStateChangedListener(
+                new AudioPlaybackMonitor.OnAudioPlayerActiveStateChangedListener() {
             @Override
-            public void onAudioPlayerStateChanged(
-                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
-                int restoreUid = -1;
-                boolean active = config == null ? false : config.isActive();
+            public void onAudioPlayerActiveStateChanged(int uid, boolean active) {
                 if (active) {
-                    restoreUid = uid;
-                } else if (prevState != AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                    // Noting to do if the prev state is not an active state.
-                    return;
+                    restoreRoute(uid);
                 } else {
                     IntArray sortedAudioPlaybackClientUids =
-                            mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
-                    for (int i = 0; i < sortedAudioPlaybackClientUids.size(); ++i) {
-                        if (mAudioPlayerStateMonitor.isPlaybackActive(
+                            mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
+                    boolean restored = false;
+                    for (int i = 0; i < sortedAudioPlaybackClientUids.size(); i++) {
+                        if (mAudioPlaybackMonitor.isPlaybackActive(
                                 sortedAudioPlaybackClientUids.get(i))) {
-                            restoreUid = sortedAudioPlaybackClientUids.get(i);
+                            restoreRoute(sortedAudioPlaybackClientUids.get(i));
+                            restored = true;
                             break;
                         }
                     }
-                }
-
-                mHandler.removeCallbacks(mRestoreBluetoothA2dpRunnable);
-                if (restoreUid >= 0) {
-                    restoreRoute(restoreUid);
-                    if (DEBUG) {
-                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
-                                + " active " + active + " restoring " + restoreUid);
-                    }
-                } else {
-                    mHandler.postDelayed(mRestoreBluetoothA2dpRunnable, WAIT_MS);
-                    if (DEBUG) {
-                        Slog.d(TAG, "onAudioPlayerStateChanged: " + "uid " + uid
-                                + " active " + active + " delaying");
+                    if (!restored) {
+                        restoreBluetoothA2dp();
                     }
                 }
             }
-        }, mHandler);
-        mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
-
+        });
         AudioRoutesInfo audioRoutes = null;
         try {
             audioRoutes = mAudioService.startWatchingRoutes(new IAudioRoutesObserver.Stub() {
@@ -290,14 +261,9 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            ClientRecord clientRecord;
             synchronized (mLock) {
-                clientRecord = mAllClientRecords.get(client.asBinder());
+                return isPlaybackActiveLocked(client);
             }
-            if (clientRecord != null) {
-                return mAudioPlayerStateMonitor.isPlaybackActive(clientRecord.mUid);
-            }
-            return false;
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -514,6 +480,14 @@
         return null;
     }
 
+    private boolean isPlaybackActiveLocked(IMediaRouterClient client) {
+        ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
+        if (clientRecord != null) {
+            return mAudioPlaybackMonitor.isPlaybackActive(clientRecord.mUid);
+        }
+        return false;
+    }
+
     private void setDiscoveryRequestLocked(IMediaRouterClient client,
             int routeTypes, boolean activeScan) {
         final IBinder binder = client.asBinder();
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index f6a81d0..aa65244 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -16,7 +16,6 @@
 
 package com.android.server.media;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.KeyguardManager;
@@ -32,7 +31,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.media.AudioManager;
-import android.media.AudioPlaybackConfiguration;
+import android.media.AudioManagerInternal;
 import android.media.AudioSystem;
 import android.media.IAudioService;
 import android.media.IRemoteVolumeController;
@@ -69,6 +68,7 @@
 import android.view.ViewConfiguration;
 
 import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.Watchdog;
 import com.android.server.Watchdog.Monitor;
@@ -104,6 +104,7 @@
 
     private KeyguardManager mKeyguardManager;
     private IAudioService mAudioService;
+    private AudioManagerInternal mAudioManagerInternal;
     private ContentResolver mContentResolver;
     private SettingsObserver mSettingsObserver;
     private INotificationManager mNotificationManager;
@@ -113,7 +114,7 @@
     // It's always not null after the MediaSessionService is started.
     private FullUserRecord mCurrentFullUserRecord;
     private MediaSessionRecord mGlobalPrioritySession;
-    private AudioPlayerStateMonitor mAudioPlayerStateMonitor;
+    private AudioPlaybackMonitor mAudioPlaybackMonitor;
 
     // Used to notify system UI when remote volume was changed. TODO find a
     // better way to handle this.
@@ -136,16 +137,11 @@
         mKeyguardManager =
                 (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
         mAudioService = getAudioService();
-        mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
-        mAudioPlayerStateMonitor.registerListener(
-                new AudioPlayerStateMonitor.OnAudioPlayerStateChangedListener() {
+        mAudioPlaybackMonitor = AudioPlaybackMonitor.getInstance(getContext(), mAudioService);
+        mAudioPlaybackMonitor.registerOnAudioPlaybackStartedListener(
+                new AudioPlaybackMonitor.OnAudioPlaybackStartedListener() {
             @Override
-            public void onAudioPlayerStateChanged(
-                    int uid, int prevState, @Nullable AudioPlaybackConfiguration config) {
-                if (config == null || !config.isActive() || config.getPlayerType()
-                        == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
-                    return;
-                }
+            public void onAudioPlaybackStarted(int uid) {
                 synchronized (mLock) {
                     FullUserRecord user =
                             getFullUserRecordLocked(UserHandle.getUserId(uid));
@@ -154,8 +150,8 @@
                     }
                 }
             }
-        }, null /* handler */);
-        mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
+        });
+        mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
         mContentResolver = getContext().getContentResolver();
         mSettingsObserver = new SettingsObserver();
         mSettingsObserver.observe();
@@ -654,7 +650,7 @@
 
         public FullUserRecord(int fullUserId) {
             mFullUserId = fullUserId;
-            mPriorityStack = new MediaSessionStack(mAudioPlayerStateMonitor, this);
+            mPriorityStack = new MediaSessionStack(mAudioPlaybackMonitor, this);
             // Restore the remembered media button receiver before the boot.
             String mediaButtonReceiver = Settings.Secure.getStringForUser(mContentResolver,
                     Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId);
@@ -1313,7 +1309,7 @@
                 for (int i = 0; i < count; i++) {
                     mUserRecords.valueAt(i).dumpLocked(pw, "");
                 }
-                mAudioPlayerStateMonitor.dump(getContext(), pw, "");
+                mAudioPlaybackMonitor.dump(pw, "");
             }
         }
 
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index 719ec36..d9fe72e 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -75,7 +75,7 @@
      */
     private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
 
-    private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
+    private final AudioPlaybackMonitor mAudioPlaybackMonitor;
     private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
 
     /**
@@ -84,6 +84,7 @@
      */
     private MediaSessionRecord mMediaButtonSession;
 
+    private MediaSessionRecord mCachedDefault;
     private MediaSessionRecord mCachedVolumeDefault;
 
     /**
@@ -92,8 +93,8 @@
     private final SparseArray<ArrayList<MediaSessionRecord>> mCachedActiveLists =
             new SparseArray<>();
 
-    MediaSessionStack(AudioPlayerStateMonitor monitor, OnMediaButtonSessionChangedListener listener) {
-        mAudioPlayerStateMonitor = monitor;
+    MediaSessionStack(AudioPlaybackMonitor monitor, OnMediaButtonSessionChangedListener listener) {
+        mAudioPlaybackMonitor = monitor;
         mOnMediaButtonSessionChangedListener = listener;
     }
 
@@ -186,13 +187,13 @@
         if (DEBUG) {
             Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2));
         }
-        IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
+        IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();
         for (int i = 0; i < audioPlaybackUids.size(); i++) {
             MediaSessionRecord mediaButtonSession =
                     findMediaButtonSession(audioPlaybackUids.get(i));
             if (mediaButtonSession != null) {
                 // Found the media button session.
-                mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
+                mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
                 if (mMediaButtonSession != mediaButtonSession) {
                     updateMediaButtonSession(mediaButtonSession);
                 }
@@ -215,7 +216,7 @@
         for (MediaSessionRecord session : mSessions) {
             if (uid == session.getUid()) {
                 if (session.getPlaybackState() != null && session.isPlaybackActive() ==
-                        mAudioPlayerStateMonitor.isPlaybackActive(session.getUid())) {
+                        mAudioPlaybackMonitor.isPlaybackActive(session.getUid())) {
                     // If there's a media session whose PlaybackState matches
                     // the audio playback state, return it immediately.
                     return session;
@@ -375,6 +376,7 @@
     }
 
     private void clearCache(int userId) {
+        mCachedDefault = null;
         mCachedVolumeDefault = null;
         mCachedActiveLists.remove(userId);
         // mCachedActiveLists may also include the list of sessions for UserHandle.USER_ALL,
diff --git a/services/core/java/com/android/server/wm/DimLayer.java b/services/core/java/com/android/server/wm/DimLayer.java
index 401547e..8fb2be8 100644
--- a/services/core/java/com/android/server/wm/DimLayer.java
+++ b/services/core/java/com/android/server/wm/DimLayer.java
@@ -119,7 +119,7 @@
         } catch (Exception e) {
             Slog.e(TAG_WM, "Exception creating Dim surface", e);
         } finally {
-            service.closeSurfaceTransaction();
+            service.closeSurfaceTransaction("DimLayer.constructSurface");
         }
     }
 
@@ -235,7 +235,7 @@
             } catch (RuntimeException e) {
                 Slog.w(TAG, "Failure setting size", e);
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("DimLayer.setBounds");
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index fe34834..2f54e0e 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1083,7 +1083,7 @@
             mService.mDisplayManagerInternal.performTraversalInTransactionFromWindowManager();
         } finally {
             if (!inTransaction) {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("setRotationUnchecked");
                 if (SHOW_LIGHT_TRANSACTIONS) {
                     Slog.i(TAG_WM, "<<< CLOSE TRANSACTION setRotationUnchecked");
                 }
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 69f557b..5ea0e1d 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -543,7 +543,7 @@
                         mDimLayer.setBounds(mTmpRect);
                         mDimLayer.show(getResizeDimLayer(), alpha, 0 /* duration */);
                     } finally {
-                        mService.closeSurfaceTransaction();
+                        mService.closeSurfaceTransaction("setResizeDimLayer");
                     }
                 }
                 mLastDimLayerRect.set(mTmpRect);
@@ -558,7 +558,7 @@
                     mService.openSurfaceTransaction();
                     mDimLayer.hide();
                 } finally {
-                    mService.closeSurfaceTransaction();
+                    mService.closeSurfaceTransaction("setResizeDimLayer");
                 }
             }
             mLastDimLayerAlpha = 0f;
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 860ff38..79d46ce 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -185,7 +185,7 @@
                 surfaceControl.setLayerStack(display.getLayerStack());
                 surfaceControl.show();
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("performDrag");
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                         TAG_WM, "<<< CLOSE TRANSACTION performDrag");
             }
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 9a955de..861fb44 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -458,7 +458,7 @@
                     + mSurfaceControl + ": pos=(" +
                     (int)(x - mThumbOffsetX) + "," + (int)(y - mThumbOffsetY) + ")");
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("notifyMoveLw");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(
                     TAG_WM, "<<< CLOSE TRANSACTION notifyMoveLocked");
         }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index a710441..bcb6e673 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -457,7 +457,7 @@
         try {
             forAllWindows(sRemoveReplacedWindowsConsumer, true /* traverseTopToBottom */);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("removeReplacedWindows");
             if (SHOW_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION removeReplacedWindows");
         }
     }
@@ -599,7 +599,7 @@
         } catch (RuntimeException e) {
             Slog.wtf(TAG, "Unhandled exception in Window Manager", e);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("performLayoutAndPlaceSurfaces");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                     "<<< CLOSE TRANSACTION performLayoutAndPlaceSurfaces");
         }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index c25b19c..3350fea 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -296,7 +296,7 @@
             setRotationInTransaction(originalRotation);
         } finally {
             if (!inTransaction) {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("ScreenRotationAnimation");
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM,
                         "<<< CLOSE TRANSACTION ScreenRotationAnimation");
             }
@@ -567,7 +567,7 @@
             } catch (OutOfResourcesException e) {
                 Slog.w(TAG, "Unable to allocate black surface", e);
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("ScreenRotationAnimation.startAnimation");
                 if (SHOW_LIGHT_TRANSACTIONS || DEBUG_STATE) Slog.i(
                         TAG_WM,
                         "<<< CLOSE TRANSACTION ScreenRotationAnimation.startAnimation");
@@ -607,7 +607,7 @@
             } catch (OutOfResourcesException e) {
                 Slog.w(TAG, "Unable to allocate black surface", e);
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("ScreenRotationAnimation.startAnimation");
                 if (SHOW_LIGHT_TRANSACTIONS || DEBUG_STATE) Slog.i(
                         TAG_WM,
                         "<<< CLOSE TRANSACTION ScreenRotationAnimation.startAnimation");
@@ -629,7 +629,7 @@
             } catch (OutOfResourcesException e) {
                 Slog.w(TAG, "Unable to allocate black surface", e);
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("ScreenRotationAnimation.startAnimation");
                 if (SHOW_LIGHT_TRANSACTIONS || DEBUG_STATE) Slog.i(
                         TAG_WM,
                         "<<< CLOSE TRANSACTION ScreenRotationAnimation.startAnimation");
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d83630d..13435d7 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -678,7 +678,7 @@
                 mChildren.get(i).forceWindowsScaleableInTransaction(force);
             }
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("forceWindowsScaleable");
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index a11b333..12f6b5a 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -637,7 +637,7 @@
         } else {
             showDimLayer();
         }
-        mService.closeSurfaceTransaction();
+        mService.closeSurfaceTransaction("updateDimLayerVisibility");
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index e409a68..1912095 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -233,7 +233,7 @@
             } catch (RuntimeException e) {
                 Slog.wtf(TAG, "Unhandled exception in Window Manager", e);
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("WindowAnimator");
                 if (SHOW_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION animate");
             }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index af1fa88..bf5f4bc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -161,7 +161,9 @@
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.ServiceManager;
+import android.os.ShellCallback;
 import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -361,6 +363,8 @@
     private static final int TRANSITION_ANIMATION_SCALE = 1;
     private static final int ANIMATION_DURATION_SCALE = 2;
 
+    final WindowTracing mWindowTracing;
+
     final private KeyguardDisableHandler mKeyguardDisableHandler;
     boolean mKeyguardGoingAway;
     // VR Vr2d Display Id.
@@ -820,15 +824,20 @@
 
     /**
      * Closes a surface transaction.
+     * @param where debug string indicating where the transaction originated
      */
-    void closeSurfaceTransaction() {
+    void closeSurfaceTransaction(String where) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "closeSurfaceTransaction");
             synchronized (mWindowMap) {
-                if (mRoot.mSurfaceTraceEnabled) {
-                    mRoot.mRemoteEventTrace.closeSurfaceTransaction();
+                try {
+                    traceStateLocked(where);
+                } finally {
+                    if (mRoot.mSurfaceTraceEnabled) {
+                        mRoot.mRemoteEventTrace.closeSurfaceTransaction();
+                    }
+                    SurfaceControl.closeTransaction();
                 }
-                SurfaceControl.closeTransaction();
             }
         } finally {
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -929,6 +938,12 @@
         }, 0);
     }
 
+    @Override
+    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+            String[] args, ShellCallback callback, ResultReceiver result) {
+        new WindowManagerShellCommand(this).exec(this, in, out, err, args, callback, result);
+    }
+
     private WindowManagerService(Context context, InputManagerService inputManager,
             boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
             WindowManagerPolicy policy) {
@@ -959,6 +974,8 @@
         mPolicy = policy;
         mTaskSnapshotController = new TaskSnapshotController(this);
 
+        mWindowTracing = WindowTracing.createDefaultAndStartLooper(context);
+
         LocalServices.addService(WindowManagerPolicy.class, mPolicy);
 
         if(mInputManager != null) {
@@ -1068,7 +1085,7 @@
         try {
             createWatermarkInTransaction();
         } finally {
-            closeSurfaceTransaction();
+            closeSurfaceTransaction("createWatermarkInTransaction");
         }
 
         showEmulatorDisplayOverlayIfNeeded();
@@ -3572,7 +3589,7 @@
                     mCircularDisplayMask = null;
                 }
             } finally {
-                closeSurfaceTransaction();
+                closeSurfaceTransaction("showCircularMask");
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM,
                         "<<< CLOSE TRANSACTION showCircularMask(visible=" + visible + ")");
             }
@@ -3597,7 +3614,7 @@
                 }
                 mEmulatorDisplayOverlay.setVisibility(true);
             } finally {
-                closeSurfaceTransaction();
+                closeSurfaceTransaction("showEmulatorDisplayOverlay");
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM,
                         "<<< CLOSE TRANSACTION showEmulatorDisplayOverlay");
             }
@@ -6320,7 +6337,7 @@
      * @param proto     Stream to write the WindowContainer object to.
      * @param trim      If true, reduce the amount of data written.
      */
-    private void writeToProtoLocked(ProtoOutputStream proto, boolean trim) {
+    void writeToProtoLocked(ProtoOutputStream proto, boolean trim) {
         mPolicy.writeToProto(proto, POLICY);
         mRoot.writeToProto(proto, ROOT_WINDOW_CONTAINER, trim);
         if (mCurrentFocus != null) {
@@ -6339,6 +6356,17 @@
         mAppTransition.writeToProto(proto, APP_TRANSITION);
     }
 
+    void traceStateLocked(String where) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "traceStateLocked");
+        try {
+            mWindowTracing.traceStateLocked(where, this);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Exception while tracing state", e);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+
     private void dumpWindowsLocked(PrintWriter pw, boolean dumpAll,
             ArrayList<WindowState> windows) {
         pw.println("WINDOW MANAGER WINDOWS (dumpsys window windows)");
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
new file mode 100644
index 0000000..4b98d9d
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 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.server.wm;
+
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+/**
+ * ShellCommands for WindowManagerService.
+ *
+ * Use with {@code adb shell cmd window ...}.
+ */
+public class WindowManagerShellCommand extends ShellCommand {
+
+    private final WindowManagerService mService;
+
+    public WindowManagerShellCommand(WindowManagerService service) {
+        mService = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        switch (cmd) {
+            case "tracing":
+                return mService.mWindowTracing.onShellCommand(this, getNextArgRequired());
+            default:
+                return handleDefaultCommands(cmd);
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("Window Manager (window) commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println();
+        pw.println("  tracing (start | stop)");
+        pw.println("    start or stop window tracing");
+        pw.println();
+    }
+}
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 5266903..86397ae 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -732,7 +732,7 @@
             mSurfaceController.setLayerStackInTransaction(getLayerStack());
             mSurfaceController.setLayer(mAnimLayer);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("createSurfaceLocked");
         }
 
         mLastHidden = true;
@@ -1711,7 +1711,7 @@
             Slog.w(TAG, "Error positioning surface of " + mWin
                     + " pos=(" + left + "," + top + ")", e);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("setWallpaperOffset");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                     "<<< CLOSE TRANSACTION setWallpaperOffset");
         }
diff --git a/services/core/java/com/android/server/wm/WindowSurfaceController.java b/services/core/java/com/android/server/wm/WindowSurfaceController.java
index edd650a..a214523 100644
--- a/services/core/java/com/android/server/wm/WindowSurfaceController.java
+++ b/services/core/java/com/android/server/wm/WindowSurfaceController.java
@@ -259,7 +259,7 @@
                     mSurfaceControl.setLayer(layer);
                 }
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("setLayer");
             }
         }
     }
@@ -385,7 +385,7 @@
         try {
             mSurfaceControl.setTransparentRegionHint(region);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("setTransparentRegion");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                     "<<< CLOSE TRANSACTION setTransparentRegion");
         }
@@ -403,7 +403,7 @@
         try {
             mSurfaceControl.setOpaque(isOpaque);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("setOpaqueLocked");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION setOpaqueLocked");
         }
     }
@@ -420,7 +420,7 @@
         try {
             mSurfaceControl.setSecure(isSecure);
         } finally {
-            mService.closeSurfaceTransaction();
+            mService.closeSurfaceTransaction("setSecure");
             if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION setSecureLocked");
         }
     }
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index d57fdd2..cd5e475 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -429,7 +429,7 @@
             try {
                 mService.mAnimator.orAnimating(appAnimator.showAllWindowsLocked());
             } finally {
-                mService.closeSurfaceTransaction();
+                mService.closeSurfaceTransaction("handleAppTransitionReadyLocked");
                 if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                         "<<< CLOSE TRANSACTION handleAppTransitionReadyLocked()");
             }
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
new file mode 100644
index 0000000..5657f6c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 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.server.wm;
+
+import static com.android.server.wm.proto.WindowManagerTraceFileProto.ENTRY;
+import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER;
+import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.proto.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS;
+import static com.android.server.wm.proto.WindowManagerTraceProto.WHERE;
+import static com.android.server.wm.proto.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE;
+
+import android.content.Context;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * A class that allows window manager to dump its state continuously to a trace file, such that a
+ * time series of window manager state can be analyzed after the fact.
+ */
+class WindowTracing {
+
+    private static final String TAG = "WindowTracing";
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final Object mLock = new Object();
+    private final File mTraceFile;
+    private final BlockingQueue<ProtoOutputStream> mWriteQueue = new ArrayBlockingQueue<>(200);
+
+    private boolean mEnabled;
+    private volatile boolean mEnabledLockFree;
+
+    WindowTracing(File file) {
+        mTraceFile = file;
+    }
+
+    void startTrace(PrintWriter pw) throws IOException {
+        synchronized (mLock) {
+            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
+            mWriteQueue.clear();
+            mTraceFile.delete();
+            try (OutputStream os = new FileOutputStream(mTraceFile)) {
+                ProtoOutputStream proto = new ProtoOutputStream(os);
+                proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+                proto.flush();
+            }
+            mEnabled = mEnabledLockFree = true;
+        }
+    }
+
+    private void logAndPrintln(PrintWriter pw, String msg) {
+        Log.i(TAG, msg);
+        pw.println(msg);
+        pw.flush();
+    }
+
+    void stopTrace(PrintWriter pw) {
+        synchronized (mLock) {
+            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
+            mEnabled = mEnabledLockFree = false;
+            while (!mWriteQueue.isEmpty()) {
+                if (mEnabled) {
+                    logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
+                    throw new IllegalStateException("tracing enabled while waiting for flush.");
+                }
+                try {
+                    mLock.wait();
+                    mLock.notify();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
+        }
+    }
+
+    void appendTraceEntry(ProtoOutputStream proto) {
+        if (!mEnabledLockFree) {
+            return;
+        }
+
+        if (!mWriteQueue.offer(proto)) {
+            Log.e(TAG, "Dropping window trace entry, queue full");
+        }
+    }
+
+    void loop() {
+        for (;;) {
+            loopOnce();
+        }
+    }
+
+    @VisibleForTesting
+    void loopOnce() {
+        ProtoOutputStream proto;
+        try {
+            proto = mWriteQueue.take();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+        }
+
+        synchronized (mLock) {
+            try {
+                Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile");
+                try (OutputStream os = new FileOutputStream(mTraceFile, true /* append */)) {
+                    os.write(proto.getBytes());
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to write file " + mTraceFile, e);
+            } finally {
+                Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+            }
+            mLock.notify();
+        }
+    }
+
+    boolean isEnabled() {
+        return mEnabledLockFree;
+    }
+
+    static WindowTracing createDefaultAndStartLooper(Context context) {
+        File file = new File("/data/system/window_trace.proto");
+        WindowTracing windowTracing = new WindowTracing(file);
+        new Thread(windowTracing::loop, "window_tracing").start();
+        return windowTracing;
+    }
+
+    int onShellCommand(ShellCommand shell, String cmd) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        try {
+            switch (cmd) {
+                case "start":
+                    startTrace(pw);
+                    return 0;
+                case "stop":
+                    stopTrace(pw);
+                    return 0;
+                default:
+                    pw.println("Unknown command: " + cmd);
+                    return -1;
+            }
+        } catch (IOException e) {
+            logAndPrintln(pw, e.toString());
+            throw new RuntimeException(e);
+        }
+    }
+
+    void traceStateLocked(String where, WindowManagerService service) {
+        if (!isEnabled()) {
+            return;
+        }
+        ProtoOutputStream os = new ProtoOutputStream();
+        long tokenOuter = os.start(ENTRY);
+        os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
+        os.write(WHERE, where);
+
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToProtoLocked");
+        try {
+            long tokenInner = os.start(WINDOW_MANAGER_SERVICE);
+            service.writeToProtoLocked(os, true /* trim */);
+            os.end(tokenInner);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+        os.end(tokenOuter);
+        appendTraceEntry(os);
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
new file mode 100644
index 0000000..ad9aea7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTracingTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 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.server.wm;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.wm.proto.WindowManagerTraceProto;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Test class for {@link WindowTracing}.
+ *
+ * Build/Install/Run:
+ *  bit FrameworksServicesTests:com.android.server.wm.WindowTracingTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WindowTracingTest extends WindowTestsBase {
+
+    private static final byte[] MAGIC_HEADER = new byte[] {
+        0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
+    };
+
+    private Context mTestContext;
+    private WindowTracing mWindowTracing;
+    private WindowManagerService mWmMock;
+    private File mFile;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mWmMock = mock(WindowManagerService.class);
+
+        mTestContext = InstrumentationRegistry.getContext();
+
+        mFile = mTestContext.getFileStreamPath("tracing_test.dat");
+        mFile.delete();
+
+        mWindowTracing = new WindowTracing(mFile);
+    }
+
+    @Test
+    public void isEnabled_returnsFalseByDefault() throws Exception {
+        assertFalse(mWindowTracing.isEnabled());
+    }
+
+    @Test
+    public void isEnabled_returnsTrueAfterStart() throws Exception {
+        mWindowTracing.startTrace(mock(PrintWriter.class));
+        assertTrue(mWindowTracing.isEnabled());
+    }
+
+    @Test
+    public void isEnabled_returnsFalseAfterStop() throws Exception {
+        mWindowTracing.startTrace(mock(PrintWriter.class));
+        mWindowTracing.stopTrace(mock(PrintWriter.class));
+        assertFalse(mWindowTracing.isEnabled());
+    }
+
+    @Test
+    public void trace_discared_whenNotTracing() throws Exception {
+        mWindowTracing.traceStateLocked("where", mWmMock);
+        verifyZeroInteractions(mWmMock);
+    }
+
+    @Test
+    public void trace_dumpsWindowManagerState_whenTracing() throws Exception {
+        mWindowTracing.startTrace(mock(PrintWriter.class));
+        mWindowTracing.traceStateLocked("where", mWmMock);
+
+        verify(mWmMock).writeToProtoLocked(any(), eq(true));
+    }
+
+    @Test
+    public void traceFile_startsWithMagicHeader() throws Exception {
+        mWindowTracing.startTrace(mock(PrintWriter.class));
+        mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+        byte[] header = new byte[MAGIC_HEADER.length];
+        try (InputStream is = new FileInputStream(mFile)) {
+            assertEquals(MAGIC_HEADER.length, is.read(header));
+            assertArrayEquals(MAGIC_HEADER, header);
+        }
+    }
+
+    @Test
+    @Ignore("Figure out why this test is crashing when setting up mWmMock.")
+    public void tracing_endsUpInFile() throws Exception {
+        mWindowTracing.startTrace(mock(PrintWriter.class));
+
+        doAnswer((inv) -> {
+            inv.<ProtoOutputStream>getArgument(0).write(
+                    WindowManagerTraceProto.WHERE, "TEST_WM_PROTO");
+            return null;
+        }).when(mWmMock).writeToProtoLocked(any(), any());
+        mWindowTracing.traceStateLocked("TEST_WHERE", mWmMock);
+
+        mWindowTracing.stopTrace(mock(PrintWriter.class));
+
+        byte[] file = new byte[1000];
+        int fileLength;
+        try (InputStream is = new FileInputStream(mFile)) {
+            fileLength = is.read(file);
+            assertTrue(containsBytes(file, fileLength,
+                    "TEST_WHERE".getBytes(StandardCharsets.UTF_8)));
+            assertTrue(containsBytes(file, fileLength,
+                    "TEST_WM_PROTO".getBytes(StandardCharsets.UTF_8)));
+        }
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+
+        mFile.delete();
+    }
+
+    /** Return true if {@code needle} appears anywhere in {@code haystack[0..length]} */
+    boolean containsBytes(byte[] haystack, int haystackLenght, byte[] needle) {
+        Preconditions.checkArgument(haystackLenght > 0);
+        Preconditions.checkArgument(needle.length > 0);
+
+        outer: for (int i = 0; i <= haystackLenght - needle.length; i++) {
+            for (int j = 0; j < needle.length; j++) {
+                if (haystack[i+j] != needle[j]) {
+                    continue outer;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Test
+    public void test_containsBytes() {
+        byte[] haystack = "hello_world".getBytes(StandardCharsets.UTF_8);
+        assertTrue(containsBytes(haystack, haystack.length,
+                "hello".getBytes(StandardCharsets.UTF_8)));
+        assertTrue(containsBytes(haystack, haystack.length,
+                "world".getBytes(StandardCharsets.UTF_8)));
+        assertFalse(containsBytes(haystack, 6,
+                "world".getBytes(StandardCharsets.UTF_8)));
+        assertFalse(containsBytes(haystack, haystack.length,
+                "world_".getBytes(StandardCharsets.UTF_8)));
+        assertFalse(containsBytes(haystack, haystack.length,
+                "absent".getBytes(StandardCharsets.UTF_8)));
+    }
+}