Fix issue #16311398: Limit number of documents a process can open
In application processes, monitor for when we start getting close
to the Dalvik heap limit, and ask the activity manager to try to
prune old activity instances in that case.
Add an explicit API for apps to ask that they have their own
activity instances cleaned up, if they want.
Fix some bugs in launching activities that were not correctly
applying the "multi task" behavior in the appropriate situations
of document-centric recents.
Clean up the activity manager's process removal code to all share
a common path.
Add a new "Spam" option to ActivityTests, which continually creates
new tasks, checking that the activity manager will now prune old
tasks rather than letting the app run out of RAM.
And while I was was doing this, I found problems with the path
for bringing an empty task to the foreground -- it could make
a new task instead of re-starting the root activity in the
existing task. This is fixed, and some code in the recents
UI for working around the bug is removed.
And as long as I am doing that, we now have nice hooks in to
the activity manager for AppTask to give some APIs for better
managing the task, so add those along with more tests for these
APIs in ActivityTests.
We should look at also having the activity manager try to prune
old tasks when it sees app processes being killed, to better balance
memory use across multiple processes when some processes may host
many documents. That however is for another CL...
Change-Id: I2bb81c3f92819350c868c7a7470b35817eb9bea9
diff --git a/api/current.txt b/api/current.txt
index 4d8d635..6909af82 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3517,6 +3517,7 @@
method public void postponeEnterTransition();
method public void recreate();
method public void registerForContextMenu(android.view.View);
+ method public boolean releaseInstance();
method public final deprecated void removeDialog(int);
method public void reportFullyDrawn();
method public boolean requestVisibleBehind(boolean);
@@ -3636,7 +3637,9 @@
public static class ActivityManager.AppTask {
method public void finishAndRemoveTask();
method public android.app.ActivityManager.RecentTaskInfo getTaskInfo();
+ method public void moveToFront();
method public void setExcludeFromRecents(boolean);
+ method public void startActivity(android.content.Context, android.content.Intent, android.os.Bundle);
}
public static class ActivityManager.MemoryInfo implements android.os.Parcelable {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 2e66a4c..9b7cc1c 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -4704,6 +4704,26 @@
}
/**
+ * Ask that the local app instance of this activity be released to free up its memory.
+ * This is asking for the activity to be destroyed, but does <b>not</b> finish the activity --
+ * a new instance of the activity will later be re-created if needed due to the user
+ * navigating back to it.
+ *
+ * @return Returns true if the activity was in a state that it has started the process
+ * of destroying its current instance; returns false if for any reason this could not
+ * be done: it is currently visible to the user, it is already being destroyed, it is
+ * being finished, it hasn't yet saved its state, etc.
+ */
+ public boolean releaseInstance() {
+ try {
+ return ActivityManagerNative.getDefault().releaseActivityInstance(mToken);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ /**
* Called when an activity you launched exits, giving you the requestCode
* you started it with, the resultCode it returned, and any additional
* data from it. The <var>resultCode</var> will be
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 2667bcd..58790f6 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -2617,6 +2617,40 @@
}
/**
+ * Bring this task to the foreground. If it contains activities, they will be
+ * brought to the foreground with it and their instances re-created if needed.
+ * If it doesn't contain activities, the root activity of the task will be
+ * re-launched.
+ */
+ public void moveToFront() {
+ try {
+ mAppTaskImpl.moveToFront();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Invalid AppTask", e);
+ }
+ }
+
+ /**
+ * Start an activity in this task. Brings the task to the foreground. If this task
+ * is not currently active (that is, its id < 0), then the activity being started
+ * needs to be started as a new task and the Intent's ComponentName must match the
+ * base ComponenentName of the recent task entry. Otherwise, the activity being
+ * started must <b>not</b> be launched as a new task -- not through explicit intent
+ * flags nor implicitly as the singleTask or singleInstance launch modes.
+ *
+ * <p>See {@link Activity#startActivity(android.content.Intent, android.os.Bundle)
+ * Activity.startActivity} for more information.</p>
+ *
+ * @param intent The Intent describing the new activity to be launched on the task.
+ * @param options Optional launch options.
+ */
+ public void startActivity(Context context, Intent intent, Bundle options) {
+ ActivityThread thread = ActivityThread.currentActivityThread();
+ thread.getInstrumentation().execStartActivityFromAppTask(context,
+ thread.getApplicationThread(), mAppTaskImpl, intent, options);
+ }
+
+ /**
* Modify the {@link Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} flag in the root
* Intent of this AppTask.
*
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 82af99b..36e8892 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -365,6 +365,23 @@
return true;
}
+ case RELEASE_ACTIVITY_INSTANCE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ boolean res = releaseActivityInstance(token);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case RELEASE_SOME_ACTIVITIES_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IApplicationThread app = ApplicationThreadNative.asInterface(data.readStrongBinder());
+ releaseSomeActivities(app);
+ reply.writeNoException();
+ return true;
+ }
+
case WILL_ACTIVITY_BE_VISIBLE_TRANSACTION: {
data.enforceInterface(IActivityManager.descriptor);
IBinder token = data.readStrongBinder();
@@ -2661,6 +2678,28 @@
data.recycle();
reply.recycle();
}
+ public boolean releaseActivityInstance(IBinder token) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(RELEASE_ACTIVITY_INSTANCE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void releaseSomeActivities(IApplicationThread app) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(app.asBinder());
+ mRemote.transact(RELEASE_SOME_ACTIVITIES_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
public boolean willActivityBeVisible(IBinder token) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2136209..0356093 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -102,8 +102,6 @@
import com.android.org.conscrypt.TrustedCertificateStore;
import com.google.android.collect.Lists;
-import dalvik.system.VMRuntime;
-
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
@@ -201,6 +199,7 @@
String mInstrumentedLibDir = null;
boolean mSystemThread = false;
boolean mJitEnabled = false;
+ boolean mSomeActivitiesChanged = false;
// These can be accessed by multiple threads; mPackages is the lock.
// XXX For now we keep around information about all packages we have
@@ -2353,6 +2352,7 @@
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
if (r.profileFd != null) {
mProfiler.setProfiler(r.profileFile, r.profileFd);
@@ -2495,6 +2495,7 @@
public void handleCancelVisibleBehind(IBinder token) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
+ mSomeActivitiesChanged = true;
final Activity activity = r.activity;
if (activity.mVisibleBehind) {
activity.mCalled = false;
@@ -2984,6 +2985,7 @@
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
// TODO Push resumeArgs into the activity for consideration
ActivityClientRecord r = performResumeActivity(token, clearHide);
@@ -3175,6 +3177,7 @@
ActivityManagerNative.getDefault().activityPaused(token, r.persistentState);
} catch (RemoteException ex) {
}
+ mSomeActivitiesChanged = true;
}
}
@@ -3413,6 +3416,7 @@
info.state = r.state;
info.persistentState = r.persistentState;
mH.post(info);
+ mSomeActivitiesChanged = true;
}
final void performRestartActivity(IBinder token) {
@@ -3446,6 +3450,7 @@
TAG, "Handle window " + r + " visibility: " + show);
updateVisibility(r, show);
}
+ mSomeActivitiesChanged = true;
}
private void handleSleeping(IBinder token, boolean sleeping) {
@@ -3743,6 +3748,7 @@
// If the system process has died, it's game over for everyone.
}
}
+ mSomeActivitiesChanged = true;
}
public final void requestRelaunchActivity(IBinder token,
@@ -3805,6 +3811,7 @@
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
+ mSomeActivitiesChanged = true;
Configuration changedConfig = null;
int configChanges = 0;
@@ -4107,6 +4114,8 @@
performConfigurationChanged(r.activity, mCompatConfiguration);
freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(mCompatConfiguration));
+
+ mSomeActivitiesChanged = true;
}
final void handleProfilerControl(boolean start, ProfilerControlData pcd, int profileType) {
@@ -5045,17 +5054,38 @@
android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
UserHandle.myUserId());
RuntimeInit.setApplicationObject(mAppThread.asBinder());
- IActivityManager mgr = ActivityManagerNative.getDefault();
+ final IActivityManager mgr = ActivityManagerNative.getDefault();
try {
mgr.attachApplication(mAppThread);
} catch (RemoteException ex) {
// Ignore
}
+ // Watch for getting close to heap limit.
+ BinderInternal.addGcWatcher(new Runnable() {
+ @Override public void run() {
+ if (!mSomeActivitiesChanged) {
+ return;
+ }
+ Runtime runtime = Runtime.getRuntime();
+ long dalvikMax = runtime.maxMemory();
+ long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
+ if (dalvikUsed > ((3*dalvikMax)/4)) {
+ if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)
+ + " total=" + (runtime.totalMemory()/1024)
+ + " used=" + (dalvikUsed/1024));
+ mSomeActivitiesChanged = false;
+ try {
+ mgr.releaseSomeActivities(mAppThread);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ });
} else {
// Don't set application object here -- if the system crashes,
// we can't display an alert, we just want to die die die.
android.ddm.DdmHandleAppName.setAppName("system_process",
- UserHandle.myUserId());
+ UserHandle.myUserId());
try {
mInstrumentation = new Instrumentation();
ContextImpl context = ContextImpl.createAppContext(
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 69e1710..57c4b71 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -97,6 +97,8 @@
public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException;
public boolean finishActivityAffinity(IBinder token) throws RemoteException;
public void finishVoiceTask(IVoiceInteractionSession session) throws RemoteException;
+ public boolean releaseActivityInstance(IBinder token) throws RemoteException;
+ public void releaseSomeActivities(IApplicationThread app) throws RemoteException;
public boolean willActivityBeVisible(IBinder token) throws RemoteException;
public Intent registerReceiver(IApplicationThread caller, String callerPackage,
IIntentReceiver receiver, IntentFilter filter,
@@ -771,4 +773,6 @@
int START_ACTIVITY_AS_CALLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+232;
int ADD_APP_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+233;
int GET_APP_TASK_THUMBNAIL_SIZE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+234;
+ int RELEASE_ACTIVITY_INSTANCE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+235;
+ int RELEASE_SOME_ACTIVITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+236;
}
diff --git a/core/java/android/app/IAppTask.aidl b/core/java/android/app/IAppTask.aidl
index 4e38c36..37fead9 100644
--- a/core/java/android/app/IAppTask.aidl
+++ b/core/java/android/app/IAppTask.aidl
@@ -17,10 +17,15 @@
package android.app;
import android.app.ActivityManager;
+import android.content.Intent;
+import android.os.Bundle;
/** @hide */
interface IAppTask {
void finishAndRemoveTask();
ActivityManager.RecentTaskInfo getTaskInfo();
+ void moveToFront();
+ int startActivity(IBinder whoThread, String callingPackage,
+ in Intent intent, String resolvedType, in Bundle options);
void setExcludeFromRecents(boolean exclude);
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index b28d7cc..bc71bad 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -1702,6 +1702,40 @@
return null;
}
+ /**
+ * Special version!
+ * @hide
+ */
+ public void execStartActivityFromAppTask(
+ Context who, IBinder contextThread, IAppTask appTask,
+ Intent intent, Bundle options) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess();
+ int result = appTask.startActivity(whoThread.asBinder(), who.getBasePackageName(),
+ intent, intent.resolveTypeIfNeeded(who.getContentResolver()), options);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ }
+ return;
+ }
+
/*package*/ final void init(ActivityThread thread,
Context instrContext, Context appContext, ComponentName component,
IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2241716..95d1351 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6320,8 +6320,7 @@
* processes as soon as they are no longer needed. If 0, the normal
* extended lifetime is used.
*/
- public static final String ALWAYS_FINISH_ACTIVITIES =
- "always_finish_activities";
+ public static final String ALWAYS_FINISH_ACTIVITIES = "always_finish_activities";
/**
* Use Dock audio output for media:
diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java
index 3b0f0f4..240d9df 100644
--- a/core/java/com/android/internal/os/BinderInternal.java
+++ b/core/java/com/android/internal/os/BinderInternal.java
@@ -21,6 +21,7 @@
import android.util.EventLog;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
/**
* Private and debugging Binder APIs.
@@ -28,19 +29,35 @@
* @see IBinder
*/
public class BinderInternal {
- static WeakReference<GcWatcher> mGcWatcher
+ static WeakReference<GcWatcher> sGcWatcher
= new WeakReference<GcWatcher>(new GcWatcher());
- static long mLastGcTime;
-
+ static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
+ static Runnable[] sTmpWatchers = new Runnable[1];
+ static long sLastGcTime;
+
static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
- mLastGcTime = SystemClock.uptimeMillis();
- mGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
+ sLastGcTime = SystemClock.uptimeMillis();
+ synchronized (sGcWatchers) {
+ sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
+ }
+ for (int i=0; i<sTmpWatchers.length; i++) {
+ if (sTmpWatchers[i] != null) {
+ sTmpWatchers[i].run();
+ }
+ }
+ sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}
-
+
+ public static void addGcWatcher(Runnable watcher) {
+ synchronized (sGcWatchers) {
+ sGcWatchers.add(watcher);
+ }
+ }
+
/**
* Add the calling thread to the IPC thread pool. This function does
* not return until the current process is exiting.
@@ -58,7 +75,7 @@
* SystemClock.uptimeMillis()} of the last garbage collection.
*/
public static long getLastGcTime() {
- return mLastGcTime;
+ return sLastGcTime;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
index 354eb55..c229930 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/AlternateRecentsComponent.java
@@ -212,21 +212,9 @@
// Bring an active task to the foreground
mSystemServicesProxy.moveTaskToFront(toTask.key.id, launchOpts);
} else {
- // Launch the activity anew with the desired animation
- boolean isDocument = Utilities.isDocument(toTask.key.baseIntent);
- Intent intent = new Intent(toTask.key.baseIntent);
- intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
- | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- if (!isDocument) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
try {
mSystemServicesProxy.startActivityFromRecents(toTask.key.id, launchOpts);
} catch (ActivityNotFoundException anfe) {}
-
- // Remove the old task from activity manager
- RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(toTask.key.id,
- isDocument);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 07a7e74..0379a22 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -446,13 +446,6 @@
// Bring an active task to the foreground
ssp.moveTaskToFront(task.key.id, launchOpts);
} else {
- // Launch the activity anew with the desired animation
- Intent i = new Intent(task.key.baseIntent);
- i.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
- | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- if (!Utilities.isDocument(i)) {
- i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
try {
ssp.startActivityFromRecents(task.key.id, launchOpts);
if (launchOpts == null && lockToTask) {
@@ -461,9 +454,6 @@
} catch (ActivityNotFoundException anfe) {
Console.logError(getContext(), "Could not start Activity");
}
-
- // And clean up the old task
- onTaskViewDismissed(task);
}
}
};
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 1ce073a..89e3f49 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -332,6 +332,7 @@
final ArrayList<HandlerChecker> blockedCheckers;
final String subject;
final boolean allowRestart;
+ int debuggerWasConnected = 0;
synchronized (this) {
long timeout = CHECK_INTERVAL;
// Make sure we (re)spin the checkers that have become idle within
@@ -341,17 +342,27 @@
hc.scheduleCheckLocked();
}
+ if (debuggerWasConnected > 0) {
+ debuggerWasConnected--;
+ }
+
// NOTE: We use uptimeMillis() here because we do not want to increment the time we
// wait while asleep. If the device is asleep then the thing that we are waiting
// to timeout on is asleep as well and won't have a chance to run, causing a false
// positive on when to kill things.
long start = SystemClock.uptimeMillis();
while (timeout > 0) {
+ if (Debug.isDebuggerConnected()) {
+ debuggerWasConnected = 2;
+ }
try {
wait(timeout);
} catch (InterruptedException e) {
Log.wtf(TAG, e);
}
+ if (Debug.isDebuggerConnected()) {
+ debuggerWasConnected = 2;
+ }
timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);
}
@@ -450,7 +461,12 @@
// Only kill the process if the debugger is not attached.
if (Debug.isDebuggerConnected()) {
+ debuggerWasConnected = 2;
+ }
+ if (debuggerWasConnected >= 2) {
Slog.w(TAG, "Debugger connected: Watchdog is *not* killing the system process");
+ } else if (debuggerWasConnected > 0) {
+ Slog.w(TAG, "Debugger was connected: Watchdog is *not* killing the system process");
} else if (!allowRestart) {
Slog.w(TAG, "Restart not allowed: Watchdog is *not* killing the system process");
} else {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e794b83..296b0e2 100755
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -33,6 +33,7 @@
import android.Manifest;
import android.app.AppOpsManager;
+import android.app.ApplicationThreadNative;
import android.app.IActivityContainer;
import android.app.IActivityContainerCallback;
import android.app.IAppTask;
@@ -2831,8 +2832,7 @@
if (proc.baseProcessTracker != null) {
proc.baseProcessTracker.reportCachedKill(proc.pkgList, proc.lastCachedPss);
}
- killUnneededProcessLocked(proc, Long.toString(proc.lastCachedPss)
- + "k from cached");
+ proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", true);
} else if (proc != null && !keepIfLarge
&& mLastMemoryLevel > ProcessStats.ADJ_MEM_FACTOR_NORMAL
&& proc.setProcState >= ActivityManager.PROCESS_STATE_CACHED_EMPTY) {
@@ -2841,8 +2841,7 @@
if (proc.baseProcessTracker != null) {
proc.baseProcessTracker.reportCachedKill(proc.pkgList, proc.lastCachedPss);
}
- killUnneededProcessLocked(proc, Long.toString(proc.lastCachedPss)
- + "k from cached");
+ proc.kill(Long.toString(proc.lastCachedPss) + "k from cached", true);
}
}
return proc;
@@ -3318,7 +3317,8 @@
intent.setComponent(new ComponentName(
ri.activityInfo.packageName, ri.activityInfo.name));
mStackSupervisor.startActivityLocked(null, intent, null, ri.activityInfo,
- null, null, null, null, 0, 0, 0, null, 0, null, false, null, null);
+ null, null, null, null, 0, 0, 0, null, 0, null, false, null, null,
+ null);
}
}
}
@@ -3462,7 +3462,7 @@
// TODO: Switch to user app stacks here.
return mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType,
null, null, resultTo, resultWho, requestCode, startFlags, profileFile, profileFd,
- null, null, options, userId, null);
+ null, null, options, userId, null, null);
}
@Override
@@ -3512,7 +3512,7 @@
int ret = mStackSupervisor.startActivityMayWait(null, targetUid, targetPackage, intent,
resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
null, null, null, null, options, UserHandle.getUserId(sourceRecord.app.uid),
- null);
+ null, null);
return ret;
} catch (SecurityException e) {
// XXX need to figure out how to propagate to original app.
@@ -3542,7 +3542,7 @@
// TODO: Switch to user app stacks here.
mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType,
null, null, resultTo, resultWho, requestCode, startFlags, profileFile, profileFd,
- res, null, options, userId, null);
+ res, null, options, userId, null, null);
return res;
}
@@ -3557,7 +3557,7 @@
// TODO: Switch to user app stacks here.
int ret = mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent,
resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
- null, null, null, config, options, userId, null);
+ null, null, null, config, options, userId, null, null);
return ret;
}
@@ -3615,7 +3615,7 @@
// TODO: Switch to user app stacks here.
return mStackSupervisor.startActivityMayWait(null, callingUid, callingPackage, intent,
resolvedType, session, interactor, null, null, 0, startFlags,
- profileFile, profileFd, null, null, options, userId, null);
+ profileFile, profileFd, null, null, options, userId, null, null);
}
@Override
@@ -3713,7 +3713,7 @@
int res = mStackSupervisor.startActivityLocked(r.app.thread, intent,
r.resolvedType, aInfo, null, null, resultTo != null ? resultTo.appToken : null,
resultWho, requestCode, -1, r.launchedFromUid, r.launchedFromPackage, 0,
- options, false, null, null);
+ options, false, null, null, null);
Binder.restoreCallingIdentity(origId);
r.finishing = wasFinishing;
@@ -3732,36 +3732,42 @@
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
+ return startActivityFromRecentsInner(taskId, options);
+ }
+
+ final int startActivityFromRecentsInner(int taskId, Bundle options) {
+ final TaskRecord task;
final int callingUid;
final String callingPackage;
final Intent intent;
final int userId;
synchronized (this) {
- final TaskRecord task = recentTaskForIdLocked(taskId);
+ task = recentTaskForIdLocked(taskId);
if (task == null) {
- throw new ActivityNotFoundException("Task " + taskId + " not found.");
+ throw new IllegalArgumentException("Task " + taskId + " not found.");
}
callingUid = task.mCallingUid;
callingPackage = task.mCallingPackage;
intent = task.intent;
+ intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
userId = task.userId;
}
return startActivityInPackage(callingUid, callingPackage, intent, null, null, null, 0, 0,
- options, userId, null);
+ options, userId, null, task);
}
final int startActivityInPackage(int uid, String callingPackage,
Intent intent, String resolvedType, IBinder resultTo,
String resultWho, int requestCode, int startFlags, Bundle options, int userId,
- IActivityContainer container) {
+ IActivityContainer container, TaskRecord inTask) {
userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId,
false, ALLOW_FULL_ONLY, "startActivityInPackage", null);
// TODO: Switch to user app stacks here.
- int ret = mStackSupervisor.startActivityMayWait(null, uid, callingPackage, intent, resolvedType,
- null, null, resultTo, resultWho, requestCode, startFlags,
- null, null, null, null, options, userId, container);
+ int ret = mStackSupervisor.startActivityMayWait(null, uid, callingPackage, intent,
+ resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
+ null, null, null, null, options, userId, container, inTask);
return ret;
}
@@ -4155,6 +4161,35 @@
}
@Override
+ public boolean releaseActivityInstance(IBinder token) {
+ synchronized(this) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ ActivityRecord r = ActivityRecord.isInStackLocked(token);
+ if (r.task == null || r.task.stack == null) {
+ return false;
+ }
+ return r.task.stack.safelyDestroyActivityLocked(r, "app-req");
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
+ @Override
+ public void releaseSomeActivities(IApplicationThread appInt) {
+ synchronized(this) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ ProcessRecord app = getRecordForAppLocked(appInt);
+ mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
+ @Override
public boolean willActivityBeVisible(IBinder token) {
synchronized(this) {
ActivityStack stack = ActivityRecord.getStackLocked(token);
@@ -4568,8 +4603,7 @@
// 0 == continue, -1 = kill process immediately
int res = mController.appEarlyNotResponding(app.processName, app.pid, annotation);
if (res < 0 && app.pid != MY_PID) {
- Process.killProcess(app.pid);
- Process.killProcessGroup(app.info.uid, app.pid);
+ app.kill("anr", true);
}
} catch (RemoteException e) {
mController = null;
@@ -4675,8 +4709,7 @@
int res = mController.appNotResponding(app.processName, app.pid, info.toString());
if (res != 0) {
if (res < 0 && app.pid != MY_PID) {
- Process.killProcess(app.pid);
- Process.killProcessGroup(app.info.uid, app.pid);
+ app.kill("anr", true);
} else {
synchronized (this) {
mServices.scheduleServiceTimeoutLocked(app);
@@ -4696,7 +4729,7 @@
synchronized (this) {
if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {
- killUnneededProcessLocked(app, "background ANR");
+ app.kill("bg anr", true);
return;
}
@@ -5420,8 +5453,7 @@
if (app.isolated) {
mBatteryStatsService.removeIsolatedUid(app.uid, app.info.uid);
}
- killUnneededProcessLocked(app, reason);
- Process.killProcessGroup(app.info.uid, app.pid);
+ app.kill(reason, true);
handleAppDiedLocked(app, true, allowRestart);
removeLruProcessLocked(app);
@@ -5469,7 +5501,7 @@
checkAppInLaunchingProvidersLocked(app, true);
// Take care of any services that are waiting for the process.
mServices.processStartTimedOutLocked(app);
- killUnneededProcessLocked(app, "start timeout");
+ app.kill("start timeout", true);
if (mBackupTarget != null && mBackupTarget.app.pid == pid) {
Slog.w(TAG, "Unattached app died before backup, skipping");
try {
@@ -7915,17 +7947,6 @@
}
}
- private void killUnneededProcessLocked(ProcessRecord pr, String reason) {
- if (!pr.killedByAm) {
- Slog.i(TAG, "Killing " + pr.toShortString() + " (adj " + pr.setAdj + "): " + reason);
- EventLog.writeEvent(EventLogTags.AM_KILL, pr.userId, pr.pid,
- pr.processName, pr.setAdj, reason);
- pr.killedByAm = true;
- Process.killProcessQuiet(pr.pid);
- Process.killProcessGroup(pr.info.uid, pr.pid);
- }
- }
-
private void cleanUpRemovedTaskLocked(TaskRecord tr, int flags) {
tr.disposeThumbnail();
mRecentTasks.remove(tr);
@@ -7969,7 +7990,7 @@
continue;
}
if (pr.setSchedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE) {
- killUnneededProcessLocked(pr, "remove task");
+ pr.kill("remove task", true);
} else {
pr.waitingToKill = "remove task";
}
@@ -8022,32 +8043,36 @@
if (DEBUG_STACK) Slog.d(TAG, "moveTaskToFront: moving taskId=" + taskId);
synchronized(this) {
- if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(),
- Binder.getCallingUid(), "Task to front")) {
- ActivityOptions.abort(options);
+ moveTaskToFrontLocked(taskId, flags, options);
+ }
+ }
+
+ void moveTaskToFrontLocked(int taskId, int flags, Bundle options) {
+ if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(),
+ Binder.getCallingUid(), "Task to front")) {
+ ActivityOptions.abort(options);
+ return;
+ }
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ final TaskRecord task = mStackSupervisor.anyTaskForIdLocked(taskId);
+ if (task == null) {
return;
}
- final long origId = Binder.clearCallingIdentity();
- try {
- final TaskRecord task = mStackSupervisor.anyTaskForIdLocked(taskId);
- if (task == null) {
- return;
- }
- if (mStackSupervisor.isLockTaskModeViolation(task)) {
- mStackSupervisor.showLockTaskToast();
- Slog.e(TAG, "moveTaskToFront: Attempt to violate Lock Task Mode");
- return;
- }
- final ActivityRecord prev = mStackSupervisor.topRunningActivityLocked();
- if (prev != null && prev.isRecentsActivity()) {
- task.setTaskToReturnTo(ActivityRecord.RECENTS_ACTIVITY_TYPE);
- }
- mStackSupervisor.findTaskToMoveToFrontLocked(task, flags, options);
- } finally {
- Binder.restoreCallingIdentity(origId);
+ if (mStackSupervisor.isLockTaskModeViolation(task)) {
+ mStackSupervisor.showLockTaskToast();
+ Slog.e(TAG, "moveTaskToFront: Attempt to violate Lock Task Mode");
+ return;
}
- ActivityOptions.abort(options);
+ final ActivityRecord prev = mStackSupervisor.topRunningActivityLocked();
+ if (prev != null && prev.isRecentsActivity()) {
+ task.setTaskToReturnTo(ActivityRecord.RECENTS_ACTIVITY_TYPE);
+ }
+ mStackSupervisor.findTaskToMoveToFrontLocked(task, flags, options);
+ } finally {
+ Binder.restoreCallingIdentity(origId);
}
+ ActivityOptions.abort(options);
}
@Override
@@ -10145,7 +10170,7 @@
}
int adj = proc.setAdj;
if (adj >= worstType && !proc.killedByAm) {
- killUnneededProcessLocked(proc, reason);
+ proc.kill(reason, true);
killed = true;
}
}
@@ -10189,7 +10214,7 @@
final int adj = proc.setAdj;
if (adj > belowAdj && !proc.killedByAm) {
- killUnneededProcessLocked(proc, reason);
+ proc.kill(reason, true);
killed = true;
}
}
@@ -10308,8 +10333,8 @@
&& proc.setProcState <= ActivityManager.PROCESS_STATE_SERVICE) {
if (doKilling && proc.initialIdlePss != 0
&& proc.lastPss > ((proc.initialIdlePss*3)/2)) {
- killUnneededProcessLocked(proc, "idle maint (pss " + proc.lastPss
- + " from " + proc.initialIdlePss + ")");
+ proc.kill("idle maint (pss " + proc.lastPss
+ + " from " + proc.initialIdlePss + ")", true);
}
}
} else if (proc.setProcState < ActivityManager.PROCESS_STATE_HOME) {
@@ -10778,7 +10803,7 @@
}
if (app.pid > 0 && app.pid != MY_PID) {
handleAppCrashLocked(app, null, null, null);
- killUnneededProcessLocked(app, "user request after error");
+ app.kill("user request after error", true);
}
}
}
@@ -11345,8 +11370,11 @@
} else {
Slog.w(TAG, "Force-killing crashed app " + name
+ " at watcher's request");
- Process.killProcess(pid);
if (r != null) {
+ r.kill("crash", true);
+ } else {
+ // Huh.
+ Process.killProcess(pid);
Process.killProcessGroup(uid, pid);
}
}
@@ -13695,9 +13723,9 @@
if (!capp.persistent && capp.thread != null
&& capp.pid != 0
&& capp.pid != MY_PID) {
- killUnneededProcessLocked(capp, "depends on provider "
+ capp.kill("depends on provider "
+ cpr.name.flattenToShortString()
- + " in dying proc " + (proc != null ? proc.processName : "??"));
+ + " in dying proc " + (proc != null ? proc.processName : "??"), true);
}
} else if (capp.thread != null && conn.provider.provider != null) {
try {
@@ -16553,8 +16581,7 @@
stats.reportExcessiveWakeLocked(app.info.uid, app.processName,
realtimeSince, wtimeUsed);
}
- killUnneededProcessLocked(app, "excessive wake held " + wtimeUsed
- + " during " + realtimeSince);
+ app.kill("excessive wake held " + wtimeUsed + " during " + realtimeSince, true);
app.baseProcessTracker.reportExcessiveWake(app.pkgList);
} else if (doCpuKills && uptimeSince > 0
&& ((cputimeUsed*100)/uptimeSince) >= 25) {
@@ -16562,8 +16589,7 @@
stats.reportExcessiveCpuLocked(app.info.uid, app.processName,
uptimeSince, cputimeUsed);
}
- killUnneededProcessLocked(app, "excessive cpu " + cputimeUsed
- + " during " + uptimeSince);
+ app.kill("excessive cpu " + cputimeUsed + " during " + uptimeSince, true);
app.baseProcessTracker.reportExcessiveCpu(app.pkgList);
} else {
app.lastWakeTime = wtime;
@@ -16598,7 +16624,7 @@
+ " to " + app.curSchedGroup);
if (app.waitingToKill != null &&
app.setSchedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE) {
- killUnneededProcessLocked(app, app.waitingToKill);
+ app.kill(app.waitingToKill, true);
success = false;
} else {
if (true) {
@@ -16978,19 +17004,19 @@
mNumCachedHiddenProcs++;
numCached++;
if (numCached > cachedProcessLimit) {
- killUnneededProcessLocked(app, "cached #" + numCached);
+ app.kill("cached #" + numCached, true);
}
break;
case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
if (numEmpty > ProcessList.TRIM_EMPTY_APPS
&& app.lastActivityTime < oldTime) {
- killUnneededProcessLocked(app, "empty for "
+ app.kill("empty for "
+ ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
- / 1000) + "s");
+ / 1000) + "s", true);
} else {
numEmpty++;
if (numEmpty > emptyProcessLimit) {
- killUnneededProcessLocked(app, "empty #" + numEmpty);
+ app.kill("empty #" + numEmpty, true);
}
}
break;
@@ -17006,7 +17032,7 @@
// definition not re-use the same process again, and it is
// good to avoid having whatever code was running in them
// left sitting around after no longer needed.
- killUnneededProcessLocked(app, "isolated not needed");
+ app.kill("isolated not needed", true);
}
if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
@@ -17234,11 +17260,7 @@
+ (app.thread != null ? app.thread.asBinder() : null)
+ ")\n");
if (app.pid > 0 && app.pid != MY_PID) {
- EventLog.writeEvent(EventLogTags.AM_KILL, app.userId, app.pid,
- app.processName, app.setAdj, "empty");
- app.killedByAm = true;
- Process.killProcessQuiet(app.pid);
- Process.killProcessGroup(app.info.uid, app.pid);
+ app.kill("empty", false);
} else {
try {
app.thread.scheduleExit();
@@ -18290,14 +18312,15 @@
long origId = Binder.clearCallingIdentity();
try {
TaskRecord tr = recentTaskForIdLocked(mTaskId);
- if (tr != null) {
- // Only kill the process if we are not a new document
- int flags = tr.getBaseIntent().getFlags();
- boolean isDocument = (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) ==
- Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
- removeTaskByIdLocked(mTaskId,
- !isDocument ? ActivityManager.REMOVE_TASK_KILL_PROCESS : 0);
+ if (tr == null) {
+ throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
+ // Only kill the process if we are not a new document
+ int flags = tr.getBaseIntent().getFlags();
+ boolean isDocument = (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) ==
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+ removeTaskByIdLocked(mTaskId,
+ !isDocument ? ActivityManager.REMOVE_TASK_KILL_PROCESS : 0);
} finally {
Binder.restoreCallingIdentity(origId);
}
@@ -18312,17 +18335,64 @@
long origId = Binder.clearCallingIdentity();
try {
TaskRecord tr = recentTaskForIdLocked(mTaskId);
- if (tr != null) {
- return createRecentTaskInfoFromTaskRecord(tr);
+ if (tr == null) {
+ throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
+ return createRecentTaskInfoFromTaskRecord(tr);
} finally {
Binder.restoreCallingIdentity(origId);
}
- return null;
}
}
@Override
+ public void moveToFront() {
+ checkCaller();
+
+ final TaskRecord tr;
+ synchronized (ActivityManagerService.this) {
+ tr = recentTaskForIdLocked(mTaskId);
+ if (tr == null) {
+ throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+ }
+ if (tr.getRootActivity() != null) {
+ long origId = Binder.clearCallingIdentity();
+ try {
+ moveTaskToFrontLocked(tr.taskId, 0, null);
+ return;
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
+ startActivityFromRecentsInner(tr.taskId, null);
+ }
+
+ @Override
+ public int startActivity(IBinder whoThread, String callingPackage,
+ Intent intent, String resolvedType, Bundle options) {
+ checkCaller();
+
+ int callingUser = UserHandle.getCallingUserId();
+ TaskRecord tr;
+ IApplicationThread appThread;
+ synchronized (ActivityManagerService.this) {
+ tr = recentTaskForIdLocked(mTaskId);
+ if (tr == null) {
+ throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+ }
+ appThread = ApplicationThreadNative.asInterface(whoThread);
+ if (appThread == null) {
+ throw new IllegalArgumentException("Bad app thread " + appThread);
+ }
+ }
+ return mStackSupervisor.startActivityMayWait(appThread, -1, callingPackage, intent,
+ resolvedType, null, null, null, null, 0, 0, null, null,
+ null, null, options, callingUser, null, tr);
+ }
+
+ @Override
public void setExcludeFromRecents(boolean exclude) {
checkCaller();
@@ -18330,14 +18400,15 @@
long origId = Binder.clearCallingIdentity();
try {
TaskRecord tr = recentTaskForIdLocked(mTaskId);
- if (tr != null) {
- Intent intent = tr.getBaseIntent();
- if (exclude) {
- intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- } else {
- intent.setFlags(intent.getFlags()
- & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- }
+ if (tr == null) {
+ throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+ }
+ Intent intent = tr.getBaseIntent();
+ if (exclude) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ } else {
+ intent.setFlags(intent.getFlags()
+ & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index fcbe71e..694142a 100755
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -1054,6 +1054,24 @@
return null;
}
+ final boolean isDestroyable() {
+ if (finishing || app == null || state == ActivityState.DESTROYING
+ || state == ActivityState.DESTROYED) {
+ // This would be redundant.
+ return false;
+ }
+ if (task == null || task.stack == null || this == task.stack.mResumedActivity
+ || this == task.stack.mPausingActivity || !haveState || !stopped) {
+ // We're not ready for this kind of thing.
+ return false;
+ }
+ if (visible) {
+ // The user would notice this!
+ return false;
+ }
+ return true;
+ }
+
private static String createImageFilename(long createTime, int taskId) {
return String.valueOf(taskId) + ACTIVITY_ICON_SUFFIX + createTime +
TaskPersister.IMAGE_EXTENSION;
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 41ed4ce7..fd1474f 100755
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -36,11 +36,13 @@
import static com.android.server.am.ActivityStackSupervisor.DEBUG_ADD_REMOVE;
import static com.android.server.am.ActivityStackSupervisor.DEBUG_APP;
import static com.android.server.am.ActivityStackSupervisor.DEBUG_CONTAINERS;
+import static com.android.server.am.ActivityStackSupervisor.DEBUG_RELEASE;
import static com.android.server.am.ActivityStackSupervisor.DEBUG_SAVED_STATE;
import static com.android.server.am.ActivityStackSupervisor.DEBUG_SCREENSHOTS;
import static com.android.server.am.ActivityStackSupervisor.DEBUG_STATES;
import static com.android.server.am.ActivityStackSupervisor.HOME_STACK_ID;
+import android.util.ArraySet;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.os.BatteryStatsImpl;
import com.android.server.Watchdog;
@@ -2910,7 +2912,7 @@
int res = mStackSupervisor.startActivityLocked(srec.app.thread, destIntent,
null, aInfo, null, null, parent.appToken, null,
0, -1, parent.launchedFromUid, parent.launchedFromPackage,
- 0, null, true, null, null);
+ 0, null, true, null, null, null);
foundParentInTask = res == ActivityManager.START_SUCCESS;
} catch (RemoteException e) {
foundParentInTask = false;
@@ -3058,15 +3060,10 @@
if (!lastIsOpaque) {
continue;
}
- // We can destroy this one if we have its icicle saved and
- // it is not in the process of pausing/stopping/finishing.
- if (r.app != null && r != mResumedActivity && r != mPausingActivity
- && r.haveState && !r.visible && r.stopped
- && r.state != ActivityState.DESTROYING
- && r.state != ActivityState.DESTROYED) {
+ if (r.isDestroyable()) {
if (DEBUG_SWITCH) Slog.v(TAG, "Destroying " + r + " in state " + r.state
+ " resumed=" + mResumedActivity
- + " pausing=" + mPausingActivity);
+ + " pausing=" + mPausingActivity + " for reason " + reason);
if (destroyActivityLocked(r, true, reason)) {
activityRemoved = true;
}
@@ -3078,6 +3075,60 @@
}
}
+ final boolean safelyDestroyActivityLocked(ActivityRecord r, String reason) {
+ if (r.isDestroyable()) {
+ if (DEBUG_SWITCH) Slog.v(TAG, "Destroying " + r + " in state " + r.state
+ + " resumed=" + mResumedActivity
+ + " pausing=" + mPausingActivity + " for reason " + reason);
+ return destroyActivityLocked(r, true, reason);
+ }
+ return false;
+ }
+
+ final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks,
+ String reason) {
+ // Iterate over tasks starting at the back (oldest) first.
+ if (DEBUG_RELEASE) Slog.d(TAG, "Trying to release some activities in " + app);
+ int maxTasks = tasks.size() / 4;
+ if (maxTasks < 1) {
+ maxTasks = 1;
+ }
+ int numReleased = 0;
+ for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
+ final TaskRecord task = mTaskHistory.get(taskNdx);
+ if (!tasks.contains(task)) {
+ continue;
+ }
+ if (DEBUG_RELEASE) Slog.d(TAG, "Looking for activities to release in " + task);
+ int curNum = 0;
+ final ArrayList<ActivityRecord> activities = task.mActivities;
+ for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
+ final ActivityRecord activity = activities.get(actNdx);
+ if (activity.app == app && activity.isDestroyable()) {
+ if (DEBUG_RELEASE) Slog.v(TAG, "Destroying " + activity
+ + " in state " + activity.state + " resumed=" + mResumedActivity
+ + " pausing=" + mPausingActivity + " for reason " + reason);
+ destroyActivityLocked(activity, true, reason);
+ if (activities.get(actNdx) != activity) {
+ // Was removed from list, back up so we don't miss the next one.
+ actNdx--;
+ }
+ curNum++;
+ }
+ }
+ if (curNum > 0) {
+ numReleased += curNum;
+ maxTasks--;
+ if (mTaskHistory.get(taskNdx) != task) {
+ // The entire task got removed, back up so we don't miss the next one.
+ taskNdx--;
+ }
+ }
+ }
+ if (DEBUG_RELEASE) Slog.d(TAG, "Done releasing: did " + numReleased + " activities");
+ return numReleased;
+ }
+
/**
* Destroy the current CLIENT SIDE instance of an activity. This may be
* called both when actually finishing an activity, or when performing
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 8aec392..8058c05 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -84,6 +84,7 @@
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.service.voice.IVoiceInteractionSession;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.Slog;
import android.util.SparseArray;
@@ -115,10 +116,11 @@
static final boolean DEBUG_APP = DEBUG || false;
static final boolean DEBUG_CONTAINERS = DEBUG || false;
static final boolean DEBUG_IDLE = DEBUG || false;
- static final boolean DEBUG_VISIBLE_BEHIND = DEBUG || false;
+ static final boolean DEBUG_RELEASE = DEBUG || false;
static final boolean DEBUG_SAVED_STATE = DEBUG || false;
static final boolean DEBUG_SCREENSHOTS = DEBUG || false;
static final boolean DEBUG_STATES = DEBUG || false;
+ static final boolean DEBUG_VISIBLE_BEHIND = DEBUG || false;
public static final int HOME_STACK_ID = 0;
@@ -781,7 +783,7 @@
void startHomeActivity(Intent intent, ActivityInfo aInfo) {
moveHomeStackTaskToTop(HOME_ACTIVITY_TYPE);
startActivityLocked(null, intent, null, aInfo, null, null, null, null, 0, 0, 0, null, 0,
- null, false, null, null);
+ null, false, null, null, null);
}
final int startActivityMayWait(IApplicationThread caller, int callingUid,
@@ -789,7 +791,7 @@
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int startFlags, String profileFile,
ParcelFileDescriptor profileFd, WaitResult outResult, Configuration config,
- Bundle options, int userId, IActivityContainer iContainer) {
+ Bundle options, int userId, IActivityContainer iContainer, TaskRecord inTask) {
// Refuse possible leaked file descriptors
if (intent != null && intent.hasFileDescriptors()) {
throw new IllegalArgumentException("File descriptors passed in Intent");
@@ -899,7 +901,7 @@
int res = startActivityLocked(caller, intent, resolvedType, aInfo,
voiceSession, voiceInteractor, resultTo, resultWho,
requestCode, callingPid, callingUid, callingPackage, startFlags, options,
- componentSpecified, null, container);
+ componentSpecified, null, container, inTask);
Binder.restoreCallingIdentity(origId);
@@ -1014,7 +1016,7 @@
}
int res = startActivityLocked(caller, intent, resolvedTypes[i],
aInfo, null, null, resultTo, null, -1, callingPid, callingUid, callingPackage,
- 0, theseOptions, componentSpecified, outActivity, null);
+ 0, theseOptions, componentSpecified, outActivity, null, null);
if (res < 0) {
return res;
}
@@ -1241,7 +1243,8 @@
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode,
int callingPid, int callingUid, String callingPackage, int startFlags, Bundle options,
- boolean componentSpecified, ActivityRecord[] outActivity, ActivityContainer container) {
+ boolean componentSpecified, ActivityRecord[] outActivity, ActivityContainer container,
+ TaskRecord inTask) {
int err = ActivityManager.START_SUCCESS;
ProcessRecord callerApp = null;
@@ -1453,7 +1456,7 @@
doPendingActivityLaunchesLocked(false);
err = startActivityUncheckedLocked(r, sourceRecord, voiceSession, voiceInteractor,
- startFlags, true, options);
+ startFlags, true, options, inTask);
if (err < 0) {
// If someone asked to have the keyguard dismissed on the next
@@ -1536,10 +1539,9 @@
}
}
- final int startActivityUncheckedLocked(ActivityRecord r,
- ActivityRecord sourceRecord,
+ final int startActivityUncheckedLocked(ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags,
- boolean doResume, Bundle options) {
+ boolean doResume, Bundle options, TaskRecord inTask) {
final Intent intent = r.intent;
final int callingUid = r.launchedFromUid;
@@ -1571,8 +1573,9 @@
}
}
- final boolean launchTaskBehind = r.mLaunchTaskBehind &&
- (launchFlags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0;
+ final boolean launchTaskBehind = r.mLaunchTaskBehind
+ && !launchSingleTask && !launchSingleInstance
+ && (launchFlags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0;
if (r.resultTo != null && (launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
// For whatever reason this activity is being launched into a new
@@ -1591,6 +1594,15 @@
launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
}
+ // If we are actually going to launch in to a new task, there are some cases where
+ // we further want to do multiple task.
+ if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
+ if (launchTaskBehind
+ || r.info.documentLaunchMode == ActivityInfo.DOCUMENT_LAUNCH_ALWAYS) {
+ launchFlags |= Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+ }
+ }
+
// We'll invoke onUserLeaving before onPause only if the launching
// activity did not explicitly state that this is an automated launch.
mUserLeaving = (launchFlags & Intent.FLAG_ACTIVITY_NO_USER_ACTION) == 0;
@@ -1624,7 +1636,7 @@
if (sourceRecord == null) {
// This activity is not being started from another... in this
// case we -always- start a new task.
- if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
+ if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) == 0 && inTask == null) {
Slog.w(TAG, "startActivity called from non-Activity context; forcing " +
"Intent.FLAG_ACTIVITY_NEW_TASK for: " + intent);
launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
@@ -1642,7 +1654,7 @@
ActivityInfo newTaskInfo = null;
Intent newTaskIntent = null;
- final ActivityStack sourceStack;
+ ActivityStack sourceStack;
if (sourceRecord != null) {
if (sourceRecord.finishing) {
// If the source is finishing, we can't further count it as our source. This
@@ -1666,12 +1678,51 @@
sourceStack = null;
}
- intent.setFlags(launchFlags);
-
boolean addingToTask = false;
boolean movedHome = false;
TaskRecord reuseTask = null;
ActivityStack targetStack;
+
+ intent.setFlags(launchFlags);
+
+ // If the caller is not coming from another activity, but has given us an
+ // explicit task into which they would like us to launch the new activity,
+ // then let's see about doing that.
+ if (sourceRecord == null && inTask != null && inTask.stack != null) {
+ // If this task is empty, then we are adding the first activity -- it
+ // determines the root, and must be launching as a NEW_TASK.
+ if (inTask.getRootActivity() == null) {
+ if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
+ && !launchSingleInstance && !launchSingleTask) {
+ throw new IllegalStateException("Caller has inTask " + inTask
+ + " but target is not a new task");
+ } else if (inTask.getBaseIntent() == null || !intent.getComponent().equals(
+ inTask.getBaseIntent().getComponent())) {
+ throw new IllegalStateException("Caller requested " + inTask + " is component "
+ + inTask.getBaseIntent() + " but starting " + intent);
+ }
+ inTask.setIntent(r);
+
+ // If the task is not empty, then we are going to add the new activity on top
+ // of the task, so it can not be launching as a new task.
+ } else {
+ if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
+ || launchSingleInstance || launchSingleTask) {
+ throw new IllegalStateException("Caller has inTask " + inTask
+ + " but target is a new task");
+ }
+ }
+ sourceStack = inTask.stack;
+ reuseTask = inTask;
+ } else {
+ inTask = null;
+ }
+
+ // We may want to try to place the new activity in to an existing task. We always
+ // do this if the target activity is singleTask or singleInstance; we will also do
+ // this if NEW_TASK has been requested, and there is not an additional qualifier telling
+ // us to still place it in a new task: multi task, always doc mode, or being asked to
+ // launch this as a new task behind the current one.
if (((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 &&
(launchFlags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0)
|| launchSingleInstance || launchSingleTask) {
@@ -1908,8 +1959,13 @@
Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r);
return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
}
- newTask = true;
- targetStack = adjustStackFocus(r, newTask);
+ if (inTask == null) {
+ // If we have an incoming task, we are just going to use that.
+ newTask = true;
+ targetStack = adjustStackFocus(r, newTask);
+ } else {
+ targetStack = inTask.stack;
+ }
if (!launchTaskBehind) {
targetStack.moveToFront();
}
@@ -1986,6 +2042,20 @@
if (DEBUG_TASKS) Slog.v(TAG, "Starting new activity " + r
+ " in existing task " + r.task + " from source " + sourceRecord);
+ } else if (inTask != null) {
+ // The calling is asking that the new activity be started in an explicit
+ // task it has provided to us.
+ if (isLockTaskModeViolation(inTask)) {
+ Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r);
+ return ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
+ }
+ targetStack = inTask.stack;
+ targetStack.moveToFront();
+ mWindowManager.moveTaskToTop(targetStack.topTask().taskId);
+ r.setTask(inTask, null);
+ if (DEBUG_TASKS) Slog.v(TAG, "Starting new activity " + r
+ + " in explicit task " + r.task);
+
} else {
// This not being started from an existing activity, and not part
// of a new task... just put it in the top task, though these days
@@ -2023,7 +2093,7 @@
while (!mPendingActivityLaunches.isEmpty()) {
PendingActivityLaunch pal = mPendingActivityLaunches.remove(0);
startActivityUncheckedLocked(pal.r, pal.sourceRecord, null, null, pal.startFlags,
- doResume && mPendingActivityLaunches.isEmpty(), null);
+ doResume && mPendingActivityLaunches.isEmpty(), null, null);
}
}
@@ -2726,6 +2796,64 @@
}
}
+ void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
+ // Examine all activities currently running in the process.
+ TaskRecord firstTask = null;
+ // Tasks is non-null only if two or more tasks are found.
+ ArraySet<TaskRecord> tasks = null;
+ if (DEBUG_RELEASE) Slog.d(TAG, "Trying to release some activities in " + app);
+ for (int i=0; i<app.activities.size(); i++) {
+ ActivityRecord r = app.activities.get(i);
+ // First, if we find an activity that is in the process of being destroyed,
+ // then we just aren't going to do anything for now; we want things to settle
+ // down before we try to prune more activities.
+ if (r.finishing || r.state == ActivityState.DESTROYING
+ || r.state == ActivityState.DESTROYED) {
+ if (DEBUG_RELEASE) Slog.d(TAG, "Abort release; already destroying: " + r);
+ return;
+ }
+ // Don't consider any activies that are currently not in a state where they
+ // can be destroyed.
+ if (r.visible || !r.stopped || !r.haveState
+ || r.state == ActivityState.RESUMED || r.state == ActivityState.PAUSING
+ || r.state == ActivityState.PAUSED || r.state == ActivityState.STOPPING) {
+ if (DEBUG_RELEASE) Slog.d(TAG, "Not releasing in-use activity: " + r);
+ continue;
+ }
+ if (r.task != null) {
+ if (DEBUG_RELEASE) Slog.d(TAG, "Collecting release task " + r.task
+ + " from " + r);
+ if (firstTask == null) {
+ firstTask = r.task;
+ } else if (firstTask != r.task) {
+ if (tasks == null) {
+ tasks = new ArraySet<>();
+ tasks.add(firstTask);
+ }
+ tasks.add(r.task);
+ }
+ }
+ }
+ if (tasks == null) {
+ if (DEBUG_RELEASE) Slog.d(TAG, "Didn't find two or more tasks to release");
+ return;
+ }
+ // If we have activities in multiple tasks that are in a position to be destroyed,
+ // let's iterate through the tasks and release the oldest one.
+ final int numDisplays = mActivityDisplays.size();
+ for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
+ final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
+ // Step through all stacks starting from behind, to hit the oldest things first.
+ for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
+ final ActivityStack stack = stacks.get(stackNdx);
+ // Try to release activities in this stack; if we manage to, we are done.
+ if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
+ return;
+ }
+ }
+ }
+ }
+
boolean switchUserLocked(int userId, UserStartedState uss) {
mUserStackInFront.put(mCurrentUser, getFocusedStack().getStackId());
final int restoreStackId = mUserStackInFront.get(userId, HOME_STACK_ID);
@@ -3499,7 +3627,7 @@
mimeType = mService.getProviderMimeType(intent.getData(), userId);
}
return startActivityMayWait(null, -1, null, intent, mimeType, null, null, null, null, 0, 0, null,
- null, null, null, null, userId, this);
+ null, null, null, null, userId, this, null);
}
@Override
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 98999e9..433ab60 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -254,7 +254,7 @@
} else {
owner.startActivityInPackage(uid, key.packageName, finalIntent,
resolvedType, resultTo, resultWho, requestCode, 0,
- options, userId, container);
+ options, userId, container, null);
}
} catch (RuntimeException e) {
Slog.w(ActivityManagerService.TAG,
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index f1bcb60..0817dd8 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -17,6 +17,8 @@
package com.android.server.am;
import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.Slog;
import com.android.internal.app.ProcessStats;
import com.android.internal.os.BatteryStatsImpl;
@@ -502,6 +504,21 @@
return adj;
}
+ void kill(String reason, boolean noisy) {
+ if (!killedByAm) {
+ if (noisy) {
+ Slog.i(ActivityManagerService.TAG, "Killing " + toShortString() + " (adj " + setAdj
+ + "): " + reason);
+ }
+ EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason);
+ Process.killProcessQuiet(pid);
+ Process.killProcessGroup(info.uid, pid);
+ if (!persistent) {
+ killedByAm = true;
+ }
+ }
+ }
+
public String toShortString() {
if (shortStringName != null) {
return shortStringName;
diff --git a/tests/ActivityTests/AndroidManifest.xml b/tests/ActivityTests/AndroidManifest.xml
index c0898d30..3fb547d 100644
--- a/tests/ActivityTests/AndroidManifest.xml
+++ b/tests/ActivityTests/AndroidManifest.xml
@@ -31,6 +31,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity android:name="SpamActivity" android:label="Spam!"
+ android:documentLaunchMode="always">
+ </activity>
+ <activity android:name="DocActivity" android:label="Some doc">
+ </activity>
<service android:name="SingleUserService"
android:singleUser="true" android:exported="true">
</service>
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
index 7f3aa77..ea0db56 100644
--- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
+++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
@@ -21,6 +21,7 @@
import android.app.Activity;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
@@ -29,13 +30,15 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.BitmapFactory;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.graphics.Bitmap;
-import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -60,6 +63,28 @@
ArrayList<ServiceConnection> mConnections = new ArrayList<ServiceConnection>();
+ static final int MSG_SPAM = 1;
+
+ final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SPAM: {
+ boolean fg = msg.arg1 != 0;
+ Intent intent = new Intent(ActivityTestMain.this, SpamActivity.class);
+ Bundle options = null;
+ if (fg) {
+ ActivityOptions opts = ActivityOptions.makeLaunchTaskBehindAnimation();
+ options = opts.toBundle();
+ }
+ startActivity(intent, options);
+ scheduleSpam(!fg);
+ } break;
+ }
+ super.handleMessage(msg);
+ }
+ };
+
class BroadcastResultReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
@@ -143,7 +168,8 @@
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add("Animate!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
- @Override public boolean onMenuItemClick(MenuItem item) {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
AlertDialog.Builder builder = new AlertDialog.Builder(ActivityTestMain.this,
R.style.SlowDialog);
builder.setTitle("This is a title");
@@ -316,6 +342,49 @@
return true;
}
});
+ menu.add("Open Doc").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override public boolean onMenuItemClick(MenuItem item) {
+ ActivityManager.AppTask task = findDocTask();
+ if (task == null) {
+ Intent intent = new Intent(ActivityTestMain.this, DocActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ | Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
+ startActivity(intent);
+ } else {
+ task.moveToFront();
+ }
+ return true;
+ }
+ });
+ menu.add("Stack Doc").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override public boolean onMenuItemClick(MenuItem item) {
+ ActivityManager.AppTask task = findDocTask();
+ if (task != null) {
+ ActivityManager.RecentTaskInfo recent = task.getTaskInfo();
+ Intent intent = new Intent(ActivityTestMain.this, DocActivity.class);
+ if (recent.id >= 0) {
+ // Stack on top.
+ intent.putExtra(DocActivity.LABEL, "Stacked");
+ task.startActivity(ActivityTestMain.this, intent, null);
+ } else {
+ // Start root activity.
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ | Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
+ intent.putExtra(DocActivity.LABEL, "New Root");
+ task.startActivity(ActivityTestMain.this, intent, null);
+ }
+ }
+ return true;
+ }
+ });
+ menu.add("Spam!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override public boolean onMenuItemClick(MenuItem item) {
+ scheduleSpam(false);
+ return true;
+ }
+ });
return true;
}
@@ -342,6 +411,12 @@
mConnections.clear();
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mHandler.removeMessages(MSG_SPAM);
+ }
+
void addAppRecents(int count) {
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
Intent intent = new Intent(Intent.ACTION_MAIN);
@@ -371,7 +446,31 @@
}
}
}
- private View scrollWrap(View view) {
+
+ ActivityManager.AppTask findDocTask() {
+ ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
+ List<ActivityManager.AppTask> tasks = am.getAppTasks();
+ if (tasks != null) {
+ for (int i=0; i<tasks.size(); i++) {
+ ActivityManager.AppTask task = tasks.get(i);
+ ActivityManager.RecentTaskInfo recent = task.getTaskInfo();
+ if (recent.baseIntent != null
+ && recent.baseIntent.getComponent().getClassName().equals(
+ DocActivity.class.getCanonicalName())) {
+ return task;
+ }
+ }
+ }
+ return null;
+ }
+
+ void scheduleSpam(boolean fg) {
+ mHandler.removeMessages(MSG_SPAM);
+ Message msg = mHandler.obtainMessage(MSG_SPAM, fg ? 1 : 0, 0);
+ mHandler.sendMessageDelayed(msg, 500);
+ }
+
+ private View scrollWrap(View view) {
ScrollView scroller = new ScrollView(this);
scroller.addView(view, new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT,
ScrollView.LayoutParams.MATCH_PARENT));
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/DocActivity.java b/tests/ActivityTests/src/com/google/android/test/activity/DocActivity.java
new file mode 100644
index 0000000..6330c79
--- /dev/null
+++ b/tests/ActivityTests/src/com/google/android/test/activity/DocActivity.java
@@ -0,0 +1,35 @@
+/*
+ * 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.google.android.test.activity;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.os.Bundle;
+
+public class DocActivity extends Activity {
+ static final String LABEL = "label";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String label = getIntent().getStringExtra(LABEL);
+ if (label != null) {
+ setTaskDescription(new ActivityManager.TaskDescription(label));
+ setTitle(label);
+ }
+ }
+}
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/SpamActivity.java b/tests/ActivityTests/src/com/google/android/test/activity/SpamActivity.java
new file mode 100644
index 0000000..e5de559
--- /dev/null
+++ b/tests/ActivityTests/src/com/google/android/test/activity/SpamActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.google.android.test.activity;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class SpamActivity extends Activity {
+ byte[] mBytes;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Chew up some RAM -- 8 megs worth.
+ mBytes = new byte[8*1024*1024];
+ }
+}