// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.multiwindow; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.AppTask; import android.content.Context; import android.content.Intent; import android.os.Build; import android.provider.Browser; import android.text.TextUtils; import org.chromium.base.ActivityState; import org.chromium.base.ApplicationStatus; import org.chromium.base.ApplicationStatus.ActivityStateListener; import org.chromium.base.ContextUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.ChromeTabbedActivity; import org.chromium.chrome.browser.ChromeTabbedActivity2; import org.chromium.chrome.browser.IntentHandler; import org.chromium.chrome.browser.document.ChromeLauncherActivity; import org.chromium.chrome.browser.util.IntentUtils; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * Utilities for detecting multi-window/multi-instance support. * * Thread-safe: This class may be accessed from any thread. */ public class MultiWindowUtils implements ActivityStateListener { // TODO(twellington): replace this with Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT once we're building // against N. public static final int FLAG_ACTIVITY_LAUNCH_ADJACENT = 0x00001000; private static AtomicReference<MultiWindowUtils> sInstance = new AtomicReference<MultiWindowUtils>(); // Used to keep track of whether ChromeTabbedActivity2 is running. A tri-state Boolean is // used in case both activities die in the background and MultiWindowUtils is recreated. private Boolean mTabbedActivity2TaskRunning; private WeakReference<ChromeTabbedActivity> mLastResumedTabbedActivity; private boolean mIsInMultiWindowModeForTesting; /** * Returns the singleton instance of MultiWindowUtils, creating it if needed. */ public static MultiWindowUtils getInstance() { if (sInstance.get() == null) { ChromeApplication application = (ChromeApplication) ContextUtils.getApplicationContext(); sInstance.compareAndSet(null, application.createMultiWindowUtils()); } return sInstance.get(); } /** * @param activity The {@link Activity} to check. * @return Whether or not {@code activity} is currently in Android N+ multi-window mode. */ public boolean isInMultiWindowMode(Activity activity) { if (mIsInMultiWindowModeForTesting) return true; if (activity == null) return false; if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { try { Method isInMultiWindowModeMethod = Activity.class.getMethod("isInMultiWindowMode"); boolean isInMultiWindowMode = (boolean) isInMultiWindowModeMethod.invoke(activity); return isInMultiWindowMode; } catch (NoSuchMethodException e) { // Ignore. } catch (IllegalAccessException e) { // Ignore. } catch (IllegalArgumentException e) { // Ignore. } catch (InvocationTargetException e) { // Ignore. } } return false; } @VisibleForTesting public void setIsInMultiWindowModeForTesting(boolean isInMultiWindowMode) { mIsInMultiWindowModeForTesting = isInMultiWindowMode; } /** * Returns whether the given activity currently supports opening tabs in or moving tabs to the * other window. */ public boolean isOpenInOtherWindowSupported(Activity activity) { // Supported only in multi-window mode and if activity supports side-by-side instances. return isInMultiWindowMode(activity) && getOpenInOtherWindowActivity(activity) != null; } /** * Returns the activity to use when handling "open in other window" or "move to other window". * Returns null if the current activity doesn't support opening/moving tabs to another activity. */ public Class<? extends Activity> getOpenInOtherWindowActivity(Activity current) { if (current instanceof ChromeTabbedActivity2) { // If a second ChromeTabbedActivity is created, MultiWindowUtils needs to listen for // activity state changes to facilitate determining which ChromeTabbedActivity should // be used for intents. ApplicationStatus.registerStateListenerForAllActivities(sInstance.get()); return ChromeTabbedActivity.class; } else if (current instanceof ChromeTabbedActivity) { mTabbedActivity2TaskRunning = true; ApplicationStatus.registerStateListenerForAllActivities(sInstance.get()); return ChromeTabbedActivity2.class; } else { return null; } } /** * Sets extras on the intent used when handling "open in other window" or * "move to other window". Specifically, sets the class, adds the launch adjacent flag, and * adds extras so that Chrome behaves correctly when the back button is pressed. * @param intent The intent to set details on. * @param activity The activity firing the intent. * @param targetActivity The class of the activity receiving the intent. */ public static void setOpenInOtherWindowIntentExtras( Intent intent, Activity activity, Class<? extends Activity> targetActivity) { intent.setClass(activity, targetActivity); intent.addFlags(MultiWindowUtils.FLAG_ACTIVITY_LAUNCH_ADJACENT); // Let Chrome know that this intent is from Chrome, so that it does not close the app when // the user presses 'back' button. intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true); } @Override public void onActivityStateChange(Activity activity, int newState) { if (newState == ActivityState.RESUMED && activity instanceof ChromeTabbedActivity) { mLastResumedTabbedActivity = new WeakReference<ChromeTabbedActivity>((ChromeTabbedActivity) activity); } } /** * Determines the correct ChromeTabbedActivity class to use for an incoming intent. * @param intent The incoming intent that is starting ChromeTabbedActivity. * @param context The current Context, used to retrieve the ActivityManager system service. * @return The ChromeTabbedActivity to use for the incoming intent. */ public Class<? extends ChromeTabbedActivity> getTabbedActivityForIntent(Intent intent, Context context) { // 1. Exit early if the build version doesn't support Android N+ multi-window mode or // ChromeTabbedActivity2 isn't running. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M || (mTabbedActivity2TaskRunning != null && !mTabbedActivity2TaskRunning)) { return ChromeTabbedActivity.class; } // 2. If the intent has a window id set, use that. if (intent.hasExtra(IntentHandler.EXTRA_WINDOW_ID)) { int windowId = IntentUtils.safeGetIntExtra(intent, IntentHandler.EXTRA_WINDOW_ID, 0); if (windowId == 1) return ChromeTabbedActivity.class; if (windowId == 2) return ChromeTabbedActivity2.class; } // 3. If only one ChromeTabbedActivity is currently in Android recents, use it. boolean tabbed2TaskRunning = isActivityTaskInRecents( ChromeTabbedActivity2.class.getName(), context); // Exit early if ChromeTabbedActivity2 isn't running. if (!tabbed2TaskRunning) { mTabbedActivity2TaskRunning = false; return ChromeTabbedActivity.class; } boolean tabbedTaskRunning = isActivityTaskInRecents( ChromeTabbedActivity.class.getName(), context); if (!tabbedTaskRunning) { return ChromeTabbedActivity2.class; } // 4. If only one of the ChromeTabbedActivity's is currently visible use it. // e.g. ChromeTabbedActivity is docked to the top of the screen and another app is docked // to the bottom. // Find the activities. Activity tabbedActivity = null; Activity tabbedActivity2 = null; for (WeakReference<Activity> reference : ApplicationStatus.getRunningActivities()) { Activity activity = reference.get(); if (activity == null) continue; if (activity.getClass().equals(ChromeTabbedActivity.class)) { tabbedActivity = activity; } else if (activity.getClass().equals(ChromeTabbedActivity2.class)) { tabbedActivity2 = activity; } } // Determine if only one is visible. boolean tabbedActivityVisible = isActivityVisible(tabbedActivity); boolean tabbedActivity2Visible = isActivityVisible(tabbedActivity2); if (tabbedActivityVisible ^ tabbedActivity2Visible) { if (tabbedActivityVisible) return ChromeTabbedActivity.class; return ChromeTabbedActivity2.class; } // 5. Use the ChromeTabbedActivity that was resumed most recently if it's still running. if (mLastResumedTabbedActivity != null) { ChromeTabbedActivity lastResumedActivity = mLastResumedTabbedActivity.get(); if (lastResumedActivity != null) { Class<?> lastResumedClassName = lastResumedActivity.getClass(); if (tabbedTaskRunning && lastResumedClassName.equals(ChromeTabbedActivity.class)) { return ChromeTabbedActivity.class; } if (tabbed2TaskRunning && lastResumedClassName.equals(ChromeTabbedActivity2.class)) { return ChromeTabbedActivity2.class; } } } // 6. Default to regular ChromeTabbedActivity. return ChromeTabbedActivity.class; } /** * Should be called when multi-instance mode is started. This method is responsible for * notifying classes that are multi-instance aware. */ public static void onMultiInstanceModeStarted() { ChromeTabbedActivity.onMultiInstanceModeStarted(); } /** * @param className The class name of the Activity to look for in Android recents * @param context The current Context, used to retrieve the ActivityManager system service. * @return True if the Activity still has a task in Android recents, regardless of whether * the Activity has been destroyed. */ @TargetApi(Build.VERSION_CODES.M) private boolean isActivityTaskInRecents(String className, Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<AppTask> appTasks = activityManager.getAppTasks(); for (AppTask task : appTasks) { if (task.getTaskInfo() == null || task.getTaskInfo().baseActivity == null) continue; String baseActivity = task.getTaskInfo().baseActivity.getClassName(); if (TextUtils.equals(baseActivity, className)) return true; } return false; } /** * @param activity The Activity whose visibility to test. * @return True iff the given Activity is currently visible. */ public static boolean isActivityVisible(Activity activity) { if (activity == null) return false; int activityState = ApplicationStatus.getStateForActivity(activity); // In Android N multi-window mode, only one activity is resumed at a time. The other // activity visible on the screen will be in the paused state. Activities not visible on // the screen will be stopped or destroyed. return activityState == ActivityState.RESUMED || activityState == ActivityState.PAUSED; } @VisibleForTesting public Boolean getTabbedActivity2TaskRunning() { return mTabbedActivity2TaskRunning; } /** * @param activity The {@link Activity} to check. * @return Whether or not {@code activity} is currently in pre-N Samsung multi-window mode. */ public boolean isLegacyMultiWindow(Activity activity) { // This logic is overridden in a subclass. return false; } /** * @param activity The {@link Activity} to check. * @return Whether or not {@code activity} should run in pre-N Samsung multi-instance mode. */ public boolean shouldRunInLegacyMultiInstanceMode(ChromeLauncherActivity activity) { return Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP && TextUtils.equals(activity.getIntent().getAction(), Intent.ACTION_MAIN) && isLegacyMultiWindow(activity) && activity.isChromeBrowserActivityRunning(); } /** * Makes |intent| able to support multi-instance in pre-N Samsung multi-window mode. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void makeLegacyMultiInstanceIntent(ChromeLauncherActivity activity, Intent intent) { if (isLegacyMultiWindow(activity)) { if (TextUtils.equals(ChromeTabbedActivity.class.getName(), intent.getComponent().getClassName())) { intent.setClassName(activity, MultiInstanceChromeTabbedActivity.class.getName()); } intent.setFlags(intent.getFlags() & ~(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT)); } } }