/*
 * Copyright (C) 2019 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.power;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;

import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Simple Log for wake lock events. Optimized to reduce memory usage.
 *
 * The wake lock events are ultimately saved in-memory in a pre-allocated byte-based ring-buffer.
 *
 * Most of the work of this log happens in the {@link BackgroundThread}.
 *
 * The main log is basically just a sequence of the two wake lock events (ACQUIRE and RELEASE).
 * Each entry in the log stores the following data:
 *  {
 *    event type (RELEASE | ACQUIRE),
 *    time (64-bit from System.currentTimeMillis()),
 *    wake-lock ID {ownerUID (int) + tag (String)},
 *    wake-lock flags
 *  }
 *
 * In order to maximize the number of entries that fit into the log, there are various efforts made
 * to compress what we store; of which two are fairly significant and contribute the most to the
 * complexity of this code:
 * A) Relative Time
 *     - Time in each log entry is stored as an 8-bit value and is relative to the time of the
 *       previous event. When relative time is too large for 8-bits, we add a third type of event
 *       called TIME_RESET, which is used to add a new 64-bit reference-time event to the log.
 *       In practice, TIME_RESETs seem to make up about 10% or less of the total events depending
 *       on the device usage.
 * B) Wake-lock tag/ID as indexes
 *     - Wake locks are often reused many times. To avoid storing large strings in the ring buffer,
 *       we maintain a {@link TagDatabase} that associates each wakelock tag with an 7-bit index.
 *       The main log stores only these 7-bit indexes instead of whole strings.
 *
 * To make the code a bit more organized, there exists a class {@link EntryByteTranslator} which
 * uses the tag database, and reference-times to convert between a {@link LogEntry} and the
 * byte sequence that is ultimately stored in the main log, {@link TheLog}.
 */
final class WakeLockLog {
    private static final String TAG = "PowerManagerService.WLLog";

    private static final boolean DEBUG = false;

    private static final int MSG_ON_WAKE_LOCK_EVENT = 1;

    private static final int TYPE_TIME_RESET = 0x0;
    private static final int TYPE_ACQUIRE = 0x1;
    private static final int TYPE_RELEASE = 0x2;
    private static final int MAX_LOG_ENTRY_BYTE_SIZE = 9;
    private static final int LOG_SIZE = 1024 * 10;
    private static final int LOG_SIZE_MIN = MAX_LOG_ENTRY_BYTE_SIZE + 1;

    private static final int TAG_DATABASE_SIZE = 128;
    private static final int TAG_DATABASE_SIZE_MAX = 128;

    private static final int LEVEL_UNKNOWN = 0;
    private static final int LEVEL_PARTIAL_WAKE_LOCK = 1;
    private static final int LEVEL_FULL_WAKE_LOCK = 2;
    private static final int LEVEL_SCREEN_DIM_WAKE_LOCK = 3;
    private static final int LEVEL_SCREEN_BRIGHT_WAKE_LOCK = 4;
    private static final int LEVEL_PROXIMITY_SCREEN_OFF_WAKE_LOCK = 5;
    private static final int LEVEL_DOZE_WAKE_LOCK = 6;
    private static final int LEVEL_DRAW_WAKE_LOCK = 7;

    private static final String[] LEVEL_TO_STRING = {
        "unknown",
        "partial",
        "full",
        "screen-dim",
        "screen-bright",
        "prox",
        "doze",
        "draw"
    };

    /**
     * Flags use the same bit field as the level, so must start at the next available bit
     * after the largest level.
     */
    private static final int FLAG_ON_AFTER_RELEASE = 0x8;
    private static final int FLAG_ACQUIRE_CAUSES_WAKEUP = 0x10;

    private static final int MASK_LOWER_6_BITS = 0x3F;
    private static final int MASK_LOWER_7_BITS = 0x7F;

    private static final String[] REDUCED_TAG_PREFIXES =
            {"*job*/", "*gms_scheduler*/", "IntentOp:"};

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");

    /**
     * Lock protects WakeLockLock.dump (binder thread) from conflicting with changes to the log
     * happening on the background thread.
     */
    private final Object mLock = new Object();

    private final Injector mInjector;
    private final TheLog mLog;
    private final TagDatabase mTagDatabase;
    private final Handler mHandler;
    private final SimpleDateFormat mDumpsysDateFormat;

    WakeLockLog() {
        this(new Injector());
    }

    @VisibleForTesting
    WakeLockLog(Injector injector) {
        mInjector = injector;
        mHandler = new WakeLockLogHandler(injector.getLooper());
        mTagDatabase = new TagDatabase(injector);
        EntryByteTranslator translator = new EntryByteTranslator(mTagDatabase);
        mLog = new TheLog(injector, translator, mTagDatabase);
        mDumpsysDateFormat = injector.getDateFormat();
    }

    /**
     * Receives notifications of an ACQUIRE wake lock event from PowerManager.
     *
     * @param tag The wake lock tag
     * @param ownerUid The owner UID of the wake lock.
     * @param flags Flags used for the wake lock.
     */
    public void onWakeLockAcquired(String tag, int ownerUid, int flags) {
        onWakeLockEvent(TYPE_ACQUIRE, tag, ownerUid, flags);
    }

    /**
     * Receives notifications of a RELEASE wake lock event from PowerManager.
     *
     * @param tag The wake lock tag
     * @param ownerUid The owner UID of the wake lock.
     */
    public void onWakeLockReleased(String tag, int ownerUid) {
        onWakeLockEvent(TYPE_RELEASE, tag, ownerUid, 0 /* flags */);
    }

    /**
     * Dumps all the wake lock data currently saved in the wake lock log to the specified
     * {@code PrintWriter}.
     *
     * @param The {@code PrintWriter} to write to.
     */
    public void dump(PrintWriter pw) {
        dump(pw, false);
    }

