/*
 * Copyright (C) 2021 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.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.function.IntPredicate;

/**
 * A per-process controller to decide whether the process can start activity or foreground service
 * (especially from background). All methods of this class must be thread safe. The caller does not
 * need to hold WM lock, e.g. lock contention of WM lock shouldn't happen when starting service.
 */
class BackgroundLaunchProcessController {
    private static final String TAG =
            TAG_WITH_CLASS_NAME ? "BackgroundLaunchProcessController" : TAG_ATM;

    /** It is {@link ActivityTaskManagerService#hasActiveVisibleWindow(int)}. */
    private final IntPredicate mUidHasActiveVisibleWindowPredicate;

    private final @Nullable BackgroundActivityStartCallback mBackgroundActivityStartCallback;

    /**
     * A set of tokens that currently contribute to this process being temporarily allowed
     * to start activities even if it's not in the foreground. The values of this map are optional
     * (can be null) and are used to trace back the grant to the notification token mechanism.
     */
    @GuardedBy("this")
    private @Nullable ArrayMap<Binder, IBinder> mBackgroundActivityStartTokens;

    /** Set of UIDs of clients currently bound to this process. */
    @GuardedBy("this")
    private @Nullable IntArray mBoundClientUids;

    BackgroundLaunchProcessController(@NonNull IntPredicate uidHasActiveVisibleWindowPredicate,
            @Nullable BackgroundActivityStartCallback callback) {
        mUidHasActiveVisibleWindowPredicate = uidHasActiveVisibleWindowPredicate;
        mBackgroundActivityStartCallback = callback;
    }

