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

import android.app.ActivityManager;
import android.app.ActivityManager.StackInfo;
import android.app.IActivityManager;
import android.app.IProcessObserver;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Service to monitor AMS for new Activity or Service launching.
 */
public class SystemActivityMonitoringService implements CarServiceBase {

    /**
     * Container to hold info on top task in an Activity stack
     */
    public static class TopTaskInfoContainer {
        public final ComponentName topActivity;
        public final int taskId;
        public final StackInfo stackInfo;

        private TopTaskInfoContainer(ComponentName topActivity, int taskId, StackInfo stackInfo) {
            this.topActivity = topActivity;
            this.taskId = taskId;
            this.stackInfo = stackInfo;
        }

        public boolean isMatching(TopTaskInfoContainer taskInfo) {
            return taskInfo != null
                    && Objects.equals(this.topActivity, taskInfo.topActivity)
                    && this.taskId == taskInfo.taskId
                    && this.stackInfo.userId == taskInfo.stackInfo.userId;
        }

        @Override
        public String toString() {
            return String.format(
                    "TaskInfoContainer [topActivity=%s, taskId=%d, stackId=%d, userId=%d",
                    topActivity, taskId, stackInfo.stackId, stackInfo.userId);
        }
    }

    public interface ActivityLaunchListener {
        /**
         * Notify launch of activity.
         * @param topTask Task information for what is currently launched.
         */
        void onActivityLaunch(TopTaskInfoContainer topTask);
    }

    private final Context mContext;
    private final IActivityManager mAm;
    private final ProcessObserver mProcessObserver;
    private final TaskListener mTaskListener;

    private final HandlerThread mMonitorHandlerThread;
    private final ActivityMonitorHandler mHandler;

    /** K: stack id, V: top task */
    private final SparseArray<TopTaskInfoContainer> mTopTasks = new SparseArray<>();
    /** K: uid, V : list of pid */
    private final Map<Integer, Set<Integer>> mForegroundUidPids = new ArrayMap<>();
    private int mFocusedStackId = -1;

    /**
     * Temporary container to dispatch tasks for onActivityLaunch. Only used in handler thread.
     * can be accessed without lock. */
    private final List<TopTaskInfoContainer> mTasksToDispatch = new LinkedList<>();
    private ActivityLaunchListener mActivityLaunchListener;