    @VisibleForTesting
    void dump(PrintWriter pw, boolean includeTagDb) {
        try {
            synchronized (mLock) {
                pw.println("Wake Lock Log");
                LogEntry tempEntry = new LogEntry();  // Temporary entry for the iterator to reuse.
                final Iterator<LogEntry> iterator = mLog.getAllItems(tempEntry);
                int numEvents = 0;
                int numResets = 0;
                while (iterator.hasNext()) {
                    String address = null;
                    if (DEBUG) {
                        // Gets the byte index in the log for the current entry.
                        address = iterator.toString();
                    }
                    LogEntry entry = iterator.next();
                    if (entry != null) {
                        if (entry.type == TYPE_TIME_RESET) {
                            numResets++;
                        } else {
                            numEvents++;
                            if (DEBUG) {
                                pw.print(address);
                            }
                            entry.dump(pw, mDumpsysDateFormat);
                        }
                    }
                }
                pw.println("  -");
                pw.println("  Events: " + numEvents + ", Time-Resets: " + numResets);
                pw.println("  Buffer, Bytes used: " + mLog.getUsedBufferSize());
                if (DEBUG || includeTagDb) {
                    pw.println("  " + mTagDatabase);
                }
            }
        } catch (Exception e) {
            pw.println("Exception dumping wake-lock log: " + e.toString());
        }
    }

    /**
     * Adds a new entry to the log based on the specified wake lock parameters.
     *
     * Grabs the current time for the event and then posts the rest of the logic (actually
     * adding it to the log) to a background thread.
     *
     * @param eventType The type of event (ACQUIRE, RELEASE);
     * @param tag The wake lock's identifying tag.
     * @param ownerUid The owner UID of the wake lock.
     * @param flags The flags used with the wake lock.
     */
    private void onWakeLockEvent(int eventType, String tag, int ownerUid,
            int flags) {
        if (tag == null) {
            Slog.w(TAG, "Insufficient data to log wakelock [tag: " + tag
                    + ", ownerUid: " + ownerUid
                    + ", flags: 0x" + Integer.toHexString(flags));
            return;
        }

        final long time = mInjector.currentTimeMillis();

        SomeArgs args = SomeArgs.obtain();
        args.arg1 = tagNameReducer(tag);
        args.argi1 = eventType;
        args.argi2 = ownerUid;
        args.argi3 = eventType == TYPE_ACQUIRE ? translateFlagsFromPowerManager(flags) : 0;
        args.argi4 = (int) ((time >> 32) & 0xFFFFFFFFL);
        args.argi5 = (int) (time & 0xFFFFFFFFL);
        mHandler.obtainMessage(MSG_ON_WAKE_LOCK_EVENT, args).sendToTarget();
    }

    /**
     * Handles a new wakelock event in the background thread.
     *
     * @param eventType The type of event (ACQUIRE, RELEASE)
     * @param tag The wake lock's identifying tag.
     * @param ownerUid The owner UID of the wake lock.
     * @param flags the flags used with the wake lock.
     */
    private void handleWakeLockEventInternal(int eventType, String tag, int ownerUid, int flags,
            long time) {
        synchronized (mLock) {
            final TagData tagData = mTagDatabase.findOrCreateTag(
                    tag, ownerUid, true /* shouldCreate */);
            mLog.addEntry(new LogEntry(time, eventType, tagData, flags));
        }
    }

    /**
     * Translates wake lock flags from PowerManager into a redefined set that fits
     * in the lower 6-bits of the return value. The results are an OR-ed combination of the
     * flags, {@code WakeLockLog.FLAG_*}, and a log-level, {@code WakeLockLog.LEVEL_*}.
     *
     * @param flags Wake lock flags including {@code PowerManager.*_WAKE_LOCK}
     *              {@link PowerManager.ACQUIRE_CAUSES_WAKEUP}, and
     *              {@link PowerManager.ON_AFTER_RELEASE}.
     * @return The compressed flags value.
     */
    int translateFlagsFromPowerManager(int flags) {
        int newFlags = 0;
        switch(PowerManager.WAKE_LOCK_LEVEL_MASK & flags) {
            case PowerManager.PARTIAL_WAKE_LOCK:
                newFlags = LEVEL_PARTIAL_WAKE_LOCK;
                break;
            case PowerManager.SCREEN_DIM_WAKE_LOCK:
                newFlags = LEVEL_SCREEN_DIM_WAKE_LOCK;
                break;
            case PowerManager.SCREEN_BRIGHT_WAKE_LOCK:
                newFlags = LEVEL_SCREEN_BRIGHT_WAKE_LOCK;
                break;
            case PowerManager.FULL_WAKE_LOCK:
                newFlags = LEVEL_FULL_WAKE_LOCK;
                break;
            case PowerManager.DOZE_WAKE_LOCK:
                newFlags = LEVEL_DOZE_WAKE_LOCK;
                break;
            case PowerManager.DRAW_WAKE_LOCK:
                newFlags = LEVEL_DRAW_WAKE_LOCK;
                break;
            case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
                newFlags = LEVEL_PROXIMITY_SCREEN_OFF_WAKE_LOCK;
                break;
            default:
                Slog.w(TAG, "Unsupported lock level for logging, flags: " + flags);
                break;
        }
        if ((flags & PowerManager.ACQUIRE_CAUSES_WAKEUP) != 0) {
            newFlags |= FLAG_ACQUIRE_CAUSES_WAKEUP;
        }
        if ((flags & PowerManager.ON_AFTER_RELEASE) != 0) {
            newFlags |= FLAG_ON_AFTER_RELEASE;
        }
        return newFlags;
    }