    boolean areBackgroundActivityStartsAllowed(int pid, int uid, String packageName,
            int appSwitchState, boolean isCheckingForFgsStart,
            boolean hasActivityInVisibleTask, boolean hasBackgroundActivityStartPrivileges,
            long lastStopAppSwitchesTime, long lastActivityLaunchTime,
            long lastActivityFinishTime) {
        // If app switching is not allowed, we ignore all the start activity grace period
        // exception so apps cannot start itself in onPause() after pressing home button.
        if (appSwitchState == APP_SWITCH_ALLOW) {
            // Allow if any activity in the caller has either started or finished very recently, and
            // it must be started or finished after last stop app switches time.
            final long now = SystemClock.uptimeMillis();
            if (now - lastActivityLaunchTime < ACTIVITY_BG_START_GRACE_PERIOD_MS
                    || now - lastActivityFinishTime < ACTIVITY_BG_START_GRACE_PERIOD_MS) {
                // If activity is started and finished before stop app switch time, we should not
                // let app to be able to start background activity even it's in grace period.
                if (lastActivityLaunchTime > lastStopAppSwitchesTime
                        || lastActivityFinishTime > lastStopAppSwitchesTime) {
                    if (DEBUG_ACTIVITY_STARTS) {
                        Slog.d(TAG, "[Process(" + pid
                                + ")] Activity start allowed: within "
                                + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period");
                    }
                    return true;
                }
                if (DEBUG_ACTIVITY_STARTS) {
                    Slog.d(TAG, "[Process(" + pid + ")] Activity start within "
                            + ACTIVITY_BG_START_GRACE_PERIOD_MS
                            + "ms grace period but also within stop app switch window");
                }

            }
        }
        // Allow if the proc is instrumenting with background activity starts privs.
        if (hasBackgroundActivityStartPrivileges) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[Process(" + pid
                        + ")] Activity start allowed: process instrumenting with background "
                        + "activity starts privileges");
            }
            return true;
        }
        // Allow if the caller has an activity in any foreground task.
        if (hasActivityInVisibleTask
                && (appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY)) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[Process(" + pid
                        + ")] Activity start allowed: process has activity in foreground task");
            }
            return true;
        }
        // Allow if the caller is bound by a UID that's currently foreground.
        if (isBoundByForegroundUid()) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[Process(" + pid
                        + ")] Activity start allowed: process bound by foreground uid");
            }
            return true;
        }
        // Allow if the flag was explicitly set.
        if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
            if (DEBUG_ACTIVITY_STARTS) {
                Slog.d(TAG, "[Process(" + pid
                        + ")] Activity start allowed: process allowed by token");
            }
            return true;
        }
        return false;
    }

    /**
     * If there are no tokens, we don't allow *by token*. If there are tokens and
     * isCheckingForFgsStart is false, we ask the callback if the start is allowed for these tokens,
     * otherwise if there is no callback we allow.
     */
    private boolean isBackgroundStartAllowedByToken(int uid, String packageName,
            boolean isCheckingForFgsStart) {
        synchronized (this) {
            if (mBackgroundActivityStartTokens == null
                    || mBackgroundActivityStartTokens.isEmpty()) {
                return false;
            }
            if (isCheckingForFgsStart) {
                // BG-FGS-start only checks if there is a token.
                return true;
            }

            if (mBackgroundActivityStartCallback == null) {
                // We have tokens but no callback to decide => allow.
                return true;
            }
            // The callback will decide.
            return mBackgroundActivityStartCallback.isActivityStartAllowed(
                    mBackgroundActivityStartTokens.values(), uid, packageName);
        }
    }

    private boolean isBoundByForegroundUid() {
        synchronized (this) {
            if (mBoundClientUids != null) {
                for (int i = mBoundClientUids.size() - 1; i >= 0; i--) {
                    if (mUidHasActiveVisibleWindowPredicate.test(mBoundClientUids.get(i))) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    void setBoundClientUids(ArraySet<Integer> boundClientUids) {
        synchronized (this) {
            if (boundClientUids == null || boundClientUids.isEmpty()) {
                mBoundClientUids = null;
                return;
            }
            if (mBoundClientUids == null) {
                mBoundClientUids = new IntArray();
            } else {
                mBoundClientUids.clear();
            }
            for (int i = boundClientUids.size() - 1; i >= 0; i--) {
                mBoundClientUids.add(boundClientUids.valueAt(i));
            }
        }
    }

    /**
     * Allows background activity starts using token {@code entity}. Optionally, you can provide
     * {@code originatingToken} if you have one such originating token, this is useful for tracing
     * back the grant in the case of the notification token.
     *
     * If {@code entity} is already added, this method will update its {@code originatingToken}.
     */
    void addOrUpdateAllowBackgroundActivityStartsToken(Binder entity,
            @Nullable IBinder originatingToken) {
        synchronized (this) {
            if (mBackgroundActivityStartTokens == null) {
                mBackgroundActivityStartTokens = new ArrayMap<>();
            }
            mBackgroundActivityStartTokens.put(entity, originatingToken);
        }
    }

    /**
     * Removes token {@code entity} that allowed background activity starts added via {@link
     * #addOrUpdateAllowBackgroundActivityStartsToken(Binder, IBinder)}.
     */
    void removeAllowBackgroundActivityStartsToken(Binder entity) {
        synchronized (this) {
            if (mBackgroundActivityStartTokens != null) {
                mBackgroundActivityStartTokens.remove(entity);
            }
        }
    }

    /**
     * Returns whether this process is allowed to close system dialogs via a background activity
     * start token that allows the close system dialogs operation (eg. notification).
     */
    boolean canCloseSystemDialogsByToken(int uid) {
        if (mBackgroundActivityStartCallback == null) {
            return false;
        }
        synchronized (this) {
            if (mBackgroundActivityStartTokens == null
                    || mBackgroundActivityStartTokens.isEmpty()) {
                return false;
            }
            return mBackgroundActivityStartCallback.canCloseSystemDialogs(
                    mBackgroundActivityStartTokens.values(), uid);
        }
    }

    void dump(PrintWriter pw, String prefix) {
        synchronized (this) {
            if (mBackgroundActivityStartTokens != null
                    && !mBackgroundActivityStartTokens.isEmpty()) {
                pw.print(prefix);
                pw.println("Background activity start tokens (token: originating token):");
                for (int i = mBackgroundActivityStartTokens.size() - 1; i >= 0; i--) {
                    pw.print(prefix);
                    pw.print("  - ");
                    pw.print(mBackgroundActivityStartTokens.keyAt(i));
                    pw.print(": ");
                    pw.println(mBackgroundActivityStartTokens.valueAt(i));
                }
            }
            if (mBoundClientUids != null && mBoundClientUids.size() > 0) {
                pw.print(prefix);
                pw.print("BoundClientUids:");
                pw.println(Arrays.toString(mBoundClientUids.toArray()));
            }
        }
    }
}
