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

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ITaskStackListener;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MutableBoolean;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;

import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.RecentsComponent;
import com.android.systemui.SystemUI;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.recents.misc.Console;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.model.RecentsTaskLoadPlan;
import com.android.systemui.recents.model.RecentsTaskLoader;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskGrouping;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.TaskStackView;
import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm;
import com.android.systemui.recents.views.TaskViewHeader;
import com.android.systemui.recents.views.TaskViewTransform;
import com.android.systemui.statusbar.phone.PhoneStatusBar;

import java.util.ArrayList;

/**
 * Annotation for a method that is only called from the primary user's SystemUI process and will be
 * proxied to the current user.
 */
@interface ProxyFromPrimaryToCurrentUser {}
/**
 * Annotation for a method that may be called from any user's SystemUI process and will be proxied
 * to the primary user.
 */
@interface ProxyFromAnyToPrimaryUser {}

/** A proxy implementation for the recents component */
public class Recents extends SystemUI
        implements ActivityOptions.OnAnimationStartedListener, RecentsComponent {

    final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "triggeredFromAltTab";
    final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "triggeredFromHomeKey";
    final public static String EXTRA_RECENTS_VISIBILITY = "recentsVisibility";

    // Owner proxy events
    final public static String ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER =
            "action_notify_recents_visibility_change";
    final public static String ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER =
            "action_screen_pinning_request";

    final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation";
    final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity";
    final public static String ACTION_HIDE_RECENTS_ACTIVITY = "action_hide_recents_activity";

    final static int sMinToggleDelay = 350;

    public final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
    public final static String sRecentsPackage = "com.android.systemui";
    public final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";

    /**
     * An implementation of ITaskStackListener, that allows us to listen for changes to the system
     * task stacks and update recents accordingly.
     */
    class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable {
        Handler mHandler;

        public TaskStackListenerImpl(Handler handler) {
            mHandler = handler;
        }

        @Override
        public void onTaskStackChanged() {
            // Debounce any task stack changes
            mHandler.removeCallbacks(this);
            mHandler.post(this);
        }

        /** Preloads the next task */
        public void run() {
            // Temporarily skip this if multi stack is enabled
            if (mConfig.multiStackEnabled) return;

            RecentsConfiguration config = RecentsConfiguration.getInstance();
            if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
                RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
                SystemServicesProxy ssp = loader.getSystemServicesProxy();
                ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask();

                // Load the next task only if we aren't svelte
                RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
                loader.preloadTasks(plan, true /* isTopTaskHome */);
                RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
                // This callback is made when a new activity is launched and the old one is paused
                // so ignore the current activity and try and preload the thumbnail for the
                // previous one.
                if (runningTaskInfo != null) {
                    launchOpts.runningTaskId = runningTaskInfo.id;
                }
                launchOpts.numVisibleTasks = 2;
                launchOpts.numVisibleTaskThumbnails = 2;
                launchOpts.onlyLoadForCache = true;
                launchOpts.onlyLoadPausedActivities = true;
                loader.loadTasks(mContext, plan, launchOpts);
            }
        }
    }

    /**
     * A proxy for Recents events which happens strictly for the owner.
     */
    class RecentsOwnerEventProxyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()) {
                case ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER:
                    visibilityChanged(intent.getBooleanExtra(EXTRA_RECENTS_VISIBILITY, false));
                    break;
                case ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER:
                    onStartScreenPinning(context);
                    break;
            }
        }
    }

    static RecentsComponent.Callbacks sRecentsComponentCallbacks;
    static RecentsTaskLoadPlan sInstanceLoadPlan;
    static Recents sInstance;

    LayoutInflater mInflater;
    SystemServicesProxy mSystemServicesProxy;
    Handler mHandler;
    TaskStackListenerImpl mTaskStackListener;
    RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver;
    RecentsAppWidgetHost mAppWidgetHost;
    boolean mBootCompleted;
    boolean mStartAnimationTriggered;
    boolean mCanReuseTaskStackViews = true;

    // Task launching
    RecentsConfiguration mConfig;
    Rect mWindowRect = new Rect();
    Rect mTaskStackBounds = new Rect();
    Rect mSystemInsets = new Rect();
    TaskViewTransform mTmpTransform = new TaskViewTransform();
    int mStatusBarHeight;
    int mNavBarHeight;
    int mNavBarWidth;

    // Header (for transition)
    TaskViewHeader mHeaderBar;
    final Object mHeaderBarLock = new Object();
    TaskStackView mDummyStackView;

    // Variables to keep track of if we need to start recents after binding
    boolean mTriggeredFromAltTab;
    long mLastToggleTime;

    Bitmap mThumbnailTransitionBitmapCache;
    Task mThumbnailTransitionBitmapCacheKey;

    public Recents() {
    }

    /**
     * Gets the singleton instance and starts it if needed. On the primary user on the device, this
     * component gets started as a normal {@link SystemUI} component. On a secondary user, this
     * lifecycle doesn't exist, so we need to start it manually here if needed.
     */
    public static Recents getInstanceAndStartIfNeeded(Context ctx) {
        if (sInstance == null) {
            sInstance = new Recents();
            sInstance.mContext = ctx;
            sInstance.start();
            sInstance.onBootCompleted();
        }
        return sInstance;
    }

    /** Creates a new broadcast intent */
    static Intent createLocalBroadcastIntent(Context context, String action) {
        Intent intent = new Intent(action);
        intent.setPackage(context.getPackageName());
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
                Intent.FLAG_RECEIVER_FOREGROUND);
        return intent;
    }

    /** Initializes the Recents. */
    @ProxyFromPrimaryToCurrentUser
    @Override
    public void start() {
        if (sInstance == null) {
            sInstance = this;
        }
        RecentsTaskLoader.initialize(mContext);
        mInflater = LayoutInflater.from(mContext);
        mSystemServicesProxy = new SystemServicesProxy(mContext);
        mHandler = new Handler();
        mTaskStackBounds = new Rect();
        mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId);

        // Register the task stack listener
        mTaskStackListener = new TaskStackListenerImpl(mHandler);
        mSystemServicesProxy.registerTaskStackListener(mTaskStackListener);

        // Only the owner has the callback to update the SysUI visibility flags, so all non-owner
        // instances of AlternateRecentsComponent needs to notify the owner when the visibility
        // changes.
        if (mSystemServicesProxy.isForegroundUserOwner()) {
            mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction(Recents.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER);
            filter.addAction(Recents.ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER);
            mContext.registerReceiverAsUser(mProxyBroadcastReceiver, UserHandle.CURRENT, filter,
                    null, mHandler);
        }

        // Initialize some static datastructures
        TaskStackViewLayoutAlgorithm.initializeCurve();
        // Load the header bar layout
        reloadHeaderBarLayout();

        // When we start, preload the data associated with the previous recent tasks.
        // We can use a new plan since the caches will be the same.
        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
        loader.preloadTasks(plan, true /* isTopTaskHome */);
        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
        launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize();
        launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
        launchOpts.onlyLoadForCache = true;
        loader.loadTasks(mContext, plan, launchOpts);
        putComponent(Recents.class, this);
    }

    @Override
    public void onBootCompleted() {
        mBootCompleted = true;
    }

    /** Shows the Recents. */
    @ProxyFromPrimaryToCurrentUser
    @Override
    public void showRecents(boolean triggeredFromAltTab, View statusBarView) {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        if (mSystemServicesProxy.isForegroundUserOwner()) {
            showRecentsInternal(triggeredFromAltTab);
        } else {
            Intent intent = createLocalBroadcastIntent(mContext,
                    RecentsUserEventProxyReceiver.ACTION_PROXY_SHOW_RECENTS_TO_USER);
            intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }

    void showRecentsInternal(boolean triggeredFromAltTab) {
        mTriggeredFromAltTab = triggeredFromAltTab;

        try {
            startRecentsActivity();
        } catch (ActivityNotFoundException e) {
            Console.logRawError("Failed to launch RecentAppsIntent", e);
        }
    }

    /** Hides the Recents. */
    @ProxyFromPrimaryToCurrentUser
    @Override
    public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        if (mSystemServicesProxy.isForegroundUserOwner()) {
            hideRecentsInternal(triggeredFromAltTab, triggeredFromHomeKey);
        } else {
            Intent intent = createLocalBroadcastIntent(mContext,
                    RecentsUserEventProxyReceiver.ACTION_PROXY_HIDE_RECENTS_TO_USER);
            intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
            intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }

    void hideRecentsInternal(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
        if (mBootCompleted) {
            // Defer to the activity to handle hiding recents, if it handles it, then it must still
            // be visible
            Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY);
            intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
            intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }

    /** Toggles the Recents activity. */
    @ProxyFromPrimaryToCurrentUser
    @Override
    public void toggleRecents(Display display, int layoutDirection, View statusBarView) {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        if (mSystemServicesProxy.isForegroundUserOwner()) {
            toggleRecentsInternal();
        } else {
            Intent intent = createLocalBroadcastIntent(mContext,
                    RecentsUserEventProxyReceiver.ACTION_PROXY_TOGGLE_RECENTS_TO_USER);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }

    void toggleRecentsInternal() {
        mTriggeredFromAltTab = false;

        try {
            toggleRecentsActivity();
        } catch (ActivityNotFoundException e) {
            Console.logRawError("Failed to launch RecentAppsIntent", e);
        }
    }

    /** Preloads info for the Recents activity. */
    @ProxyFromPrimaryToCurrentUser
    @Override
    public void preloadRecents() {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        if (mSystemServicesProxy.isForegroundUserOwner()) {
            preloadRecentsInternal();
        } else {
            Intent intent = createLocalBroadcastIntent(mContext,
                    RecentsUserEventProxyReceiver.ACTION_PROXY_PRELOAD_RECENTS_TO_USER);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }

    void preloadRecentsInternal() {
        // Preload only the raw task list into a new load plan (which will be consumed by the
        // RecentsActivity) only if there is a task to animate to.
        ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask();
        MutableBoolean topTaskHome = new MutableBoolean(true);
        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
        sInstanceLoadPlan = loader.createLoadPlan(mContext);
        if (topTask != null && !mSystemServicesProxy.isRecentsTopMost(topTask, topTaskHome)) {
            sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
            loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value);
            TaskStack top = sInstanceLoadPlan.getAllTaskStacks().get(0);
            if (top.getTaskCount() > 0) {
                preCacheThumbnailTransitionBitmapAsync(topTask, top, mDummyStackView,
                        topTaskHome.value);
            }
        }
    }

    @Override
    public void cancelPreloadingRecents() {
        // Do nothing
    }

    void showRelativeAffiliatedTask(boolean showNextTask) {
        // Return early if there is no focused stack
        int focusedStackId = mSystemServicesProxy.getFocusedStack();
        TaskStack focusedStack = null;
        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
        loader.preloadTasks(plan, true /* isTopTaskHome */);
        if (mConfig.multiStackEnabled) {
            if (focusedStackId < 0) return;
            focusedStack = plan.getTaskStack(focusedStackId);
        } else {
            focusedStack = plan.getAllTaskStacks().get(0);
        }

        // Return early if there are no tasks in the focused stack
        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;

        ActivityManager.RunningTaskInfo runningTask = mSystemServicesProxy.getTopMostTask();
        // Return early if there is no running task (can't determine affiliated tasks in this case)
        if (runningTask == null) return;
        // Return early if the running task is in the home stack (optimization)
        if (mSystemServicesProxy.isInHomeStack(runningTask.id)) return;

        // Find the task in the recents list
        ArrayList<Task> tasks = focusedStack.getTasks();
        Task toTask = null;
        ActivityOptions launchOpts = null;
        int taskCount = tasks.size();
        int numAffiliatedTasks = 0;
        for (int i = 0; i < taskCount; i++) {
            Task task = tasks.get(i);
            if (task.key.id == runningTask.id) {
                TaskGrouping group = task.group;
                Task.TaskKey toTaskKey;
                if (showNextTask) {
                    toTaskKey = group.getNextTaskInGroup(task);
                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
                            R.anim.recents_launch_next_affiliated_task_target,
                            R.anim.recents_launch_next_affiliated_task_source);
                } else {
                    toTaskKey = group.getPrevTaskInGroup(task);
                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
                            R.anim.recents_launch_prev_affiliated_task_target,
                            R.anim.recents_launch_prev_affiliated_task_source);
                }
                if (toTaskKey != null) {
                    toTask = focusedStack.findTaskWithId(toTaskKey.id);
                }
                numAffiliatedTasks = group.getTaskCount();
                break;
            }
        }

        // Return early if there is no next task
        if (toTask == null) {
            if (numAffiliatedTasks > 1) {
                if (showNextTask) {
                    mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication(
                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
                                    R.anim.recents_launch_next_affiliated_task_bounce));
                } else {
                    mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication(
                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
                                    R.anim.recents_launch_prev_affiliated_task_bounce));
                }
            }
            return;
        }

        // Keep track of actually launched affiliated tasks
        MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1);

        // Launch the task
        if (toTask.isActive) {
            // Bring an active task to the foreground
            mSystemServicesProxy.moveTaskToFront(toTask.key.id, launchOpts);
        } else {
            mSystemServicesProxy.startActivityFromRecents(mContext, toTask.key.id,
                    toTask.activityLabel, launchOpts);
        }
    }

    @Override
    public void showNextAffiliatedTask() {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        // Keep track of when the affiliated task is triggered
        MetricsLogger.count(mContext, "overview_affiliated_task_next", 1);
        showRelativeAffiliatedTask(true);
    }

    @Override
    public void showPrevAffiliatedTask() {
        // Ensure the device has been provisioned before allowing the user to interact with
        // recents
        if (!isDeviceProvisioned()) {
            return;
        }

        // Keep track of when the affiliated task is triggered
        MetricsLogger.count(mContext, "overview_affiliated_task_prev", 1);
        showRelativeAffiliatedTask(false);
    }

    /** Updates on configuration change. */
    @ProxyFromPrimaryToCurrentUser
    public void onConfigurationChanged(Configuration newConfig) {
        if (mSystemServicesProxy.isForegroundUserOwner()) {
            configurationChanged();
        } else {
            Intent intent = createLocalBroadcastIntent(mContext,
                    RecentsUserEventProxyReceiver.ACTION_PROXY_CONFIG_CHANGE_TO_USER);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
        }
    }
    void configurationChanged() {
        // Don't reuse task stack views if the configuration changes
        mCanReuseTaskStackViews = false;
        // Reload the header bar layout
        reloadHeaderBarLayout();
    }

    /** Prepares the header bar layout. */
    void reloadHeaderBarLayout() {
        Resources res = mContext.getResources();
        mWindowRect = mSystemServicesProxy.getWindowRect();
        mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
        mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height);
        mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width);
        mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy);
        mConfig.updateOnConfigurationChange();
        Rect searchBarBounds = new Rect();
        // Try and pre-emptively bind the search widget on startup to ensure that we
        // have the right thumbnail bounds to animate to.
        // Note: We have to reload the widget id before we get the task stack bounds below
        if (mSystemServicesProxy.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) {
            mConfig.getSearchBarBounds(mWindowRect.width(), mWindowRect.height(),
                    mStatusBarHeight, searchBarBounds);
        }
        mConfig.getAvailableTaskStackBounds(mWindowRect.width(), mWindowRect.height(),
                mStatusBarHeight, (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), searchBarBounds,
                mTaskStackBounds);
        if (mConfig.isLandscape && mConfig.hasTransposedNavBar) {
            mSystemInsets.set(0, mStatusBarHeight, mNavBarWidth, 0);
        } else {
            mSystemInsets.set(0, mStatusBarHeight, 0, mNavBarHeight);
        }

        // Inflate the header bar layout so that we can rebind and draw it for the transition
        TaskStack stack = new TaskStack();
        mDummyStackView = new TaskStackView(mContext, stack);
        TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm();
        Rect taskStackBounds = new Rect(mTaskStackBounds);
        taskStackBounds.bottom -= mSystemInsets.bottom;
        algo.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds);
        Rect taskViewSize = algo.getUntransformedTaskViewSize();
        int taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height);
        synchronized (mHeaderBarLock) {
            mHeaderBar = (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null,
                    false);
            mHeaderBar.measure(
                    View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY),
                    View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY));
            mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight);
        }
    }

    /** Toggles the recents activity */
    void toggleRecentsActivity() {
        // If the user has toggled it too quickly, then just eat up the event here (it's better than
        // showing a janky screenshot).
        // NOTE: Ideally, the screenshot mechanism would take the window transform into account
        if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
            return;
        }

        // If Recents is the front most activity, then we should just communicate with it directly
        // to launch the first task or dismiss itself
        ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask();
        MutableBoolean isTopTaskHome = new MutableBoolean(true);
        if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) {
            // Notify recents to toggle itself
            Intent intent = createLocalBroadcastIntent(mContext, ACTION_TOGGLE_RECENTS_ACTIVITY);
            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
            mLastToggleTime = SystemClock.elapsedRealtime();
            return;
        } else {
            // Otherwise, start the recents activity
            startRecentsActivity(topTask, isTopTaskHome.value);
        }
    }

    /** Starts the recents activity if it is not already running */
    void startRecentsActivity() {
        // Check if the top task is in the home stack, and start the recents activity
        ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask();
        MutableBoolean isTopTaskHome = new MutableBoolean(true);
        if (topTask == null || !mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) {
            startRecentsActivity(topTask, isTopTaskHome.value);
        }
    }

    /**
     * Creates the activity options for a unknown state->recents transition.
     */
    ActivityOptions getUnknownTransitionActivityOptions() {
        mStartAnimationTriggered = false;
        return ActivityOptions.makeCustomAnimation(mContext,
                R.anim.recents_from_unknown_enter,
                R.anim.recents_from_unknown_exit,
                mHandler, this);
    }

    /**
     * Creates the activity options for a home->recents transition.
     */
    ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) {
        mStartAnimationTriggered = false;
        if (fromSearchHome) {
            return ActivityOptions.makeCustomAnimation(mContext,
                    R.anim.recents_from_search_launcher_enter,
                    R.anim.recents_from_search_launcher_exit,
                    mHandler, this);
        }
        return ActivityOptions.makeCustomAnimation(mContext,
                R.anim.recents_from_launcher_enter,
                R.anim.recents_from_launcher_exit,
                mHandler, this);
    }

    /**
     * Creates the activity options for an app->recents transition.
     */
    ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask,
            TaskStack stack, TaskStackView stackView) {

        // Update the destination rect
        Task toTask = new Task();
        TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
                topTask.id, toTask);
        Rect toTaskRect = toTransform.rect;
        Bitmap thumbnail;
        if (mThumbnailTransitionBitmapCacheKey != null
                && mThumbnailTransitionBitmapCacheKey.key != null
                && mThumbnailTransitionBitmapCacheKey.key.equals(toTask.key)) {
            thumbnail = mThumbnailTransitionBitmapCache;
            mThumbnailTransitionBitmapCacheKey = null;
            mThumbnailTransitionBitmapCache = null;
        } else {
            preloadIcon(topTask);
            thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform);
        }
        if (thumbnail != null) {
            mStartAnimationTriggered = false;
            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
                    thumbnail, toTaskRect.left, toTaskRect.top, toTaskRect.width(),
                    toTaskRect.height(), mHandler, this);
        }

        // If both the screenshot and thumbnail fails, then just fall back to the default transition
        return getUnknownTransitionActivityOptions();
    }

    /**
     * Preloads the icon of a task.
     */
    void preloadIcon(ActivityManager.RunningTaskInfo task) {

        // Ensure that we load the running task's icon
        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
        launchOpts.runningTaskId = task.id;
        launchOpts.loadThumbnails = false;
        launchOpts.onlyLoadForCache = true;
        RecentsTaskLoader.getInstance().loadTasks(mContext, sInstanceLoadPlan, launchOpts);
    }

    /**
     * Caches the header thumbnail used for a window animation asynchronously into
     * {@link #mThumbnailTransitionBitmapCache}.
     */
    void preCacheThumbnailTransitionBitmapAsync(ActivityManager.RunningTaskInfo topTask,
            TaskStack stack, TaskStackView stackView, boolean isTopTaskHome) {
        preloadIcon(topTask);

        // Update the destination rect
        mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome);
        final Task toTask = new Task();
        final TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
                topTask.id, toTask);
        new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... params) {
                return drawThumbnailTransitionBitmap(toTask, toTransform);
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                mThumbnailTransitionBitmapCache = bitmap;
                mThumbnailTransitionBitmapCacheKey = toTask;
            }
        }.execute();
    }

    /**
     * Draws the header of a task used for the window animation into a bitmap.
     */
    Bitmap drawThumbnailTransitionBitmap(Task toTask, TaskViewTransform toTransform) {
        if (toTransform != null && toTask.key != null) {
            Bitmap thumbnail;
            synchronized (mHeaderBarLock) {
                int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale);
                int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale);
                thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight,
                        Bitmap.Config.ARGB_8888);
                if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
                    thumbnail.eraseColor(0xFFff0000);
                } else {
                    Canvas c = new Canvas(thumbnail);
                    c.scale(toTransform.scale, toTransform.scale);
                    mHeaderBar.rebindToTask(toTask);
                    mHeaderBar.draw(c);
                    c.setBitmap(null);
                }
            }
            return thumbnail.createAshmemBitmap();
        }
        return null;
    }

    /** Returns the transition rect for the given task id. */
    TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView,
            int runningTaskId, Task runningTaskOut) {
        // Find the running task in the TaskStack
        Task task = null;
        ArrayList<Task> tasks = stack.getTasks();
        if (runningTaskId != -1) {
            // Otherwise, try and find the task with the
            int taskCount = tasks.size();
            for (int i = taskCount - 1; i >= 0; i--) {
                Task t = tasks.get(i);
                if (t.key.id == runningTaskId) {
                    task = t;
                    runningTaskOut.copyFrom(t);
                    break;
                }
            }
        }
        if (task == null) {
            // If no task is specified or we can not find the task just use the front most one
            task = tasks.get(tasks.size() - 1);
            runningTaskOut.copyFrom(task);
        }

        // Get the transform for the running task
        stackView.getScroller().setStackScrollToInitialState();
        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
                stackView.getScroller().getStackScroll(), mTmpTransform, null);
        return mTmpTransform;
    }

    /** Starts the recents activity */
    void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) {
        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
        RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy);

        if (sInstanceLoadPlan == null) {
            // Create a new load plan if onPreloadRecents() was never triggered
            sInstanceLoadPlan = loader.createLoadPlan(mContext);
        }

        // Temporarily skip the transition (use a dummy fade) if multi stack is enabled.
        // For multi-stack we need to figure out where each of the tasks are going.
        if (mConfig.multiStackEnabled) {
            loader.preloadTasks(sInstanceLoadPlan, true);
            ArrayList<TaskStack> stacks = sInstanceLoadPlan.getAllTaskStacks();
            TaskStack stack = stacks.get(0);
            mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, true);
            TaskStackViewLayoutAlgorithm.VisibilityReport stackVr =
                    mDummyStackView.computeStackVisibilityReport();
            ActivityOptions opts = getUnknownTransitionActivityOptions();
            startAlternateRecentsActivity(topTask, opts, true /* fromHome */,
                    false /* fromSearchHome */, false /* fromThumbnail */, stackVr);
            return;
        }

        if (!sInstanceLoadPlan.hasTasks()) {
            loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
        }
        ArrayList<TaskStack> stacks = sInstanceLoadPlan.getAllTaskStacks();
        TaskStack stack = stacks.get(0);

        // Prepare the dummy stack for the transition
        mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome);
        TaskStackViewLayoutAlgorithm.VisibilityReport stackVr =
                mDummyStackView.computeStackVisibilityReport();
        boolean hasRecentTasks = stack.getTaskCount() > 0;
        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;

        if (useThumbnailTransition) {

            // Try starting with a thumbnail transition
            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack,
                    mDummyStackView);
            if (opts != null) {
                startAlternateRecentsActivity(topTask, opts, false /* fromHome */,
                        false /* fromSearchHome */, true /* fromThumbnail */, stackVr);
            } else {
                // Fall through below to the non-thumbnail transition
                useThumbnailTransition = false;
            }
        }

        if (!useThumbnailTransition) {
            // If there is no thumbnail transition, but is launching from home into recents, then
            // use a quick home transition and do the animation from home
            if (hasRecentTasks) {
                String homeActivityPackage = mSystemServicesProxy.getHomeActivityPackageName();
                String searchWidgetPackage =
                        Prefs.getString(mContext, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null);

                // Determine whether we are coming from a search owned home activity
                boolean fromSearchHome = (homeActivityPackage != null) &&
                        homeActivityPackage.equals(searchWidgetPackage);
                ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome);
                startAlternateRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome,
                        false /* fromThumbnail */, stackVr);
            } else {
                // Otherwise we do the normal fade from an unknown source
                ActivityOptions opts = getUnknownTransitionActivityOptions();
                startAlternateRecentsActivity(topTask, opts, true /* fromHome */,
                        false /* fromSearchHome */, false /* fromThumbnail */, stackVr);
            }
        }
        mLastToggleTime = SystemClock.elapsedRealtime();
    }

    /** Starts the recents activity */
    void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask,
            ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail,
            TaskStackViewLayoutAlgorithm.VisibilityReport vr) {
        // Update the configuration based on the launch options
        mConfig.launchedFromHome = fromSearchHome || fromHome;
        mConfig.launchedFromSearchHome = fromSearchHome;
        mConfig.launchedFromAppWithThumbnail = fromThumbnail;
        mConfig.launchedToTaskId = (topTask != null) ? topTask.id : -1;
        mConfig.launchedWithAltTab = mTriggeredFromAltTab;
        mConfig.launchedReuseTaskStackViews = mCanReuseTaskStackViews;
        mConfig.launchedNumVisibleTasks = vr.numVisibleTasks;
        mConfig.launchedNumVisibleThumbnails = vr.numVisibleThumbnails;
        mConfig.launchedHasConfigurationChanged = false;

        Intent intent = new Intent(sToggleRecentsAction);
        intent.setClassName(sRecentsPackage, sRecentsActivity);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
        if (opts != null) {
            mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
        } else {
            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
        }
        mCanReuseTaskStackViews = true;
    }

    /** Sets the RecentsComponent callbacks. */
    @Override
    public void setCallback(RecentsComponent.Callbacks cb) {
        sRecentsComponentCallbacks = cb;
    }

    /** Notifies the callbacks that the visibility of Recents has changed. */
    @ProxyFromAnyToPrimaryUser
    public static void notifyVisibilityChanged(Context context, SystemServicesProxy ssp,
            boolean visible) {
        if (ssp.isForegroundUserOwner()) {
            visibilityChanged(visible);
        } else {
            Intent intent = createLocalBroadcastIntent(context,
                    ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER);
            intent.putExtra(EXTRA_RECENTS_VISIBILITY, visible);
            context.sendBroadcastAsUser(intent, UserHandle.OWNER);
        }
    }
    static void visibilityChanged(boolean visible) {
        if (sRecentsComponentCallbacks != null) {
            sRecentsComponentCallbacks.onVisibilityChanged(visible);
        }
    }

    /** Notifies the status bar to trigger screen pinning. */
    @ProxyFromAnyToPrimaryUser
    public static void startScreenPinning(Context context, SystemServicesProxy ssp) {
        if (ssp.isForegroundUserOwner()) {
            onStartScreenPinning(context);
        } else {
            Intent intent = createLocalBroadcastIntent(context,
                    ACTION_PROXY_SCREEN_PINNING_REQUEST_TO_OWNER);
            context.sendBroadcastAsUser(intent, UserHandle.OWNER);
        }
    }
    static void onStartScreenPinning(Context context) {
        // For the primary user, the context for the SystemUI component is the SystemUIApplication
        SystemUIApplication app = (SystemUIApplication)
                getInstanceAndStartIfNeeded(context).mContext;
        PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class);
        if (statusBar != null) {
            statusBar.showScreenPinningRequest(false);
        }
    }

    /**
     * @return whether this device is provisioned.
     */
    private boolean isDeviceProvisioned() {
        return Settings.Global.getInt(mContext.getContentResolver(),
                Settings.Global.DEVICE_PROVISIONED, 0) != 0;
    }

    /**
     * Returns the preloaded load plan and invalidates it.
     */
    public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
        RecentsTaskLoadPlan plan = sInstanceLoadPlan;
        sInstanceLoadPlan = null;
        return plan;
    }

    /**** OnAnimationStartedListener Implementation ****/

    @Override
    public void onAnimationStarted() {
        // Notify recents to start the enter animation
        if (!mStartAnimationTriggered) {
            // There can be a race condition between the start animation callback and
            // the start of the new activity (where we register the receiver that listens
            // to this broadcast, so we add our own receiver and if that gets called, then
            // we know the activity has not yet started and we can retry sending the broadcast.
            BroadcastReceiver fallbackReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (getResultCode() == Activity.RESULT_OK) {
                        mStartAnimationTriggered = true;
                        return;
                    }

                    // Schedule for the broadcast to be sent again after some time
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            onAnimationStarted();
                        }
                    }, 25);
                }
            };

            // Send the broadcast to notify Recents that the animation has started
            Intent intent = createLocalBroadcastIntent(mContext, ACTION_START_ENTER_ANIMATION);
            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
                    fallbackReceiver, null, Activity.RESULT_CANCELED, null, null);
        }
    }
}