    /**
     * Reduce certain wakelock tags to something smaller.
     * e.g. "*job* /com.aye.bee.cee/dee.ee.eff.Gee$Eich" -> "*job* /c.a.b.c/d.e.e.Gee$Eich"
     * This is used to save space when storing the tags in the Tag Database.
     *
     * @param tag The tag name to reduce
     * @return A reduced version of the tag name.
     */
    private String tagNameReducer(String tag) {
        if (tag == null) {
            return null;
        }

        String reduciblePrefix = null;
        for (int tp = 0; tp < REDUCED_TAG_PREFIXES.length; tp++) {
            if (tag.startsWith(REDUCED_TAG_PREFIXES[tp])) {
                reduciblePrefix = REDUCED_TAG_PREFIXES[tp];
                break;
            }
        }

        if (reduciblePrefix != null) {
            final StringBuilder sb = new StringBuilder();

            // add prefix first
            sb.append(tag.substring(0, reduciblePrefix.length()));

            // Stop looping on final marker
            final int end = Math.max(tag.lastIndexOf("/"), tag.lastIndexOf("."));
            boolean printNext = true;
            int index = sb.length();  // Start looping after the prefix
            for (; index < end; index++) {
                char c = tag.charAt(index);
                boolean isMarker = (c == '.' || c == '/');
                // We print all markers and the character that follows each marker
                if (isMarker || printNext) {
                    sb.append(c);
                }
                printNext = isMarker;
            }
            sb.append(tag.substring(index));  // append everything that is left
            return sb.toString();
        }
        return tag;
    }

    /**
     * Represents a wakelock-event entry in the log.
     * Holds all the data of a wakelock. The lifetime of this class is fairly short as the data
     * within this type is eventually written to the log as bytes and the instances discarded until
     * the log is read again which is fairly infrequent.
     *
     * At the time of this writing, this can be one of three types of entries:
     * 1) Wake lock acquire
     * 2) Wake lock release
     * 3) Time reset
     */
    static class LogEntry {
        /**
         * Type of wake lock from the {@code WakeLockLog.TYPE_*} set.
         */
        public int type;

        /**
         * Time of the wake lock entry as taken from System.currentTimeMillis().
         */
        public long time;

        /**
         * Data about the wake lock tag.
         */
        public TagData tag;

        /**
         * Flags used with the wake lock.
         */
        public int flags;

        LogEntry() {}

        LogEntry(long time, int type, TagData tag, int flags) {
            set(time, type, tag, flags);
        }

        /**
         * Sets the values of the log entry.
         * This is exposed to ease the reuse of {@code LogEntry} instances.
         *
         * @param time Time of the entry.
         * @param type Type of entry.
         * @param tag Tag data of the wake lock.
         * @param flags Flags used with the wake lock.
         */
        public void set(long time, int type, TagData tag, int flags) {
            this.time = time;
            this.type = type;
            this.tag = tag;
            this.flags = flags;
        }

        /**
         * Dumps this entry to the specified {@link PrintWriter}.
         *
         * @param pw The print-writer to dump to.
         * @param dateFormat The date format to use for outputing times.
         */
        public void dump(PrintWriter pw, SimpleDateFormat dateFormat) {
            pw.println("  " + toStringInternal(dateFormat));
        }

        /**
         * Converts the entry to a string.
         * date - ownerUid - (ACQ|REL) tag [(flags)]
         * e.g., 1999-01-01 12:01:01.123 - 10012 - ACQ bluetooth_timer (partial)
         */
        @Override
        public String toString() {
            return toStringInternal(DATE_FORMAT);
        }

        /**
         * Converts the entry to a string.
         * date - ownerUid - (ACQ|REL) tag [(flags)]
         * e.g., 1999-01-01 12:01:01.123 - 10012 - ACQ bluetooth_timer (partial)
         *
         * @param dateFormat The date format to use for outputing times.
         * @return The string output of this class instance.
         */
        private String toStringInternal(SimpleDateFormat dateFormat) {
            StringBuilder sb = new StringBuilder();
            if (type == TYPE_TIME_RESET) {
                return dateFormat.format(new Date(time)) + " - RESET";
            }
            sb.append(dateFormat.format(new Date(time)))
                    .append(" - ")
                    .append(tag == null ? "---" : tag.ownerUid)
                    .append(" - ")
                    .append(type == TYPE_ACQUIRE ? "ACQ" : "REL")
                    .append(" ")
                    .append(tag == null ? "UNKNOWN" : tag.tag);
            if (type == TYPE_ACQUIRE) {
                sb.append(" (");
                flagsToString(sb);
                sb.append(")");
            }
            return sb.toString();
        }

        private void flagsToString(StringBuilder sb) {
            sb.append(LEVEL_TO_STRING[flags & 0x7]);
            if ((flags & FLAG_ON_AFTER_RELEASE) == FLAG_ON_AFTER_RELEASE) {
                sb.append(",on-after-release");
            }
            if ((flags & FLAG_ACQUIRE_CAUSES_WAKEUP) == FLAG_ACQUIRE_CAUSES_WAKEUP) {
                sb.append(",acq-causes-wake");
            }
        }
    }

    /**
     * Converts between a {@link LogEntry} instance and a byte sequence.
     *
     * This is used to convert {@link LogEntry}s to a series of bytes before being written into
     * the log, and vice-versa when reading from the log.
     *
     * This method employs the compression techniques that are mentioned in the header of
     * {@link WakeLockLog}: Relative-time and Tag-indexing.  Please see the header for the
     * description of both.
     *
     * The specific byte formats used are explained more thoroughly in the method {@link #toBytes}.
     */
    static class EntryByteTranslator {

        // Error codes that can be returned when converting to bytes.
        static final int ERROR_TIME_IS_NEGATIVE = -1;  // Relative time is negative
        static final int ERROR_TIME_TOO_LARGE = -2;  // Relative time is out of valid range (0-255)

