/*
 * 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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;

import android.annotation.Nullable;
import android.app.ActivityManager;
import android.car.builtin.app.ActivityManagerHelper;
import android.car.builtin.app.ActivityManagerHelper.ProcessObserverCallback;
import android.car.builtin.os.ProcessHelper;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

    /** Injector for injecting some system related operations. */
    @VisibleForTesting
    /* package */ interface Injector {
        void registerProcessObserverCallback(ProcessObserverCallback callback);
        void unregisterProcessObserverCallback(ProcessObserverCallback callback);
        long getPassengerActivitySetProcessGroupRetryTimeoutMs();
    }

    private static final boolean DBG = Slogf.isLoggable(CarLog.TAG_AM, Log.DEBUG);
    // Passenger Activity might not be in top-app group in the 1st try. In that case, try
    // again after this time. Retry will happen only once.
    private static final long PASSENGER_ACTIVITY_SET_PROCESS_GROUP_RETRY_MS = 200;

    private final ProcessObserverCallback mProcessObserver = new ProcessObserver();

    private final HandlerThread mMonitorHandlerThread = CarServiceUtils.getHandlerThread(
            getClass().getSimpleName());
    private final ActivityMonitorHandler mHandler = new ActivityMonitorHandler(
            mMonitorHandlerThread.getLooper(), this);

    private final Context mContext;

    private final Injector mInjector;

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final Map<Integer, Set<Integer>> mForegroundUidPids = new ArrayMap<>();

    @GuardedBy("mLock")
    private final List<ProcessObserverCallback> mCustomProcessObservers = new ArrayList<>();

    @GuardedBy("mLock")
    private boolean mAssignPassengerActivityToFgGroup;

    @GuardedBy("mLock")
    private int mDriverTopAppPid = Process.INVALID_PID;

    public SystemActivityMonitoringService(Context context) {
        this(context, new DefaultInjector());
    }

    @VisibleForTesting
    /*package*/ SystemActivityMonitoringService(Context context, Injector injector) {
        mContext = context;
        mInjector = injector;
    }

    @Override
    public void init() {
        boolean assignPassengerActivityToFgGroup = false;
        if (mContext.getResources().getBoolean(
                R.bool.config_assignPassengerActivityToForegroundCpuGroup)) {
            CarOccupantZoneService occupantService = CarLocalServices.getService(
                    CarOccupantZoneService.class);
            if (occupantService.hasDriverZone() && occupantService.hasPassengerZones()) {
                assignPassengerActivityToFgGroup = true;
            }
        }
        synchronized (mLock) {
            mAssignPassengerActivityToFgGroup = assignPassengerActivityToFgGroup;
        }
        // Monitoring both listeners are necessary as there are cases where one listener cannot
        // monitor activity change.
        mInjector.registerProcessObserverCallback(mProcessObserver);
    }

    @Override
    public void release() {
        mInjector.unregisterProcessObserverCallback(mProcessObserver);
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(IndentingPrintWriter writer) {
        writer.println("*SystemActivityMonitoringService*");
        writer.println(" Top Tasks per display:");
        synchronized (mLock) {
            writer.println(" Foreground 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(
                    "mAssignPassengerActivityToFgGroup:" + mAssignPassengerActivityToFgGroup);
        }
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dumpProto(ProtoOutputStream proto) {}

    /**
     * Returns {@code true} if given pid-uid pair is in foreground.
     */
    public boolean isInForeground(int pid, int uid) {
        Set<Integer> pids = getPidsOfForegroudApp(uid);
        if (pids == null) {
            return false;
        }
        return pids.contains(pid);
    }

    /**
     * Returns PIDs of foreground apps launched from the given UID.
     */
    @Nullable
    public Set<Integer> getPidsOfForegroudApp(int uid) {
        synchronized (mLock) {
            return mForegroundUidPids.get(uid);
        }
    }

    /** Registers a callback to get notified when the running state of a process has changed. */
    public void registerProcessObserverCallback(ProcessObserverCallback callback) {
        synchronized (mLock) {
            mCustomProcessObservers.add(callback);
        }
    }

    /** Unregisters the ProcessObserverCallback, if there is any. */
    public void unregisterProcessObserverCallback(ProcessObserverCallback callback) {
        synchronized (mLock) {
            mCustomProcessObservers.remove(callback);
        }
    }

    private void handleForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
        synchronized (mLock) {
            for (int i = 0; i < mCustomProcessObservers.size(); i++) {
                ProcessObserverCallback callback = mCustomProcessObservers.get(i);
                callback.onForegroundActivitiesChanged(pid, uid, foregroundActivities);
            }
            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 (mLock) {
            for (int i = 0; i < mCustomProcessObservers.size(); i++) {
                ProcessObserverCallback callback = mCustomProcessObservers.get(i);
                callback.onProcessDied(pid, uid);
            }
            doHandlePidGoneLocked(pid, uid);
        }
    }

    @GuardedBy("mLock")
    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);
            }
        }
    }

    /**
     * Updates the process group for given PID if it is passenger app and returns true if it should
     * be retried.
     */
    private boolean updateProcessGroupForFgApp(int pid, int uid) {
        int driverTopAppPid;
        synchronized (mLock) {
            if (!mAssignPassengerActivityToFgGroup) {
                return false;
            }
            int userId = UserManagerHelper.getUserId(uid);
            // Current user will be driver. So do not touch it.
            // User 0 will be either current user or common system UI which should run with higher
            // priority.
            if (userId == ActivityManager.getCurrentUser()
                    || userId == UserManagerHelper.USER_SYSTEM) {
                mDriverTopAppPid = pid;
                return false;
            }
            driverTopAppPid = mDriverTopAppPid;
        }
        // TODO(b/261783537) ignore profile of the current user

        CarServiceHelperWrapper helper = CarServiceHelperWrapper.getInstance();
        boolean shouldRetry = false;
        try {
            int processGroup = helper.getProcessGroup(pid);
            if (DBG) {
                Slogf.d(CarLog.TAG_AM, "doHandleProcessGroupForFgApp: pid=%d pGroup=%d",
                        pid, processGroup);
            }
            switch (processGroup) {
                case ProcessHelper.THREAD_GROUP_FOREGROUND:
                    // SetProcessGroup happens in OomAdjuster#mProcessGroupHandler in System
                    // Server, but which is the different thread with the main thread of
                    // OomAdjuster, and the focus change event is propagated to CarService
                    // through Binder, so there is race-condition between setting ProcessGroup in
                    // System Server and here.
                    // So, there are chances that to set Top App is not executed yet.
                    shouldRetry = true;
                    break;
                case ProcessHelper.THREAD_GROUP_TOP_APP:
                    // Changing to FOREGROUND requires setting it to DEFAULT
                    helper.setProcessGroup(pid, ProcessHelper.THREAD_GROUP_DEFAULT);
                    if (driverTopAppPid != Process.INVALID_PID) {
                        helper.setProcessGroup(driverTopAppPid, ProcessHelper.THREAD_GROUP_TOP_APP);
                    }
                    break;
                default:
                    // not in top-app yet, should retry
                    shouldRetry = true;
                    break;
            }
        } catch (Exception e) {
            Slogf.w(CarLog.TAG_AM, e, "Process group manipulation failed for pid:%d uid:%d",
                    pid, uid);
            // no need to retry as this PID may be already invalid.
        }
        return shouldRetry;
    }

    private void handleProcessGroupForFgApp(int pid, int uid) {
        if (updateProcessGroupForFgApp(pid, uid)) {
            if (DBG) {
                Slogf.d(CarLog.TAG_AM, "Will retry handleProcessGroupForFgApp: pid=%d, uid=%d",
                        pid, uid);
            }
            mHandler.postDelayed(() -> updateProcessGroupForFgApp(pid, uid),
                    mInjector.getPassengerActivitySetProcessGroupRetryTimeoutMs());
        }
    }

    private void handleProcessGroupForDiedApp(int pid) {
        synchronized (mLock) {
            if (!mAssignPassengerActivityToFgGroup) {
                return;
            }
            if (pid == mDriverTopAppPid) {
                mDriverTopAppPid = Process.INVALID_PID;
            }
        }
    }

    /** Handles focusChanged event. */
    public void handleFocusChanged(int pid, int uid) {
        if (DBG) Slogf.d(CarLog.TAG_AM, "notifyFocusChanged: pid=%d uid=%d", pid, uid);
        handleProcessGroupForFgApp(pid, uid);
    }

    private class ProcessObserver extends ProcessObserverCallback {
        @Override
        public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
            if (Slogf.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
                Slogf.d(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) {
            handleProcessGroupForDiedApp(pid);
            mHandler.requestProcessDied(pid, uid);
        }
    }

    private static final class ActivityMonitorHandler extends Handler {
        private static final String TAG = ActivityMonitorHandler.class.getSimpleName();

        private static final int MSG_FOREGROUND_ACTIVITIES_CHANGED = 1;
        private static final int MSG_PROCESS_DIED = 2;

        private final WeakReference<SystemActivityMonitoringService> mService;

        private ActivityMonitorHandler(Looper looper, SystemActivityMonitoringService service) {
            super(looper);
            mService = new WeakReference<SystemActivityMonitoringService>(service);
        }

        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);
        }

        @Override
        public void handleMessage(Message msg) {
            SystemActivityMonitoringService service = mService.get();
            if (service == null) {
                Slogf.i(TAG, "handleMessage null service");
                return;
            }
            switch (msg.what) {
                case MSG_FOREGROUND_ACTIVITIES_CHANGED:
                    service.handleForegroundActivitiesChanged(msg.arg1, msg.arg2,
                            (Boolean) msg.obj);
                    break;
                case MSG_PROCESS_DIED:
                    service.handleProcessDied(msg.arg1, msg.arg2);
                    break;
                default:
                    break;
            }
        }
    }

    private static class DefaultInjector implements Injector {
        @Override
        public void registerProcessObserverCallback(ProcessObserverCallback callback) {
            ActivityManagerHelper.registerProcessObserverCallback(callback);
        }

        @Override
        public void unregisterProcessObserverCallback(ProcessObserverCallback callback) {
            ActivityManagerHelper.unregisterProcessObserverCallback(callback);
        }

        @Override
        public long getPassengerActivitySetProcessGroupRetryTimeoutMs() {
            return PASSENGER_ACTIVITY_SET_PROCESS_GROUP_RETRY_MS;
        }
    }
}
