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