        private final TagDatabase mTagDatabase;

        EntryByteTranslator(TagDatabase tagDatabase) {
            mTagDatabase = tagDatabase;
        }

        /**
         * Translates the specified bytes into a LogEntry instance, if possible.
         *
         * See {@link #toBytes} for an explanation of the byte formats.
         *
         * @param bytes The bytes to read.
         * @param timeReference The reference time to use when reading the relative time from the
         *                      bytes buffer.
         * @param entryToReuse The entry instance to write to. If null, this method will create a
         *                     new instance.
         * @return The converted entry, or null if data is corrupt.
         */
        LogEntry fromBytes(byte[] bytes, long timeReference, LogEntry entryToReuse) {
            if (bytes == null || bytes.length == 0) {
                return null;
            }

            // Create an entry if non if passed in to use
            LogEntry entry = entryToReuse != null ? entryToReuse : new LogEntry();

            int type = (bytes[0] >> 6) & 0x3;
            if ((type & 0x2) == 0x2) {
                // As long as the highest order bit of the byte is set, it is a release
                type = TYPE_RELEASE;
            }
            switch (type) {
                case TYPE_ACQUIRE: {
                    if (bytes.length < 3) {
                        break;
                    }

                    int flags = bytes[0] & MASK_LOWER_6_BITS;
                    int tagIndex = bytes[1] & MASK_LOWER_7_BITS;
                    TagData tag = mTagDatabase.getTag(tagIndex);
                    long time = (bytes[2] & 0xFF) + timeReference;
                    entry.set(time, TYPE_ACQUIRE, tag, flags);
                    return entry;
                }
                case TYPE_RELEASE: {
                    if (bytes.length < 2) {
                        break;
                    }

                    int flags = 0;
                    int tagIndex = bytes[0] & MASK_LOWER_7_BITS;
                    TagData tag = mTagDatabase.getTag(tagIndex);
                    long time = (bytes[1] & 0xFF) + timeReference;
                    entry.set(time, TYPE_RELEASE, tag, flags);
                    return entry;
                }
                case TYPE_TIME_RESET: {
                    if (bytes.length < 9) {
                        break;
                    }

                    long time = ((bytes[1] & 0xFFL) << 56)
                                | ((bytes[2] & 0xFFL) << 48)
                                | ((bytes[3] & 0xFFL) << 40)
                                | ((bytes[4] & 0xFFL) << 32)
                                | ((bytes[5] & 0xFFL) << 24)
                                | ((bytes[6] & 0xFFL) << 16)
                                | ((bytes[7] & 0xFFL) << 8)
                                | (bytes[8] & 0xFFL);
                    entry.set(time, TYPE_TIME_RESET, null, 0);
                    return entry;
                }
                default:
                    Slog.w(TAG, "Type not recognized [" + type + "]", new Exception());
                    break;
            }
            return null;
        }

        /**
         * Converts and writes the specified entry into the specified byte array.
         * If the byte array is null or too small, then the method writes nothing, but still returns
         * the number of bytes necessary to write the entry.
         *
         * Byte format used for each type:
         *
         * TYPE_RELEASE:
         *                                        bits
         *                0      1      2       3       4       5       6       7
         *   bytes  0  [  1   |            7-bit wake lock tag index                ]
         *          1  [                 8-bit relative time                        ]
         *
         *
         * TYPE_ACQUIRE:
         *                                        bits
         *                0      1      2       3       4       5       6       7
         *          0  [  0      1   |            wake lock flags                   ]
         *  bytes   1  [unused|         7-bit wake lock tag index                   ]
         *          2  [                 8-bit relative time                        ]
         *
         *
         * TYPE_TIME_RESET:
         *                                        bits
         *                0      1      2       3       4       5       6       7
         *          0  [  0      0   |            unused                            ]
         *  bytes 1-9  [                  64-bit reference-time                     ]
         *
         * @param entry The entry to convert/write
         * @param bytes The buffer to write to, or null to just return the necessary bytes.
         * @param timeReference The reference-time used to calculate relative time of the entry.
         * @return The number of bytes written to buffer, or required to write to the buffer.
         */
        int toBytes(LogEntry entry, byte[] bytes, long timeReference) {
            int sizeNeeded = -1;
            switch (entry.type) {
                case TYPE_ACQUIRE: {
                    sizeNeeded = 3;
                    if (bytes != null && bytes.length >= sizeNeeded) {
                        int relativeTime = getRelativeTime(timeReference, entry.time);
                        if (relativeTime < 0) {
                            // Negative relative time indicates error code
                            return relativeTime;
                        }
                        bytes[0] = (byte) ((TYPE_ACQUIRE << 6)
                                | (entry.flags & MASK_LOWER_6_BITS));
                        bytes[1] = (byte) mTagDatabase.getTagIndex(entry.tag);
                        bytes[2] = (byte) (relativeTime & 0xFF);  // Lower 8 bits of the time
                        if (DEBUG) {
                            Slog.d(TAG, "ACQ - Setting bytes: " + Arrays.toString(bytes));
                        }
                    }
                    break;
                }
                case TYPE_RELEASE: {
                    sizeNeeded = 2;
                    if (bytes != null && bytes.length >= sizeNeeded) {
                        int relativeTime = getRelativeTime(timeReference, entry.time);
                        if (relativeTime < 0) {
                            // Negative relative time indicates error code
                            return relativeTime;
                        }
                        bytes[0] = (byte) (0x80 | mTagDatabase.getTagIndex(entry.tag));
                        bytes[1] = (byte) (relativeTime & 0xFF);  // Lower 8 bits of the time
                        if (DEBUG) {
                            Slog.d(TAG, "REL - Setting bytes: " + Arrays.toString(bytes));
                        }
                    }
                    break;
                }
                case TYPE_TIME_RESET: {
                    sizeNeeded = 9;
                    long time = entry.time;
                    if (bytes != null && bytes.length >= sizeNeeded) {
                        bytes[0] = (TYPE_TIME_RESET << 6);
                        bytes[1] = (byte) ((time >> 56) & 0xFF);
                        bytes[2] = (byte) ((time >> 48) & 0xFF);
                        bytes[3] = (byte) ((time >> 40) & 0xFF);
                        bytes[4] = (byte) ((time >> 32) & 0xFF);
                        bytes[5] = (byte) ((time >> 24) & 0xFF);
                        bytes[6] = (byte) ((time >> 16) & 0xFF);
                        bytes[7] = (byte) ((time >> 8) & 0xFF);
                        bytes[8] = (byte) (time & 0xFF);
                    }
                    break;
                }
                default:
                    throw new RuntimeException("Unknown type " +  entry);
            }

            return sizeNeeded;
        }

