Merge "Implemented visual speed-bump for notifications."
diff --git a/api/current.txt b/api/current.txt
index d3aaf2c..907c3b8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -27112,6 +27112,7 @@
method public java.lang.String iccTransmitApduLogicalChannel(int, int, int, int, int, int, java.lang.String);
method public boolean isNetworkRoaming();
method public void listen(android.telephony.PhoneStateListener, int);
+ method public java.lang.String sendEnvelopeWithStatus(java.lang.String);
field public static final java.lang.String ACTION_PHONE_STATE_CHANGED = "android.intent.action.PHONE_STATE";
field public static final java.lang.String ACTION_RESPOND_VIA_MESSAGE = "android.intent.action.RESPOND_VIA_MESSAGE";
field public static final int CALL_STATE_IDLE = 0; // 0x0
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index b4d8942..eeb5283 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1345,6 +1345,15 @@
public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
int initialCode, String initialData, Bundle initialExtras) {
+ sendOrderedBroadcastAsUser(intent, user, receiverPermission, AppOpsManager.OP_NONE,
+ resultReceiver, scheduler, initialCode, initialData, initialExtras);
+ }
+
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler,
+ int initialCode, String initialData, Bundle initialExtras) {
IIntentReceiver rd = null;
if (resultReceiver != null) {
if (mPackageInfo != null) {
diff --git a/core/java/android/app/task/ITaskCallback.aidl b/core/java/android/app/task/ITaskCallback.aidl
index ffa57d1..d8a32fd 100644
--- a/core/java/android/app/task/ITaskCallback.aidl
+++ b/core/java/android/app/task/ITaskCallback.aidl
@@ -34,14 +34,17 @@
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param taskId Unique integer used to identify this task.
+ * @param ongoing True to indicate that the client is processing the task. False if the task is
+ * complete
*/
- void acknowledgeStartMessage(int taskId);
+ void acknowledgeStartMessage(int taskId, boolean ongoing);
/**
* Immediate callback to the system after sending a stop signal, used to quickly detect ANR.
*
* @param taskId Unique integer used to identify this task.
+ * @param rescheulde Whether or not to reschedule this task.
*/
- void acknowledgeStopMessage(int taskId);
+ void acknowledgeStopMessage(int taskId, boolean reschedule);
/*
* Tell the task manager that the client is done with its execution, so that it can go on to
* the next one and stop attributing wakelock time to us etc.
diff --git a/core/java/android/app/task/TaskService.java b/core/java/android/app/task/TaskService.java
index 81333be..ab1a565 100644
--- a/core/java/android/app/task/TaskService.java
+++ b/core/java/android/app/task/TaskService.java
@@ -18,7 +18,6 @@
import android.app.Service;
import android.content.Intent;
-import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -124,22 +123,20 @@
switch (msg.what) {
case MSG_EXECUTE_TASK:
try {
- TaskService.this.onStartTask(params);
+ boolean workOngoing = TaskService.this.onStartTask(params);
+ ackStartMessage(params, workOngoing);
} catch (Exception e) {
Log.e(TAG, "Error while executing task: " + params.getTaskId());
throw new RuntimeException(e);
- } finally {
- maybeAckMessageReceived(params, MSG_EXECUTE_TASK);
}
break;
case MSG_STOP_TASK:
try {
- TaskService.this.onStopTask(params);
+ boolean ret = TaskService.this.onStopTask(params);
+ ackStopMessage(params, ret);
} catch (Exception e) {
Log.e(TAG, "Application unable to handle onStopTask.", e);
throw new RuntimeException(e);
- } finally {
- maybeAckMessageReceived(params, MSG_STOP_TASK);
}
break;
case MSG_TASK_FINISHED:
@@ -162,30 +159,34 @@
}
}
- /**
- * Messages come in on the application's main thread, so rather than run the risk of
- * waiting for an app that may be doing something foolhardy, we ack to the system after
- * processing a message. This allows us to throw up an ANR dialogue as quickly as possible.
- * @param params id of the task we're acking.
- * @param state Information about what message we're acking.
- */
- private void maybeAckMessageReceived(TaskParams params, int state) {
+ private void ackStartMessage(TaskParams params, boolean workOngoing) {
final ITaskCallback callback = params.getCallback();
final int taskId = params.getTaskId();
if (callback != null) {
try {
- if (state == MSG_EXECUTE_TASK) {
- callback.acknowledgeStartMessage(taskId);
- } else if (state == MSG_STOP_TASK) {
- callback.acknowledgeStopMessage(taskId);
- }
+ callback.acknowledgeStartMessage(taskId, workOngoing);
} catch(RemoteException e) {
Log.e(TAG, "System unreachable for starting task.");
}
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, state + ": Attempting to ack a task that has already been" +
- "processed.");
+ Log.d(TAG, "Attempting to ack a task that has already been processed.");
+ }
+ }
+ }
+
+ private void ackStopMessage(TaskParams params, boolean reschedule) {
+ final ITaskCallback callback = params.getCallback();
+ final int taskId = params.getTaskId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeStopMessage(taskId, reschedule);
+ } catch(RemoteException e) {
+ Log.e(TAG, "System unreachable for stopping task.");
+ }
+ } else {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a task that has already been processed.");
}
}
}
@@ -203,12 +204,14 @@
*
* @param params Parameters specifying info about this task, including the extras bundle you
* optionally provided at task-creation time.
+ * @return True if your service needs to process the work (on a separate thread). False if
+ * there's no more work to be done for this task.
*/
- public abstract void onStartTask(TaskParams params);
+ public abstract boolean onStartTask(TaskParams params);
/**
- * This method is called if your task should be stopped even before you've called
- * {@link #taskFinished(TaskParams, boolean)}.
+ * This method is called if the system has determined that you must stop execution of your task
+ * even before you've had a chance to call {@link #taskFinished(TaskParams, boolean)}.
*
* <p>This will happen if the requirements specified at schedule time are no longer met. For
* example you may have requested WiFi with
@@ -217,33 +220,27 @@
* {@link android.content.Task.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its
* idle maintenance window. You are solely responsible for the behaviour of your application
* upon receipt of this message; your app will likely start to misbehave if you ignore it. One
- * repercussion is that the system will cease to hold a wakelock for you.</p>
- *
- * <p>After you've done your clean-up you are still expected to call
- * {@link #taskFinished(TaskParams, boolean)} this will inform the TaskManager that all is well, and
- * allow you to reschedule your task as it is probably uncompleted. Until you call
- * taskFinished() you will not receive any newly scheduled tasks with the given task id as the
- * TaskManager will consider the task to be in an error state.</p>
+ * immediate repercussion is that the system will cease holding a wakelock for you.</p>
*
* @param params Parameters specifying info about this task.
* @return True to indicate to the TaskManager whether you'd like to reschedule this task based
- * on the criteria provided at task creation-time. False to drop the task. Regardless of the
- * value returned, your task must stop executing.
+ * on the retry criteria provided at task creation-time. False to drop the task. Regardless of
+ * the value returned, your task must stop executing.
*/
public abstract boolean onStopTask(TaskParams params);
/**
- * Callback to inform the TaskManager you have completed execution. This can be called from any
+ * Callback to inform the TaskManager you've finished executing. This can be called from any
* thread, as it will ultimately be run on your application's main thread. When the system
* receives this message it will release the wakelock being held.
* <p>
- * You can specify post-execution behaviour to the scheduler here with <code>needsReschedule
- * </code>. This will apply a back-off timer to your task based on the default, or what was
- * set with {@link android.content.Task.Builder#setBackoffCriteria(long, int)}. The
- * original requirements are always honoured even for a backed-off task.
- * Note that a task running in idle mode will not be backed-off. Instead what will happen
- * is the task will be re-added to the queue and re-executed within a future idle
- * maintenance window.
+ * You can specify post-execution behaviour to the scheduler here with
+ * <code>needsReschedule </code>. This will apply a back-off timer to your task based on
+ * the default, or what was set with
+ * {@link android.content.Task.Builder#setBackoffCriteria(long, int)}. The original
+ * requirements are always honoured even for a backed-off task. Note that a task running in
+ * idle mode will not be backed-off. Instead what will happen is the task will be re-added
+ * to the queue and re-executed within a future idle maintenance window.
* </p>
*
* @param params Parameters specifying system-provided info about this task, this was given to
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index a059e48..a364e68 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -1499,6 +1499,17 @@
@Nullable Bundle initialExtras);
/**
+ * Similar to above but takes an appOp as well, to enforce restrictions.
+ * @see #sendOrderedBroadcastAsUser(Intent, UserHandle, String,
+ * BroadcastReceiver, Handler, int, String, Bundle)
+ * @hide
+ */
+ public abstract void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ @Nullable String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ @Nullable Handler scheduler, int initialCode, @Nullable String initialData,
+ @Nullable Bundle initialExtras);
+
+ /**
* Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the
* Intent you are sending stays around after the broadcast is complete,
* so that others can quickly retrieve that data through the return
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 93f6cdf..c66355b 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -418,6 +418,16 @@
scheduler, initialCode, initialData, initialExtras);
}
+ /** @hide */
+ @Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler,
+ int initialCode, String initialData, Bundle initialExtras) {
+ mBase.sendOrderedBroadcastAsUser(intent, user, receiverPermission, appOp, resultReceiver,
+ scheduler, initialCode, initialData, initialExtras);
+ }
+
@Override
public void sendStickyBroadcast(Intent intent) {
mBase.sendStickyBroadcast(intent);
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index cf0caed..af45fa0 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -531,7 +531,8 @@
public final static class HistoryItem implements Parcelable {
public HistoryItem next;
-
+
+ // The time of this event in milliseconds, as per SystemClock.elapsedRealtime().
public long time;
public static final byte CMD_UPDATE = 0; // These can be written as deltas
@@ -645,7 +646,7 @@
public int eventCode;
public HistoryTag eventTag;
- // Only set for CMD_CURRENT_TIME.
+ // Only set for CMD_CURRENT_TIME or CMD_RESET, as per System.currentTimeMillis().
public long currentTime;
// Meta-data when reading.
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index f14e73f..c1a4fee 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -16,6 +16,7 @@
package android.view;
+import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
@@ -28,12 +29,12 @@
import com.android.internal.view.animation.NativeInterpolatorFactory;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
/**
* @hide
*/
-public final class RenderNodeAnimator {
-
+public final class RenderNodeAnimator extends Animator {
// Keep in sync with enum RenderProperty in Animator.h
public static final int TRANSLATION_X = 0;
public static final int TRANSLATION_Y = 1;
@@ -50,6 +51,11 @@
// Keep in sync with enum PaintFields in Animator.h
public static final int PAINT_STROKE_WIDTH = 0;
+
+ /**
+ * Field for the Paint alpha channel, which should be specified as a value
+ * between 0 and 255.
+ */
public static final int PAINT_ALPHA = 1;
// ViewPropertyAnimator uses a mask for its values, we need to remap them
@@ -74,8 +80,11 @@
private VirtualRefBasePtr mNativePtr;
private RenderNode mTarget;
+ private View mViewTarget;
private TimeInterpolator mInterpolator;
+
private boolean mStarted = false;
+ private boolean mFinished = false;
public int mapViewPropertyToRenderProperty(int viewProperty) {
return sViewPropertyAnimatorMap.get(viewProperty);
@@ -92,6 +101,14 @@
property.getNativeContainer(), finalValue));
}
+ /**
+ * Creates a new render node animator for a field on a Paint property.
+ *
+ * @param property The paint property to target
+ * @param paintField Paint field to animate, one of {@link #PAINT_ALPHA} or
+ * {@link #PAINT_STROKE_WIDTH}
+ * @param finalValue The target value for the property
+ */
public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) {
init(nCreateCanvasPropertyPaintAnimator(
new WeakReference<RenderNodeAnimator>(this),
@@ -115,56 +132,139 @@
if (mInterpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class)) {
ni = ((NativeInterpolatorFactory)mInterpolator).createNativeInterpolator();
} else {
- int duration = nGetDuration(mNativePtr.get());
+ long duration = nGetDuration(mNativePtr.get());
ni = FallbackLUTInterpolator.createNativeInterpolator(mInterpolator, duration);
}
nSetInterpolator(mNativePtr.get(), ni);
}
- private void start(RenderNode node) {
+ @Override
+ public void start() {
+ if (mTarget == null) {
+ throw new IllegalStateException("Missing target!");
+ }
+
if (mStarted) {
throw new IllegalStateException("Already started!");
}
+
mStarted = true;
applyInterpolator();
- mTarget = node;
mTarget.addAnimator(this);
+
+ final ArrayList<AnimatorListener> listeners = getListeners();
+ final int numListeners = listeners == null ? 0 : listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ listeners.get(i).onAnimationStart(this);
+ }
+
+ if (mViewTarget != null) {
+ // Kick off a frame to start the process
+ mViewTarget.invalidateViewProperty(true, false);
+ }
}
- public void start(View target) {
- start(target.mRenderNode);
- // Kick off a frame to start the process
- target.invalidateViewProperty(true, false);
+ @Override
+ public void cancel() {
+ mTarget.removeAnimator(this);
+
+ final ArrayList<AnimatorListener> listeners = getListeners();
+ final int numListeners = listeners == null ? 0 : listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ listeners.get(i).onAnimationCancel(this);
+ }
}
- public void start(Canvas canvas) {
+ @Override
+ public void end() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void pause() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void resume() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setTarget(View view) {
+ mViewTarget = view;
+ mTarget = view.mRenderNode;
+ }
+
+ public void setTarget(Canvas canvas) {
if (!(canvas instanceof GLES20RecordingCanvas)) {
throw new IllegalArgumentException("Not a GLES20RecordingCanvas");
}
- GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas;
- start(recordingCanvas.mNode);
+
+ final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas;
+ setTarget(recordingCanvas.mNode);
}
- public void cancel() {
- mTarget.removeAnimator(this);
+ public void setTarget(RenderNode node) {
+ mViewTarget = null;
+ mTarget = node;
}
- public void setDuration(int duration) {
+ public RenderNode getTarget() {
+ return mTarget;
+ }
+
+ @Override
+ public void setStartDelay(long startDelay) {
+ checkMutable();
+ nSetStartDelay(mNativePtr.get(), startDelay);
+ }
+
+ @Override
+ public long getStartDelay() {
+ return nGetStartDelay(mNativePtr.get());
+ }
+
+ @Override
+ public RenderNodeAnimator setDuration(long duration) {
checkMutable();
nSetDuration(mNativePtr.get(), duration);
+ return this;
}
+ @Override
+ public long getDuration() {
+ return nGetDuration(mNativePtr.get());
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mStarted && !mFinished;
+ }
+
+ @Override
public void setInterpolator(TimeInterpolator interpolator) {
checkMutable();
mInterpolator = interpolator;
}
- long getNativeAnimator() {
- return mNativePtr.get();
+ @Override
+ public TimeInterpolator getInterpolator() {
+ return mInterpolator;
}
private void onFinished() {
+ mFinished = true;
mTarget.removeAnimator(this);
+
+ final ArrayList<AnimatorListener> listeners = getListeners();
+ final int numListeners = listeners == null ? 0 : listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ listeners.get(i).onAnimationEnd(this);
+ }
+ }
+
+ long getNativeAnimator() {
+ return mNativePtr.get();
}
// Called by native
@@ -181,7 +281,9 @@
long canvasProperty, float deltaValue);
private static native long nCreateCanvasPropertyPaintAnimator(WeakReference<RenderNodeAnimator> weakThis,
long canvasProperty, int paintField, float deltaValue);
- private static native void nSetDuration(long nativePtr, int duration);
- private static native int nGetDuration(long nativePtr);
+ private static native void nSetDuration(long nativePtr, long duration);
+ private static native long nGetDuration(long nativePtr);
+ private static native void nSetStartDelay(long nativePtr, long startDelay);
+ private static native long nGetStartDelay(long nativePtr);
private static native void nSetInterpolator(long animPtr, long interpolatorPtr);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 5141877..fb7d57d 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -4774,8 +4774,8 @@
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
- manageFocusHotspot(true, oldFocus);
onFocusChanged(true, direction, previouslyFocusedRect);
+ manageFocusHotspot(true, oldFocus);
refreshDrawableState();
}
}
@@ -6752,6 +6752,24 @@
}
/**
+ * Sets the pressed state for this view and provides a touch coordinate for
+ * animation hinting.
+ *
+ * @param pressed Pass true to set the View's internal state to "pressed",
+ * or false to reverts the View's internal state from a
+ * previously set "pressed" state.
+ * @param x The x coordinate of the touch that caused the press
+ * @param y The y coordinate of the touch that caused the press
+ */
+ private void setPressed(boolean pressed, float x, float y) {
+ if (pressed) {
+ setHotspot(R.attr.state_pressed, x, y);
+ }
+
+ setPressed(pressed);
+ }
+
+ /**
* Sets the pressed state for this view.
*
* @see #isClickable()
@@ -6769,6 +6787,10 @@
mPrivateFlags &= ~PFLAG_PRESSED;
}
+ if (!pressed) {
+ clearHotspot(R.attr.state_pressed);
+ }
+
if (needsRefresh) {
refreshDrawableState();
}
@@ -8993,7 +9015,6 @@
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
- clearHotspot(R.attr.state_pressed);
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
@@ -9026,8 +9047,7 @@
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
- setHotspot(R.attr.state_pressed, x, y);
- setPressed(true);
+ setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
@@ -9061,8 +9081,6 @@
}
removeTapCallback();
- } else {
- clearHotspot(R.attr.state_pressed);
}
break;
@@ -9175,7 +9193,6 @@
*/
private void removeUnsetPressCallback() {
if ((mPrivateFlags & PFLAG_PRESSED) != 0 && mUnsetPressedState != null) {
- clearHotspot(R.attr.state_pressed);
setPressed(false);
removeCallbacks(mUnsetPressedState);
}
@@ -19234,8 +19251,7 @@
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
- setHotspot(R.attr.state_pressed, x, y);
- setPressed(true);
+ setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
@@ -19516,7 +19532,6 @@
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
- clearHotspot(R.attr.state_pressed);
setPressed(false);
}
}
diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java
index 438e164..74a3eec 100644
--- a/core/java/android/widget/Switch.java
+++ b/core/java/android/widget/Switch.java
@@ -666,6 +666,8 @@
case MotionEvent.ACTION_CANCEL: {
if (mTouchMode == TOUCH_MODE_DRAGGING) {
stopDrag(ev);
+ // Allow super class to handle pressed state, etc.
+ super.onTouchEvent(ev);
return true;
}
mTouchMode = TOUCH_MODE_IDLE;
@@ -801,7 +803,7 @@
}
@Override
- protected void onDraw(Canvas canvas) {
+ public void draw(Canvas c) {
final Rect tempRect = mTempRect;
final Drawable trackDrawable = mTrackDrawable;
final Drawable thumbDrawable = mThumbDrawable;
@@ -815,9 +817,6 @@
trackDrawable.getPadding(tempRect);
final int switchInnerLeft = switchLeft + tempRect.left;
- final int switchInnerTop = switchTop + tempRect.top;
- final int switchInnerRight = switchRight - tempRect.right;
- final int switchInnerBottom = switchBottom - tempRect.bottom;
// Relies on mTempRect, MUST be called first!
final int thumbPos = getThumbOffset();
@@ -833,8 +832,26 @@
background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
}
+ // Draw the background.
+ super.draw(c);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
+ final Rect tempRect = mTempRect;
+ final Drawable trackDrawable = mTrackDrawable;
+ final Drawable thumbDrawable = mThumbDrawable;
+ trackDrawable.getPadding(tempRect);
+
+ final int switchTop = mSwitchTop;
+ final int switchBottom = mSwitchBottom;
+ final int switchInnerLeft = mSwitchLeft + tempRect.left;
+ final int switchInnerTop = switchTop + tempRect.top;
+ final int switchInnerRight = mSwitchRight - tempRect.right;
+ final int switchInnerBottom = switchBottom - tempRect.bottom;
+
if (mSplitTrack) {
final Insets insets = thumbDrawable.getOpticalInsets();
thumbDrawable.copyBounds(tempRect);
@@ -861,7 +878,8 @@
}
mTextPaint.drawableState = drawableState;
- final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2;
+ final Rect thumbBounds = thumbDrawable.getBounds();
+ final int left = (thumbBounds.left + thumbBounds.right) / 2 - switchText.getWidth() / 2;
final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
canvas.translate(left, top);
switchText.draw(canvas);
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 956c86d..8428f66 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -235,7 +235,8 @@
int mWakeLockNesting;
boolean mWakeLockImportant;
- public boolean mRecordAllWakeLocks;
+ boolean mRecordAllWakeLocks;
+ boolean mNoAutoReset;
int mScreenState = Display.STATE_UNKNOWN;
StopwatchTimer mScreenOnTimer;
@@ -2314,9 +2315,6 @@
}
}
- private String mInitialAcquireWakeName;
- private int mInitialAcquireWakeUid = -1;
-
public void setRecordAllWakeLocksLocked(boolean enabled) {
mRecordAllWakeLocks = enabled;
if (!enabled) {
@@ -2325,6 +2323,13 @@
}
}
+ public void setNoAutoReset(boolean enabled) {
+ mNoAutoReset = enabled;
+ }
+
+ private String mInitialAcquireWakeName;
+ private int mInitialAcquireWakeUid = -1;
+
public void noteStartWakeLocked(int uid, int pid, String name, String historyName, int type,
boolean unimportantForLogging, long elapsedRealtime, long uptime) {
uid = mapUid(uid);
@@ -2355,6 +2360,7 @@
} else if (mRecordAllWakeLocks) {
if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_START, historyName,
uid, 0)) {
+ mWakeLockNesting++;
return;
}
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_WAKE_LOCK_START,
@@ -5942,9 +5948,9 @@
// we have gone through a significant charge (from a very low
// level to a now very high level).
boolean reset = false;
- if (oldStatus == BatteryManager.BATTERY_STATUS_FULL
+ if (!mNoAutoReset && (oldStatus == BatteryManager.BATTERY_STATUS_FULL
|| level >= 90
- || (mDischargeCurrentLevel < 20 && level >= 80)) {
+ || (mDischargeCurrentLevel < 20 && level >= 80))) {
doWrite = true;
resetAllStatsLocked();
mDischargeStartLevel = level;
diff --git a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
index aec2b7e..1feb943 100644
--- a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
+++ b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
@@ -34,11 +34,11 @@
* Used to cache the float[] LUT for use across multiple native
* interpolator creation
*/
- public FallbackLUTInterpolator(TimeInterpolator interpolator, int duration) {
+ public FallbackLUTInterpolator(TimeInterpolator interpolator, long duration) {
mLut = createLUT(interpolator, duration);
}
- private static float[] createLUT(TimeInterpolator interpolator, int duration) {
+ private static float[] createLUT(TimeInterpolator interpolator, long duration) {
long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
int numAnimFrames = (int) Math.ceil(duration / animIntervalMs);
@@ -59,7 +59,7 @@
/**
* Used to create a one-shot float[] LUT & native interpolator
*/
- public static long createNativeInterpolator(TimeInterpolator interpolator, int duration) {
+ public static long createNativeInterpolator(TimeInterpolator interpolator, long duration) {
float[] lut = createLUT(interpolator, duration);
return NativeInterpolatorFactoryHelper.createLutInterpolator(lut);
}
diff --git a/core/jni/android_view_RenderNodeAnimator.cpp b/core/jni/android_view_RenderNodeAnimator.cpp
index ea2f96e..e19ce36 100644
--- a/core/jni/android_view_RenderNodeAnimator.cpp
+++ b/core/jni/android_view_RenderNodeAnimator.cpp
@@ -116,15 +116,26 @@
return reinterpret_cast<jlong>( animator );
}
-static void setDuration(JNIEnv* env, jobject clazz, jlong animatorPtr, jint duration) {
+static void setDuration(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong duration) {
LOG_ALWAYS_FATAL_IF(duration < 0, "Duration cannot be negative");
BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
animator->setDuration(duration);
}
-static jint getDuration(JNIEnv* env, jobject clazz, jlong animatorPtr) {
+static jlong getDuration(JNIEnv* env, jobject clazz, jlong animatorPtr) {
BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
- return static_cast<jint>(animator->duration());
+ return static_cast<jlong>(animator->duration());
+}
+
+static void setStartDelay(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong startDelay) {
+ LOG_ALWAYS_FATAL_IF(startDelay < 0, "Start delay cannot be negative");
+ BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
+ animator->setStartDelay(startDelay);
+}
+
+static jlong getStartDelay(JNIEnv* env, jobject clazz, jlong animatorPtr) {
+ BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr);
+ return static_cast<jlong>(animator->startDelay());
}
static void setInterpolator(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong interpolatorPtr) {
@@ -146,8 +157,10 @@
{ "nCreateAnimator", "(Ljava/lang/ref/WeakReference;IF)J", (void*) createAnimator },
{ "nCreateCanvasPropertyFloatAnimator", "(Ljava/lang/ref/WeakReference;JF)J", (void*) createCanvasPropertyFloatAnimator },
{ "nCreateCanvasPropertyPaintAnimator", "(Ljava/lang/ref/WeakReference;JIF)J", (void*) createCanvasPropertyPaintAnimator },
- { "nSetDuration", "(JI)V", (void*) setDuration },
- { "nGetDuration", "(J)I", (void*) getDuration },
+ { "nSetDuration", "(JJ)V", (void*) setDuration },
+ { "nGetDuration", "(J)J", (void*) getDuration },
+ { "nSetStartDelay", "(JJ)V", (void*) setStartDelay },
+ { "nGetStartDelay", "(J)J", (void*) getStartDelay },
{ "nSetInterpolator", "(JJ)V", (void*) setInterpolator },
#endif
};
diff --git a/core/res/res/values/styles_quantum.xml b/core/res/res/values/styles_quantum.xml
index a49b89a..d04bddf 100644
--- a/core/res/res/values/styles_quantum.xml
+++ b/core/res/res/values/styles_quantum.xml
@@ -427,7 +427,10 @@
<item name="paddingEnd">8dp</item>
</style>
- <style name="Widget.Quantum.CheckedTextView" parent="Widget.CheckedTextView"/>
+ <style name="Widget.Quantum.CheckedTextView" parent="Widget.CheckedTextView">
+ <item name="drawablePadding">4dip</item>
+ </style>
+
<style name="Widget.Quantum.TextSelectHandle" parent="Widget.TextSelectHandle"/>
<style name="Widget.Quantum.TextSuggestionsPopupWindow" parent="Widget.TextSuggestionsPopupWindow"/>
<style name="Widget.Quantum.AbsListView" parent="Widget.AbsListView"/>
@@ -441,10 +444,12 @@
<style name="Widget.Quantum.CompoundButton.CheckBox" parent="Widget.CompoundButton.CheckBox">
<item name="background">?attr/selectableItemBackground</item>
+ <item name="drawablePadding">4dip</item>
</style>
<style name="Widget.Quantum.CompoundButton.RadioButton" parent="Widget.CompoundButton.RadioButton">
<item name="background">?attr/selectableItemBackground</item>
+ <item name="drawablePadding">4dip</item>
</style>
<style name="Widget.Quantum.CompoundButton.Star" parent="Widget.CompoundButton.Star">
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java
index 218a057..24e8de6 100644
--- a/graphics/java/android/graphics/drawable/Ripple.java
+++ b/graphics/java/android/graphics/drawable/Ripple.java
@@ -17,227 +17,220 @@
package android.graphics.drawable;
import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
-import android.view.animation.DecelerateInterpolator;
+import android.view.HardwareCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.animation.AccelerateInterpolator;
+
+import java.util.ArrayList;
/**
* Draws a Quantum Paper ripple.
*/
class Ripple {
- private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator();
+ private static final TimeInterpolator INTERPOLATOR = new AccelerateInterpolator();
- /** Starting radius for a ripple. */
- private static final int STARTING_RADIUS_DP = 16;
+ private static final float GLOBAL_SPEED = 1.0f;
+ private static final float WAVE_TOUCH_DOWN_ACCELERATION = 512.0f * GLOBAL_SPEED;
+ private static final float WAVE_TOUCH_UP_ACCELERATION = 1024.0f * GLOBAL_SPEED;
+ private static final float WAVE_OPACITY_DECAY_VELOCITY = 1.6f / GLOBAL_SPEED;
+ private static final float WAVE_OUTER_OPACITY_VELOCITY = 1.2f * GLOBAL_SPEED;
- /** Radius when finger is outside view bounds. */
- private static final int OUTSIDE_RADIUS_DP = 16;
-
- /** Radius when finger is inside view bounds. */
- private static final int INSIDE_RADIUS_DP = 96;
-
- /** Margin when constraining outside touches (fraction of outer radius). */
- private static final float OUTSIDE_MARGIN = 0.8f;
-
- /** Resistance factor when constraining outside touches. */
- private static final float OUTSIDE_RESISTANCE = 0.7f;
-
- /** Minimum alpha value during a pulse animation. */
- private static final float PULSE_MIN_ALPHA = 0.5f;
-
- /** Duration for animating the trailing edge of the ripple. */
- private static final int EXIT_DURATION = 600;
-
- /** Duration for animating the leading edge of the ripple. */
- private static final int ENTER_DURATION = 400;
-
- /** Duration for animating the ripple alpha in and out. */
- private static final int FADE_DURATION = 50;
-
- /** Minimum elapsed time between start of enter and exit animations. */
- private static final int EXIT_MIN_DELAY = 200;
-
- /** Duration for animating between inside and outside touch. */
- private static final int OUTSIDE_DURATION = 300;
-
- /** Duration for animating pulses. */
- private static final int PULSE_DURATION = 400;
-
- /** Interval between pulses while inside and fully entered. */
- private static final int PULSE_INTERVAL = 400;
-
- /** Delay before pulses start. */
- private static final int PULSE_DELAY = 500;
+ // Hardware animators.
+ private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
+ private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
private final Drawable mOwner;
- /** Bounds used for computing max radius and containment. */
+ /** Bounds used for computing max radius. */
private final Rect mBounds;
- /** Configured maximum ripple radius when the center is outside the bounds. */
- private final int mMaxOutsideRadius;
-
- /** Configured maximum ripple radius. */
- private final int mMaxInsideRadius;
-
- private ObjectAnimator mOuter;
- private ObjectAnimator mInner;
- private ObjectAnimator mAlpha;
+ /** Full-opacity color for drawing this ripple. */
+ private final int mColor;
/** Maximum ripple radius. */
- private int mMaxRadius;
-
private float mOuterRadius;
- private float mInnerRadius;
- private float mAlphaMultiplier;
- /** Center x-coordinate. */
+ // Hardware rendering properties.
+ private CanvasProperty<Paint> mPropPaint;
+ private CanvasProperty<Float> mPropRadius;
+ private CanvasProperty<Float> mPropX;
+ private CanvasProperty<Float> mPropY;
+ private CanvasProperty<Paint> mPropOuterPaint;
+ private CanvasProperty<Float> mPropOuterRadius;
+ private CanvasProperty<Float> mPropOuterX;
+ private CanvasProperty<Float> mPropOuterY;
+
+ // Software animators.
+ private ObjectAnimator mAnimRadius;
+ private ObjectAnimator mAnimOpacity;
+ private ObjectAnimator mAnimOuterOpacity;
+ private ObjectAnimator mAnimX;
+ private ObjectAnimator mAnimY;
+
+ // Software rendering properties.
+ private float mOuterOpacity = 0;
+ private float mOpacity = 1;
+ private float mRadius = 0;
+ private float mOuterX;
+ private float mOuterY;
private float mX;
-
- /** Center y-coordinate. */
private float mY;
- /** Whether the center is within the parent bounds. */
- private boolean mInsideBounds;
+ private boolean mFinished;
- /** Whether to pulse this ripple. */
- private boolean mPulseEnabled;
+ /** Whether we should be drawing hardware animations. */
+ private boolean mHardwareAnimating;
- /** Temporary hack since we can't check finished state of animator. */
- private boolean mExitFinished;
-
- /** Whether this ripple has ever moved. */
- private boolean mHasMoved;
+ /** Whether we can use hardware acceleration for the exit animation. */
+ private boolean mCanUseHardware;
/**
* Creates a new ripple.
*/
- public Ripple(Drawable owner, Rect bounds, float density, boolean pulseEnabled) {
+ public Ripple(Drawable owner, Rect bounds, int color) {
mOwner = owner;
mBounds = bounds;
- mPulseEnabled = pulseEnabled;
+ mColor = color | 0xFF000000;
- mOuterRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
- mMaxOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
- mMaxInsideRadius = (int) (density * INSIDE_RADIUS_DP + 0.5f);
- mMaxRadius = Math.min(mMaxInsideRadius, Math.max(bounds.width(), bounds.height()));
+ final float halfWidth = bounds.width() / 2.0f;
+ final float halfHeight = bounds.height() / 2.0f;
+ mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+ mOuterX = 0;
+ mOuterY = 0;
}
- public void setOuterRadius(float r) {
- mOuterRadius = r;
+ public void setRadius(float r) {
+ mRadius = r;
invalidateSelf();
}
- public float getOuterRadius() {
- return mOuterRadius;
+ public float getRadius() {
+ return mRadius;
}
- public void setInnerRadius(float r) {
- mInnerRadius = r;
+ public void setOpacity(float a) {
+ mOpacity = a;
invalidateSelf();
}
- public float getInnerRadius() {
- return mInnerRadius;
+ public float getOpacity() {
+ return mOpacity;
}
- public void setAlphaMultiplier(float a) {
- mAlphaMultiplier = a;
+ public void setOuterOpacity(float a) {
+ mOuterOpacity = a;
invalidateSelf();
}
- public float getAlphaMultiplier() {
- return mAlphaMultiplier;
+ public float getOuterOpacity() {
+ return mOuterOpacity;
+ }
+
+ public void setX(float x) {
+ mX = x;
+ invalidateSelf();
+ }
+
+ public float getX() {
+ return mX;
+ }
+
+ public void setY(float y) {
+ mY = y;
+ invalidateSelf();
+ }
+
+ public float getY() {
+ return mY;
}
/**
* Returns whether this ripple has finished exiting.
*/
public boolean isFinished() {
- return mExitFinished;
+ return mFinished;
}
/**
- * Called when the bounds change.
- */
- public void onBoundsChanged() {
- mMaxRadius = Math.min(mMaxInsideRadius, Math.max(mBounds.width(), mBounds.height()));
-
- updateInsideBounds();
- }
-
- private void updateInsideBounds() {
- final boolean insideBounds = mBounds.contains((int) (mX + 0.5f), (int) (mY + 0.5f));
- if (mInsideBounds != insideBounds || !mHasMoved) {
- mInsideBounds = insideBounds;
- mHasMoved = true;
-
- if (insideBounds) {
- enter();
- } else {
- outside();
- }
- }
- }
-
- /**
- * Draws the ripple using the specified paint.
+ * Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
- final Rect bounds = mBounds;
- final float outerRadius = mOuterRadius;
- final float innerRadius = mInnerRadius;
- final float alphaMultiplier = mAlphaMultiplier;
+ final boolean canUseHardware = c.isHardwareAccelerated();
+ if (mCanUseHardware != canUseHardware && mCanUseHardware) {
+ // We've switched from hardware to non-hardware mode. Panic.
+ cancelHardwareAnimations();
+ }
+ mCanUseHardware = canUseHardware;
+
+ final boolean hasContent;
+ if (canUseHardware && mHardwareAnimating) {
+ hasContent = drawHardware((HardwareCanvas) c);
+ } else {
+ hasContent = drawSoftware(c, p);
+ }
+
+ return hasContent;
+ }
+
+ private boolean drawHardware(HardwareCanvas c) {
+ // If we have any pending hardware animations, cancel any running
+ // animations and start those now.
+ final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
+ final int N = pendingAnimations == null ? 0 : pendingAnimations.size();
+ if (N > 0) {
+ cancelHardwareAnimations();
+
+ for (int i = 0; i < N; i++) {
+ pendingAnimations.get(i).setTarget(c);
+ pendingAnimations.get(i).start();
+ }
+
+ mRunningAnimations.addAll(pendingAnimations);
+ pendingAnimations.clear();
+ }
+
+ c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+
+ return true;
+ }
+
+ private boolean drawSoftware(Canvas c, Paint p) {
+ final float radius = mRadius;
+ final float opacity = mOpacity;
+ final float outerOpacity = mOuterOpacity;
// Cache the paint alpha so we can restore it later.
final int paintAlpha = p.getAlpha();
- final int alpha = (int) (paintAlpha * alphaMultiplier + 0.5f);
+ final int alpha = (int) (255 * opacity + 0.5f);
+ final int outerAlpha = (int) (255 * outerOpacity + 0.5f);
- // Apply resistance effect when outside bounds.
- final float x;
- final float y;
- if (mInsideBounds) {
- x = mX;
- y = mY;
- } else {
- // TODO: We need to do this outside of draw() so that our dirty
- // bounds accurately reflect resistance.
- x = looseConstrain(mX, bounds.left, bounds.right,
- mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
- y = looseConstrain(mY, bounds.top, bounds.bottom,
- mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
+ boolean hasContent = false;
+
+ if (outerAlpha > 0 && alpha > 0) {
+ p.setAlpha(Math.min(alpha, outerAlpha));
+ p.setStyle(Style.FILL);
+ c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
+ hasContent = true;
}
- final boolean hasContent;
- if (alphaMultiplier <= 0 || innerRadius >= outerRadius) {
- // Nothing to draw.
- hasContent = false;
- } else if (innerRadius > 0) {
- // Draw a ring.
- final float strokeWidth = outerRadius - innerRadius;
- final float strokeRadius = innerRadius + strokeWidth / 2.0f;
- p.setAlpha(alpha);
- p.setStyle(Style.STROKE);
- p.setStrokeWidth(strokeWidth);
- c.drawCircle(x, y, strokeRadius, p);
- hasContent = true;
- } else if (outerRadius > 0) {
- // Draw a circle.
+ if (opacity > 0 && radius > 0) {
p.setAlpha(alpha);
p.setStyle(Style.FILL);
- c.drawCircle(x, y, outerRadius, p);
+ c.drawCircle(mX, mY, radius, p);
hasContent = true;
- } else {
- hasContent = false;
}
p.setAlpha(paintAlpha);
+
return hasContent;
}
@@ -245,156 +238,279 @@
* Returns the maximum bounds for this ripple.
*/
public void getBounds(Rect bounds) {
+ final int outerX = (int) mOuterX;
+ final int outerY = (int) mOuterY;
+ final int r = (int) mOuterRadius;
+ bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
+
final int x = (int) mX;
final int y = (int) mY;
- final int maxRadius = mMaxRadius;
- bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
+ bounds.union(x - r, y - r, x + r, y + r);
}
/**
- * Updates the center coordinates.
+ * Starts the enter animation at the specified absolute coordinates.
*/
- public void move(float x, float y) {
- mX = x;
- mY = y;
+ public void enter(float x, float y) {
+ mX = x - mBounds.exactCenterX();
+ mY = y - mBounds.exactCenterY();
- updateInsideBounds();
+ final int radiusDuration = (int)
+ (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION) + 0.5);
+ final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY);
+
+ final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", 0, mOuterRadius);
+ radius.setAutoCancel(true);
+ radius.setDuration(radiusDuration);
+
+ final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "x", mOuterX);
+ cX.setAutoCancel(true);
+ cX.setDuration(radiusDuration);
+
+ final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "y", mOuterY);
+ cY.setAutoCancel(true);
+ cY.setDuration(radiusDuration);
+
+ final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
+ outer.setAutoCancel(true);
+ outer.setDuration(outerDuration);
+
+ mAnimRadius = radius;
+ mAnimOuterOpacity = outer;
+ mAnimX = cX;
+ mAnimY = cY;
+
+ // Enter animations always run on the UI thread, since it's unlikely
+ // that anything interesting is happening until the user lifts their
+ // finger.
+ radius.start();
+ outer.start();
+ cX.start();
+ cY.start();
+ }
+
+ /**
+ * Starts the exit animation.
+ */
+ public void exit() {
+ cancelSoftwareAnimations();
+
+ final float remaining;
+ if (mAnimRadius != null && mAnimRadius.isRunning()) {
+ remaining = mOuterRadius - mRadius;
+ } else {
+ remaining = mOuterRadius;
+ }
+
+ final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
+ + WAVE_TOUCH_DOWN_ACCELERATION)) + 0.5);
+ final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
+
+ // Determine at what time the inner and outer opacity intersect.
+ // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
+ // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
+ final int outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity)
+ / (WAVE_OPACITY_DECAY_VELOCITY + WAVE_OUTER_OPACITY_VELOCITY) + 0.5f));
+ final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
+ * WAVE_OUTER_OPACITY_VELOCITY / 1000) + 0.5f);
+
+ if (mCanUseHardware) {
+ exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+ } else {
+ exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+ }
+ }
+
+ private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection,
+ int inflectionOpacity) {
+ mPendingAnimations.clear();
+
+ final Paint outerPaint = new Paint();
+ outerPaint.setAntiAlias(true);
+ outerPaint.setColor(mColor);
+ outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
+ outerPaint.setStyle(Style.FILL);
+ mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
+ mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
+ mPropOuterX = CanvasProperty.createFloat(mOuterX);
+ mPropOuterY = CanvasProperty.createFloat(mOuterY);
+
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setColor(mColor);
+ paint.setAlpha((int) (255 * mOpacity + 0.5f));
+ paint.setStyle(Style.FILL);
+ mPropPaint = CanvasProperty.createPaint(paint);
+ mPropRadius = CanvasProperty.createFloat(mRadius);
+ mPropX = CanvasProperty.createFloat(mX);
+ mPropY = CanvasProperty.createFloat(mY);
+
+ final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mOuterRadius);
+ radius.setDuration(radiusDuration);
+
+ final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX);
+ x.setDuration(radiusDuration);
+
+ final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY);
+ y.setDuration(radiusDuration);
+
+ final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacity.setDuration(opacityDuration);
+ opacity.addListener(mAnimationListener);
+
+ final RenderNodeAnimator outerOpacity;
+ if (outerInflection > 0) {
+ // Outer opacity continues to increase for a bit.
+ outerOpacity = new RenderNodeAnimator(
+ mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
+ outerOpacity.setDuration(outerInflection);
+
+ // Chain the outer opacity exit animation.
+ final int outerDuration = opacityDuration - outerInflection;
+ if (outerDuration > 0) {
+ final RenderNodeAnimator outerFadeOut = new RenderNodeAnimator(
+ mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ outerFadeOut.setDuration(outerDuration);
+ outerFadeOut.setStartDelay(outerInflection);
+
+ mPendingAnimations.add(outerFadeOut);
+ }
+ } else {
+ outerOpacity = new RenderNodeAnimator(
+ mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ outerOpacity.setDuration(opacityDuration);
+ }
+
+ mPendingAnimations.add(radius);
+ mPendingAnimations.add(opacity);
+ mPendingAnimations.add(outerOpacity);
+ mPendingAnimations.add(x);
+ mPendingAnimations.add(y);
+
+ mHardwareAnimating = true;
+
invalidateSelf();
}
- /**
- * Starts the exit animation. If {@link #enter()} was called recently, the
- * animation may be postponed.
- */
- public void exit() {
- mExitFinished = false;
+ private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
+ float inflectionOpacity) {
+ final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", mOuterRadius);
+ radius.setAutoCancel(true);
+ radius.setDuration(radiusDuration);
- final ObjectAnimator inner = ObjectAnimator.ofFloat(this, "innerRadius", 0, mMaxRadius);
- inner.setAutoCancel(true);
- inner.setDuration(EXIT_DURATION);
- inner.setInterpolator(INTERPOLATOR);
- inner.addListener(mAnimationListener);
+ final ObjectAnimator x = ObjectAnimator.ofFloat(this, "x", mOuterX);
+ x.setAutoCancel(true);
+ x.setDuration(radiusDuration);
- if (mOuter != null && mOuter.isStarted()) {
- // If we haven't been running the enter animation for long enough,
- // delay the exit animator.
- final int elapsed = (int) (mOuter.getAnimatedFraction() * mOuter.getDuration());
- final int delay = Math.max(0, EXIT_MIN_DELAY - elapsed);
- inner.setStartDelay(delay);
+ final ObjectAnimator y = ObjectAnimator.ofFloat(this, "y", mOuterY);
+ y.setAutoCancel(true);
+ y.setDuration(radiusDuration);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "opacity", 0);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(opacityDuration);
+ opacity.addListener(mAnimationListener);
+
+ final ObjectAnimator outerOpacity;
+ if (outerInflection > 0) {
+ // Outer opacity continues to increase for a bit.
+ outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", inflectionOpacity);
+ outerOpacity.setDuration(outerInflection);
+
+ // Chain the outer opacity exit animation.
+ final int outerDuration = opacityDuration - outerInflection;
+ outerOpacity.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final ObjectAnimator outerFadeOut = ObjectAnimator.ofFloat(Ripple.this,
+ "outerOpacity", 0);
+ outerFadeOut.setDuration(outerDuration);
+
+ mAnimOuterOpacity = outerFadeOut;
+
+ outerFadeOut.start();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ animation.removeListener(this);
+ }
+ });
+ } else {
+ outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
+ outerOpacity.setDuration(opacityDuration);
}
- inner.start();
+ mAnimRadius = radius;
+ mAnimOpacity = opacity;
+ mAnimOuterOpacity = outerOpacity;
+ mAnimX = opacity;
+ mAnimY = opacity;
- final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 0);
- alpha.setAutoCancel(true);
- alpha.setDuration(EXIT_DURATION);
- alpha.start();
-
- mInner = inner;
- mAlpha = alpha;
+ radius.start();
+ opacity.start();
+ outerOpacity.start();
+ x.start();
+ y.start();
}
/**
* Cancel all animations.
*/
public void cancel() {
- if (mInner != null) {
- mInner.cancel();
+ cancelSoftwareAnimations();
+ cancelHardwareAnimations();
+ }
+
+ private void cancelSoftwareAnimations() {
+ if (mAnimRadius != null) {
+ mAnimRadius.cancel();
}
- if (mOuter != null) {
- mOuter.cancel();
+ if (mAnimOpacity != null) {
+ mAnimOpacity.cancel();
}
- if (mAlpha != null) {
- mAlpha.cancel();
+ if (mAnimOuterOpacity != null) {
+ mAnimOuterOpacity.cancel();
}
+
+ if (mAnimX != null) {
+ mAnimX.cancel();
+ }
+
+ if (mAnimY != null) {
+ mAnimY.cancel();
+ }
+ }
+
+ /**
+ * Cancels any running hardware animations.
+ */
+ private void cancelHardwareAnimations() {
+ final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
+ final int N = runningAnimations == null ? 0 : runningAnimations.size();
+ for (int i = 0; i < N; i++) {
+ runningAnimations.get(i).cancel();
+ }
+
+ runningAnimations.clear();
}
private void invalidateSelf() {
mOwner.invalidateSelf();
}
- /**
- * Starts the enter animation.
- */
- private void enter() {
- final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerRadius", mMaxRadius);
- outer.setAutoCancel(true);
- outer.setDuration(ENTER_DURATION);
- outer.setInterpolator(INTERPOLATOR);
- outer.start();
-
- final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1);
- if (mPulseEnabled) {
- alpha.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- final ObjectAnimator pulse = ObjectAnimator.ofFloat(
- this, "alphaMultiplier", 1, PULSE_MIN_ALPHA);
- pulse.setAutoCancel(true);
- pulse.setDuration(PULSE_DURATION + PULSE_INTERVAL);
- pulse.setRepeatCount(ObjectAnimator.INFINITE);
- pulse.setRepeatMode(ObjectAnimator.REVERSE);
- pulse.setStartDelay(PULSE_DELAY);
- pulse.start();
-
- mAlpha = pulse;
- }
- });
- }
- alpha.setAutoCancel(true);
- alpha.setDuration(FADE_DURATION);
- alpha.start();
-
- mOuter = outer;
- mAlpha = alpha;
- }
-
- /**
- * Starts the outside transition animation.
- */
- private void outside() {
- final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerRadius", mMaxOutsideRadius);
- outer.setAutoCancel(true);
- outer.setDuration(OUTSIDE_DURATION);
- outer.setInterpolator(INTERPOLATOR);
- outer.start();
-
- final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1);
- alpha.setAutoCancel(true);
- alpha.setDuration(FADE_DURATION);
- alpha.start();
-
- mOuter = outer;
- mAlpha = alpha;
- }
-
- /**
- * Constrains a value within a specified asymptotic margin outside a minimum
- * and maximum.
- */
- private static float looseConstrain(float value, float min, float max, float margin,
- float factor) {
- // TODO: Can we use actual spring physics here?
- if (value < min) {
- return min - Math.min(margin, (float) Math.pow(min - value, factor));
- } else if (value > max) {
- return max + Math.min(margin, (float) Math.pow(value - max, factor));
- } else {
- return value;
- }
- }
-
- private final AnimatorListener mAnimationListener = new AnimatorListenerAdapter() {
+ private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- if (animation == mInner) {
- mExitFinished = true;
- mOuterRadius = 0;
- mInnerRadius = 0;
- mAlphaMultiplier = 1;
- }
+ mFinished = true;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mFinished = true;
}
};
}
diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
index 8128b5f..a55a4b2 100644
--- a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
+++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java
@@ -24,6 +24,7 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
+import android.graphics.PointF;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
@@ -33,6 +34,7 @@
import android.util.SparseArray;
import com.android.internal.R;
+import com.android.org.bouncycastle.util.Arrays;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -40,11 +42,36 @@
import java.io.IOException;
/**
- * Documentation pending.
+ * Drawable that shows a ripple effect in response to state changes. The
+ * anchoring position of the ripple for a given state may be specified by
+ * calling {@link #setHotspot(int, float, float)} with the corresponding state
+ * attribute identifier.
+ * <p>
+ * A touch feedback drawable may contain multiple child layers, including a
+ * special mask layer that is not drawn to the screen. A single layer may be set
+ * as the mask by specifying its android:id value as {@link android.R.id#mask}.
+ * <p>
+ * If a mask layer is set, the ripple effect will be masked against that layer
+ * before it is blended onto the composite of the remaining child layers.
+ * <p>
+ * If no mask layer is set, the ripple effect is simply blended onto the
+ * composite of the child layers using the specified
+ * {@link android.R.styleable#TouchFeedbackDrawable_tintMode}.
+ * <p>
+ * If no child layers or mask is specified and the ripple is set as a View
+ * background, the ripple will be blended onto the first available parent
+ * background within the View's hierarchy using the specified
+ * {@link android.R.styleable#TouchFeedbackDrawable_tintMode}. In this case, the
+ * drawing region may extend outside of the Drawable bounds.
+ *
+ * @attr ref android.R.styleable#DrawableStates_state_focused
+ * @attr ref android.R.styleable#DrawableStates_state_pressed
*/
public class TouchFeedbackDrawable extends LayerDrawable {
private static final String LOG_TAG = TouchFeedbackDrawable.class.getSimpleName();
private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN);
+ private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP);
+ private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP);
private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER);
/** The maximum number of ripples supported. */
@@ -63,10 +90,22 @@
private final TouchFeedbackState mState;
- /** Lazily-created map of touch hotspot IDs to ripples. */
- private SparseArray<Ripple> mRipples;
+ /**
+ * Lazily-created map of pending hotspot locations. These may be modified by
+ * calls to {@link #setHotspot(int, float, float)}.
+ */
+ private SparseArray<PointF> mPendingHotspots;
- /** Lazily-created array of actively animating ripples. */
+ /**
+ * Lazily-created map of active hotspot locations. These may be modified by
+ * calls to {@link #setHotspot(int, float, float)}.
+ */
+ private SparseArray<Ripple> mActiveHotspots;
+
+ /**
+ * Lazily-created array of actively animating ripples. Inactive ripples are
+ * pruned during draw(). The locations of these will not change.
+ */
private Ripple[] mAnimatingRipples;
private int mAnimatingRipplesCount = 0;
@@ -96,24 +135,18 @@
protected boolean onStateChange(int[] stateSet) {
super.onStateChange(stateSet);
- // TODO: Implicitly tie states to ripple IDs. For now, just clear
- // focused and pressed if they aren't in the state set.
- boolean hasFocused = false;
- boolean hasPressed = false;
- for (int i = 0; i < stateSet.length; i++) {
- if (stateSet[i] == R.attr.state_pressed) {
- hasPressed = true;
- } else if (stateSet[i] == R.attr.state_focused) {
- hasFocused = true;
- }
- }
-
- if (!hasPressed) {
+ final boolean pressed = Arrays.contains(stateSet, R.attr.state_pressed);
+ if (!pressed) {
removeHotspot(R.attr.state_pressed);
+ } else {
+ activateHotspot(R.attr.state_pressed);
}
- if (!hasFocused) {
+ final boolean focused = Arrays.contains(stateSet, R.attr.state_focused);
+ if (!focused) {
removeHotspot(R.attr.state_focused);
+ } else {
+ activateHotspot(R.attr.state_focused);
}
if (mRipplePaint != null && mState.mTint != null) {
@@ -138,19 +171,7 @@
mHotspotBounds.set(bounds);
}
- onHotspotBoundsChange();
- }
-
- private void onHotspotBoundsChange() {
- final int x = mHotspotBounds.centerX();
- final int y = mHotspotBounds.centerY();
- final int N = mAnimatingRipplesCount;
- for (int i = 0; i < N; i++) {
- if (mState.mPinned) {
- mAnimatingRipples[i].move(x, y);
- }
- mAnimatingRipples[i].onBoundsChanged();
- }
+ invalidateSelf();
}
@Override
@@ -172,7 +193,7 @@
@Override
public boolean isStateful() {
- return super.isStateful() || mState.mTint != null && mState.mTint.isStateful();
+ return true;
}
/**
@@ -213,7 +234,7 @@
throws XmlPullParserException, IOException {
final TypedArray a = obtainAttributes(
r, theme, attrs, R.styleable.TouchFeedbackDrawable);
- inflateStateFromTypedArray(a);
+ updateStateFromTypedArray(a);
a.recycle();
super.inflate(r, parser, attrs, theme);
@@ -245,25 +266,23 @@
/**
* Initializes the constant state from the values in the typed array.
*/
- private void inflateStateFromTypedArray(TypedArray a) {
+ private void updateStateFromTypedArray(TypedArray a) {
final TouchFeedbackState state = mState;
// Extract the theme attributes, if any.
- final int[] themeAttrs = a.extractThemeAttrs();
- state.mTouchThemeAttrs = themeAttrs;
+ state.mTouchThemeAttrs = a.extractThemeAttrs();
- if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tint] == 0) {
- mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint);
+ final ColorStateList tint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint);
+ if (tint != null) {
+ mState.mTint = tint;
}
- if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tintMode] == 0) {
- mState.setTintMode(Drawable.parseTintMode(
- a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1), Mode.SRC_ATOP));
+ final int tintMode = a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1);
+ if (tintMode != -1) {
+ mState.setTintMode(Drawable.parseTintMode(tintMode, Mode.SRC_ATOP));
}
- if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_pinned] == 0) {
- mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
- }
+ mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, mState.mPinned);
}
/**
@@ -283,38 +302,14 @@
super.applyTheme(t);
final TouchFeedbackState state = mState;
- if (state == null) {
- throw new RuntimeException(
- "Can't apply theme to <touch-feedback> with no constant state");
+ if (state == null || state.mTouchThemeAttrs == null) {
+ return;
}
- final int[] themeAttrs = state.mTouchThemeAttrs;
- if (themeAttrs != null) {
- final TypedArray a = t.resolveAttributes(
- themeAttrs, R.styleable.TouchFeedbackDrawable);
- updateStateFromTypedArray(a);
- a.recycle();
- }
- }
-
- /**
- * Updates the constant state from the values in the typed array.
- */
- private void updateStateFromTypedArray(TypedArray a) {
- final TouchFeedbackState state = mState;
-
- if (a.hasValue(R.styleable.TouchFeedbackDrawable_tint)) {
- state.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint);
- }
-
- if (a.hasValue(R.styleable.TouchFeedbackDrawable_tintMode)) {
- mState.setTintMode(Drawable.parseTintMode(
- a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1), Mode.SRC_ATOP));
- }
-
- if (a.hasValue(R.styleable.TouchFeedbackDrawable_pinned)) {
- mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false);
- }
+ final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
+ R.styleable.TouchFeedbackDrawable);
+ updateStateFromTypedArray(a);
+ a.recycle();
}
@Override
@@ -329,59 +324,123 @@
@Override
public void setHotspot(int id, float x, float y) {
- if (mRipples == null) {
- mRipples = new SparseArray<Ripple>();
- mAnimatingRipples = new Ripple[MAX_RIPPLES];
+ if (mState.mPinned && !circleContains(mHotspotBounds, x, y)) {
+ x = mHotspotBounds.exactCenterX();
+ y = mHotspotBounds.exactCenterY();
}
- if (mAnimatingRipplesCount >= MAX_RIPPLES) {
- Log.e(LOG_TAG, "Max ripple count exceeded", new RuntimeException());
+ final int[] stateSet = getState();
+ if (!Arrays.contains(stateSet, id)) {
+ // The hotspot is not active, so just modify the pending location.
+ getOrCreatePendingHotspot(id).set(x, y);
return;
}
- final Ripple ripple = mRipples.get(id);
- if (ripple == null) {
- final Rect bounds = mHotspotBounds;
- if (mState.mPinned) {
- x = bounds.exactCenterX();
- y = bounds.exactCenterY();
- }
-
- // TODO: Clean this up in the API.
- final boolean pulse = (id != R.attr.state_focused);
- final Ripple newRipple = new Ripple(this, bounds, mDensity, pulse);
- newRipple.move(x, y);
-
- mAnimatingRipples[mAnimatingRipplesCount++] = newRipple;
- mRipples.put(id, newRipple);
- } else if (mState.mPinned) {
- final Rect bounds = mHotspotBounds;
- x = bounds.exactCenterX();
- y = bounds.exactCenterY();
- ripple.move(x, y);
- } else {
- ripple.move(x, y);
+ if (mAnimatingRipplesCount >= MAX_RIPPLES) {
+ // This should never happen unless the user is tapping like a maniac
+ // or there is a bug that's preventing ripples from being removed.
+ Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException());
+ return;
}
+
+ if (mActiveHotspots == null) {
+ mActiveHotspots = new SparseArray<Ripple>();
+ mAnimatingRipples = new Ripple[MAX_RIPPLES];
+ }
+
+ final Ripple ripple = mActiveHotspots.get(id);
+ if (ripple != null) {
+ // The hotspot is active, but we can't move it because it's probably
+ // busy animating the center position.
+ return;
+ }
+
+ // The hotspot needs to be made active.
+ createActiveHotspot(id, x, y);
+ }
+
+ private boolean circleContains(Rect bounds, float x, float y) {
+ final float pX = bounds.exactCenterX() - x;
+ final float pY = bounds.exactCenterY() - y;
+ final double pointRadius = Math.sqrt(pX * pX + pY * pY);
+
+ final float bX = bounds.width() / 2.0f;
+ final float bY = bounds.height() / 2.0f;
+ final double boundsRadius = Math.sqrt(bX * bX + bY * bY);
+
+ return pointRadius < boundsRadius;
+ }
+
+ private PointF getOrCreatePendingHotspot(int id) {
+ final PointF p;
+ if (mPendingHotspots == null) {
+ mPendingHotspots = new SparseArray<>(2);
+ p = null;
+ } else {
+ p = mPendingHotspots.get(id);
+ }
+
+ if (p == null) {
+ final PointF newPoint = new PointF();
+ mPendingHotspots.put(id, newPoint);
+ return newPoint;
+ } else {
+ return p;
+ }
+ }
+
+ /**
+ * Moves a hotspot from pending to active.
+ */
+ private void activateHotspot(int id) {
+ final SparseArray<PointF> pendingHotspots = mPendingHotspots;
+ if (pendingHotspots != null) {
+ final int index = pendingHotspots.indexOfKey(id);
+ if (index >= 0) {
+ final PointF hotspot = pendingHotspots.valueAt(index);
+ pendingHotspots.removeAt(index);
+ createActiveHotspot(id, hotspot.x, hotspot.y);
+ }
+ }
+ }
+
+ /**
+ * Creates an active hotspot at the specified location.
+ */
+ private void createActiveHotspot(int id, float x, float y) {
+ final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
+ final Ripple newRipple = new Ripple(this, mHotspotBounds, color);
+ newRipple.enter(x, y);
+
+ if (mAnimatingRipples == null) {
+ mAnimatingRipples = new Ripple[MAX_RIPPLES];
+ }
+ mAnimatingRipples[mAnimatingRipplesCount++] = newRipple;
+
+ if (mActiveHotspots == null) {
+ mActiveHotspots = new SparseArray<Ripple>();
+ }
+ mActiveHotspots.put(id, newRipple);
}
@Override
public void removeHotspot(int id) {
- if (mRipples == null) {
+ if (mActiveHotspots == null) {
return;
}
- final Ripple ripple = mRipples.get(id);
+ final Ripple ripple = mActiveHotspots.get(id);
if (ripple != null) {
ripple.exit();
- mRipples.remove(id);
+ mActiveHotspots.remove(id);
}
}
@Override
public void clearHotspots() {
- if (mRipples != null) {
- mRipples.clear();
+ if (mActiveHotspots != null) {
+ mActiveHotspots.clear();
}
final int count = mAnimatingRipplesCount;
@@ -402,7 +461,6 @@
public void setHotspotBounds(int left, int top, int right, int bottom) {
mOverrideBounds = true;
mHotspotBounds.set(left, top, right, bottom);
- onHotspotBoundsChange();
}
@Override
@@ -412,9 +470,9 @@
final ChildDrawable[] array = mLayerState.mChildren;
final boolean maskOnly = mState.mMask != null && N == 1;
- int restoreToCount = drawRippleLayer(canvas, bounds, maskOnly);
+ int restoreToCount = drawRippleLayer(canvas, maskOnly);
- if (restoreToCount >= 0) {
+ if (restoreToCount >= 0) {
// We have a ripple layer that contains ripples. If we also have an
// explicit mask drawable, apply it now using DST_IN blending.
if (mState.mMask != null) {
@@ -450,7 +508,7 @@
}
}
- private int drawRippleLayer(Canvas canvas, Rect bounds, boolean maskOnly) {
+ private int drawRippleLayer(Canvas canvas, boolean maskOnly) {
final int count = mAnimatingRipplesCount;
if (count == 0) {
return -1;
@@ -458,7 +516,7 @@
final Ripple[] ripples = mAnimatingRipples;
final boolean projected = isProjected();
- final Rect layerBounds = projected ? getDirtyBounds() : bounds;
+ final Rect layerBounds = projected ? getDirtyBounds() : getBounds();
// Separate the ripple color and alpha channel. The alpha will be
// applied when we merge the ripples down to the canvas.
@@ -479,6 +537,7 @@
boolean drewRipples = false;
int restoreToCount = -1;
+ int restoreTranslate = -1;
int animatingCount = 0;
// Draw ripples and update the animating ripples array.
@@ -509,6 +568,10 @@
restoreToCount = canvas.saveLayer(layerBounds.left, layerBounds.top,
layerBounds.right, layerBounds.bottom, layerPaint);
layerPaint.setAlpha(255);
+
+ restoreTranslate = canvas.save();
+ // Translate the canvas to the current hotspot bounds.
+ canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
}
drewRipples |= ripple.draw(canvas, ripplePaint);
@@ -519,6 +582,11 @@
mAnimatingRipplesCount = animatingCount;
+ // Always restore the translation.
+ if (restoreTranslate >= 0) {
+ canvas.restoreToCount(restoreTranslate);
+ }
+
// If we created a layer with no content, merge it immediately.
if (restoreToCount >= 0 && !drewRipples) {
canvas.restoreToCount(restoreToCount);
@@ -543,11 +611,14 @@
dirtyBounds.set(drawingBounds);
drawingBounds.setEmpty();
+ final int cX = (int) mHotspotBounds.exactCenterX();
+ final int cY = (int) mHotspotBounds.exactCenterY();
final Rect rippleBounds = mTempRect;
final Ripple[] activeRipples = mAnimatingRipples;
final int N = mAnimatingRipplesCount;
for (int i = 0; i < N; i++) {
activeRipples[i].getBounds(rippleBounds);
+ rippleBounds.offset(cX, cY);
drawingBounds.union(rippleBounds);
}
@@ -563,11 +634,11 @@
static class TouchFeedbackState extends LayerState {
int[] mTouchThemeAttrs;
- ColorStateList mTint;
- PorterDuffXfermode mTintXfermode;
- PorterDuffXfermode mTintXfermodeInverse;
+ ColorStateList mTint = null;
+ PorterDuffXfermode mTintXfermode = SRC_ATOP;
+ PorterDuffXfermode mTintXfermodeInverse = DST_ATOP;
Drawable mMask;
- boolean mPinned;
+ boolean mPinned = false;
public TouchFeedbackState(
TouchFeedbackState orig, TouchFeedbackDrawable owner, Resources res) {
diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp
index 83eedfb..b80f7e9 100644
--- a/libs/hwui/Animator.cpp
+++ b/libs/hwui/Animator.cpp
@@ -37,7 +37,10 @@
, mInterpolator(0)
, mPlayState(NEEDS_START)
, mStartTime(0)
- , mDuration(300){
+ , mDelayUntil(0)
+ , mDuration(300)
+ , mStartDelay(0) {
+
}
BaseRenderNodeAnimator::~BaseRenderNodeAnimator() {
@@ -49,10 +52,6 @@
mInterpolator = interpolator;
}
-void BaseRenderNodeAnimator::setDuration(nsecs_t duration) {
- mDuration = duration;
-}
-
void BaseRenderNodeAnimator::setStartValue(float value) {
LOG_ALWAYS_FATAL_IF(mPlayState != NEEDS_START,
"Cannot set the start value after the animator has started!");
@@ -68,7 +67,24 @@
}
}
+void BaseRenderNodeAnimator::setDuration(nsecs_t duration) {
+ mDuration = duration;
+}
+
+void BaseRenderNodeAnimator::setStartDelay(nsecs_t startDelay) {
+ mStartDelay = startDelay;
+}
+
bool BaseRenderNodeAnimator::animate(RenderNode* target, TreeInfo& info) {
+ if (mPlayState == PENDING && mStartDelay > 0 && mDelayUntil == 0) {
+ mDelayUntil = info.frameTimeMs + mStartDelay;
+ return false;
+ }
+
+ if (mDelayUntil > info.frameTimeMs) {
+ return false;
+ }
+
if (mPlayState == PENDING) {
mPlayState = RUNNING;
mStartTime = info.frameTimeMs;
diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h
index fe88cbf..7741617 100644
--- a/libs/hwui/Animator.h
+++ b/libs/hwui/Animator.h
@@ -44,6 +44,8 @@
ANDROID_API void setInterpolator(Interpolator* interpolator);
ANDROID_API void setDuration(nsecs_t durationInMs);
ANDROID_API nsecs_t duration() { return mDuration; }
+ ANDROID_API void setStartDelay(nsecs_t startDelayInMs);
+ ANDROID_API nsecs_t startDelay() { return mStartDelay; }
ANDROID_API void setListener(AnimationListener* listener) {
mListener = listener;
}
@@ -82,10 +84,12 @@
Interpolator* mInterpolator;
PlayState mPlayState;
- long mStartTime;
- long mDuration;
+ nsecs_t mStartTime;
+ nsecs_t mDelayUntil;
+ nsecs_t mDuration;
+ nsecs_t mStartDelay;
- sp<AnimationListener> mListener;
+ sp<AnimationListener> mListener;
};
class RenderPropertyAnimator : public BaseRenderNodeAnimator {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 908bfbd..249422b 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -623,8 +623,8 @@
pw.println(" --charged: only output data since last charged.");
pw.println(" --reset: reset the stats, clearing all current data.");
pw.println(" --write: force write current collected stats to disk.");
- pw.println(" --enable: enable an option: full-wake-history.");
- pw.println(" --disable: disable an option: full-wake-history.");
+ pw.println(" --enable: enable an option: full-wake-history, no-auto-reset.");
+ pw.println(" --disable: disable an option: full-wake-history, no-auto-reset.");
pw.println(" -h: print this help text.");
pw.println(" <package.name>: optional name of package to filter output by.");
}
@@ -640,6 +640,10 @@
synchronized (mStats) {
mStats.setRecordAllWakeLocksLocked(enable);
}
+ } else if ("no-auto-reset".equals(args[i])) {
+ synchronized (mStats) {
+ mStats.setNoAutoReset(enable);
+ }
} else {
pw.println("Unknown enable/disable option: " + args[i]);
dumpHelp(pw);
diff --git a/services/core/java/com/android/server/task/TaskCompletedListener.java b/services/core/java/com/android/server/task/TaskCompletedListener.java
new file mode 100644
index 0000000..0210442
--- /dev/null
+++ b/services/core/java/com/android/server/task/TaskCompletedListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.task;
+
+/**
+ * Used for communication between {@link com.android.server.task.TaskServiceContext} and the
+ * {@link com.android.server.task.TaskManagerService}.
+ */
+public interface TaskCompletedListener {
+
+ /**
+ * Callback for when a task is completed.
+ * @param needsReschedule Whether the implementing class should reschedule this task.
+ */
+ public void onTaskCompleted(int serviceToken, int taskId, boolean needsReschedule);
+
+ /**
+ * Callback for when the implementing class needs to clean up the
+ * {@link com.android.server.task.TaskServiceContext}. The scheduler can get this callback
+ * several times if the TaskServiceContext got into a bad state (for e.g. the client crashed
+ * and it needs to clean up).
+ */
+ public void onAllTasksCompleted(int serviceToken);
+}
diff --git a/services/core/java/com/android/server/task/TaskList.java b/services/core/java/com/android/server/task/TaskList.java
deleted file mode 100644
index d2b8440..0000000
--- a/services/core/java/com/android/server/task/TaskList.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.task;
-
-import android.content.ComponentName;
-import android.content.Task;
-
-import com.android.server.task.controllers.TaskStatus;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Maintain a list of classes, and accessor methods/logic for these tasks.
- * This class offers the following functionality:
- * - When a task is added, it will determine if the task requirements have changed (update) and
- * whether the controllers need to be updated.
- * - Persists Tasks, figures out when to to rewrite the Task to disk.
- * - Is threadsafe.
- * - Handles rescheduling of tasks.
- * - When a periodic task is executed and must be re-added.
- * - When a task fails and the client requests that it be retried with backoff.
- */
-public class TaskList {
-
- final List<TaskStatus> mTasks;
-
- TaskList() {
- mTasks = intialiseTaskMapFromDisk();
- }
-
- /**
- * Add a task to the master list, persisting it if necessary.
- * @param task Task to add.
- * @param persistable true if the TaskQueue should persist this task to the disk.
- * @return true if this operation was successful. If false, this task was neither added nor
- * persisted.
- */
- // TODO: implement this when i decide whether i want to key by TaskStatus
- public boolean add(Task task, boolean persistable) {
- return true;
- }
-
- /**
- * Remove the provided task. Will also delete the task if it was persisted. Note that this
- * function does not return the validity of the operation, as we assume a delete will always
- * succeed.
- * @param task Task to remove.
- */
- public void remove(Task task) {
-
- }
-
- /**
- *
- * @return
- */
- // TODO: Implement this.
- private List<TaskStatus> intialiseTaskMapFromDisk() {
- return new ArrayList<TaskStatus>();
- }
-}
diff --git a/services/core/java/com/android/server/task/TaskManagerService.java b/services/core/java/com/android/server/task/TaskManagerService.java
index 5df4b2a..1b3a927 100644
--- a/services/core/java/com/android/server/task/TaskManagerService.java
+++ b/services/core/java/com/android/server/task/TaskManagerService.java
@@ -17,16 +17,15 @@
package com.android.server.task;
import android.content.Context;
+import android.content.Task;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.Log;
import android.util.SparseArray;
import com.android.server.task.controllers.TaskStatus;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* Responsible for taking tasks representing work to be performed by a client app, and determining
* based on the criteria specified when that task should be run against the client application's
@@ -34,25 +33,29 @@
* @hide
*/
public class TaskManagerService extends com.android.server.SystemService
- implements StateChangedListener {
+ implements StateChangedListener, TaskCompletedListener {
+ static final String TAG = "TaskManager";
/** Master list of tasks. */
- private final TaskList mTaskList;
+ private final TaskStore mTasks;
+
+ /** Check the pending queue and start any tasks. */
+ static final int MSG_RUN_PENDING = 0;
+ /** Initiate the stop task flow. */
+ static final int MSG_STOP_TASK = 1;
+ /** */
+ static final int MSG_CHECK_TASKS = 2;
/**
* Track Services that have currently active or pending tasks. The index is provided by
* {@link TaskStatus#getServiceToken()}
*/
- private final SparseArray<TaskServiceContext> mPendingTaskServices =
+ private final SparseArray<TaskServiceContext> mActiveServices =
new SparseArray<TaskServiceContext>();
private final TaskHandler mHandler;
private class TaskHandler extends Handler {
- /** Check the pending queue and start any tasks. */
- static final int MSG_RUN_PENDING = 0;
- /** Initiate the stop task flow. */
- static final int MSG_STOP_TASK = 1;
public TaskHandler(Looper looper) {
super(looper);
@@ -67,21 +70,42 @@
case MSG_STOP_TASK:
break;
+ case MSG_CHECK_TASKS:
+ checkTasks();
+ break;
}
}
/**
- * Helper to post a message to this handler that will run through the pending queue and
- * start any tasks it can.
+ * Called when we need to run through the list of all tasks and start/stop executing one or
+ * more of them.
*/
- void sendRunPendingTasksMessage() {
- Message m = Message.obtain(this, MSG_RUN_PENDING);
- m.sendToTarget();
+ private void checkTasks() {
+ synchronized (mTasks) {
+ final SparseArray<TaskStatus> tasks = mTasks.getTasks();
+ for (int i = 0; i < tasks.size(); i++) {
+ TaskStatus ts = tasks.valueAt(i);
+ if (ts.isReady() && ! isCurrentlyActive(ts)) {
+ assignTaskToServiceContext(ts);
+ }
+ }
+ }
}
+ }
- void sendOnStopMessage(TaskStatus taskStatus) {
-
- }
+ /**
+ * Entry point from client to schedule the provided task.
+ * This will add the task to the
+ * @param task Task object containing execution parameters
+ * @param userId The id of the user this task is for.
+ * @param uId The package identifier of the application this task is for.
+ * @param canPersistTask Whether or not the client has the appropriate permissions for persisting
+ * of this task.
+ * @return Result of this operation. See <code>TaskManager#RESULT_*</code> return codes.
+ */
+ public int schedule(Task task, int userId, int uId, boolean canPersistTask) {
+ TaskStatus taskStatus = mTasks.addNewTaskForUser(task, userId, uId, canPersistTask);
+ return 0;
}
/**
@@ -95,7 +119,7 @@
*/
public TaskManagerService(Context context) {
super(context);
- mTaskList = new TaskList();
+ mTasks = new TaskStore(context);
mHandler = new TaskHandler(context.getMainLooper());
}
@@ -104,20 +128,19 @@
}
+ // StateChangedListener implementations.
+
/**
- * Offboard work to our handler thread as quickly as possible, b/c this call is probably being
+ * Off-board work to our handler thread as quickly as possible, b/c this call is probably being
* made on the main thread.
+ * For now this takes the task and if it's ready to run it will run it. In future we might not
+ * provide the task, so that the StateChangedListener has to run through its list of tasks to
+ * see which are ready. This will further decouple the controllers from the execution logic.
* @param taskStatus The state of the task which has changed.
*/
@Override
public void onTaskStateChanged(TaskStatus taskStatus) {
- if (taskStatus.isReady()) {
-
- } else {
- if (mPendingTaskServices.get(taskStatus.getServiceToken()) != null) {
- // The task is either pending or being executed, which we have to cancel.
- }
- }
+ postCheckTasksMessage();
}
@@ -125,4 +148,60 @@
public void onTaskDeadlineExpired(TaskStatus taskStatus) {
}
+
+ // TaskCompletedListener implementations.
+
+ /**
+ * A task just finished executing. We fetch the
+ * {@link com.android.server.task.controllers.TaskStatus} from the store and depending on
+ * whether we want to reschedule we readd it to the controllers.
+ * @param serviceToken key for the service context in {@link #mActiveServices}.
+ * @param taskId Id of the task that is complete.
+ * @param needsReschedule Whether the implementing class should reschedule this task.
+ */
+ @Override
+ public void onTaskCompleted(int serviceToken, int taskId, boolean needsReschedule) {
+ final TaskServiceContext serviceContext = mActiveServices.get(serviceToken);
+ if (serviceContext == null) {
+ Log.e(TAG, "Task completed for invalid service context; " + serviceToken);
+ return;
+ }
+
+ }
+
+ @Override
+ public void onClientExecutionCompleted(int serviceToken) {
+
+ }
+
+ private void assignTaskToServiceContext(TaskStatus ts) {
+ TaskServiceContext serviceContext =
+ mActiveServices.get(ts.getServiceToken());
+ if (serviceContext == null) {
+ serviceContext = new TaskServiceContext(this, mHandler.getLooper(), ts);
+ mActiveServices.put(ts.getServiceToken(), serviceContext);
+ }
+ serviceContext.addPendingTask(ts);
+ }
+
+ /**
+ * @param ts TaskStatus we are querying against.
+ * @return Whether or not the task represented by the status object is currently being run or
+ * is pending.
+ */
+ private boolean isCurrentlyActive(TaskStatus ts) {
+ TaskServiceContext serviceContext = mActiveServices.get(ts.getServiceToken());
+ if (serviceContext == null) {
+ return false;
+ }
+ return serviceContext.hasTaskPending(ts);
+ }
+
+ /**
+ * Post a message to {@link #mHandler} to run through the list of tasks and start/stop any that
+ * are eligible.
+ */
+ private void postCheckTasksMessage() {
+ mHandler.obtainMessage(MSG_CHECK_TASKS).sendToTarget();
+ }
}
diff --git a/services/core/java/com/android/server/task/TaskServiceContext.java b/services/core/java/com/android/server/task/TaskServiceContext.java
index 65c6fa5..2d148d5 100644
--- a/services/core/java/com/android/server/task/TaskServiceContext.java
+++ b/services/core/java/com/android/server/task/TaskServiceContext.java
@@ -16,79 +16,500 @@
package com.android.server.task;
+import android.app.ActivityManager;
import android.app.task.ITaskCallback;
import android.app.task.ITaskService;
+import android.app.task.TaskParams;
import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
import android.content.ServiceConnection;
-import android.content.Task;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
import com.android.server.task.controllers.TaskStatus;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* Maintains information required to bind to a {@link android.app.task.TaskService}. This binding
- * can then be reused to start concurrent tasks on the TaskService. Information here is unique
- * within this service.
+ * is reused to start concurrent tasks on the TaskService. Information here is unique
+ * to the service.
* Functionality provided by this class:
* - Managages wakelock for the service.
* - Sends onStartTask() and onStopTask() messages to client app, and handles callbacks.
* -
*/
public class TaskServiceContext extends ITaskCallback.Stub implements ServiceConnection {
+ private static final String TAG = "TaskServiceContext";
+ /** Define the maximum # of tasks allowed to run on a service at once. */
+ private static final int defaultMaxActiveTasksPerService =
+ ActivityManager.isLowRamDeviceStatic() ? 1 : 3;
+ /** Amount of time a task is allowed to execute for before being considered timed-out. */
+ private static final long EXECUTING_TIMESLICE_MILLIS = 5 * 60 * 1000;
+ /** Amount of time the TaskManager will wait for a response from an app for a message. */
+ private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
+ /** String prefix for all wakelock names. */
+ private static final String TM_WAKELOCK_PREFIX = "*task*/";
+ private static final String[] VERB_STRINGS = {
+ "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_PENDING"
+ };
+
+ // States that a task occupies while interacting with the client.
+ private static final int VERB_STARTING = 0;
+ private static final int VERB_EXECUTING = 1;
+ private static final int VERB_STOPPING = 2;
+ private static final int VERB_PENDING = 3;
+
+ // Messages that result from interactions with the client service.
+ /** System timed out waiting for a response. */
+ private static final int MSG_TIMEOUT = 0;
+ /** Received a callback from client. */
+ private static final int MSG_CALLBACK = 1;
+ /** Run through list and start any ready tasks.*/
+ private static final int MSG_CHECK_PENDING = 2;
+ /** Cancel an active task. */
+ private static final int MSG_CANCEL = 3;
+ /** Add a pending task. */
+ private static final int MSG_ADD_PENDING = 4;
+ /** Client crashed, so we need to wind things down. */
+ private static final int MSG_SHUTDOWN = 5;
+
+ /** Used to identify this task service context when communicating with the TaskManager. */
+ final int token;
final ComponentName component;
- int uid;
+ final int userId;
ITaskService service;
+ private final Handler mCallbackHandler;
+ /** Tasks that haven't been sent to the client for execution yet. */
+ private final SparseArray<ActiveTask> mPending;
+ /** Used for service binding, etc. */
+ private final Context mContext;
+ /** Make callbacks to {@link TaskManagerService} to inform on task completion status. */
+ final private TaskCompletedListener mCompletedListener;
+ private final PowerManager.WakeLock mWakeLock;
/** Whether this service is actively bound. */
boolean mBound;
- TaskServiceContext(Task task) {
- this.component = task.getService();
- }
-
- public void stopTask() {
-
- }
-
- public void startTask(Task task) {
-
+ TaskServiceContext(TaskManagerService taskManager, Looper looper, TaskStatus taskStatus) {
+ mContext = taskManager.getContext();
+ this.component = taskStatus.getServiceComponent();
+ this.token = taskStatus.getServiceToken();
+ this.userId = taskStatus.getUserId();
+ mCallbackHandler = new TaskServiceHandler(looper);
+ mPending = new SparseArray<ActiveTask>();
+ mCompletedListener = taskManager;
+ final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ TM_WAKELOCK_PREFIX + component.getPackageName());
+ mWakeLock.setWorkSource(new WorkSource(taskStatus.getUid()));
+ mWakeLock.setReferenceCounted(false);
}
@Override
public void taskFinished(int taskId, boolean reschedule) {
-
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0)
+ .sendToTarget();
}
@Override
- public void acknowledgeStopMessage(int taskId) {
-
+ public void acknowledgeStopMessage(int taskId, boolean reschedule) {
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0)
+ .sendToTarget();
}
@Override
- public void acknowledgeStartMessage(int taskId) {
+ public void acknowledgeStartMessage(int taskId, boolean ongoing) {
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, ongoing ? 1 : 0).sendToTarget();
+ }
+ /**
+ * Queue up this task to run on the client. This will execute the task as quickly as possible.
+ * @param ts Status of the task to run.
+ */
+ public void addPendingTask(TaskStatus ts) {
+ final TaskParams params = new TaskParams(ts.getTaskId(), ts.getExtras(), this);
+ final ActiveTask newTask = new ActiveTask(params, VERB_PENDING);
+ mCallbackHandler.obtainMessage(MSG_ADD_PENDING, newTask).sendToTarget();
+ if (!mBound) {
+ Intent intent = new Intent().setComponent(component);
+ boolean binding = mContext.bindServiceAsUser(intent, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
+ new UserHandle(userId));
+ if (!binding) {
+ Log.e(TAG, component.getShortClassName() + " unavailable.");
+ cancelPendingTask(ts);
+ }
+ }
+ }
+
+ /**
+ * Called externally when a task that was scheduled for execution should be cancelled.
+ * @param ts The status of the task to cancel.
+ */
+ public void cancelPendingTask(TaskStatus ts) {
+ mCallbackHandler.obtainMessage(MSG_CANCEL, ts.getTaskId(), -1 /* arg2 */)
+ .sendToTarget();
+ }
+
+ /**
+ * MSG_TIMEOUT is sent with the {@link com.android.server.task.TaskServiceContext.ActiveTask}
+ * set in the {@link Message#obj} field. This makes it easier to remove timeouts for a given
+ * ActiveTask.
+ * @param op Operation that is taking place.
+ */
+ private void scheduleOpTimeOut(ActiveTask op) {
+ mCallbackHandler.removeMessages(MSG_TIMEOUT, op);
+
+ final long timeoutMillis = (op.verb == VERB_EXECUTING) ?
+ EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS;
+ if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) {
+ Slog.d(TAG, "Scheduling time out for '" + component.getShortClassName() + "' tId: " +
+ op.params.getTaskId() + ", in " + (timeoutMillis / 1000) + " s");
+ }
+ Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, op);
+ mCallbackHandler.sendMessageDelayed(m, timeoutMillis);
}
/**
* @return true if this task is pending or active within this context.
*/
public boolean hasTaskPending(TaskStatus taskStatus) {
- return true;
+ synchronized (mPending) {
+ return mPending.get(taskStatus.getTaskId()) != null;
+ }
}
public boolean isBound() {
return mBound;
}
+ /**
+ * We acquire/release the wakelock on onServiceConnected/unbindService. This mirrors the work
+ * we intend to send to the client - we stop sending work when the service is unbound so until
+ * then we keep the wakelock.
+ * @param name The concrete component name of the service that has
+ * been connected.
+ * @param service The IBinder of the Service's communication channel,
+ */
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
-
mBound = true;
+ this.service = ITaskService.Stub.asInterface(service);
+ // Remove all timeouts. We've just connected to the client so there are no other
+ // MSG_TIMEOUTs at this point.
+ mCallbackHandler.removeMessages(MSG_TIMEOUT);
+ mWakeLock.acquire();
+ mCallbackHandler.obtainMessage(MSG_CHECK_PENDING).sendToTarget();
}
+ /**
+ * When the client service crashes we can have a couple tasks executing, in various stages of
+ * undress. We'll cancel all of them and request that they be rescheduled.
+ * @param name The concrete component name of the service whose
+ */
@Override
public void onServiceDisconnected(ComponentName name) {
- mBound = false;
+ // Service disconnected... probably client crashed.
+ startShutdown();
+ }
+
+ /**
+ * We don't just shutdown outright - we make sure the scheduler isn't going to send us any more
+ * tasks, then we do the shutdown.
+ */
+ private void startShutdown() {
+ mCompletedListener.onClientExecutionCompleted(token);
+ mCallbackHandler.obtainMessage(MSG_SHUTDOWN).sendToTarget();
+ }
+
+ /** Tracks a task across its various state changes. */
+ private static class ActiveTask {
+ final TaskParams params;
+ int verb;
+ AtomicBoolean cancelled = new AtomicBoolean();
+
+ ActiveTask(TaskParams params, int verb) {
+ this.params = params;
+ this.verb = verb;
+ }
+
+ @Override
+ public String toString() {
+ return params.getTaskId() + " " + VERB_STRINGS[verb];
+ }
+ }
+
+ /**
+ * Handles the lifecycle of the TaskService binding/callbacks, etc. The convention within this
+ * class is to append 'H' to each function name that can only be called on this handler. This
+ * isn't strictly necessary because all of these functions are private, but helps clarity.
+ */
+ private class TaskServiceHandler extends Handler {
+ TaskServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_ADD_PENDING:
+ if (message.obj != null) {
+ ActiveTask pendingTask = (ActiveTask) message.obj;
+ mPending.put(pendingTask.params.getTaskId(), pendingTask);
+ }
+ // fall through.
+ case MSG_CHECK_PENDING:
+ checkPendingTasksH();
+ break;
+ case MSG_CALLBACK:
+ ActiveTask receivedCallback = mPending.get(message.arg1);
+ removeMessages(MSG_TIMEOUT, receivedCallback);
+
+ if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) {
+ Log.d(TAG, "MSG_CALLBACK of : " + receivedCallback);
+ }
+
+ if (receivedCallback.verb == VERB_STARTING) {
+ final boolean workOngoing = message.arg2 == 1;
+ handleStartedH(receivedCallback, workOngoing);
+ } else if (receivedCallback.verb == VERB_EXECUTING ||
+ receivedCallback.verb == VERB_STOPPING) {
+ final boolean reschedule = message.arg2 == 1;
+ handleFinishedH(receivedCallback, reschedule);
+ } else {
+ if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unrecognised callback: " + receivedCallback);
+ }
+ }
+ break;
+ case MSG_CANCEL:
+ ActiveTask cancelled = mPending.get(message.arg1);
+ handleCancelH(cancelled);
+ break;
+ case MSG_TIMEOUT:
+ // Timeout msgs have the ActiveTask ref so we can remove them easily.
+ handleOpTimeoutH((ActiveTask) message.obj);
+ break;
+ case MSG_SHUTDOWN:
+ handleShutdownH();
+ break;
+ default:
+ Log.e(TAG, "Unrecognised message: " + message);
+ }
+ }
+
+ /**
+ * State behaviours.
+ * VERB_STARTING -> Successful start, change task to VERB_EXECUTING and post timeout.
+ * _PENDING -> Error
+ * _EXECUTING -> Error
+ * _STOPPING -> Error
+ */
+ private void handleStartedH(ActiveTask started, boolean workOngoing) {
+ switch (started.verb) {
+ case VERB_STARTING:
+ started.verb = VERB_EXECUTING;
+ if (!workOngoing) {
+ // Task is finished already so fast-forward to handleFinished.
+ handleFinishedH(started, false);
+ return;
+ } else if (started.cancelled.get()) {
+ // Cancelled *while* waiting for acknowledgeStartMessage from client.
+ handleCancelH(started);
+ return;
+ } else {
+ scheduleOpTimeOut(started);
+ }
+ break;
+ default:
+ Log.e(TAG, "Handling started task but task wasn't starting! " + started);
+ return;
+ }
+ }
+
+ /**
+ * VERB_EXECUTING -> Client called taskFinished(), clean up and notify done.
+ * _STOPPING -> Successful finish, clean up and notify done.
+ * _STARTING -> Error
+ * _PENDING -> Error
+ */
+ private void handleFinishedH(ActiveTask executedTask, boolean reschedule) {
+ switch (executedTask.verb) {
+ case VERB_EXECUTING:
+ case VERB_STOPPING:
+ closeAndCleanupTaskH(executedTask, reschedule);
+ break;
+ default:
+ Log.e(TAG, "Got an execution complete message for a task that wasn't being" +
+ "executed. " + executedTask);
+ }
+ }
+
+ /**
+ * A task can be in various states when a cancel request comes in:
+ * VERB_PENDING -> Remove from queue.
+ * _STARTING -> Mark as cancelled and wait for {@link #acknowledgeStartMessage(int)}.
+ * _EXECUTING -> call {@link #sendStopMessageH}}.
+ * _ENDING -> No point in doing anything here, so we ignore.
+ */
+ private void handleCancelH(ActiveTask cancelledTask) {
+ switch (cancelledTask.verb) {
+ case VERB_PENDING:
+ mPending.remove(cancelledTask.params.getTaskId());
+ break;
+ case VERB_STARTING:
+ cancelledTask.cancelled.set(true);
+ break;
+ case VERB_EXECUTING:
+ cancelledTask.verb = VERB_STOPPING;
+ sendStopMessageH(cancelledTask);
+ break;
+ case VERB_STOPPING:
+ // Nada.
+ break;
+ default:
+ Log.e(TAG, "Cancelling a task without a valid verb: " + cancelledTask);
+ break;
+ }
+ }
+
+ /**
+ * This TaskServiceContext is shutting down. Remove all the tasks from the pending queue
+ * and reschedule them as if they had failed.
+ * Before posting this message, caller must invoke
+ * {@link com.android.server.task.TaskCompletedListener#onClientExecutionCompleted(int)}
+ */
+ private void handleShutdownH() {
+ for (int i = 0; i < mPending.size(); i++) {
+ ActiveTask at = mPending.valueAt(i);
+ closeAndCleanupTaskH(at, true /* needsReschedule */);
+ }
+ mWakeLock.release();
+ mContext.unbindService(TaskServiceContext.this);
+ service = null;
+ mBound = false;
+ }
+
+ /**
+ * MSG_TIMEOUT gets processed here.
+ * @param timedOutTask The task that timed out.
+ */
+ private void handleOpTimeoutH(ActiveTask timedOutTask) {
+ if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) {
+ Log.d(TAG, "MSG_TIMEOUT of " + component.getShortClassName() + " : "
+ + timedOutTask.params.getTaskId());
+ }
+
+ final int taskId = timedOutTask.params.getTaskId();
+ switch (timedOutTask.verb) {
+ case VERB_STARTING:
+ // Client unresponsive - wedged or failed to respond in time. We don't really
+ // know what happened so let's log it and notify the TaskManager
+ // FINISHED/NO-RETRY.
+ Log.e(TAG, "No response from client for onStartTask '" +
+ component.getShortClassName() + "' tId: " + taskId);
+ closeAndCleanupTaskH(timedOutTask, false /* needsReschedule */);
+ break;
+ case VERB_STOPPING:
+ // At least we got somewhere, so fail but ask the TaskManager to reschedule.
+ Log.e(TAG, "No response from client for onStopTask, '" +
+ component.getShortClassName() + "' tId: " + taskId);
+ closeAndCleanupTaskH(timedOutTask, true /* needsReschedule */);
+ break;
+ case VERB_EXECUTING:
+ // Not an error - client ran out of time.
+ Log.i(TAG, "Client timed out while executing (no taskFinished received)." +
+ " Reporting failure and asking for reschedule. " +
+ component.getShortClassName() + "' tId: " + taskId);
+ sendStopMessageH(timedOutTask);
+ break;
+ default:
+ Log.e(TAG, "Handling timeout for an unknown active task state: "
+ + timedOutTask);
+ return;
+ }
+ }
+
+ /**
+ * Called on the handler thread. Checks the state of the pending queue and starts the task
+ * if it can. The task only starts if there is capacity on the service.
+ */
+ private void checkPendingTasksH() {
+ if (!mBound) {
+ return;
+ }
+ for (int i = 0; i < mPending.size() && i < defaultMaxActiveTasksPerService; i++) {
+ ActiveTask at = mPending.valueAt(i);
+ if (at.verb != VERB_PENDING) {
+ continue;
+ }
+ sendStartMessageH(at);
+ }
+ }
+
+ /**
+ * Already running, need to stop. Rund on handler.
+ * @param stoppingTask Task we are sending onStopMessage for. This task will be moved from
+ * VERB_EXECUTING -> VERB_STOPPING.
+ */
+ private void sendStopMessageH(ActiveTask stoppingTask) {
+ mCallbackHandler.removeMessages(MSG_TIMEOUT, stoppingTask);
+ if (stoppingTask.verb != VERB_EXECUTING) {
+ Log.e(TAG, "Sending onStopTask for a task that isn't started. " + stoppingTask);
+ // TODO: Handle error?
+ return;
+ }
+ try {
+ service.stopTask(stoppingTask.params);
+ stoppingTask.verb = VERB_STOPPING;
+ scheduleOpTimeOut(stoppingTask);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending onStopTask to client.", e);
+ closeAndCleanupTaskH(stoppingTask, false);
+ }
+ }
+
+ /** Start the task on the service. */
+ private void sendStartMessageH(ActiveTask pendingTask) {
+ if (pendingTask.verb != VERB_PENDING) {
+ Log.e(TAG, "Sending onStartTask for a task that isn't pending. " + pendingTask);
+ // TODO: Handle error?
+ }
+ try {
+ service.startTask(pendingTask.params);
+ pendingTask.verb = VERB_STARTING;
+ scheduleOpTimeOut(pendingTask);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending onStart message to '" + component.getShortClassName()
+ + "' ", e);
+ }
+ }
+
+ /**
+ * The provided task has finished, either by calling
+ * {@link android.app.task.TaskService#taskFinished(android.app.task.TaskParams, boolean)}
+ * or from acknowledging the stop message we sent. Either way, we're done tracking it and
+ * we want to clean up internally.
+ */
+ private void closeAndCleanupTaskH(ActiveTask completedTask, boolean reschedule) {
+ removeMessages(MSG_TIMEOUT, completedTask);
+ mPending.remove(completedTask.params.getTaskId());
+ if (mPending.size() == 0) {
+ startShutdown();
+ }
+ mCompletedListener.onTaskCompleted(token, completedTask.params.getTaskId(), reschedule);
+ }
}
}
diff --git a/services/core/java/com/android/server/task/TaskStore.java b/services/core/java/com/android/server/task/TaskStore.java
new file mode 100644
index 0000000..3bfc8a5
--- /dev/null
+++ b/services/core/java/com/android/server/task/TaskStore.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.task;
+
+import android.content.Context;
+import android.content.Task;
+import android.util.SparseArray;
+
+import com.android.server.task.controllers.TaskStatus;
+
+/**
+ * Maintain a list of classes, and accessor methods/logic for these tasks.
+ * This class offers the following functionality:
+ * - When a task is added, it will determine if the task requirements have changed (update) and
+ * whether the controllers need to be updated.
+ * - Persists Tasks, figures out when to to rewrite the Task to disk.
+ * - Is threadsafe.
+ * - Handles rescheduling of tasks.
+ * - When a periodic task is executed and must be re-added.
+ * - When a task fails and the client requests that it be retried with backoff.
+ * - This class is <strong>not</strong> thread-safe.
+ */
+public class TaskStore {
+
+ /**
+ * Master list, indexed by {@link com.android.server.task.controllers.TaskStatus#hashCode()}.
+ */
+ final SparseArray<TaskStatus> mTasks;
+ final Context mContext;
+
+ TaskStore(Context context) {
+ mTasks = intialiseTaskMapFromDisk();
+ mContext = context;
+ }
+
+ /**
+ * Add a task to the master list, persisting it if necessary.
+ * Will first check to see if the task already exists. If so, it will replace it.
+ * {@link android.content.pm.PackageManager} is queried to see if the calling package has
+ * permission to
+ * @param task Task to add.
+ * @return The initialised TaskStatus object if this operation was successful, null if it
+ * failed.
+ */
+ public TaskStatus addNewTaskForUser(Task task, int userId, int uId,
+ boolean canPersistTask) {
+ TaskStatus taskStatus = TaskStatus.getForTaskAndUser(task, userId, uId);
+ if (canPersistTask && task.isPeriodic()) {
+ if (writeStatusToDisk()) {
+ mTasks.put(taskStatus.hashCode(), taskStatus);
+ }
+ }
+ return taskStatus;
+ }
+
+ /**
+ * Remove the provided task. Will also delete the task if it was persisted. Note that this
+ * function does not return the validity of the operation, as we assume a delete will always
+ * succeed.
+ * @param task Task to remove.
+ */
+ public void remove(Task task) {
+
+ }
+
+ /**
+ * Every time the state changes we write all the tasks in one swathe, instead of trying to
+ * track incremental changes.
+ */
+ private boolean writeStatusToDisk() {
+ return true;
+ }
+
+ /**
+ *
+ * @return
+ */
+ // TODO: Implement this.
+ private SparseArray<TaskStatus> intialiseTaskMapFromDisk() {
+ return new SparseArray<TaskStatus>();
+ }
+
+ /**
+ * @return The live array of TaskStatus objects.
+ */
+ public SparseArray<TaskStatus> getTasks() {
+ return mTasks;
+ }
+}
diff --git a/services/core/java/com/android/server/task/controllers/ConnectivityController.java b/services/core/java/com/android/server/task/controllers/ConnectivityController.java
index 5cca77c..fad41d9 100644
--- a/services/core/java/com/android/server/task/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/task/controllers/ConnectivityController.java
@@ -41,6 +41,11 @@
private final BroadcastReceiver mConnectivityChangedReceiver =
new ConnectivityChangedReceiver();
+ /** Track whether the latest active network is metered. */
+ private boolean mMetered;
+ /** Track whether the latest active network is connected. */
+ private boolean mConnectivity;
+
public ConnectivityController(TaskManagerService service) {
super(service);
// Register connectivity changed BR.
@@ -51,31 +56,30 @@
}
@Override
- public void maybeTrackTaskState(TaskStatus taskStatus) {
+ public void maybeStartTrackingTask(TaskStatus taskStatus) {
if (taskStatus.hasConnectivityConstraint() || taskStatus.hasMeteredConstraint()) {
+ taskStatus.connectivityConstraintSatisfied.set(mConnectivity);
+ taskStatus.meteredConstraintSatisfied.set(mMetered);
mTrackedTasks.add(taskStatus);
}
}
@Override
- public void removeTaskStateIfTracked(TaskStatus taskStatus) {
+ public void maybeStopTrackingTask(TaskStatus taskStatus) {
mTrackedTasks.remove(taskStatus);
}
/**
- * @param isConnected Whether the active network is connected for the given uid
- * @param isMetered Whether the active network is metered for the given uid. This is
- * necessarily false if <code>isConnected</code> is false.
* @param userId Id of the user for whom we are updating the connectivity state.
*/
- private void updateTrackedTasks(boolean isConnected, boolean isMetered, int userId) {
+ private void updateTrackedTasks(int userId) {
for (TaskStatus ts : mTrackedTasks) {
if (ts.userId != userId) {
continue;
}
- boolean prevIsConnected = ts.connectivityConstraintSatisfied.getAndSet(isConnected);
- boolean prevIsMetered = ts.meteredConstraintSatisfied.getAndSet(isMetered);
- if (prevIsConnected != isConnected || prevIsMetered != isMetered) {
+ boolean prevIsConnected = ts.connectivityConstraintSatisfied.getAndSet(mConnectivity);
+ boolean prevIsMetered = ts.meteredConstraintSatisfied.getAndSet(mMetered);
+ if (prevIsConnected != mConnectivity || prevIsMetered != mMetered) {
mStateChangedListener.onTaskStateChanged(ts);
}
}
@@ -83,12 +87,13 @@
class ConnectivityChangedReceiver extends BroadcastReceiver {
/**
- * We'll receive connectivity changes for each user here, which we'll process independently.
+ * We'll receive connectivity changes for each user here, which we process independently.
* We are only interested in the active network here. We're only interested in the active
* network, b/c the end result of this will be for apps to try to hit the network.
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/
+ // TODO: Test whether this will be called twice for each user.
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
@@ -103,13 +108,13 @@
// This broadcast gets sent a lot, only update if the active network has changed.
if (activeNetwork.getType() == networkType) {
final int userid = context.getUserId();
- boolean isMetered = false;
- boolean isConnected =
+ mMetered = false;
+ mConnectivity =
!intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
if (isConnected) { // No point making the call if we know there's no conn.
- isMetered = connManager.isActiveNetworkMetered();
+ mMetered = connManager.isActiveNetworkMetered();
}
- updateTrackedTasks(isConnected, isMetered, userid);
+ updateTrackedTasks(userid);
}
} else {
Log.w(TAG, "Unrecognised action in intent: " + action);
diff --git a/services/core/java/com/android/server/task/controllers/TaskStatus.java b/services/core/java/com/android/server/task/controllers/TaskStatus.java
index 230b049..d96fedc 100644
--- a/services/core/java/com/android/server/task/controllers/TaskStatus.java
+++ b/services/core/java/com/android/server/task/controllers/TaskStatus.java
@@ -18,6 +18,8 @@
import android.content.ComponentName;
import android.content.Task;
+import android.content.pm.PackageParser;
+import android.os.Bundle;
import android.os.SystemClock;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -36,7 +38,9 @@
public class TaskStatus {
final int taskId;
final int userId;
- ComponentName component;
+ final int uId;
+ final ComponentName component;
+ final Bundle extras;
final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean();
final AtomicBoolean timeConstraintSatisfied = new AtomicBoolean();
@@ -60,15 +64,17 @@
/** Generate a TaskStatus object for a given task and uid. */
// TODO: reimplement this to reuse these objects instead of creating a new one each time?
- static TaskStatus getForTaskAndUid(Task task, int uId) {
- return new TaskStatus(task, uId);
+ public static TaskStatus getForTaskAndUser(Task task, int userId, int uId) {
+ return new TaskStatus(task, userId, uId);
}
/** Set up the state of a newly scheduled task. */
- TaskStatus(Task task, int userId) {
+ TaskStatus(Task task, int userId, int uId) {
this.taskId = task.getTaskId();
this.userId = userId;
this.component = task.getService();
+ this.extras = task.getExtras();
+ this.uId = uId;
hasChargingConstraint = task.isRequireCharging();
hasIdleConstraint = task.isRequireDeviceIdle();
@@ -94,6 +100,26 @@
hasConnectivityConstraint = task.getNetworkCapabilities() == Task.NetworkType.ANY;
}
+ public int getTaskId() {
+ return taskId;
+ }
+
+ public ComponentName getServiceComponent() {
+ return component;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public int getUid() {
+ return uId;
+ }
+
+ public Bundle getExtras() {
+ return extras;
+ }
+
boolean hasConnectivityConstraint() {
return hasConnectivityConstraint;
}
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index a39c116..c1b9a33 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -24,11 +24,14 @@
import org.xmlpull.v1.XmlPullParserException;
import android.Manifest;
+import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustListener;
import android.app.trust.ITrustManager;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
@@ -46,6 +49,7 @@
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Slog;
+import android.util.SparseBooleanArray;
import android.util.Xml;
import java.io.IOException;
@@ -81,6 +85,8 @@
private final ArraySet<AgentInfo> mActiveAgents = new ArraySet<AgentInfo>();
private final ArrayList<ITrustListener> mTrustListeners = new ArrayList<ITrustListener>();
+ private final DevicePolicyReceiver mDevicePolicyReceiver = new DevicePolicyReceiver();
+ private final SparseBooleanArray mUserHasAuthenticatedSinceBoot = new SparseBooleanArray();
private final Context mContext;
private UserManager mUserManager;
@@ -105,8 +111,8 @@
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && !isSafeMode()) {
- // Listen for package changes
mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
+ mDevicePolicyReceiver.register(mContext);
refreshAgentList();
}
}
@@ -158,8 +164,13 @@
mObsoleteAgents.addAll(mActiveAgents);
for (UserInfo userInfo : userInfos) {
+ int disabledFeatures = lockPatternUtils.getDevicePolicyManager()
+ .getKeyguardDisabledFeatures(null, userInfo.id);
+ boolean disableTrustAgents =
+ (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS) != 0;
+
List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id);
- if (enabledAgents == null) {
+ if (disableTrustAgents || enabledAgents == null) {
continue;
}
List<ResolveInfo> resolveInfos = pm.queryIntentServicesAsUser(TRUST_AGENT_INTENT,
@@ -259,6 +270,9 @@
// Agent dispatch and aggregation
private boolean aggregateIsTrusted(int userId) {
+ if (!mUserHasAuthenticatedSinceBoot.get(userId)) {
+ return false;
+ }
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
if (info.userId == userId) {
@@ -277,6 +291,11 @@
info.agent.onUnlockAttempt(successful);
}
}
+
+ if (successful && !mUserHasAuthenticatedSinceBoot.get(userId)) {
+ mUserHasAuthenticatedSinceBoot.put(userId, true);
+ updateTrust(userId);
+ }
}
// Listeners
@@ -384,4 +403,24 @@
return true;
}
};
+
+ private class DevicePolicyReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(
+ intent.getAction())) {
+ refreshAgentList();
+ }
+ }
+
+ public void register(Context context) {
+ context.registerReceiverAsUser(this,
+ UserHandle.ALL,
+ new IntentFilter(
+ DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+ null /* permission */,
+ null /* scheduler */);
+ }
+ }
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 7002744..2bf9ef1 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -1773,12 +1773,15 @@
*
* Input parameters equivalent to TS 27.007 AT+CCHO command.
*
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#SIM_COMMUNICATION SIM_COMMUNICATION}
+ *
* @param AID Application id. See ETSI 102.221 and 101.220.
* @return The logical channel id which is negative on error.
*/
public int iccOpenLogicalChannel(String AID) {
try {
- return getITelephony().iccOpenLogicalChannel(AID);
+ return getITelephony().iccOpenLogicalChannel(AID);
} catch (RemoteException ex) {
} catch (NullPointerException ex) {
}
@@ -1790,13 +1793,16 @@
*
* Input parameters equivalent to TS 27.007 AT+CCHC command.
*
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#SIM_COMMUNICATION SIM_COMMUNICATION}
+ *
* @param channel is the channel id to be closed as retruned by a successful
* iccOpenLogicalChannel.
* @return true if the channel was closed successfully.
*/
public boolean iccCloseLogicalChannel(int channel) {
try {
- return getITelephony().iccCloseLogicalChannel(channel);
+ return getITelephony().iccCloseLogicalChannel(channel);
} catch (RemoteException ex) {
} catch (NullPointerException ex) {
}
@@ -1808,6 +1814,9 @@
*
* Input parameters equivalent to TS 27.007 AT+CGLA command.
*
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#SIM_COMMUNICATION SIM_COMMUNICATION}
+ *
* @param channel is the channel id to be closed as returned by a successful
* iccOpenLogicalChannel.
* @param cla Class of the APDU command.
@@ -1823,8 +1832,30 @@
public String iccTransmitApduLogicalChannel(int channel, int cla,
int instruction, int p1, int p2, int p3, String data) {
try {
- return getITelephony().iccTransmitApduLogicalChannel(channel, cla,
- instruction, p1, p2, p3, data);
+ return getITelephony().iccTransmitApduLogicalChannel(channel, cla,
+ instruction, p1, p2, p3, data);
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+ return "";
+ }
+
+ /**
+ * Send ENVELOPE to the SIM and return the response.
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#SIM_COMMUNICATION SIM_COMMUNICATION}
+ *
+ * @param content String containing SAT/USAT response in hexadecimal
+ * format starting with command tag. See TS 102 223 for
+ * details.
+ * @return The APDU response from the ICC card, with the last 4 bytes
+ * being the status word. If the command fails, returns an empty
+ * string.
+ */
+ public String sendEnvelopeWithStatus(String content) {
+ try {
+ return getITelephony().sendEnvelopeWithStatus(content);
} catch (RemoteException ex) {
} catch (NullPointerException ex) {
}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 72398ad..8b80bfa 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -373,6 +373,18 @@
int p1, int p2, int p3, String data);
/**
+ * Send ENVELOPE to the SIM and returns the response.
+ *
+ * @param contents String containing SAT/USAT response in hexadecimal
+ * format starting with command tag. See TS 102 223 for
+ * details.
+ * @return The APDU response from the ICC card, with the last 4 bytes
+ * being the status word. If the command fails, returns an empty
+ * string.
+ */
+ String sendEnvelopeWithStatus(String content);
+
+ /**
* Read one of the NV items defined in {@link RadioNVItems} / {@code ril_nv_items.h}.
* Used for device configuration by some CDMA operators.
*
diff --git a/test-runner/src/android/test/mock/MockContext.java b/test-runner/src/android/test/mock/MockContext.java
index 0d9cd18..c162bf28 100644
--- a/test-runner/src/android/test/mock/MockContext.java
+++ b/test-runner/src/android/test/mock/MockContext.java
@@ -350,7 +350,17 @@
throw new UnsupportedOperationException();
}
+ /** @hide */
@Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler,
+ int initialCode, String initialData, Bundle initialExtras) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+
public void sendStickyBroadcast(Intent intent) {
throw new UnsupportedOperationException();
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index fde4e1a..d31239b 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -1284,6 +1284,14 @@
}
@Override
+ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
+ String receiverPermission, int appOp, BroadcastReceiver resultReceiver,
+ Handler scheduler,
+ int initialCode, String initialData, Bundle initialExtras) {
+ // pass
+ }
+
+ @Override
public void sendStickyBroadcast(Intent arg0) {
// pass
diff --git a/tools/layoutlib/rename_font/README b/tools/layoutlib/rename_font/README
new file mode 100644
index 0000000..600b756
--- /dev/null
+++ b/tools/layoutlib/rename_font/README
@@ -0,0 +1,9 @@
+This tool is used to rename the PS name encoded inside the ttf font that we ship
+with the SDK. There is bug in Java that returns incorrect results for
+java.awt.Font#layoutGlyphVector() if two fonts with same name but differnt
+versions are loaded. As a workaround, we rename all the fonts that we ship with
+the SDK by appending the font version to its name.
+
+
+The build_font.py copies all files from input_dir to output_dir while renaming
+the font files (*.ttf) in the process.
diff --git a/tools/layoutlib/rename_font/Roboto-Regular.ttf b/tools/layoutlib/rename_font/Roboto-Regular.ttf
new file mode 100644
index 0000000..7469063
--- /dev/null
+++ b/tools/layoutlib/rename_font/Roboto-Regular.ttf
Binary files differ
diff --git a/tools/layoutlib/rename_font/build_font.py b/tools/layoutlib/rename_font/build_font.py
new file mode 100755
index 0000000..ea3dccc
--- /dev/null
+++ b/tools/layoutlib/rename_font/build_font.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python
+
+# 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.
+
+"""
+Rename the PS name of all fonts in the input directory and copy them to the
+output directory.
+
+Usage: build_font.py /path/to/input_fonts/ /path/to/output_fonts/
+
+"""
+
+import sys
+# fontTools is available at platform/external/fonttools
+from fontTools import ttx
+import re
+import os
+from lxml import etree
+import shutil
+import glob
+
+def main(argv):
+ if len(argv) != 2:
+ print "Usage: build_font.py /path/to/input_fonts/ /path/to/out/dir/"
+ sys.exit(1)
+ if not os.path.isdir(argv[0]):
+ print argv[0] + "is not a valid directory"
+ sys.exit(1)
+ if not os.path.isdir(argv[1]):
+ print argv[1] + "is not a valid directory"
+ sys.exit(1)
+ cwd = os.getcwd()
+ os.chdir(argv[1])
+ files = glob.glob('*')
+ for filename in files:
+ os.remove(filename)
+ os.chdir(cwd)
+ for filename in os.listdir(argv[0]):
+ if not os.path.splitext(filename)[1].lower() == ".ttf":
+ shutil.copy(os.path.join(argv[0], filename), argv[1])
+ continue
+ print os.path.join(argv[0], filename)
+ old_ttf_path = os.path.join(argv[0], filename)
+ # run ttx to generate an xml file in the output folder which represents all
+ # its info
+ ttx_args = ["-d", argv[1], old_ttf_path]
+ ttx.main(ttx_args)
+ # the path to the output file. The file name is the fontfilename.ttx
+ ttx_path = os.path.join(argv[1], filename)
+ ttx_path = ttx_path[:-1] + "x"
+ # now parse the xml file to change its PS name.
+ tree = etree.parse(ttx_path)
+ encoding = tree.docinfo.encoding
+ root = tree.getroot()
+ for name in root.iter('name'):
+ [old_ps_name, version] = get_font_info(name)
+ new_ps_name = old_ps_name + version
+ update_name(name, new_ps_name)
+ tree.write(ttx_path, xml_declaration=True, encoding=encoding )
+ # generate the udpated font now.
+ ttx_args = ["-d", argv[1], ttx_path]
+ ttx.main(ttx_args)
+ # delete the temp ttx file.
+ os.remove(ttx_path)
+
+def get_font_info(tag):
+ ps_name = None
+ ps_version = None
+ for namerecord in tag.iter('namerecord'):
+ if 'nameID' in namerecord.attrib:
+ # if the tag has nameID=6, it is the postscript name of the font.
+ # see: http://scripts.sil.org/cms/scripts/page.php?item_id=IWS-Chapter08#3054f18b
+ if namerecord.attrib['nameID'] == '6':
+ if ps_name is not None:
+ if not sanitize(namerecord.text) == ps_name:
+ sys.exit('found multiple possibilities of the font name')
+ else:
+ ps_name = sanitize(namerecord.text)
+ # nameID=5 means the font version
+ if namerecord.attrib['nameID'] == '5':
+ if ps_version is not None:
+ if not ps_version == get_version(namerecord.text):
+ sys.exit('found multiple possibilities of the font version')
+ else:
+ ps_version = get_version(namerecord.text)
+ if ps_name is not None and ps_version is not None:
+ return [ps_name, ps_version]
+ sys.exit('didn\'t find the font name or version')
+
+
+def update_name(tag, name):
+ for namerecord in tag.iter('namerecord'):
+ if 'nameID' in namerecord.attrib:
+ if namerecord.attrib['nameID'] == '6':
+ namerecord.text = name
+
+def sanitize(string):
+ return re.sub(r'[^\w-]+', '', string)
+
+def get_version(string):
+ # The string must begin with "Version n.nn "
+ # to extract n.nn, we return the second entry in the split strings.
+ string = string.strip()
+ if not string.startswith("Version "):
+ sys.exit('mal-formed font version')
+ return sanitize(string.split()[1])
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/tools/layoutlib/rename_font/test.py b/tools/layoutlib/rename_font/test.py
new file mode 100755
index 0000000..d4c86cb
--- /dev/null
+++ b/tools/layoutlib/rename_font/test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+"""Tests build_font.py by renaming a font.
+
+The test copies Roboto-Regular.ttf to a tmp directory and ask build_font.py to rename it and put in another dir.
+We then use ttx to dump the new font to its xml and check if rename was successful
+
+To test locally, use:
+PYTHONPATH="$PYTHONPATH:/path/to/android/checkout/external/fonttools/Lib" ./test.py
+"""
+
+import unittest
+import build_font
+
+from fontTools import ttx
+import os
+from lxml import etree
+import shutil
+import tempfile
+
+class MyTest(unittest.TestCase):
+ def test(self):
+ font_name = "Roboto-Regular.ttf"
+ srcdir = tempfile.mkdtemp()
+ print "srcdir: " + srcdir
+ shutil.copy(font_name, srcdir)
+ destdir = tempfile.mkdtemp()
+ print "destdir: " + destdir
+ self.assertTrue(build_font.main([srcdir, destdir]) is None)
+ out_path = os.path.join(destdir, font_name)
+ ttx.main([out_path])
+ ttx_path = out_path[:-1] + "x"
+ tree = etree.parse(ttx_path)
+ root = tree.getroot()
+ name_tag = root.find('name')
+ [f_name, f_version] = build_font.get_font_info(name_tag)
+ shutil.rmtree(srcdir)
+ shutil.rmtree(destdir)
+ self.assertEqual(f_name, "Roboto-Regular1200310")
+
+
+
+if __name__ == '__main__':
+ unittest.main()