summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDianne Hackborn <hackbod@google.com>2015-05-20 18:18:46 -0700
committerDianne Hackborn <hackbod@google.com>2015-05-21 16:45:29 -0700
commitb5a380d409a1431a38db978864b9d85b689e3cce (patch)
tree5cd36c4c095331869e1019739500b517fbf516a6
parentaba3ecb976cacd7c92fe8f8afae20d112781d68e (diff)
downloadframeworks_base-b5a380d409a1431a38db978864b9d85b689e3cce.zip
frameworks_base-b5a380d409a1431a38db978864b9d85b689e3cce.tar.gz
frameworks_base-b5a380d409a1431a38db978864b9d85b689e3cce.tar.bz2
Add API to track usage time of apps.
This adds a new ActivityOption for the caller to ask the system to track the time the user is in the app it launches, delivering the result when they are done. The time interval tracked is from when the app launches the activity until the user leaves that app's flow. They are considered to stay in the flow as long as new activities are being launched or returned to from the original flow, even if they cross package or task boundaries. For example, if the originator starts an activity to view an image, and while there the user selects to share, which launches gmail in a new task, and they complete the share, the time during that entire operation will be included. The user is considered to complete the operation once they switch to another activity that is not part of the tracked flow. For example, use the notification shade, launcher, or recents to launch or switch to another app. Simply going in to these navigation elements does not break the flow (although the launcher and recents stops time tracking of the session), it is the act of going somewhere else that completes the tracking. The data is delivered to the app through a PendingIntent, which includes the total time the app was in the flow along with a time break-down by app package. Change-Id: If1cf8892d422c52ec5042eba0e15a8e7e8f83abf
-rw-r--r--api/current.txt4
-rw-r--r--api/system-current.txt4
-rw-r--r--core/java/android/app/ActivityOptions.java68
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java50
-rwxr-xr-xservices/core/java/com/android/server/am/ActivityRecord.java9
-rw-r--r--services/core/java/com/android/server/am/ActivityStack.java22
-rw-r--r--services/core/java/com/android/server/am/ActivityStackSupervisor.java30
-rw-r--r--services/core/java/com/android/server/am/AppTimeTracker.java122
-rw-r--r--tests/ActivityTests/AndroidManifest.xml1
-rw-r--r--tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java15
-rw-r--r--tests/ActivityTests/src/com/google/android/test/activity/TrackTimeReceiver.java33
11 files changed, 349 insertions, 9 deletions
diff --git a/api/current.txt b/api/current.txt
index bbdb878..9d4f1b3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3745,6 +3745,7 @@ package android.app {
}
public class ActivityOptions {
+ method public static android.app.ActivityOptions makeBasic();
method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
method public static android.app.ActivityOptions makeScaleUpAnimation(android.view.View, int, int, int, int);
@@ -3752,8 +3753,11 @@ package android.app {
method public static android.app.ActivityOptions makeSceneTransitionAnimation(android.app.Activity, android.util.Pair<android.view.View, java.lang.String>...);
method public static android.app.ActivityOptions makeTaskLaunchBehind();
method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
+ method public void requestUsageTimeReport(android.app.PendingIntent);
method public android.os.Bundle toBundle();
method public void update(android.app.ActivityOptions);
+ field public static final java.lang.String EXTRA_USAGE_REPORT_PACKAGES = "android.package";
+ field public static final java.lang.String EXTRA_USAGE_REPORT_TIME = "android.time";
}
public class AlarmManager {
diff --git a/api/system-current.txt b/api/system-current.txt
index 1a3673d..3f0188f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3837,6 +3837,7 @@ package android.app {
}
public class ActivityOptions {
+ method public static android.app.ActivityOptions makeBasic();
method public static android.app.ActivityOptions makeClipRevealAnimation(android.view.View, int, int, int, int);
method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int);
method public static android.app.ActivityOptions makeScaleUpAnimation(android.view.View, int, int, int, int);
@@ -3844,8 +3845,11 @@ package android.app {
method public static android.app.ActivityOptions makeSceneTransitionAnimation(android.app.Activity, android.util.Pair<android.view.View, java.lang.String>...);
method public static android.app.ActivityOptions makeTaskLaunchBehind();
method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int);
+ method public void requestUsageTimeReport(android.app.PendingIntent);
method public android.os.Bundle toBundle();
method public void update(android.app.ActivityOptions);
+ field public static final java.lang.String EXTRA_USAGE_REPORT_PACKAGES = "android.package";
+ field public static final java.lang.String EXTRA_USAGE_REPORT_TIME = "android.time";
}
public class AlarmManager {
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 8909b28..9f23b43 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -25,6 +25,7 @@ import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.Pair;
+import android.util.Slog;
import android.view.View;
import android.view.Window;
@@ -39,6 +40,19 @@ public class ActivityOptions {
private static final String TAG = "ActivityOptions";
/**
+ * A long in the extras delivered by {@link #requestUsageTimeReport} that contains
+ * the total time (in ms) the user spent in the app.
+ */
+ public static final String EXTRA_USAGE_REPORT_TIME = "android.time";
+
+ /**
+ * A Bundle in the extras delivered by {@link #requestUsageTimeReport} that contains
+ * detailed information about the time spent in each package associated with the app;
+ * each key is a package name, whose value is a long containing the time (in ms).
+ */
+ public static final String EXTRA_USAGE_REPORT_PACKAGES = "android.package";
+
+ /**
* The package name that created the options.
* @hide
*/
@@ -118,6 +132,8 @@ public class ActivityOptions {
private static final String KEY_RESULT_CODE = "android:resultCode";
private static final String KEY_EXIT_COORDINATOR_INDEX = "android:exitCoordinatorIndex";
+ private static final String KEY_USAGE_TIME_REPORT = "android:usageTimeReport";
+
/** @hide */
public static final int ANIM_NONE = 0;
/** @hide */
@@ -160,6 +176,7 @@ public class ActivityOptions {
private Intent mResultData;
private int mResultCode;
private int mExitCoordinatorIndex;
+ private PendingIntent mUsageTimeReport;
/**
* Create an ActivityOptions specifying a custom animation to run when
@@ -586,6 +603,15 @@ public class ActivityOptions {
return opts;
}
+ /**
+ * Create a basic ActivityOptions that has no special animation associated with it.
+ * Other options can still be set.
+ */
+ public static ActivityOptions makeBasic() {
+ final ActivityOptions opts = new ActivityOptions();
+ return opts;
+ }
+
/** @hide */
public boolean getLaunchTaskBehind() {
return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
@@ -597,6 +623,11 @@ public class ActivityOptions {
/** @hide */
public ActivityOptions(Bundle opts) {
mPackageName = opts.getString(KEY_PACKAGE_NAME);
+ try {
+ mUsageTimeReport = opts.getParcelable(KEY_USAGE_TIME_REPORT);
+ } catch (RuntimeException e) {
+ Slog.w(TAG, e);
+ }
mAnimationType = opts.getInt(KEY_ANIM_TYPE);
switch (mAnimationType) {
case ANIM_CUSTOM:
@@ -730,6 +761,11 @@ public class ActivityOptions {
public Intent getResultData() { return mResultData; }
/** @hide */
+ public PendingIntent getUsageTimeReport() {
+ return mUsageTimeReport;
+ }
+
+ /** @hide */
public static void abort(Bundle options) {
if (options != null) {
(new ActivityOptions(options)).abort();
@@ -745,6 +781,7 @@ public class ActivityOptions {
if (otherOptions.mPackageName != null) {
mPackageName = otherOptions.mPackageName;
}
+ mUsageTimeReport = otherOptions.mUsageTimeReport;
mTransitionReceiver = null;
mSharedElementNames = null;
mIsReturning = false;
@@ -828,6 +865,9 @@ public class ActivityOptions {
b.putString(KEY_PACKAGE_NAME, mPackageName);
}
b.putInt(KEY_ANIM_TYPE, mAnimationType);
+ if (mUsageTimeReport != null) {
+ b.putParcelable(KEY_USAGE_TIME_REPORT, mUsageTimeReport);
+ }
switch (mAnimationType) {
case ANIM_CUSTOM:
b.putInt(KEY_ANIM_ENTER_RES_ID, mCustomEnterResId);
@@ -873,6 +913,34 @@ public class ActivityOptions {
}
/**
+ * Ask the the system track that time the user spends in the app being launched, and
+ * report it back once done. The report will be sent to the given receiver, with
+ * the extras {@link #EXTRA_USAGE_REPORT_TIME} and {@link #EXTRA_USAGE_REPORT_PACKAGES}
+ * filled in.
+ *
+ * <p>The time interval tracked is from launching this activity until the user leaves
+ * that activity's flow. They are considered to stay in the flow as long as
+ * new activities are being launched or returned to from the original flow,
+ * even if this crosses package or task boundaries. For example, if the originator
+ * starts an activity to view an image, and while there the user selects to share,
+ * which launches their email app in a new task, and they complete the share, the
+ * time during that entire operation will be included until they finally hit back from
+ * the original image viewer activity.</p>
+ *
+ * <p>The user is considered to complete a flow once they switch to another
+ * activity that is not part of the tracked flow. This may happen, for example, by
+ * using the notification shade, launcher, or recents to launch or switch to another
+ * app. Simply going in to these navigation elements does not break the flow (although
+ * the launcher and recents stops time tracking of the session); it is the act of
+ * going somewhere else that completes the tracking.</p>
+ *
+ * @param receiver A broadcast receiver that willl receive the report.
+ */
+ public void requestUsageTimeReport(PendingIntent receiver) {
+ mUsageTimeReport = receiver;
+ }
+
+ /**
* Return the filtered options only meant to be seen by the target activity itself
* @hide
*/
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3599d80..fbaa0b6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -426,6 +426,11 @@ public final class ActivityManagerService extends ActivityManagerNative
private int mLastFocusedUserId;
/**
+ * If non-null, we are tracking the time the user spends in the currently focused app.
+ */
+ private AppTimeTracker mCurAppTimeTracker;
+
+ /**
* List of intents that were used to start the most recent tasks.
*/
private final RecentTasks mRecentTasks;
@@ -1330,7 +1335,8 @@ public final class ActivityManagerService extends ActivityManagerNative
static final int POST_DUMP_HEAP_NOTIFICATION_MSG = 51;
static final int DELETE_DUMPHEAP_MSG = 52;
static final int FOREGROUND_PROFILE_CHANGED_MSG = 53;
- static final int DISPATCH_UIDS_CHANGED = 54;
+ static final int DISPATCH_UIDS_CHANGED_MSG = 54;
+ static final int REPORT_TIME_TRACKER_MSG = 55;
static final int FIRST_ACTIVITY_STACK_MSG = 100;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -1563,7 +1569,7 @@ public final class ActivityManagerService extends ActivityManagerNative
dispatchProcessDied(pid, uid);
break;
}
- case DISPATCH_UIDS_CHANGED: {
+ case DISPATCH_UIDS_CHANGED_MSG: {
dispatchUidsChanged();
} break;
}
@@ -1988,6 +1994,10 @@ public final class ActivityManagerService extends ActivityManagerNative
case FOREGROUND_PROFILE_CHANGED_MSG: {
dispatchForegroundProfileChanged(msg.arg1);
} break;
+ case REPORT_TIME_TRACKER_MSG: {
+ AppTimeTracker tracker = (AppTimeTracker)msg.obj;
+ tracker.deliverResult(mContext);
+ } break;
}
}
};
@@ -2573,6 +2583,27 @@ public final class ActivityManagerService extends ActivityManagerNative
if (DEBUG_FOCUS) Slog.d(TAG_FOCUS, "setFocusedActivityLocked: r=" + r);
ActivityRecord last = mFocusedActivity;
mFocusedActivity = r;
+ if (r.task.taskType != ActivityRecord.HOME_ACTIVITY_TYPE
+ && r.task.taskType != ActivityRecord.RECENTS_ACTIVITY_TYPE) {
+ if (mCurAppTimeTracker != r.appTimeTracker) {
+ // We are switching app tracking. Complete the current one.
+ if (mCurAppTimeTracker != null) {
+ mCurAppTimeTracker.stop();
+ mHandler.obtainMessage(REPORT_TIME_TRACKER_MSG,
+ mCurAppTimeTracker).sendToTarget();
+ mStackSupervisor.clearOtherAppTimeTrackers(r.appTimeTracker);
+ mCurAppTimeTracker = null;
+ }
+ if (r.appTimeTracker != null) {
+ mCurAppTimeTracker = r.appTimeTracker;
+ startTimeTrackingFocusedActivityLocked();
+ }
+ } else {
+ startTimeTrackingFocusedActivityLocked();
+ }
+ } else {
+ r.appTimeTracker = null;
+ }
if (r.task != null && r.task.voiceInteractor != null) {
startRunningVoiceLocked(r.task.voiceSession, r.info.applicationInfo.uid);
} else {
@@ -10141,14 +10172,24 @@ public final class ActivityManagerService extends ActivityManagerNative
}
}
+ void startTimeTrackingFocusedActivityLocked() {
+ if (!mSleeping && mCurAppTimeTracker != null && mFocusedActivity != null) {
+ mCurAppTimeTracker.start(mFocusedActivity.packageName);
+ }
+ }
+
void updateSleepIfNeededLocked() {
if (mSleeping && !shouldSleepLocked()) {
mSleeping = false;
+ startTimeTrackingFocusedActivityLocked();
mTopProcessState = ActivityManager.PROCESS_STATE_TOP;
mStackSupervisor.comeOutOfSleepIfNeededLocked();
updateOomAdjLocked();
} else if (!mSleeping && shouldSleepLocked()) {
mSleeping = true;
+ if (mCurAppTimeTracker != null) {
+ mCurAppTimeTracker.stop();
+ }
mTopProcessState = ActivityManager.PROCESS_STATE_TOP_SLEEPING;
mStackSupervisor.goingToSleepLocked();
updateOomAdjLocked();
@@ -13351,6 +13392,9 @@ public final class ActivityManagerService extends ActivityManagerNative
+ " mOrigWaitForDebugger=" + mOrigWaitForDebugger);
}
}
+ if (mCurAppTimeTracker != null) {
+ mCurAppTimeTracker.dumpWithHeader(pw, " ", true);
+ }
if (mMemWatchProcesses.getMap().size() > 0) {
pw.println(" Mem watch processes:");
final ArrayMap<String, SparseArray<Pair<Long, String>>> procs
@@ -18469,7 +18513,7 @@ public final class ActivityManagerService extends ActivityManagerNative
if (mPendingUidChanges.size() == 0) {
if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
"*** Enqueueing dispatch uid changed!");
- mUiHandler.obtainMessage(DISPATCH_UIDS_CHANGED).sendToTarget();
+ mUiHandler.obtainMessage(DISPATCH_UIDS_CHANGED_MSG).sendToTarget();
}
final int NA = mAvailUidChanges.size();
if (NA > 0) {
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index ca1fd6a..54dd491 100755
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -20,6 +20,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.*;
import static com.android.server.am.TaskRecord.INVALID_TASK_ID;
import android.app.ActivityManager.TaskDescription;
+import android.app.PendingIntent;
import android.os.PersistableBundle;
import android.os.Trace;
@@ -142,6 +143,7 @@ final class ActivityRecord {
ArrayList<ReferrerIntent> newIntents; // any pending new intents for single-top mode
ActivityOptions pendingOptions; // most recently given options
ActivityOptions returningOptions; // options that are coming back via convertToTranslucent
+ AppTimeTracker appTimeTracker; // set if we are tracking the time in this app/task/activity
HashSet<ConnectionRecord> connections; // All ConnectionRecord we hold
UriPermissionOwner uriPermissions; // current special URI access perms.
ProcessRecord app; // if non-null, hosting application
@@ -262,6 +264,9 @@ final class ActivityRecord {
if (pendingOptions != null) {
pw.print(prefix); pw.print("pendingOptions="); pw.println(pendingOptions);
}
+ if (appTimeTracker != null) {
+ appTimeTracker.dumpWithHeader(pw, prefix, false);
+ }
if (uriPermissions != null) {
uriPermissions.dump(pw, prefix);
}
@@ -463,6 +468,10 @@ final class ActivityRecord {
if (options != null) {
pendingOptions = new ActivityOptions(options);
mLaunchTaskBehind = pendingOptions.getLaunchTaskBehind();
+ PendingIntent usageReport = pendingOptions.getUsageTimeReport();
+ if (usageReport != null) {
+ appTimeTracker = new AppTimeTracker(usageReport);
+ }
}
// This starts out true, since the initial state of an activity
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 6574538..a4c557f 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -1447,6 +1447,19 @@ final class ActivityStack {
mHandler.sendEmptyMessageDelayed(TRANSLUCENT_TIMEOUT_MSG, TRANSLUCENT_CONVERSION_TIMEOUT);
}
+ void clearOtherAppTimeTrackers(AppTimeTracker except) {
+ for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
+ final TaskRecord task = mTaskHistory.get(taskNdx);
+ final ArrayList<ActivityRecord> activities = task.mActivities;
+ for (int activityNdx = activities.size() - 1; activityNdx >= 0; --activityNdx) {
+ final ActivityRecord r = activities.get(activityNdx);
+ if ( r.appTimeTracker != except) {
+ r.appTimeTracker = null;
+ }
+ }
+ }
+ }
+
/**
* Called as activities below the top translucent activity are redrawn. When the last one is
* redrawn notify the top activity by calling
@@ -3622,7 +3635,7 @@ final class ActivityStack {
}
final void moveTaskToFrontLocked(TaskRecord tr, boolean noAnimation, Bundle options,
- String reason) {
+ AppTimeTracker timeTracker, String reason) {
if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "moveTaskToFront: " + tr);
final int numTasks = mTaskHistory.size();
@@ -3637,6 +3650,13 @@ final class ActivityStack {
return;
}
+ if (timeTracker != null) {
+ // The caller wants a time tracker associated with this task.
+ for (int i = tr.mActivities.size() - 1; i >= 0; i--) {
+ tr.mActivities.get(i).appTimeTracker = timeTracker;
+ }
+ }
+
// Shift all activities with this task up to the top
// of the stack, keeping them in the same internal order.
insertTaskAtTop(tr, null);
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 5eee34f..ff70629 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1567,6 +1567,12 @@ public final class ActivityStackSupervisor implements DisplayListener {
outActivity[0] = r;
}
+ if (r.appTimeTracker == null && sourceRecord != null) {
+ // If the caller didn't specify an explicit time tracker, we want to continue
+ // tracking under any it has.
+ r.appTimeTracker = sourceRecord.appTimeTracker;
+ }
+
final ActivityStack stack = mFocusedStack;
if (voiceSession == null && (stack.mResumedActivity == null
|| stack.mResumedActivity.info.applicationInfo.uid != callingUid)) {
@@ -1950,7 +1956,7 @@ public final class ActivityStackSupervisor implements DisplayListener {
}
movedHome = true;
targetStack.moveTaskToFrontLocked(intentActivity.task, noAnimation,
- options, "bringingFoundTaskToFront");
+ options, r.appTimeTracker, "bringingFoundTaskToFront");
movedToFront = true;
if ((launchFlags &
(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_TASK_ON_HOME))
@@ -2192,7 +2198,7 @@ public final class ActivityStackSupervisor implements DisplayListener {
final TaskRecord topTask = targetStack.topTask();
if (topTask != sourceTask) {
targetStack.moveTaskToFrontLocked(sourceTask, noAnimation, options,
- "sourceTaskToFront");
+ r.appTimeTracker, "sourceTaskToFront");
}
if (!addingToTask && (launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
// In this case, we are adding the activity to an existing
@@ -2239,14 +2245,15 @@ public final class ActivityStackSupervisor implements DisplayListener {
+ " 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
+ // The caller 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.moveTaskToFrontLocked(inTask, noAnimation, options, "inTaskToFront");
+ targetStack.moveTaskToFrontLocked(inTask, noAnimation, options, r.appTimeTracker,
+ "inTaskToFront");
// Check whether we should actually launch the new activity in to the task,
// or just reuse the current activity on top.
@@ -2628,7 +2635,9 @@ public final class ActivityStackSupervisor implements DisplayListener {
+ task + " to front. Stack is null");
return;
}
- task.stack.moveTaskToFrontLocked(task, false /* noAnimation */, options, reason);
+ task.stack.moveTaskToFrontLocked(task, false /* noAnimation */, options,
+ task.getTopActivity() == null ? null : task.getTopActivity().appTimeTracker,
+ reason);
if (DEBUG_STACK) Slog.d(TAG_STACK,
"findTaskToMoveToFront: moved to front of stack=" + task.stack);
}
@@ -3143,6 +3152,17 @@ public final class ActivityStackSupervisor implements DisplayListener {
}
}
+ void clearOtherAppTimeTrackers(AppTimeTracker except) {
+ for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
+ final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
+ final int topStackNdx = stacks.size() - 1;
+ for (int stackNdx = topStackNdx; stackNdx >= 0; --stackNdx) {
+ final ActivityStack stack = stacks.get(stackNdx);
+ stack.clearOtherAppTimeTrackers(except);
+ }
+ }
+ }
+
void scheduleDestroyAllActivities(ProcessRecord app, String reason) {
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
diff --git a/services/core/java/com/android/server/am/AppTimeTracker.java b/services/core/java/com/android/server/am/AppTimeTracker.java
new file mode 100644
index 0000000..bddd66f
--- /dev/null
+++ b/services/core/java/com/android/server/am/AppTimeTracker.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 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.server.am;
+
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.MutableLong;
+import android.util.TimeUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Tracks the time a user spent in an app.
+ */
+public class AppTimeTracker {
+ private final PendingIntent mReceiver;
+
+ private long mTotalTime;
+ private final ArrayMap<String, MutableLong> mPackageTimes = new ArrayMap<>();
+
+ private long mStartedTime;
+ private String mStartedPackage;
+ private MutableLong mStartedPackageTime;
+
+ public AppTimeTracker(PendingIntent receiver) {
+ mReceiver = receiver;
+ }
+
+ public void start(String packageName) {
+ long now = SystemClock.elapsedRealtime();
+ if (mStartedTime == 0) {
+ mStartedTime = now;
+ }
+ if (!packageName.equals(mStartedPackage)) {
+ if (mStartedPackageTime != null) {
+ long elapsedTime = now - mStartedTime;
+ mStartedPackageTime.value += elapsedTime;
+ mTotalTime += elapsedTime;
+ }
+ mStartedPackage = packageName;
+ mStartedPackageTime = mPackageTimes.get(packageName);
+ if (mStartedPackageTime == null) {
+ mStartedPackageTime = new MutableLong(0);
+ mPackageTimes.put(packageName, mStartedPackageTime);
+ }
+ }
+ }
+
+ public void stop() {
+ if (mStartedTime != 0) {
+ long elapsedTime = SystemClock.elapsedRealtime() - mStartedTime;
+ mTotalTime += elapsedTime;
+ if (mStartedPackageTime != null) {
+ mStartedPackageTime.value += elapsedTime;
+ }
+ mStartedPackage = null;
+ mStartedPackageTime = null;
+ }
+ }
+
+ public void deliverResult(Context context) {
+ stop();
+ Bundle extras = new Bundle();
+ extras.putLong(ActivityOptions.EXTRA_USAGE_REPORT_TIME, mTotalTime);
+ Bundle pkgs = new Bundle();
+ for (int i=mPackageTimes.size()-1; i>=0; i--) {
+ pkgs.putLong(mPackageTimes.keyAt(i), mPackageTimes.valueAt(i).value);
+ }
+ extras.putBundle(ActivityOptions.EXTRA_USAGE_REPORT_PACKAGES, pkgs);
+ Intent fillinIntent = new Intent();
+ fillinIntent.putExtras(extras);
+ try {
+ mReceiver.send(context, 0, fillinIntent);
+ } catch (PendingIntent.CanceledException e) {
+ }
+ }
+
+ public void dumpWithHeader(PrintWriter pw, String prefix, boolean details) {
+ pw.print(prefix); pw.print("AppTimeTracker #");
+ pw.print(Integer.toHexString(System.identityHashCode(this)));
+ pw.println(":");
+ dump(pw, prefix + " ", details);
+ }
+
+ public void dump(PrintWriter pw, String prefix, boolean details) {
+ pw.print(prefix); pw.print("mReceiver="); pw.println(mReceiver);
+ pw.print(prefix); pw.print("mTotalTime=");
+ TimeUtils.formatDuration(mTotalTime, pw);
+ pw.println();
+ for (int i = 0; i < mPackageTimes.size(); i++) {
+ pw.print(prefix); pw.print("mPackageTime:"); pw.print(mPackageTimes.keyAt(i));
+ pw.print("=");
+ TimeUtils.formatDuration(mPackageTimes.valueAt(i).value, pw);
+ pw.println();
+ }
+ if (details && mStartedTime != 0) {
+ pw.print(prefix); pw.print("mStartedTime=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime(), mStartedTime, pw);
+ pw.println();
+ pw.print(prefix); pw.print("mStartedPackage="); pw.println(mStartedPackage);
+ }
+ }
+}
diff --git a/tests/ActivityTests/AndroidManifest.xml b/tests/ActivityTests/AndroidManifest.xml
index 33d40ad..c105491 100644
--- a/tests/ActivityTests/AndroidManifest.xml
+++ b/tests/ActivityTests/AndroidManifest.xml
@@ -75,5 +75,6 @@
<provider android:name="SingleUserProvider"
android:authorities="com.google.android.test.activity.single_user"
android:singleUser="true" android:exported="true" />
+ <receiver android:name="TrackTimeReceiver" />
</application>
</manifest>
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 4281c68..fc66d6d 100644
--- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
+++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java
@@ -23,6 +23,7 @@ import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.AlertDialog;
+import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -412,6 +413,20 @@ public class ActivityTestMain extends Activity {
return true;
}
});
+ menu.add("Track time").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, "We are sharing this with you!");
+ ActivityOptions options = ActivityOptions.makeBasic();
+ Intent receiveIntent = new Intent(ActivityTestMain.this, TrackTimeReceiver.class);
+ receiveIntent.putExtra("something", "yeah, this is us!");
+ options.requestUsageTimeReport(PendingIntent.getBroadcast(ActivityTestMain.this,
+ 0, receiveIntent, PendingIntent.FLAG_CANCEL_CURRENT));
+ startActivity(Intent.createChooser(intent, "Who do you love?"), options.toBundle());
+ return true;
+ }
+ });
return true;
}
diff --git a/tests/ActivityTests/src/com/google/android/test/activity/TrackTimeReceiver.java b/tests/ActivityTests/src/com/google/android/test/activity/TrackTimeReceiver.java
new file mode 100644
index 0000000..c30d33a
--- /dev/null
+++ b/tests/ActivityTests/src/com/google/android/test/activity/TrackTimeReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.ActivityOptions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class TrackTimeReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle data = intent.getExtras();
+ data.getLong(ActivityOptions.EXTRA_USAGE_REPORT_TIME);
+ Log.i("ActivityTest", "Received time: " + data);
+ }
+}