        /**
         * Calculates the relative time between the specified time and timeReference.  The relative
         * time is expected to be non-negative and fit within 8-bits (values between 0-255). If the
         * relative time is outside of that range an error code will be returned instead.
         *
         * @param time
         * @param timeReference
         * @return The relative time between time and timeReference, or an error code.
         */
        private int getRelativeTime(long timeReference, long time) {
            if (time < timeReference) {
                if (DEBUG) {
                    Slog.w(TAG, "ERROR_TIME_IS_NEGATIVE");
                }
                return ERROR_TIME_IS_NEGATIVE;
            }
            long relativeTime = time - timeReference;
            if (relativeTime > 255) {
                if (DEBUG) {
                    Slog.w(TAG, "ERROR_TIME_TOO_LARGE");
                }
                return ERROR_TIME_TOO_LARGE;
            }
            return (int) relativeTime;
        }
    }

    /**
     * Main implementation of the ring buffer used to store the log entries.  This class takes
     * {@link LogEntry} instances and adds them to the ring buffer, utilizing
     * {@link EntryByteTranslator} to convert byte {@link LogEntry} to bytes within the buffer.
     *
     * This class also implements the logic around TIME_RESET events. Since the LogEntries store
     * their time (8-bit) relative to the previous event, this class can add {@link TYPE_TIME_RESET}
     * LogEntries as necessary to allow a LogEntry's relative time to fit within that range.
     */
    static class TheLog {
        private final EntryByteTranslator mTranslator;

        /**
         * Temporary buffer used when converting a new entry to bytes for writing to the buffer.
         * Allocating once allows us to avoid allocating a buffer with each write.
         */
        private final byte[] mTempBuffer = new byte[MAX_LOG_ENTRY_BYTE_SIZE];

        /**
         * Second temporary buffer used when reading and writing bytes from the buffer.
         * A second temporary buffer is necessary since additional items can be read concurrently
         * from {@link mTempBuffer}. E.g., Adding an entry to a full buffer requires removing
         * other entries from the buffer.
         */
        private final byte[] mReadWriteTempBuffer = new byte[MAX_LOG_ENTRY_BYTE_SIZE];

        /**
         * Main log buffer.
         */
        private final byte[] mBuffer;

        /**
         * Start index of the ring buffer.
         */
        private int mStart = 0;

        /**
         * Current end index of the ring buffer.
         */
        private int mEnd = 0;

        /**
         * Start time of the entries in the buffer. The first item stores an 8-bit time that is
         * relative to this value.
         */
        private long mStartTime = 0;

        /**
         * The time of the last entry in the buffer. Reading the time from the last entry to
         * calculate the relative time of a new one is sufficiently hard to prefer saving the value
         * here instead.
         */
        private long mLatestTime = 0;

        /**
         * Counter for number of changes (adds or removes) that have been done to the buffer.
         */
        private long mChangeCount = 0;

        private final TagDatabase mTagDatabase;

        TheLog(Injector injector, EntryByteTranslator translator, TagDatabase tagDatabase) {
            final int logSize = Math.max(injector.getLogSize(), LOG_SIZE_MIN);
            mBuffer = new byte[logSize];

            mTranslator = translator;
            mTagDatabase = tagDatabase;

            // Register to be notified when an older tag is removed from the TagDatabase to make
            // room for a new entry.
            mTagDatabase.setCallback(new TagDatabase.Callback() {
                @Override public void onIndexRemoved(int index) {
                    removeTagIndex(index);
                }
            });
        }

        /**
         * Returns the amount of space being used in the ring buffer (in bytes).
         *
         * @return Used buffer size in bytes.
         */
        int getUsedBufferSize() {
            return mBuffer.length - getAvailableSpace();
        }

        /**
         * Adds the specified {@link LogEntry} to the log by converting it to bytes and writing
         * those bytes to the buffer.
         *
         * This method can have side effects of removing old values from the ring buffer and
         * adding an extra TIME_RESET entry if necessary.
         */
        void addEntry(LogEntry entry) {
            if (isBufferEmpty()) {
                // First item being added, do initialization.
                mStartTime = mLatestTime = entry.time;
            }

            int size = mTranslator.toBytes(entry, mTempBuffer, mLatestTime);
            if (size == EntryByteTranslator.ERROR_TIME_IS_NEGATIVE) {
                return;  // Wholly unexpected circumstance...just break out now.
            } else if (size == EntryByteTranslator.ERROR_TIME_TOO_LARGE) {
                // The relative time between the last entry and this new one is too large
                // to fit in our byte format...we need to create a new Time-Reset event and add
                // that to the log first.
                addEntry(new LogEntry(entry.time, TYPE_TIME_RESET, null, 0));
                size = mTranslator.toBytes(entry, mTempBuffer, mLatestTime);
            }

            if (size > MAX_LOG_ENTRY_BYTE_SIZE || size <= 0) {
                Slog.w(TAG, "Log entry size is out of expected range: " + size);
                return;
            }

            // In case the buffer is full or nearly full, ensure there is a proper amount of space
            // for the new entry.
            if (!makeSpace(size)) {
                return;  // Doesn't fit
            }

            if (DEBUG) {
                Slog.d(TAG, "Wrote New Entry @(" + mEnd + ") [" + entry + "] as "
                        + Arrays.toString(mTempBuffer));
            }
            // Set the bytes and update our end index & timestamp.
            writeBytesAt(mEnd, mTempBuffer, size);
            if (DEBUG) {
                Slog.d(TAG, "Read written Entry @(" + mEnd + ") ["
                        + readEntryAt(mEnd, mLatestTime, null));
            }
            mEnd = (mEnd + size) % mBuffer.length;
            mLatestTime = entry.time;

            TagDatabase.updateTagTime(entry.tag, entry.time);
            mChangeCount++;
        }

