/*
 * Copyright (C) 2019 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.systemui.statusbar.notification;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.AppGlobals;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.SynchronousUserSwitchObserver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.Pair;

import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.NotificationChannels;

import java.util.List;
import java.util.concurrent.Executor;

import javax.inject.Inject;
import javax.inject.Singleton;

/** The class to show notification(s) of instant apps. This may show multiple notifications on
 * splitted screen.
 */
@Singleton
public class InstantAppNotifier extends SystemUI
        implements CommandQueue.Callbacks, KeyguardStateController.Callback {
    private static final String TAG = "InstantAppNotifier";
    public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;

    private final Handler mHandler = new Handler();
    private final Executor mUiBgExecutor;
    private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
    private final CommandQueue mCommandQueue;
    private boolean mDockedStackExists;
    private KeyguardStateController mKeyguardStateController;
    private final Divider mDivider;

    @Inject
    public InstantAppNotifier(Context context, CommandQueue commandQueue,
            @UiBackground Executor uiBgExecutor, Divider divider) {
        super(context);
        mDivider = divider;
        mCommandQueue = commandQueue;
        mUiBgExecutor = uiBgExecutor;
    }

    @Override
    public void start() {
        mKeyguardStateController = Dependency.get(KeyguardStateController.class);

        // listen for user / profile change.
        try {
            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
        } catch (RemoteException e) {
            // Ignore
        }

        mCommandQueue.addCallback(this);
        mKeyguardStateController.addCallback(this);

        mDivider.registerInSplitScreenListener(
                exists -> {
                    mDockedStackExists = exists;
                    updateForegroundInstantApps();
                });

        // Clear out all old notifications on startup (only present in the case where sysui dies)
        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
        for (StatusBarNotification notification : noMan.getActiveNotifications()) {
            if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
                noMan.cancel(notification.getTag(), notification.getId());
            }
        }
    }

    @Override
    public void appTransitionStarting(
            int displayId, long startTime, long duration, boolean forced) {
        if (mContext.getDisplayId() == displayId) {
            updateForegroundInstantApps();
        }
    }

    @Override
    public void onKeyguardShowingChanged() {
        updateForegroundInstantApps();
    }

    @Override
    public void preloadRecentApps() {
        updateForegroundInstantApps();
    }

    private final SynchronousUserSwitchObserver mUserSwitchListener =
            new SynchronousUserSwitchObserver() {
                @Override
                public void onUserSwitching(int newUserId) throws RemoteException {}

                @Override
                public void onUserSwitchComplete(int newUserId) throws RemoteException {
                    mHandler.post(
                            () -> {
                                updateForegroundInstantApps();
                            });
                }
            };


    private void updateForegroundInstantApps() {
        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
        IPackageManager pm = AppGlobals.getPackageManager();
        mUiBgExecutor.execute(
                () -> {
                    ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
                    try {
                        final ActivityManager.StackInfo focusedStack =
                                ActivityTaskManager.getService().getFocusedStackInfo();
                        if (focusedStack != null) {
                            final int windowingMode =
                                    focusedStack.configuration.windowConfiguration
                                            .getWindowingMode();
                            if (windowingMode == WINDOWING_MODE_FULLSCREEN
                                    || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
                                    || windowingMode == WINDOWING_MODE_FREEFORM) {
                                checkAndPostForStack(focusedStack, notifs, noMan, pm);
                            }
                        }
                        if (mDockedStackExists) {
                            checkAndPostForPrimaryScreen(notifs, noMan, pm);
                        }
                    } catch (RemoteException e) {
                        e.rethrowFromSystemServer();
                    }

                    // Cancel all the leftover notifications that don't have a foreground
                    // process anymore.
                    notifs.forEach(
                            v -> {
                                mCurrentNotifs.remove(v);

                                noMan.cancelAsUser(
                                        v.first,
                                        SystemMessageProto.SystemMessage.NOTE_INSTANT_APPS,
                                        new UserHandle(v.second));
                            });
                });
    }

    /**
     * Posts an instant app notification if the top activity of the primary container in the
     * splitted screen is an instant app and the corresponding instant app notification is not
     * posted yet. If the notification already exists, this method removes it from {@code
     * notifs} in the arguments.
     */
    private void checkAndPostForPrimaryScreen(
            @NonNull ArraySet<Pair<String, Integer>> notifs,
            @NonNull NotificationManager noMan,
            @NonNull IPackageManager pm) {
        try {
            final ActivityManager.StackInfo info =
                    ActivityTaskManager.getService()
                            .getStackInfo(
                                    WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED);
            checkAndPostForStack(info, notifs, noMan, pm);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * Posts an instant app notification if the top activity of the given stack is an instant app
     * and the corresponding instant app notification is not posted yet. If the notification already
     * exists, this method removes it from {@code notifs} in the arguments.
     */
    private void checkAndPostForStack(
            @Nullable ActivityManager.StackInfo info,
            @NonNull ArraySet<Pair<String, Integer>> notifs,
            @NonNull NotificationManager noMan,
            @NonNull IPackageManager pm) {
        try {
            if (info == null || info.topActivity == null) return;
            String pkg = info.topActivity.getPackageName();
            Pair<String, Integer> key = new Pair<>(pkg, info.userId);
            if (!notifs.remove(key)) {
                // TODO: Optimize by not always needing to get application info.
                // Maybe cache non-instant-app packages?
                ApplicationInfo appInfo =
                        pm.getApplicationInfo(
                                pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, info.userId);
                if (appInfo.isInstantApp()) {
                    postInstantAppNotif(
                            pkg,
                            info.userId,
                            appInfo,
                            noMan,
                            info.taskIds[info.taskIds.length - 1]);
                }
            }
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /** Posts an instant app notification. */
    private void postInstantAppNotif(
            @NonNull String pkg,
            int userId,
            @NonNull ApplicationInfo appInfo,
            @NonNull NotificationManager noMan,
            int taskId) {
        final Bundle extras = new Bundle();
        extras.putString(
                Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString(R.string.instant_apps));
        mCurrentNotifs.add(new Pair<>(pkg, userId));

        String helpUrl = mContext.getString(R.string.instant_apps_help_url);
        boolean hasHelpUrl = !helpUrl.isEmpty();
        String message =
                mContext.getString(
                        hasHelpUrl
                                ? R.string.instant_apps_message_with_help
                                : R.string.instant_apps_message);

        UserHandle user = UserHandle.of(userId);
        PendingIntent appInfoAction =
                PendingIntent.getActivityAsUser(
                        mContext,
                        0,
                        new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                                .setData(Uri.fromParts("package", pkg, null)),
                        PendingIntent.FLAG_IMMUTABLE,
                        null,
                        user);
        Notification.Action action =
                new Notification.Action.Builder(
                                null, mContext.getString(R.string.app_info), appInfoAction)
                        .build();
        PendingIntent helpCenterIntent =
                hasHelpUrl
                        ? PendingIntent.getActivityAsUser(
                                mContext,
                                0,
                                new Intent(Intent.ACTION_VIEW).setData(Uri.parse(helpUrl)),
                                PendingIntent.FLAG_IMMUTABLE,
                                null,
                                user)
                        : null;

        Intent browserIntent = getTaskIntent(taskId, userId);
        Notification.Builder builder =
                new Notification.Builder(mContext, NotificationChannels.GENERAL);
        if (browserIntent != null && browserIntent.isWebIntent()) {
            // Make sure that this doesn't resolve back to an instant app
            browserIntent
                    .setComponent(null)
                    .setPackage(null)
                    .addFlags(Intent.FLAG_IGNORE_EPHEMERAL)
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            PendingIntent pendingIntent =
                    PendingIntent.getActivityAsUser(
                            mContext,
                            0 /* requestCode */,
                            browserIntent,
                            PendingIntent.FLAG_IMMUTABLE /* flags */,
                            null,
                            user);
            ComponentName aiaComponent = null;
            try {
                aiaComponent = AppGlobals.getPackageManager().getInstantAppInstallerComponent();
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
            Intent goToWebIntent =
                    new Intent()
                            .setComponent(aiaComponent)
                            .setAction(Intent.ACTION_VIEW)
                            .addCategory(Intent.CATEGORY_BROWSABLE)
                            .addCategory("unique:" + System.currentTimeMillis())
                            .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
                            .putExtra(
                                    Intent.EXTRA_VERSION_CODE,
                                    (int) (appInfo.versionCode & 0x7fffffff))
                            .putExtra(Intent.EXTRA_LONG_VERSION_CODE, appInfo.longVersionCode)
                            .putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, pendingIntent);

            PendingIntent webPendingIntent = PendingIntent.getActivityAsUser(mContext, 0,
                    goToWebIntent, PendingIntent.FLAG_IMMUTABLE, null, user);
            Notification.Action webAction =
                    new Notification.Action.Builder(
                                    null, mContext.getString(R.string.go_to_web), webPendingIntent)
                            .build();
            builder.addAction(webAction);
        }

        noMan.notifyAsUser(
                pkg,
                SystemMessage.NOTE_INSTANT_APPS,
                builder.addExtras(extras)
                        .addAction(action)
                        .setContentIntent(helpCenterIntent)
                        .setColor(mContext.getColor(R.color.instant_apps_color))
                        .setContentTitle(
                                mContext.getString(
                                        R.string.instant_apps_title,
                                        appInfo.loadLabel(mContext.getPackageManager())))
                        .setLargeIcon(Icon.createWithResource(pkg, appInfo.icon))
                        .setSmallIcon(
                                Icon.createWithResource(
                                        mContext.getPackageName(), R.drawable.instant_icon))
                        .setContentText(message)
                        .setStyle(new Notification.BigTextStyle().bigText(message))
                        .setOngoing(true)
                        .build(),
                new UserHandle(userId));
    }

    @Nullable
    private Intent getTaskIntent(int taskId, int userId) {
        try {
            final List<ActivityManager.RecentTaskInfo> tasks =
                    ActivityTaskManager.getService()
                            .getRecentTasks(NUM_TASKS_FOR_INSTANT_APP_INFO, 0, userId)
                            .getList();
            for (int i = 0; i < tasks.size(); i++) {
                if (tasks.get(i).id == taskId) {
                    return tasks.get(i).baseIntent;
                }
            }
        } catch (RemoteException e) {
            // Fall through
        }
        return null;
    }
}