    public SystemActivityMonitoringService(Context context) {
        mContext = context;
        mMonitorHandlerThread = new HandlerThread(CarLog.TAG_AM);
        mMonitorHandlerThread.start();
        mHandler = new ActivityMonitorHandler(mMonitorHandlerThread.getLooper());
        mProcessObserver = new ProcessObserver();
        mTaskListener = new TaskListener();
        mAm = ActivityManager.getService();
        // Monitoring both listeners are necessary as there are cases where one listener cannot
        // monitor activity change.
        try {
            mAm.registerProcessObserver(mProcessObserver);
            mAm.registerTaskStackListener(mTaskListener);
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "cannot register activity monitoring", e);
            throw new RuntimeException(e);
        }
        updateTasks();
    }

    @Override
    public void init() {
    }

    @Override
    public void release() {
    }

    @Override
    public void dump(PrintWriter writer) {
        writer.println("*SystemActivityMonitoringService*");
        writer.println(" Top Tasks:");
        synchronized (this) {
            for (int i = 0; i < mTopTasks.size(); i++) {
                TopTaskInfoContainer info = mTopTasks.valueAt(i);
                if (info != null) {
                    writer.println(info);
                }
            }
            writer.println(" Foregroud uid-pids:");
            for (Integer key : mForegroundUidPids.keySet()) {
                Set<Integer> pids = mForegroundUidPids.get(key);
                if (pids == null) {
                    continue;
                }
                writer.println("uid:" + key + ", pids:" + Arrays.toString(pids.toArray()));
            }
            writer.println(" focused stack:" + mFocusedStackId);
        }
    }

    /**
     * Block the current task: Launch new activity with given Intent and finish the current task.
     * @param currentTask task to finish
     * @param newActivityIntent Intent for new Activity
     */
    public void blockActivity(TopTaskInfoContainer currentTask, Intent newActivityIntent) {
        mHandler.requestBlockActivity(currentTask, newActivityIntent);
    }

    public List<TopTaskInfoContainer> getTopTasks() {
        LinkedList<TopTaskInfoContainer> tasks = new LinkedList<>();
        synchronized (this) {
            for (int i = 0; i < mTopTasks.size(); i++) {
                tasks.add(mTopTasks.valueAt(i));
            }
        }
        return tasks;
    }

    public boolean isInForeground(int pid, int uid) {
        synchronized (this) {
            Set<Integer> pids = mForegroundUidPids.get(uid);
            if (pids == null) {
                return false;
            }
            if (pids.contains(pid)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Attempts to restart a task.
     *
     * <p>Restarts a task by sending an empty intent with flag
     * {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} to its root activity. If the task does not exist,
     * do nothing.
     *
     * @param taskId id of task to be restarted.
     */
    public void restartTask(int taskId) {
        String rootActivityName = null;
        int userId = 0;
        try {
            findRootActivityName:
            for (StackInfo info : mAm.getAllStackInfos()) {
                for (int i = 0; i < info.taskIds.length; i++) {
                    if (info.taskIds[i] == taskId) {
                        rootActivityName = info.taskNames[i];
                        userId = info.userId;
                        if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
                            Log.d(CarLog.TAG_AM, "Root activity is " + rootActivityName);
                            Log.d(CarLog.TAG_AM, "User id is " + userId);
                        }
                        // Break out of nested loop.
                        break findRootActivityName;
                    }
                }
            }
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "Could not get stack info", e);
            return;
        }

        if (rootActivityName == null) {
            Log.e(CarLog.TAG_AM, "Could not find root activity with task id " + taskId);
            return;
        }

        Intent rootActivityIntent = new Intent();
        rootActivityIntent.setComponent(ComponentName.unflattenFromString(rootActivityName));
        // Clear the task the root activity is running in and start it in a new task.
        // Effectively restart root activity.
        rootActivityIntent.addFlags(
                Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);

        if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
            Log.i(CarLog.TAG_AM, "restarting root activity with user id " + userId);
        }
        mContext.startActivityAsUser(rootActivityIntent, new UserHandle(userId));
    }

    public void registerActivityLaunchListener(ActivityLaunchListener listener) {
        synchronized (this) {
            mActivityLaunchListener = listener;
        }
    }

    private void updateTasks() {
        List<StackInfo> infos;
        try {
            infos = mAm.getAllStackInfos();
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "cannot getTasks", e);
            return;
        }
        int focusedStackId = -1;
        try {
            // TODO(b/66955160): Someone on the Auto-team should probably re-work the code in the
            // synchronized block below based on this new API.
            final StackInfo focusedStackInfo = mAm.getFocusedStackInfo();
            if (focusedStackInfo != null) {
                focusedStackId = focusedStackInfo.stackId;
            }
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "cannot getFocusedStackId", e);
            return;
        }
        mTasksToDispatch.clear();
        ActivityLaunchListener listener;
        synchronized (this) {
            listener = mActivityLaunchListener;
            for (StackInfo info : infos) {
                int stackId = info.stackId;
                if (info.taskNames.length == 0 || !info.visible) { // empty stack or not shown
                    mTopTasks.remove(stackId);
                    continue;
                }
                TopTaskInfoContainer newTopTaskInfo = new TopTaskInfoContainer(
                        info.topActivity, info.taskIds[info.taskIds.length - 1], info);
                TopTaskInfoContainer currentTopTaskInfo = mTopTasks.get(stackId);

                // if a new task is added to stack or focused stack changes, should notify
                if (currentTopTaskInfo == null ||
                        !currentTopTaskInfo.isMatching(newTopTaskInfo) ||
                        (focusedStackId == stackId && focusedStackId != mFocusedStackId)) {
                    mTopTasks.put(stackId, newTopTaskInfo);
                    mTasksToDispatch.add(newTopTaskInfo);
                    if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
                        Log.i(CarLog.TAG_AM, "New top task: " + newTopTaskInfo);
                    }
                }
            }
            mFocusedStackId = focusedStackId;
        }
        if (listener != null) {
            for (TopTaskInfoContainer topTask : mTasksToDispatch) {
                if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
                    Log.i(CarLog.TAG_AM, "activity launched:" + topTask.toString());
                }
                listener.onActivityLaunch(topTask);
            }
        }
    }

    public StackInfo getFocusedStackForTopActivity(ComponentName activity) {
        StackInfo focusedStack;
        try {
            focusedStack = mAm.getFocusedStackInfo();
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "cannot getFocusedStackId", e);
            return null;
        }
        if (focusedStack.taskNames.length == 0) { // nothing in focused stack
            return null;
        }
        ComponentName topActivity = ComponentName.unflattenFromString(
                focusedStack.taskNames[focusedStack.taskNames.length - 1]);
        if (topActivity.equals(activity)) {
            return focusedStack;
        } else {
            return null;
        }
    }

    private void handleForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
        synchronized (this) {
            if (foregroundActivities) {
                Set<Integer> pids = mForegroundUidPids.get(uid);
                if (pids == null) {
                    pids = new ArraySet<Integer>();
                    mForegroundUidPids.put(uid, pids);
                }
                pids.add(pid);
            } else {
                doHandlePidGoneLocked(pid, uid);
            }
        }
    }

    private void handleProcessDied(int pid, int uid) {
        synchronized (this) {
            doHandlePidGoneLocked(pid, uid);
        }
    }

    private void doHandlePidGoneLocked(int pid, int uid) {
        Set<Integer> pids = mForegroundUidPids.get(uid);
        if (pids != null) {
            pids.remove(pid);
            if (pids.isEmpty()) {
                mForegroundUidPids.remove(uid);
            }
        }
    }

    /**
     * block the current task with the provided new activity.
     */
    private void handleBlockActivity(TopTaskInfoContainer currentTask, Intent newActivityIntent) {
        mContext.startActivityAsUser(newActivityIntent,
                new UserHandle(currentTask.stackInfo.userId));
        // Now make stack with new activity focused.
        findTaskAndGrantFocus(newActivityIntent.getComponent());
    }

    private void findTaskAndGrantFocus(ComponentName activity) {
        List<StackInfo> infos;
        try {
            infos = mAm.getAllStackInfos();
        } catch (RemoteException e) {
            Log.e(CarLog.TAG_AM, "cannot getTasks", e);
            return;
        }
        for (StackInfo info : infos) {
            if (info.taskNames.length == 0) {
                continue;
            }
            ComponentName topActivity = ComponentName.unflattenFromString(
                    info.taskNames[info.taskNames.length - 1]);
            if (activity.equals(topActivity)) {
                try {
                    mAm.setFocusedStack(info.stackId);
                } catch (RemoteException e) {
                    Log.e(CarLog.TAG_AM, "cannot setFocusedStack to stack:" + info.stackId, e);
                }
                return;
            }
        }
        Log.i(CarLog.TAG_AM, "cannot give focus, cannot find Activity:" + activity);
    }

    private class ProcessObserver extends IProcessObserver.Stub {
        @Override
        public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
            if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
                Log.i(CarLog.TAG_AM,
                        String.format("onForegroundActivitiesChanged uid %d pid %d fg %b",
                    uid, pid, foregroundActivities));
            }
            mHandler.requestForegroundActivitiesChanged(pid, uid, foregroundActivities);
        }

        @Override
        public void onProcessDied(int pid, int uid) {
            mHandler.requestProcessDied(pid, uid);
        }
    }

    private class TaskListener extends TaskStackListener {
        @Override
        public void onTaskStackChanged() {
            if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
                Log.i(CarLog.TAG_AM, "onTaskStackChanged");
            }
            mHandler.requestUpdatingTask();
        }
    }

    private class ActivityMonitorHandler extends Handler {
        private static final int MSG_UPDATE_TASKS = 0;
        private static final int MSG_FOREGROUND_ACTIVITIES_CHANGED = 1;
        private static final int MSG_PROCESS_DIED = 2;
        private static final int MSG_BLOCK_ACTIVITY = 3;

        private ActivityMonitorHandler(Looper looper) {
            super(looper);
        }

        private void requestUpdatingTask() {
            Message msg = obtainMessage(MSG_UPDATE_TASKS);
            sendMessage(msg);
        }

        private void requestForegroundActivitiesChanged(int pid, int uid,
                boolean foregroundActivities) {
            Message msg = obtainMessage(MSG_FOREGROUND_ACTIVITIES_CHANGED, pid, uid,
                    Boolean.valueOf(foregroundActivities));
            sendMessage(msg);
        }

        private void requestProcessDied(int pid, int uid) {
            Message msg = obtainMessage(MSG_PROCESS_DIED, pid, uid);
            sendMessage(msg);
        }

        private void requestBlockActivity(TopTaskInfoContainer currentTask,
                Intent newActivityIntent) {
            Message msg = obtainMessage(MSG_BLOCK_ACTIVITY,
                    new Pair<TopTaskInfoContainer, Intent>(currentTask, newActivityIntent));
            sendMessage(msg);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_UPDATE_TASKS:
                    updateTasks();
                    break;
                case MSG_FOREGROUND_ACTIVITIES_CHANGED:
                    handleForegroundActivitiesChanged(msg.arg1, msg.arg2, (Boolean) msg.obj);
                    updateTasks();
                    break;
                case MSG_PROCESS_DIED:
                    handleProcessDied(msg.arg1, msg.arg2);
                    break;
                case MSG_BLOCK_ACTIVITY:
                    Pair<TopTaskInfoContainer, Intent> pair =
                        (Pair<TopTaskInfoContainer, Intent>) msg.obj;
                    handleBlockActivity(pair.first, pair.second);
                    break;
            }
        }
    }
}