        /**
         * Returns an {@link Iterator} of {@link LogEntry}s for all the entries in the log.
         *
         * If the log is modified while the entries are being read, the iterator will throw a
         * {@link ConcurrentModificationExceptoin}.
         *
         * @param tempEntry A temporary {@link LogEntry} instance to use so that new instances
         *                  aren't allocated with every call to {@code Iterator.next}.
         */
        Iterator<LogEntry> getAllItems(final LogEntry tempEntry) {
            return new Iterator<LogEntry>() {
                private int mCurrent = mStart;  // Current read position in the log.
                private long mCurrentTimeReference = mStartTime;  // Current time-reference to use.
                private final long mChangeValue = mChangeCount;  // Used to track if buffer changed.

                /**
                 * @return True if there are more elements to iterate through, false otherwise.\
                 * @throws ConcurrentModificationException if the buffer contents change.
                 */
                @Override
                public boolean hasNext() {
                    checkState();
                    return mCurrent != mEnd;
                }

                /**
                 * Returns the next element in the iterator.
                 *
                 * @return The next entry in the iterator
                 * @throws NoSuchElementException if iterator reaches the end.
                 * @throws ConcurrentModificationException if buffer contents change.
                 */
                @Override
                public LogEntry next() {
                    checkState();

                    if (!hasNext()) {
                        throw new NoSuchElementException("No more entries left.");
                    }

                    LogEntry entry = readEntryAt(mCurrent, mCurrentTimeReference, tempEntry);
                    int size = mTranslator.toBytes(entry, null, mStartTime);
                    mCurrent = (mCurrent + size) % mBuffer.length;
                    mCurrentTimeReference = entry.time;

                    return entry;
                }

                @Override public String toString() {
                    return "@" + mCurrent;
                }

                /**
                 * @throws ConcurrentModificationException if the underlying buffer has changed
                 * since this iterator was instantiated.
                 */
                private void checkState() {
                    if (mChangeValue != mChangeCount) {
                        throw new ConcurrentModificationException("Buffer modified, old change: "
                                + mChangeValue + ", new change: " + mChangeCount);
                    }
                }
            };
        }

        /**
         * Cleans up old tag index references from the entire log.
         * Called when an older wakelock tag is removed from the tag database. This happens
         * when the database needed additional room for newer tags.
         *
         * This is a fairly expensive operation.  Reads all the entries from the buffer, which can
         * be around 1500 for a 10Kb buffer. It will write back any entries that use the tag as
         * well, but that's not many of them. Commonly-used tags dont ever make it to this part.
         *
         * If necessary, in the future we can keep track of the number of tag-users the same way we
         * keep track of a tag's last-used-time to stop having to do this for old tags that dont
         * have entries in the logs any more. Light testing has shown that for a 10Kb
         * buffer, there are about 5 or fewer (of 1500) entries with "UNKNOWN" tag...which means
         * this operation does happen, but not very much.
         *
         * @param tagIndex The index of the tag, as stored in the log
         */
        private void removeTagIndex(int tagIndex) {
            if (isBufferEmpty()) {
                return;
            }

            int readIndex = mStart;
            long timeReference = mStartTime;
            final LogEntry reusableEntryInstance = new LogEntry();
            while (readIndex != mEnd) {
                LogEntry entry = readEntryAt(readIndex, timeReference, reusableEntryInstance);
                if (DEBUG) {
                    Slog.d(TAG, "Searching to remove tags @ " + readIndex + ": " + entry);
                }
                if (entry == null) {
                    Slog.w(TAG, "Entry is unreadable - Unexpected @ " + readIndex);
                    break;  // cannot continue if entries are now corrupt
                }
                if (entry.tag != null && entry.tag.index == tagIndex) {
                    // We found an entry that uses the tag being removed. Re-write the
                    // entry back without a tag.
                    entry.tag = null;  // remove the tag, and write it back
                    writeEntryAt(readIndex, entry, timeReference);
                    if (DEBUG) {
                        Slog.d(TAG, "Remove tag index: " + tagIndex + " @ " + readIndex);
                    }
                }
                timeReference = entry.time;
                int entryByteSize = mTranslator.toBytes(entry, null, 0L);
                readIndex = (readIndex + entryByteSize) % mBuffer.length;
            }
        }

        /**
         * Removes entries from the buffer until the specified amount of space is available for use.
         *
         * @param spaceNeeded The number of bytes needed in the buffer.
         * @return True if there is space enough in the buffer, false otherwise.
         */
        private boolean makeSpace(int spaceNeeded) {
            // Test the size of the buffer can fit it first, so that we dont loop forever in the
            // following while loop.
            if (mBuffer.length < spaceNeeded + 1) {
                return false;
            }

            // We check spaceNeeded + 1 so that mStart + mEnd aren't equal.  We use them being equal
            // to mean that the buffer is empty...so avoid that.
            while (getAvailableSpace() < (spaceNeeded + 1)) {
                removeOldestItem();
            }
            return true;
        }

