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

import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;

import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.PendingIntent;
import android.content.IIntentSender;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.RingBuffer;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.AlarmManagerInternal;
import com.android.server.LocalServices;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.SafeActivityOptions;

import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Helper class for {@link ActivityManagerService} responsible for managing pending intents.
 *
 * <p>This class uses {@link #mLock} to synchronize access to internal state and doesn't make use of
 * {@link ActivityManagerService} lock since there can be direct calls into this class from outside
 * AM. This helps avoid deadlocks.
 */
public class PendingIntentController {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "PendingIntentController" : TAG_AM;
    private static final String TAG_MU = TAG + POSTFIX_MU;

    /** @see {@link #mRecentIntentsPerUid}.  */
    private static final int RECENT_N = 10;

    /** Lock for internal state. */
    final Object mLock = new Object();
    final Handler mH;
    ActivityManagerInternal mAmInternal;
    final UserController mUserController;
    final ActivityTaskManagerInternal mAtmInternal;

    /** Set of IntentSenderRecord objects that are currently active. */
    final HashMap<PendingIntentRecord.Key, WeakReference<PendingIntentRecord>> mIntentSenderRecords
            = new HashMap<>();

    /** The number of PendingIntentRecord per uid */
    @GuardedBy("mLock")
    private final SparseIntArray mIntentsPerUid = new SparseIntArray();

    /** The recent PendingIntentRecord, up to {@link #RECENT_N} per uid */
    @GuardedBy("mLock")
    private final SparseArray<RingBuffer<String>> mRecentIntentsPerUid = new SparseArray<>();

    private final ActivityManagerConstants mConstants;

    PendingIntentController(Looper looper, UserController userController,
            ActivityManagerConstants constants) {
        mH = new Handler(looper);
        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
        mUserController = userController;
        mConstants = constants;
    }

    void onActivityManagerInternalAdded() {
        synchronized (mLock) {
            mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
        }
    }

    public PendingIntentRecord getIntentSender(int type, String packageName,
            @Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho,
            int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) {
        synchronized (mLock) {
            if (DEBUG_MU) Slog.v(TAG_MU, "getIntentSender(): uid=" + callingUid);

            // We're going to be splicing together extras before sending, so we're
            // okay poking into any contained extras.
            if (intents != null) {
                for (int i = 0; i < intents.length; i++) {
                    intents[i].setDefusable(true);
                }
            }
            Bundle.setDefusable(bOptions, true);

            final boolean noCreate = (flags & PendingIntent.FLAG_NO_CREATE) != 0;
            final boolean cancelCurrent = (flags & PendingIntent.FLAG_CANCEL_CURRENT) != 0;
            final boolean updateCurrent = (flags & PendingIntent.FLAG_UPDATE_CURRENT) != 0;
            flags &= ~(PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_CANCEL_CURRENT
                    | PendingIntent.FLAG_UPDATE_CURRENT);

            PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
                    token, resultWho, requestCode, intents, resolvedTypes, flags,
                    SafeActivityOptions.fromBundle(bOptions), userId);
            WeakReference<PendingIntentRecord> ref;
            ref = mIntentSenderRecords.get(key);
            PendingIntentRecord rec = ref != null ? ref.get() : null;
            if (rec != null) {
                if (!cancelCurrent) {
                    if (updateCurrent) {
                        if (rec.key.requestIntent != null) {
                            rec.key.requestIntent.replaceExtras(intents != null ?
                                    intents[intents.length - 1] : null);
                        }
                        if (intents != null) {
                            intents[intents.length - 1] = rec.key.requestIntent;
                            rec.key.allIntents = intents;
                            rec.key.allResolvedTypes = resolvedTypes;
                        } else {
                            rec.key.allIntents = null;
                            rec.key.allResolvedTypes = null;
                        }
                    }
                    return rec;
                }
                makeIntentSenderCanceled(rec);
                mIntentSenderRecords.remove(key);
                decrementUidStatLocked(rec);
            }
            if (noCreate) {
                return rec;
            }
            rec = new PendingIntentRecord(this, key, callingUid);
            mIntentSenderRecords.put(key, rec.ref);
            incrementUidStatLocked(rec);
            return rec;
        }
    }

    boolean removePendingIntentsForPackage(String packageName, int userId, int appId,
            boolean doIt) {

        boolean didSomething = false;
        synchronized (mLock) {

            // Remove pending intents.  For now we only do this when force stopping users, because
            // we have some problems when doing this for packages -- app widgets are not currently
            // cleaned up for such packages, so they can be left with bad pending intents.
            if (mIntentSenderRecords.size() <= 0) {
                return false;
            }

            Iterator<WeakReference<PendingIntentRecord>> it
                    = mIntentSenderRecords.values().iterator();
            while (it.hasNext()) {
                WeakReference<PendingIntentRecord> wpir = it.next();
                if (wpir == null) {
                    it.remove();
                    continue;
                }
                PendingIntentRecord pir = wpir.get();
                if (pir == null) {
                    it.remove();
                    continue;
                }
                if (packageName == null) {
                    // Stopping user, remove all objects for the user.
                    if (pir.key.userId != userId) {
                        // Not the same user, skip it.
                        continue;
                    }
                } else {
                    if (UserHandle.getAppId(pir.uid) != appId) {
                        // Different app id, skip it.
                        continue;
                    }
                    if (userId != UserHandle.USER_ALL && pir.key.userId != userId) {
                        // Different user, skip it.
                        continue;
                    }
                    if (!pir.key.packageName.equals(packageName)) {
                        // Different package, skip it.
                        continue;
                    }
                }
                if (!doIt) {
                    return true;
                }
                didSomething = true;
                it.remove();
                makeIntentSenderCanceled(pir);
                decrementUidStatLocked(pir);
                if (pir.key.activity != null) {
                    final Message m = PooledLambda.obtainMessage(
                            PendingIntentController::clearPendingResultForActivity, this,
                            pir.key.activity, pir.ref);
                    mH.sendMessage(m);
                }
            }
        }

        return didSomething;
    }

    public void cancelIntentSender(IIntentSender sender) {
        if (!(sender instanceof PendingIntentRecord)) {
            return;
        }
        synchronized (mLock) {
            final PendingIntentRecord rec = (PendingIntentRecord) sender;
            try {
                final int uid = AppGlobals.getPackageManager().getPackageUid(rec.key.packageName,
                        MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getCallingUserId());
                if (!UserHandle.isSameApp(uid, Binder.getCallingUid())) {
                    String msg = "Permission Denial: cancelIntentSender() from pid="
                            + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
                            + " is not allowed to cancel package " + rec.key.packageName;
                    Slog.w(TAG, msg);
                    throw new SecurityException(msg);
                }
            } catch (RemoteException e) {
                throw new SecurityException(e);
            }
            cancelIntentSender(rec, true);
        }
    }

    public void cancelIntentSender(PendingIntentRecord rec, boolean cleanActivity) {
        synchronized (mLock) {
            makeIntentSenderCanceled(rec);
            mIntentSenderRecords.remove(rec.key);
            decrementUidStatLocked(rec);
            if (cleanActivity && rec.key.activity != null) {
                final Message m = PooledLambda.obtainMessage(
                        PendingIntentController::clearPendingResultForActivity, this,
                        rec.key.activity, rec.ref);
                mH.sendMessage(m);
            }
        }
    }

    void registerIntentSenderCancelListener(IIntentSender sender, IResultReceiver receiver) {
        if (!(sender instanceof PendingIntentRecord)) {
            return;
        }
        boolean isCancelled;
        synchronized (mLock) {
            PendingIntentRecord pendingIntent = (PendingIntentRecord) sender;
            isCancelled = pendingIntent.canceled;
            if (!isCancelled) {
                pendingIntent.registerCancelListenerLocked(receiver);
            }
        }
        if (isCancelled) {
            try {
                receiver.send(Activity.RESULT_CANCELED, null);
            } catch (RemoteException e) {
            }
        }
    }

    void unregisterIntentSenderCancelListener(IIntentSender sender,
            IResultReceiver receiver) {
        if (!(sender instanceof PendingIntentRecord)) {
            return;
        }
        synchronized (mLock) {
            ((PendingIntentRecord) sender).unregisterCancelListenerLocked(receiver);
        }
    }

    void setPendingIntentWhitelistDuration(IIntentSender target, IBinder whitelistToken,
            long duration) {
        if (!(target instanceof PendingIntentRecord)) {
            Slog.w(TAG, "markAsSentFromNotification(): not a PendingIntentRecord: " + target);
            return;
        }
        synchronized (mLock) {
            ((PendingIntentRecord) target).setWhitelistDurationLocked(whitelistToken, duration);
        }
    }

    private void makeIntentSenderCanceled(PendingIntentRecord rec) {
        rec.canceled = true;
        final RemoteCallbackList<IResultReceiver> callbacks = rec.detachCancelListenersLocked();
        if (callbacks != null) {
            final Message m = PooledLambda.obtainMessage(
                    PendingIntentController::handlePendingIntentCancelled, this, callbacks);
            mH.sendMessage(m);
        }
        final AlarmManagerInternal ami = LocalServices.getService(AlarmManagerInternal.class);
        ami.remove(new PendingIntent(rec));
    }

    private void handlePendingIntentCancelled(RemoteCallbackList<IResultReceiver> callbacks) {
        int N = callbacks.beginBroadcast();
        for (int i = 0; i < N; i++) {
            try {
                callbacks.getBroadcastItem(i).send(Activity.RESULT_CANCELED, null);
            } catch (RemoteException e) {
                // Process is not longer running...whatever.
            }
        }
        callbacks.finishBroadcast();
        // We have to clean up the RemoteCallbackList here, because otherwise it will
        // needlessly hold the enclosed callbacks until the remote process dies.
        callbacks.kill();
    }

    private void clearPendingResultForActivity(IBinder activityToken,
            WeakReference<PendingIntentRecord> pir) {
        mAtmInternal.clearPendingResultForActivity(activityToken, pir);
    }

    void dumpPendingIntents(PrintWriter pw, boolean dumpAll, String dumpPackage) {
        synchronized (mLock) {
            boolean printed = false;

            pw.println("ACTIVITY MANAGER PENDING INTENTS (dumpsys activity intents)");

            if (mIntentSenderRecords.size() > 0) {
                // Organize these by package name, so they are easier to read.
                final ArrayMap<String, ArrayList<PendingIntentRecord>> byPackage = new ArrayMap<>();
                final ArrayList<WeakReference<PendingIntentRecord>> weakRefs = new ArrayList<>();
                final Iterator<WeakReference<PendingIntentRecord>> it
                        = mIntentSenderRecords.values().iterator();
                while (it.hasNext()) {
                    WeakReference<PendingIntentRecord> ref = it.next();
                    PendingIntentRecord rec = ref != null ? ref.get() : null;
                    if (rec == null) {
                        weakRefs.add(ref);
                        continue;
                    }
                    if (dumpPackage != null && !dumpPackage.equals(rec.key.packageName)) {
                        continue;
                    }
                    ArrayList<PendingIntentRecord> list = byPackage.get(rec.key.packageName);
                    if (list == null) {
                        list = new ArrayList<>();
                        byPackage.put(rec.key.packageName, list);
                    }
                    list.add(rec);
                }
                for (int i = 0; i < byPackage.size(); i++) {
                    ArrayList<PendingIntentRecord> intents = byPackage.valueAt(i);
                    printed = true;
                    pw.print("  * "); pw.print(byPackage.keyAt(i));
                    pw.print(": "); pw.print(intents.size()); pw.println(" items");
                    for (int j = 0; j < intents.size(); j++) {
                        pw.print("    #"); pw.print(j); pw.print(": "); pw.println(intents.get(j));
                        if (dumpAll) {
                            intents.get(j).dump(pw, "      ");
                        }
                    }
                }
                if (weakRefs.size() > 0) {
                    printed = true;
                    pw.println("  * WEAK REFS:");
                    for (int i = 0; i < weakRefs.size(); i++) {
                        pw.print("    #"); pw.print(i); pw.print(": "); pw.println(weakRefs.get(i));
                    }
                }
            }

            final int sizeOfIntentsPerUid = mIntentsPerUid.size();
            if (sizeOfIntentsPerUid > 0) {
                for (int i = 0; i < sizeOfIntentsPerUid; i++) {
                    pw.print("  * UID: ");
                    pw.print(mIntentsPerUid.keyAt(i));
                    pw.print(" total: ");
                    pw.println(mIntentsPerUid.valueAt(i));
                }
            }

            if (!printed) {
                pw.println("  (nothing)");
            }
        }
    }

    /**
     * Increment the number of the PendingIntentRecord for the given uid, log a warning
     * if there are too many for this uid already.
     */
    @GuardedBy("mLock")
    void incrementUidStatLocked(final PendingIntentRecord pir) {
        final int uid = pir.uid;
        final int idx = mIntentsPerUid.indexOfKey(uid);
        int newCount = 1;
        if (idx >= 0) {
            newCount = mIntentsPerUid.valueAt(idx) + 1;
            mIntentsPerUid.setValueAt(idx, newCount);
        } else {
            mIntentsPerUid.put(uid, newCount);
        }

        // If the number is within the range [threshold - N + 1, threshold], log it into buffer
        final int lowBound = mConstants.PENDINGINTENT_WARNING_THRESHOLD - RECENT_N + 1;
        RingBuffer<String> recentHistory = null;
        if (newCount == lowBound) {
            recentHistory = new RingBuffer(String.class, RECENT_N);
            mRecentIntentsPerUid.put(uid, recentHistory);
        } else if (newCount > lowBound && newCount <= mConstants.PENDINGINTENT_WARNING_THRESHOLD) {
            recentHistory = mRecentIntentsPerUid.get(uid);
        }
        if (recentHistory == null) {
            return;
        }

        recentHistory.append(pir.key.toString());

        // Output the log if we are hitting the threshold
        if (newCount == mConstants.PENDINGINTENT_WARNING_THRESHOLD) {
            Slog.wtf(TAG, "Too many PendingIntent created for uid " + uid
                    + ", recent " + RECENT_N + ": " + Arrays.toString(recentHistory.toArray()));
            // Clear the buffer, as we don't want to spam the log when the numbers
            // are jumping up and down around the threshold.
            mRecentIntentsPerUid.remove(uid);
        }
    }

    /**
     * Decrement the number of the PendingIntentRecord for the given uid.
     */
    @GuardedBy("mLock")
    void decrementUidStatLocked(final PendingIntentRecord pir) {
        final int uid = pir.uid;
        final int idx = mIntentsPerUid.indexOfKey(uid);
        if (idx >= 0) {
            final int newCount = mIntentsPerUid.valueAt(idx) - 1;
            // If we are going below the low threshold, no need to keep logs.
            if (newCount == mConstants.PENDINGINTENT_WARNING_THRESHOLD - RECENT_N) {
                mRecentIntentsPerUid.delete(uid);
            }
            if (newCount == 0) {
                mIntentsPerUid.removeAt(idx);
            } else {
                mIntentsPerUid.setValueAt(idx, newCount);
            }
        }
    }
}
