/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.os;

import static android.internal.perfetto.protos.AndroidTrackEventOuterClass.AndroidMessageQueue.MESSAGE_CODE;
import static android.internal.perfetto.protos.AndroidTrackEventOuterClass.AndroidMessageQueue.MESSAGE_DELAY_MS;
import static android.internal.perfetto.protos.AndroidTrackEventOuterClass.AndroidMessageQueue.RECEIVING_THREAD_NAME;
import static android.internal.perfetto.protos.AndroidTrackEventOuterClass.AndroidMessageQueue.SENDING_THREAD_NAME;
import static android.internal.perfetto.protos.AndroidTrackEventOuterClass.AndroidTrackEvent.MESSAGE_QUEUE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
import android.compat.annotation.UnsupportedAppUsage;
import android.ravenwood.annotation.RavenwoodRedirect;
import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;

import java.util.Objects;

/**
  * Class used to run a message loop for a thread.  Threads by default do
  * not have a message loop associated with them; to create one, call
  * {@link #prepare} in the thread that is to run the loop, and then
  * {@link #loop} to have it process messages until the loop is stopped.
  *
  * <p>Most interaction with a message loop is through the
  * {@link Handler} class.
  *
  * <p>This is a typical example of the implementation of a Looper thread,
  * using the separation of {@link #prepare} and {@link #loop} to create an
  * initial Handler to communicate with the Looper.
  *
  * <pre>
  *  class LooperThread extends Thread {
  *      public Handler mHandler;
  *
  *      public void run() {
  *          Looper.prepare();
  *
  *          mHandler = new Handler(Looper.myLooper()) {
  *              public void handleMessage(Message msg) {
  *                  // process incoming messages here
  *              }
  *          };
  *
  *          Looper.loop();
  *      }
  *  }</pre>
  */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
@RavenwoodRedirectionClass("Looper_ravenwood")
public final class Looper {
    /*
     * API Implementation Note:
     *
     * This class contains the code required to set up and manage an event loop
     * based on MessageQueue.  APIs that affect the state of the queue should be
     * defined on MessageQueue or Handler rather than on Looper itself.  For example,
     * idle handlers and sync barriers are defined on the queue whereas preparing the
     * thread, looping, and quitting are defined on the looper.
     */

    private static final String TAG = "Looper";

    private static class NoImagePreloadHolder {
        // Enable/Disable verbose logging with a system prop. e.g.
        // adb shell 'setprop log.looper.slow.verbose false && stop && start'
        private static final boolean sVerboseLogging =
                SystemProperties.getBoolean("log.looper.slow.verbose", false);
    }

    // sThreadLocal.get() will return null unless you've called prepare().
    @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    @UnsupportedAppUsage
    private static Looper sMainLooper;  // guarded by Looper.class
    private static Observer sObserver;

    @UnsupportedAppUsage
    final MessageQueue mQueue;
    final Thread mThread;
    private boolean mInLoop;

    @UnsupportedAppUsage
    private Printer mLogging;
    private long mTraceTag;

    /**
     * If set, the looper will show a warning log if a message dispatch takes longer than this.
     */
    private long mSlowDispatchThresholdMs;

    /**
     * If set, the looper will show a warning log if a message delivery (actual delivery time -
     * post time) takes longer than this.
     */
    private long mSlowDeliveryThresholdMs;

    /**
     * True if a message delivery takes longer than {@link #mSlowDeliveryThresholdMs}.
     */
    private boolean mSlowDeliveryDetected;

    /**
     * Whether Looper clears Thread.interrupted() between tasks.
     *
     * When enabled, tasks don't propagate and pollute each other's interrupted state.
     * When disabled, the backwards-compatible behavior is kept,
     * preserving the legacy behavior and any associated app bugs.
     *
     * @hide
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.CINNAMON_BUN)
    @Overridable // Can be overridden on user builds
    public static final long LOOPER_CLEARS_THREAD_INTERRUPTED = 458413887L;

    /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. See also: {@link #prepare()}
     *
     * @deprecated The main looper for your application is created by the Android environment,
     *   so you should never need to call this function yourself.
     */
    @Deprecated
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