        /**
         * Returns the available space of the ring buffer.
         */
        private int getAvailableSpace() {
            return mEnd > mStart ? mBuffer.length - (mEnd - mStart) :
                    (mEnd < mStart ? mStart - mEnd :
                     mBuffer.length);
        }

        /**
         * Removes the oldest item from the buffer if the buffer is not empty.
         */
        private void removeOldestItem() {
            if (isBufferEmpty()) {
                // No items to remove
                return;
            }

            // Copy the contents of the start of the buffer to our temporary buffer.
            LogEntry entry = readEntryAt(mStart, mStartTime, null);
            if (DEBUG) {
                Slog.d(TAG, "Removing oldest item at @ " + mStart + ", found: " + entry);
            }
            int size = mTranslator.toBytes(entry, null, mStartTime);
            mStart = (mStart + size) % mBuffer.length;
            mStartTime = entry.time;  // new start time
            mChangeCount++;
        }

        /**
         * Returns true if the buffer is currently unused (contains zero entries).
         *
         * @return True if empty, false otherwise.
         */
        private boolean isBufferEmpty() {
            return mStart == mEnd;
        }

        /**
         * Reads an entry from the specified index in the buffer.
         *
         * @param index Index into the buffer from which to read.
         * @param timeReference Reference time to use when creating the {@link LogEntry}.
         * @param entryToSet Temporary entry to use instead of allocating a new one.
         * @return the log-entry instance that was read.
         */
        private LogEntry readEntryAt(int index, long timeReference, LogEntry entryToSet) {
            for (int i = 0; i < MAX_LOG_ENTRY_BYTE_SIZE; i++) {
                int indexIntoMainBuffer = (index + i) % mBuffer.length;
                if (indexIntoMainBuffer == mEnd) {
                    break;
                }
                mReadWriteTempBuffer[i] = mBuffer[indexIntoMainBuffer];
            }
            return mTranslator.fromBytes(mReadWriteTempBuffer, timeReference, entryToSet);
        }

        /**
         * Write a specified {@link LogEntry} to the buffer at the specified index.
         *
         * @param index Index in which to write in the buffer.
         * @param entry The entry to write into the buffer.
         * @param timeReference The reference time to use when calculating the relative time.
         */
        private void writeEntryAt(int index, LogEntry entry, long timeReference) {
            int size = mTranslator.toBytes(entry, mReadWriteTempBuffer, timeReference);
            if (size > 0) {
                if (DEBUG) {
                    Slog.d(TAG, "Writing Entry (" + index + ") [" + entry + "] as "
                            + Arrays.toString(mReadWriteTempBuffer));
                }
                writeBytesAt(index, mReadWriteTempBuffer, size);
            }
        }

        /**
         * Write the specified bytes into the buffer at the specified index.
         * Handling wrap-around calculation for the ring-buffer.
         *
         * @param index The index from which to start writing.
         * @param buffer The buffer of bytes to be written.
         * @param size The amount of bytes to write from {@code buffer} to the log.
         */
        private void writeBytesAt(int index, byte[] buffer, int size) {
            for (int i = 0; i < size; i++) {
                int indexIntoMainBuffer = (index + i) % mBuffer.length;
                mBuffer[indexIntoMainBuffer] = buffer[i];
            }
            if (DEBUG) {
                Slog.d(TAG, "Write Byte: " + Arrays.toString(buffer));
            }
        }
    }

    /**
     * An in-memory database of wake lock {@link TagData}. All tags stored in the database are given
     * a 7-bit index. This index is then used by {@link TheLog} when translating {@link LogEntry}
     * instanced into bytes.
     *
     * If a new tag is added when the database is full, the oldest tag is removed. The oldest tag
     * is calcualted using {@link TagData.lastUsedTime}.
     */
    static class TagDatabase {
        private final int mInvalidIndex;
        private final TagData[] mArray;
        private Callback mCallback;

        TagDatabase(Injector injector) {
            int size = Math.min(injector.getTagDatabaseSize(), TAG_DATABASE_SIZE_MAX);

            // Largest possible index used as "INVALID", hence the (size - 1) sizing.
            mArray = new TagData[size - 1];
            mInvalidIndex = size - 1;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Tag Database: size(").append(mArray.length).append(")");
            int entries = 0;
            int byteEstimate = 0;
            int tagSize = 0;
            int tags = 0;
            for (int i = 0; i < mArray.length; i++) {
                byteEstimate += 8;  // reference pointer
                TagData data = mArray[i];
                if (data != null) {
                    entries++;
                    byteEstimate += data.getByteSize();
                    if (data.tag != null) {
                        tags++;
                        tagSize += data.tag.length();
                    }
                }
            }
            sb.append(", entries: ").append(entries);
            sb.append(", Bytes used: ").append(byteEstimate);
            if (DEBUG) {
                sb.append(", Avg tag size: ").append(tagSize / tags);
                sb.append("\n    ").append(Arrays.toString(mArray));
            }
            return sb.toString();
        }

        /**
         * Sets the callback.
         *
         * @param callback The callback to set.
         */
        public void setCallback(Callback callback) {
            mCallback = callback;
        }

        /**
         * Returns the tag corresponding to the specified index.
         *
         * @param index The index to search for.
         */
        public TagData getTag(int index) {
            if (index < 0 || index >= mArray.length || index == mInvalidIndex) {
                return null;
            }
            return mArray[index];
        }

        /**
         * Returns an existing tag for the specified wake lock tag + ownerUid.
         *
         * @param tag The wake lock tag.
         * @param ownerUid The wake lock's ownerUid.
         * @return the TagData instance.
         */
        public TagData getTag(String tag, int ownerUid) {
            return findOrCreateTag(tag, ownerUid, false /* shouldCreate */);
        }

        /**
         * Returns the index for the corresponding tag.
         *
         * @param tagData The tag-data to search for.
         * @return the corresponding index, or mInvalidIndex of none is found.
         */
        public int getTagIndex(TagData tagData) {
            return tagData == null ? mInvalidIndex : tagData.index;
        }

        /**
         * Returns a tag instance for the specified wake lock tag and ownerUid. If the data
         * does not exist in the database, it will be created if so specified by
         * {@code shouldCreate}.
         *
         * @param tagStr The wake lock's tag.
         * @param ownerUid The wake lock's owner Uid.
         * @param shouldCreate True when the tag should be created if it doesn't already exist.
         * @return The tag-data instance that was found or created.
         */
        public TagData findOrCreateTag(String tagStr, int ownerUid, boolean shouldCreate) {
            int firstAvailable = -1;
            TagData oldest = null;
            int oldestIndex = -1;

            // Loop through and find the tag to be used.
            TagData tag = new TagData(tagStr, ownerUid);
            for (int i = 0; i < mArray.length; i++) {
                TagData current = mArray[i];
                if (tag.equals(current)) {
                    // found it
                    return current;
                } else if (!shouldCreate) {
                    continue;
                } else if (current != null) {
                    // See if this entry is the oldest entry, in case
                    // we need to replace it.
                    if (oldest == null || current.lastUsedTime < oldest.lastUsedTime) {
                        oldestIndex = i;
                        oldest = current;
                    }
                } else if (firstAvailable == -1) {
                    firstAvailable = i;
                }
            }

            // Item not found, and we shouldn't create one.
            if (!shouldCreate) {
                return null;
            }

            // If we need to remove an index, report to listeners that we are removing an index.
            boolean useOldest = firstAvailable == -1;
            if (useOldest && mCallback != null) {
                if (DEBUG) {
                    Slog.d(TAG, "Removing tag index: " + oldestIndex + " = " + oldest);
                }
                mCallback.onIndexRemoved(oldestIndex);
            }
            setToIndex(tag, firstAvailable != -1 ? firstAvailable : oldestIndex);
            return tag;
        }

        /**
         * Updates the last-used-time of the specified tag.
         *
         * @param tag The tag to update.
         * @param time The new last-used-time for the tag.
         */
        public static void updateTagTime(TagData tag, long time) {
            if (tag != null) {
                tag.lastUsedTime = time;
            }
        }

        /**
         * Sets a specified tag to the specified index.
         */
        private void setToIndex(TagData tag, int index) {
            if (index < 0 || index >= mArray.length) {
                return;
            }
            TagData current = mArray[index];
            if (current != null) {
                // clean up the reference in the TagData instance first.
                current.index = mInvalidIndex;

                if (DEBUG) {
                    Slog.d(TAG, "Replaced tag " + current.tag + " from index " + index + " with tag"
                            + tag);
                }
            }

            mArray[index] = tag;
            tag.index = index;
        }

        /**
         * Callback on which to be notified of changes to {@link TagDatabase}.
         */
        interface Callback {

            /**
             * Handles removals of TagData indexes.
             *
             * @param index the index being removed.
             */
            void onIndexRemoved(int index);
        }
    }

    /**
     * This class represents unique wake lock tags that are stored in {@link TagDatabase}.
     * Contains both the wake lock tag data (tag + ownerUid) as well as index and last-used
     * time data as it relates to the tag-database.
     */
    static class TagData {
        public String tag;  // Wake lock tag
        public int ownerUid;  // Wake lock owner Uid
        public int index;  // Index of the tag in the tag-database
        public long lastUsedTime;  // Last time that this entry was used

        TagData(String tag, int ownerUid) {
            this.tag = tag;
            this.ownerUid = ownerUid;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o instanceof TagData) {
                TagData other = (TagData) o;
                return TextUtils.equals(tag, other.tag) && ownerUid == other.ownerUid;
            }
            return false;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            if (DEBUG) {
                sb.append("(").append(index).append(")");
            }
            return "[" + ownerUid + " ; " + tag + "]";
        }

        /**
         * Returns an estimate of the number of bytes used by each instance of this class.
         * Used for debug purposes.
         *
         * @return the size of this tag-data.
         */
        int getByteSize() {
            int bytes = 0;
            bytes += 8;  // tag reference-pointer;
            bytes += tag == null ? 0 : tag.length() * 2;
            bytes += 4;  // ownerUid
            bytes += 4;  // index
            bytes += 8;  // lastUsedTime
            return bytes;
        }
    }

    /**
     * Injector used by {@link WakeLockLog} for testing purposes.
     */
    public static class Injector {
        public Looper getLooper() {
            return BackgroundThread.get().getLooper();
        }

        public int getTagDatabaseSize() {
            return TAG_DATABASE_SIZE;
        }

        public int getLogSize() {
            return LOG_SIZE;
        }

        public long currentTimeMillis() {
            return System.currentTimeMillis();
        }

        public SimpleDateFormat getDateFormat() {
            return DATE_FORMAT;
        }
    }

    private class WakeLockLogHandler extends Handler {
        WakeLockLogHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message message) {
            switch(message.what) {
                case MSG_ON_WAKE_LOCK_EVENT:
                    final SomeArgs args = (SomeArgs) message.obj;
                    final String tag = (String) args.arg1;
                    final int eventType = args.argi1;
                    final int ownerUid = args.argi2;
                    final int flags = args.argi3;
                    final long time = (((long) args.argi4) << 32) + (args.argi5 & 0xFFFFFFFFL);
                    args.recycle();
                    handleWakeLockEventInternal(eventType, tag, ownerUid, flags, time);
                    break;
                default:
                    break;
            }
        }
    }
}