    /**
     * Force the application's main looper to the given value.  The main looper is typically
     * configured automatically by the OS, so this capability is only intended to enable testing.
     *
     * @hide
     */
    public static void setMainLooperForTest(@NonNull Looper looper) {
        synchronized (Looper.class) {
            sMainLooper = Objects.requireNonNull(looper);
        }
    }

    /**
     * Clear the application's main looper to be undefined.  The main looper is typically
     * configured automatically by the OS, so this capability is only intended to enable testing.
     *
     * @hide
     */
    public static void clearMainLooperForTest() {
        synchronized (Looper.class) {
            sMainLooper = null;
        }
    }

    /**
     * Set the transaction observer for all Loopers in this process.
     *
     * @hide
     */
    public static void setObserver(@Nullable Observer observer) {
        sObserver = observer;
    }

    // On Ravenwood, compat-IDs may not be initialized when it's called,
    // so we have a separate check in Looper_ravenwood.
    @RavenwoodRedirect(bug = 470164731)
    private static boolean isLooperClearsThreadInterruptedEnabled() {
        return CompatChanges.isChangeEnabled(LOOPER_CLEARS_THREAD_INTERRUPTED);
    }

    /**
     * Poll and deliver single message, return true if the outer loop should continue.
     */
    @SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
            "ClearIdentityCallNotFollowedByTryFinally"})
    private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }

        if (PerfettoTrace.isMQCategoryEnabled()) {
            final long messageDelayMs = Math.max(0L, msg.when - SystemClock.uptimeMillis());
            if (PerfettoTrace.IS_USE_SDK_TRACING_API_V3) {
                com.android.internal.dev.perfetto.sdk.PerfettoTrace
                        .begin(PerfettoCategories.MQ_CATEGORY, "message_queue_receive")
                        .beginProto()
                        .beginNested(MESSAGE_QUEUE)
                        .addField(SENDING_THREAD_NAME, msg.sendingThreadName)
                        .addField(RECEIVING_THREAD_NAME,
                                    Thread.currentThread().getName())
                        .addField(MESSAGE_CODE, msg.what)
                        .addField(MESSAGE_DELAY_MS, messageDelayMs)
                        .endNested()
                        .endProto()
                        .setTerminatingFlow(msg.eventId)
                        .emit();
            }
        }

        // This must be in a local variabe, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
        }
        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;

        final boolean hasOverride = thresholdOverride >= 0;
        if (hasOverride) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride)
                && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride);

        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);
            if (isLooperClearsThreadInterruptedEnabled()) {
                // Clear the interrupted state of the thread after dispatching the message.
                // This ensures that a new message dispatch starts with a clean interrupted state.
                Thread.interrupted();
            }
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            boolean slow = false;

            if (!me.mSlowDeliveryDetected || NoImagePreloadHolder.sVerboseLogging) {
                slow = showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart,
                        "delivery", msg);
            }
            if (me.mSlowDeliveryDetected) {
                if (!slow && (dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    me.mSlowDeliveryDetected = false;
                }
            } else if (slow) {
                // A slow delivery is detected, suppressing further logs unless verbose logging
                // is enabled.
                me.mSlowDeliveryDetected = true;
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }
        if (PerfettoTrace.isMQCategoryEnabled()) {
            if (PerfettoTrace.IS_USE_SDK_TRACING_API_V3) {
                com.android.internal.dev.perfetto.sdk.PerfettoTrace
                        .end(PerfettoCategories.MQ_CATEGORY)
                        .emit();
            }
        }

        msg.recycleUnchecked();

        return true;
    }

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    @SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
            "ClearIdentityCallNotFollowedByTryFinally",
            "ResultOfClearIdentityCallNotStoredInVariable"})
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }

        me.mInLoop = true;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride = getThresholdOverride();

        me.mSlowDeliveryDetected = false;

        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

    @android.ravenwood.annotation.RavenwoodRedirect
    private static int getThresholdOverride() {
        // Allow overriding the threshold for all processes' main looper with a system prop.
        // e.g. adb shell 'setprop log.looper.any.main.slow 1 && stop && start'
        if (myLooper() == getMainLooper()) {
            final int globalOverride = SystemProperties.getInt("log.looper.any.main.slow", -1);
            if (globalOverride >= 0) {
                return globalOverride;
            }
        }

        // Allow overriding the threshold for all threads within a process with a system prop.
        // e.g. adb shell 'setprop log.looper.1000.any.slow 1 && stop && start'
        final int processOverride = SystemProperties.getInt("log.looper."
                + Process.myUid() + ".any.slow", -1);
        if (processOverride >= 0) {
            return processOverride;
        }

        return SystemProperties.getInt("log.looper."
                + Process.myUid() + "."
                + Thread.currentThread().getName()
                + ".slow", -1);
    }

    private static int getThreadGroup() {
        int threadGroup = Process.THREAD_GROUP_DEFAULT;

        if (!Process.isIsolated()) {
            threadGroup = Process.getProcessGroup(Process.myTid());
        }
        return threadGroup;
    }

    private static String threadGroupToString(int threadGroup) {
        switch (threadGroup) {
            case Process.THREAD_GROUP_BACKGROUND:
                return "BACKGROUND";
            case Process.THREAD_GROUP_FOREGROUND:
                return "FOREGROUND";
            case Process.THREAD_GROUP_SYSTEM:
                return "SYSTEM";
            case Process.THREAD_GROUP_AUDIO_APP:
                return "AUDIO_APP";
            case Process.THREAD_GROUP_AUDIO_SYS:
                return "AUDIO_SYS";
            case Process.THREAD_GROUP_TOP_APP:
                return "TOP_APP";
            case Process.THREAD_GROUP_RT_APP:
                return "RT_APP";
            case Process.THREAD_GROUP_RESTRICTED:
                return "RESTRICTED";
            default:
                return "UNKNOWN";
        }
    }

    private static boolean showSlowLog(long threshold, long measureStart, long measureEnd,
            String what, Message msg) {
        final long actualTime = measureEnd - measureStart;
        if (actualTime < threshold) {
            return false;
        }

        String name = Process.myProcessName();
        String threadGroup = threadGroupToString(getThreadGroup());
        boolean isMain = myLooper() == getMainLooper();

        // For slow delivery, the current message isn't really important, but log it anyway.
        Slog.w(TAG, "Slow " + what + " took " + actualTime + "ms "
                + Thread.currentThread().getName() + " app=" + name
                + " main=" + isMain + " group=" + threadGroup
                + " h=" + msg.target.getClass().getName() + " c=" + msg.callback
                + " m=" + msg.what);
        return true;
    }

    /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

    /**
     * Return the {@link MessageQueue} object associated with the current
     * thread.  This must be called from a thread running a Looper, or a
     * NullPointerException will be thrown.
     */
    public static @NonNull MessageQueue myQueue() {
        return myLooper().mQueue;
    }

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

    /**
     * Returns true if the current thread is this looper's thread.
     */
    public boolean isCurrentThread() {
        return Thread.currentThread() == mThread;
    }

    /**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled, a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch, identifying the
     * target Handler and message contents.
     *
     * Message logging introduces a performance penalty, and is disabled by
     * default.
     *
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     */
    public void setMessageLogging(@Nullable Printer printer) {
        if (printer != null) {
            StrictMode.noteSlowCall("setMessageLogging");
        }
        mLogging = printer;
    }

    /** @hide */
    @UnsupportedAppUsage
    public void setTraceTag(long traceTag) {
        mTraceTag = traceTag;
    }

    /**
     * Set a thresholds for slow dispatch/delivery log.
     * @hide
     */
    public void setSlowLogThresholdMs(long slowDispatchThresholdMs, long slowDeliveryThresholdMs) {
        mSlowDispatchThresholdMs = slowDispatchThresholdMs;
        mSlowDeliveryThresholdMs = slowDeliveryThresholdMs;
    }

    /**
     * Quits the looper.
     * <p>
     * Causes the {@link #loop} method to terminate without processing any
     * more messages in the message queue.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p>
     * If {@link #quit} or {@link #quitSafely} is called multiple times, the first call
     * will have an effect and the subsequent calls will be no-ops.
     * </p><p class="note">
     * Using this method may be unsafe because some messages may not be delivered
     * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
     * that all pending work is completed in an orderly manner.
     * </p>
     *
     * @see #quitSafely
     */
    public void quit() {
        mQueue.quit(false);
    }

    /**
     * Quits the looper safely.
     * <p>
     * Causes the {@link #loop} method to terminate as soon as all remaining messages
     * in the message queue that are already due to be delivered have been handled.
     * However pending delayed messages with due times in the future will not be
     * delivered before the loop terminates.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p>
     * If {@link #quit} or {@link #quitSafely} is called multiple times, the first call
     * will have an effect and the subsequent calls will be no-ops.
     * </p>
     */
    public void quitSafely() {
        mQueue.quit(true);
    }

    /**
     * Gets the Thread associated with this Looper.
     *
     * @return The looper's thread.
     */
    public @NonNull Thread getThread() {
        return mThread;
    }

    /**
     * Gets this looper's message queue.
     *
     * @return The looper's message queue.
     */
    public @NonNull MessageQueue getQueue() {
        return mQueue;
    }

    /**
     * Dumps the state of the looper for debugging purposes.
     *
     * @param pw A printer to receive the contents of the dump.
     * @param prefix A prefix to prepend to each line which is printed.
     */
    public void dump(@NonNull Printer pw, @NonNull String prefix) {
        pw.println(prefix + toString());
        mQueue.dump(pw, prefix + "  ", null);
    }

    /**
     * Dumps the state of the looper for debugging purposes.
     *
     * @param pw A printer to receive the contents of the dump.
     * @param prefix A prefix to prepend to each line which is printed.
     * @param handler Only dump messages for this Handler.
     * @hide
     */
    public void dump(@NonNull Printer pw, @NonNull String prefix, Handler handler) {
        pw.println(prefix + toString());
        mQueue.dump(pw, prefix + "  ", handler);
    }

    /** @hide */
    public void dumpDebug(ProtoOutputStream proto, long fieldId) {
        final long looperToken = proto.start(fieldId);
        proto.write(LooperProto.THREAD_NAME, mThread.getName());
        proto.write(LooperProto.THREAD_ID, mThread.getId());
        if (mQueue != null) {
            mQueue.dumpDebug(proto, LooperProto.QUEUE);
        }
        proto.end(looperToken);
    }

    @Override
    public String toString() {
        return "Looper (" + mThread.getName() + ", tid " + mThread.getId()
                + ") {" + Integer.toHexString(System.identityHashCode(this)) + "}";
    }

    /** @hide */
    public interface Observer {
        /**
         * Called right before a message is dispatched.
         *
         * <p> The token type is not specified to allow the implementation to specify its own type.
         *
         * @return a token used for collecting telemetry when dispatching a single message.
         *         The token token must be passed back exactly once to either
         *         {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}
         *         and must not be reused again.
         *
         */
        Object messageDispatchStarting();

        /**
         * Called when a message was processed by a Handler.
         *
         * @param token Token obtained by previously calling
         *              {@link Observer#messageDispatchStarting} on the same Observer instance.
         * @param msg The message that was dispatched.
         */
        void messageDispatched(Object token, Message msg);

        /**
         * Called when an exception was thrown while processing a message.
         *
         * @param token Token obtained by previously calling
         *              {@link Observer#messageDispatchStarting} on the same Observer instance.
         * @param msg The message that was dispatched and caused an exception.
         * @param exception The exception that was thrown.
         */
        void dispatchingThrewException(Object token, Message msg, Exception exception);
    }
}
