diff options
Diffstat (limited to 'core/java')
279 files changed, 14994 insertions, 8094 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index e5a5e98..eae0a4c 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -28,6 +28,7 @@ import android.content.res.XmlResourceParser; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; +import android.util.TypedValue; import android.util.Xml; import android.view.accessibility.AccessibilityEvent; @@ -182,9 +183,14 @@ public class AccessibilityServiceInfo implements Parcelable { private boolean mCanRetrieveWindowContent; /** - * Description of the accessibility service. + * Resource id of the description of the accessibility service. */ - private String mDescription; + private int mDescriptionResId; + + /** + * Non localized description of the accessibility service. + */ + private String mNonLocalizedDescription; /** * Creates a new instance. @@ -256,8 +262,15 @@ public class AccessibilityServiceInfo implements Parcelable { mCanRetrieveWindowContent = asAttributes.getBoolean( com.android.internal.R.styleable.AccessibilityService_canRetrieveWindowContent, false); - mDescription = asAttributes.getString( + TypedValue peekedValue = asAttributes.peekValue( com.android.internal.R.styleable.AccessibilityService_description); + if (peekedValue != null) { + mDescriptionResId = peekedValue.resourceId; + CharSequence nonLocalizedDescription = peekedValue.coerceToString(); + if (nonLocalizedDescription != null) { + mNonLocalizedDescription = nonLocalizedDescription.toString().trim(); + } + } asAttributes.recycle(); } catch (NameNotFoundException e) { throw new XmlPullParserException( "Unable to create context for: " @@ -331,15 +344,38 @@ public class AccessibilityServiceInfo implements Parcelable { } /** - * Description of the accessibility service. + * Gets the non-localized description of the accessibility service. * <p> * <strong>Statically set from * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong> * </p> * @return The description. + * + * @deprecated Use {@link #loadDescription(PackageManager)}. */ public String getDescription() { - return mDescription; + return mNonLocalizedDescription; + } + + /** + * The localized description of the accessibility service. + * <p> + * <strong>Statically set from + * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * @return The localized description. + */ + public String loadDescription(PackageManager packageManager) { + if (mDescriptionResId == 0) { + return mNonLocalizedDescription; + } + ServiceInfo serviceInfo = mResolveInfo.serviceInfo; + CharSequence description = packageManager.getText(serviceInfo.packageName, + mDescriptionResId, serviceInfo.applicationInfo); + if (description != null) { + return description.toString().trim(); + } + return null; } /** @@ -359,7 +395,8 @@ public class AccessibilityServiceInfo implements Parcelable { parcel.writeParcelable(mResolveInfo, 0); parcel.writeString(mSettingsActivityName); parcel.writeInt(mCanRetrieveWindowContent ? 1 : 0); - parcel.writeString(mDescription); + parcel.writeInt(mDescriptionResId); + parcel.writeString(mNonLocalizedDescription); } private void initFromParcel(Parcel parcel) { @@ -372,7 +409,8 @@ public class AccessibilityServiceInfo implements Parcelable { mResolveInfo = parcel.readParcelable(null); mSettingsActivityName = parcel.readString(); mCanRetrieveWindowContent = (parcel.readInt() == 1); - mDescription = parcel.readString(); + mDescriptionResId = parcel.readInt(); + mNonLocalizedDescription = parcel.readString(); } @Override diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7c41082..e53b313 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -33,14 +33,14 @@ interface IAccessibilityServiceConnection { * Finds an {@link AccessibilityNodeInfo} by accessibility id. * * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique view id or virtual descendant id. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, - int accessibilityViewId, int interactionId, + long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** @@ -51,15 +51,15 @@ interface IAccessibilityServiceConnection { * * @param text The searched text. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id from where to start the search. - * Use {@link android.view.View#NO_ID} to start from the root. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use {@link android.view.View#NO_ID} to start from the root. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ - float findAccessibilityNodeInfosByViewText(String text, int accessibilityWindowId, - int accessibilityViewId, int interractionId, + float findAccessibilityNodeInfosByText(String text, int accessibilityWindowId, + long accessibilityNodeId, int interractionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** @@ -75,7 +75,7 @@ interface IAccessibilityServiceConnection { * @param threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ - float findAccessibilityNodeInfosByViewTextInActiveWindow(String text, + float findAccessibilityNodeInfosByTextInActiveWindow(String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); @@ -96,14 +96,14 @@ interface IAccessibilityServiceConnection { * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param accessibilityWindowId The id of the window. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique view id or virtual descendant id. * @param action The action to perform. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. * @param threadId The id of the calling thread. * @return Whether the action was performed. */ - boolean performAccessibilityAction(int accessibilityWindowId, int accessibilityViewId, + boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); } diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 4f3405b..5fee4de 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -62,6 +62,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -275,7 +276,7 @@ public class AccountManagerService accountNames.add(accountName); } } - for (HashMap.Entry<String, ArrayList<String>> cur + for (Map.Entry<String, ArrayList<String>> cur : accountNamesByType.entrySet()) { final String accountType = cur.getKey(); final ArrayList<String> accountNames = cur.getValue(); diff --git a/core/java/android/accounts/ChooseTypeAndAccountActivity.java b/core/java/android/accounts/ChooseTypeAndAccountActivity.java index c3c9d16..136c68c 100644 --- a/core/java/android/accounts/ChooseTypeAndAccountActivity.java +++ b/core/java/android/accounts/ChooseTypeAndAccountActivity.java @@ -216,7 +216,7 @@ public class ChooseTypeAndAccountActivity extends Activity if (mPendingRequest == REQUEST_NULL) { // If there are no allowable accounts go directly to add account - if (mAccountInfos.isEmpty()) { + if (shouldSkipToChooseAccountTypeFlow()) { startChooseAccountTypeActivity(); return; } @@ -265,6 +265,12 @@ public class ChooseTypeAndAccountActivity extends Activity mPendingRequest = REQUEST_NULL; if (resultCode == RESULT_CANCELED) { + // if cancelling out of addAccount and the original state caused us to skip this, + // finish this activity + if (shouldSkipToChooseAccountTypeFlow()) { + setResult(Activity.RESULT_CANCELED); + finish(); + } return; } @@ -318,6 +324,14 @@ public class ChooseTypeAndAccountActivity extends Activity finish(); } + /** + * convenience method to check if we should skip the accounts list display and immediately + * jump to the flow that asks the user to select from the account type list + */ + private boolean shouldSkipToChooseAccountTypeFlow() { + return mAccountInfos.isEmpty(); + } + protected void runAddAccountForAuthenticator(String type) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "runAddAccountForAuthenticator: " + type); diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java index 7f0ea99..274a9d5 100644 --- a/core/java/android/animation/LayoutTransition.java +++ b/core/java/android/animation/LayoutTransition.java @@ -321,13 +321,13 @@ public class LayoutTransition { public long getStartDelay(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: - return mChangingAppearingDuration; + return mChangingAppearingDelay; case CHANGE_DISAPPEARING: - return mChangingDisappearingDuration; + return mChangingDisappearingDelay; case APPEARING: - return mAppearingDuration; + return mAppearingDelay; case DISAPPEARING: - return mDisappearingDuration; + return mDisappearingDelay; } // shouldn't reach here return 0; @@ -1024,18 +1024,25 @@ public class LayoutTransition { * * @param parent The ViewGroup to which the View is being added. * @param child The View being added to the ViewGroup. + * @param changesLayout Whether the removal will cause changes in the layout of other views + * in the container. INVISIBLE views becoming VISIBLE will not cause changes and thus will not + * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ - public void addChild(ViewGroup parent, View child) { + private void addChild(ViewGroup parent, View child, boolean changesLayout) { // Want disappearing animations to finish up before proceeding cancel(DISAPPEARING); - // Also, cancel changing animations so that we start fresh ones from current locations - cancel(CHANGE_APPEARING); + if (changesLayout) { + // Also, cancel changing animations so that we start fresh ones from current locations + cancel(CHANGE_APPEARING); + } if (mListeners != null) { for (TransitionListener listener : mListeners) { listener.startTransition(this, parent, child, APPEARING); } } - runChangeTransition(parent, child, APPEARING); + if (changesLayout) { + runChangeTransition(parent, child, APPEARING); + } runAppearingTransition(parent, child); } @@ -1048,8 +1055,31 @@ public class LayoutTransition { * @param parent The ViewGroup to which the View is being added. * @param child The View being added to the ViewGroup. */ + public void addChild(ViewGroup parent, View child) { + addChild(parent, child, true); + } + + /** + * @deprecated Use {@link #showChild(android.view.ViewGroup, android.view.View, int)}. + */ + @Deprecated public void showChild(ViewGroup parent, View child) { - addChild(parent, child); + addChild(parent, child, true); + } + + /** + * This method is called by ViewGroup when a child view is about to be made visible in the + * container. This callback starts the process of a transition; we grab the starting + * values, listen for changes to all of the children of the container, and start appropriate + * animations. + * + * @param parent The ViewGroup in which the View is being made visible. + * @param child The View being made visible. + * @param oldVisibility The previous visibility value of the child View, either + * {@link View#GONE} or {@link View#INVISIBLE}. + */ + public void showChild(ViewGroup parent, View child, int oldVisibility) { + addChild(parent, child, oldVisibility == View.GONE); } /** @@ -1060,18 +1090,25 @@ public class LayoutTransition { * * @param parent The ViewGroup from which the View is being removed. * @param child The View being removed from the ViewGroup. + * @param changesLayout Whether the removal will cause changes in the layout of other views + * in the container. Views becoming INVISIBLE will not cause changes and thus will not + * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ - public void removeChild(ViewGroup parent, View child) { + private void removeChild(ViewGroup parent, View child, boolean changesLayout) { // Want appearing animations to finish up before proceeding cancel(APPEARING); - // Also, cancel changing animations so that we start fresh ones from current locations - cancel(CHANGE_DISAPPEARING); + if (changesLayout) { + // Also, cancel changing animations so that we start fresh ones from current locations + cancel(CHANGE_DISAPPEARING); + } if (mListeners != null) { for (TransitionListener listener : mListeners) { listener.startTransition(this, parent, child, DISAPPEARING); } } - runChangeTransition(parent, child, DISAPPEARING); + if (changesLayout) { + runChangeTransition(parent, child, DISAPPEARING); + } runDisappearingTransition(parent, child); } @@ -1084,8 +1121,31 @@ public class LayoutTransition { * @param parent The ViewGroup from which the View is being removed. * @param child The View being removed from the ViewGroup. */ + public void removeChild(ViewGroup parent, View child) { + removeChild(parent, child, true); + } + + /** + * @deprecated Use {@link #hideChild(android.view.ViewGroup, android.view.View, int)}. + */ + @Deprecated public void hideChild(ViewGroup parent, View child) { - removeChild(parent, child); + removeChild(parent, child, true); + } + + /** + * This method is called by ViewGroup when a child view is about to be hidden in + * container. This callback starts the process of a transition; we grab the starting + * values, listen for changes to all of the children of the container, and start appropriate + * animations. + * + * @param parent The parent ViewGroup of the View being hidden. + * @param child The View being hidden. + * @param newVisibility The new visibility value of the child View, either + * {@link View#GONE} or {@link View#INVISIBLE}. + */ + public void hideChild(ViewGroup parent, View child, int newVisibility) { + removeChild(parent, child, newVisibility == View.GONE); } /** diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 55e95b0..c7a129e 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AndroidRuntimeException; +import android.view.Choreographer; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; @@ -52,17 +53,10 @@ public class ValueAnimator extends Animator { * Internal constants */ - /* - * The default amount of time in ms between animation frames - */ - private static final long DEFAULT_FRAME_DELAY = 10; - /** - * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent - * by the handler to itself to process the next animation frame + * Messages sent to timing handler: START is sent when an animation first begins. */ static final int ANIMATION_START = 0; - static final int ANIMATION_FRAME = 1; /** * Values used with internal variable mPlayingState to indicate the current state of an @@ -90,70 +84,15 @@ public class ValueAnimator extends Animator { */ long mSeekTime = -1; - // TODO: We access the following ThreadLocal variables often, some of them on every update. - // If ThreadLocal access is significantly expensive, we may want to put all of these - // fields into a structure sot hat we just access ThreadLocal once to get the reference - // to that structure, then access the structure directly for each field. - // The static sAnimationHandler processes the internal timing loop on which all animations // are based private static ThreadLocal<AnimationHandler> sAnimationHandler = new ThreadLocal<AnimationHandler>(); - // The per-thread list of all active animations - private static final ThreadLocal<ArrayList<ValueAnimator>> sAnimations = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - // The per-thread set of animations to be started on the next animation frame - private static final ThreadLocal<ArrayList<ValueAnimator>> sPendingAnimations = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - /** - * Internal per-thread collections used to avoid set collisions as animations start and end - * while being processed. - */ - private static final ThreadLocal<ArrayList<ValueAnimator>> sDelayedAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - private static final ThreadLocal<ArrayList<ValueAnimator>> sEndingAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - - private static final ThreadLocal<ArrayList<ValueAnimator>> sReadyAnims = - new ThreadLocal<ArrayList<ValueAnimator>>() { - @Override - protected ArrayList<ValueAnimator> initialValue() { - return new ArrayList<ValueAnimator>(); - } - }; - // The time interpolator to be used if none is set on the animation private static final TimeInterpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); - // type evaluators for the primitive types handled by this implementation - private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); - private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); - /** * Used to indicate whether the animation is currently playing in reverse. This causes the * elapsed fraction to be inverted to calculate the appropriate values. @@ -224,9 +163,6 @@ public class ValueAnimator extends Animator { // The amount of time in ms to delay starting the animation after start() is called private long mStartDelay = 0; - // The number of milliseconds between animation frames - private static long sFrameDelay = DEFAULT_FRAME_DELAY; - // The number of times the animation will repeat. The default is 0, which means the animation // will play only once private int mRepeatCount = 0; @@ -573,119 +509,146 @@ public class ValueAnimator extends Animator { * animations possible. * */ - private static class AnimationHandler extends Handler { + private static class AnimationHandler extends Handler + implements Choreographer.OnAnimateListener { + // The per-thread list of all active animations + private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>(); + + // The per-thread set of animations to be started on the next animation frame + private final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>(); + /** - * There are only two messages that we care about: ANIMATION_START and - * ANIMATION_FRAME. The START message is sent when an animation's start() - * method is called. It cannot start synchronously when start() is called + * Internal per-thread collections used to avoid set collisions as animations start and end + * while being processed. + */ + private final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>(); + private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>(); + private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>(); + + private final Choreographer mChoreographer; + private boolean mIsChoreographed; + + private AnimationHandler() { + mChoreographer = Choreographer.getInstance(); + } + + /** + * The START message is sent when an animation's start() method is called. + * It cannot start synchronously when start() is called * because the call may be on the wrong thread, and it would also not be * synchronized with other animations because it would not start on a common * timing pulse. So each animation sends a START message to the handler, which * causes the handler to place the animation on the active animations queue and * start processing frames for that animation. - * The FRAME message is the one that is sent over and over while there are any - * active animations to process. */ @Override public void handleMessage(Message msg) { - boolean callAgain = true; - ArrayList<ValueAnimator> animations = sAnimations.get(); - ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get(); switch (msg.what) { - // TODO: should we avoid sending frame message when starting if we - // were already running? case ANIMATION_START: - ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get(); - if (animations.size() > 0 || delayedAnims.size() > 0) { - callAgain = false; - } - // pendingAnims holds any animations that have requested to be started - // We're going to clear sPendingAnimations, but starting animation may - // cause more to be added to the pending list (for example, if one animation - // starting triggers another starting). So we loop until sPendingAnimations - // is empty. - while (pendingAnimations.size() > 0) { - ArrayList<ValueAnimator> pendingCopy = - (ArrayList<ValueAnimator>) pendingAnimations.clone(); - pendingAnimations.clear(); - int count = pendingCopy.size(); - for (int i = 0; i < count; ++i) { - ValueAnimator anim = pendingCopy.get(i); - // If the animation has a startDelay, place it on the delayed list - if (anim.mStartDelay == 0) { - anim.startAnimation(); - } else { - delayedAnims.add(anim); - } - } - } - // fall through to process first frame of new animations - case ANIMATION_FRAME: - // currentTime holds the common time for all animations processed - // during this frame - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - ArrayList<ValueAnimator> readyAnims = sReadyAnims.get(); - ArrayList<ValueAnimator> endingAnims = sEndingAnims.get(); - - // First, process animations currently sitting on the delayed queue, adding - // them to the active animations if they are ready - int numDelayedAnims = delayedAnims.size(); - for (int i = 0; i < numDelayedAnims; ++i) { - ValueAnimator anim = delayedAnims.get(i); - if (anim.delayedAnimationFrame(currentTime)) { - readyAnims.add(anim); - } - } - int numReadyAnims = readyAnims.size(); - if (numReadyAnims > 0) { - for (int i = 0; i < numReadyAnims; ++i) { - ValueAnimator anim = readyAnims.get(i); - anim.startAnimation(); - anim.mRunning = true; - delayedAnims.remove(anim); - } - readyAnims.clear(); - } + doAnimationStart(); + break; + } + } - // Now process all active animations. The return value from animationFrame() - // tells the handler whether it should now be ended - int numAnims = animations.size(); - int i = 0; - while (i < numAnims) { - ValueAnimator anim = animations.get(i); - if (anim.animationFrame(currentTime)) { - endingAnims.add(anim); - } - if (animations.size() == numAnims) { - ++i; - } else { - // An animation might be canceled or ended by client code - // during the animation frame. Check to see if this happened by - // seeing whether the current index is the same as it was before - // calling animationFrame(). Another approach would be to copy - // animations to a temporary list and process that list instead, - // but that entails garbage and processing overhead that would - // be nice to avoid. - --numAnims; - endingAnims.remove(anim); - } - } - if (endingAnims.size() > 0) { - for (i = 0; i < endingAnims.size(); ++i) { - endingAnims.get(i).endAnimation(); - } - endingAnims.clear(); + private void doAnimationStart() { + // mPendingAnimations holds any animations that have requested to be started + // We're going to clear mPendingAnimations, but starting animation may + // cause more to be added to the pending list (for example, if one animation + // starting triggers another starting). So we loop until mPendingAnimations + // is empty. + while (mPendingAnimations.size() > 0) { + ArrayList<ValueAnimator> pendingCopy = + (ArrayList<ValueAnimator>) mPendingAnimations.clone(); + mPendingAnimations.clear(); + int count = pendingCopy.size(); + for (int i = 0; i < count; ++i) { + ValueAnimator anim = pendingCopy.get(i); + // If the animation has a startDelay, place it on the delayed list + if (anim.mStartDelay == 0) { + anim.startAnimation(this); + } else { + mDelayedAnims.add(anim); } + } + } + doAnimationFrame(); + } - // If there are still active or delayed animations, call the handler again - // after the frameDelay - if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) { - sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay - - (AnimationUtils.currentAnimationTimeMillis() - currentTime))); - } - break; + private void doAnimationFrame() { + // currentTime holds the common time for all animations processed + // during this frame + long currentTime = AnimationUtils.currentAnimationTimeMillis(); + + // First, process animations currently sitting on the delayed queue, adding + // them to the active animations if they are ready + int numDelayedAnims = mDelayedAnims.size(); + for (int i = 0; i < numDelayedAnims; ++i) { + ValueAnimator anim = mDelayedAnims.get(i); + if (anim.delayedAnimationFrame(currentTime)) { + mReadyAnims.add(anim); + } + } + int numReadyAnims = mReadyAnims.size(); + if (numReadyAnims > 0) { + for (int i = 0; i < numReadyAnims; ++i) { + ValueAnimator anim = mReadyAnims.get(i); + anim.startAnimation(this); + anim.mRunning = true; + mDelayedAnims.remove(anim); + } + mReadyAnims.clear(); + } + + // Now process all active animations. The return value from animationFrame() + // tells the handler whether it should now be ended + int numAnims = mAnimations.size(); + int i = 0; + while (i < numAnims) { + ValueAnimator anim = mAnimations.get(i); + if (anim.animationFrame(currentTime)) { + mEndingAnims.add(anim); + } + if (mAnimations.size() == numAnims) { + ++i; + } else { + // An animation might be canceled or ended by client code + // during the animation frame. Check to see if this happened by + // seeing whether the current index is the same as it was before + // calling animationFrame(). Another approach would be to copy + // animations to a temporary list and process that list instead, + // but that entails garbage and processing overhead that would + // be nice to avoid. + --numAnims; + mEndingAnims.remove(anim); + } + } + if (mEndingAnims.size() > 0) { + for (i = 0; i < mEndingAnims.size(); ++i) { + mEndingAnims.get(i).endAnimation(this); + } + mEndingAnims.clear(); + } + + // If there are still active or delayed animations, schedule a future call to + // onAnimate to process the next frame of the animations. + if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) { + if (!mIsChoreographed) { + mIsChoreographed = true; + mChoreographer.addOnAnimateListener(this); + } + mChoreographer.scheduleAnimation(); + } else { + if (mIsChoreographed) { + mIsChoreographed = false; + mChoreographer.removeOnAnimateListener(this); + } } } + + @Override + public void onAnimate() { + doAnimationFrame(); + } } /** @@ -715,10 +678,13 @@ public class ValueAnimator extends Animator { * function because the same delay will be applied to all animations, since they are all * run off of a single timing loop. * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * * @return the requested time between frames, in milliseconds */ public static long getFrameDelay() { - return sFrameDelay; + return Choreographer.getFrameDelay(); } /** @@ -728,10 +694,13 @@ public class ValueAnimator extends Animator { * function because the same delay will be applied to all animations, since they are all * run off of a single timing loop. * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * * @param frameDelay the requested time between frames, in milliseconds */ public static void setFrameDelay(long frameDelay) { - sFrameDelay = frameDelay; + Choreographer.setFrameDelay(frameDelay); } /** @@ -928,7 +897,8 @@ public class ValueAnimator extends Animator { mPlayingState = STOPPED; mStarted = true; mStartedDelay = false; - sPendingAnimations.get().add(this); + AnimationHandler animationHandler = getOrCreateAnimationHandler(); + animationHandler.mPendingAnimations.add(this); if (mStartDelay == 0) { // This sets the initial value of the animation, prior to actually starting it running setCurrentPlayTime(getCurrentPlayTime()); @@ -944,11 +914,6 @@ public class ValueAnimator extends Animator { } } } - AnimationHandler animationHandler = sAnimationHandler.get(); - if (animationHandler == null) { - animationHandler = new AnimationHandler(); - sAnimationHandler.set(animationHandler); - } animationHandler.sendEmptyMessage(ANIMATION_START); } @@ -961,8 +926,10 @@ public class ValueAnimator extends Animator { public void cancel() { // Only cancel if the animation is actually running or has been started and is about // to run - if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) || - sDelayedAnims.get().contains(this)) { + AnimationHandler handler = getOrCreateAnimationHandler(); + if (mPlayingState != STOPPED + || handler.mPendingAnimations.contains(this) + || handler.mDelayedAnims.contains(this)) { // Only notify listeners if the animator has actually started if (mRunning && mListeners != null) { ArrayList<AnimatorListener> tmpListeners = @@ -971,16 +938,17 @@ public class ValueAnimator extends Animator { listener.onAnimationCancel(this); } } - endAnimation(); + endAnimation(handler); } } @Override public void end() { - if (!sAnimations.get().contains(this) && !sPendingAnimations.get().contains(this)) { + AnimationHandler handler = getOrCreateAnimationHandler(); + if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) { // Special case if the animation has not yet started; get it ready for ending mStartedDelay = false; - startAnimation(); + startAnimation(handler); } else if (!mInitialized) { initAnimation(); } @@ -991,7 +959,7 @@ public class ValueAnimator extends Animator { } else { animateValue(1f); } - endAnimation(); + endAnimation(handler); } @Override @@ -1027,10 +995,10 @@ public class ValueAnimator extends Animator { * Called internally to end an animation by removing it from the animations list. Must be * called on the UI thread. */ - private void endAnimation() { - sAnimations.get().remove(this); - sPendingAnimations.get().remove(this); - sDelayedAnims.get().remove(this); + private void endAnimation(AnimationHandler handler) { + handler.mAnimations.remove(this); + handler.mPendingAnimations.remove(this); + handler.mDelayedAnims.remove(this); mPlayingState = STOPPED; if (mRunning && mListeners != null) { ArrayList<AnimatorListener> tmpListeners = @@ -1048,9 +1016,9 @@ public class ValueAnimator extends Animator { * Called internally to start an animation by adding it to the active animations list. Must be * called on the UI thread. */ - private void startAnimation() { + private void startAnimation(AnimationHandler handler) { initAnimation(); - sAnimations.get().add(this); + handler.mAnimations.add(this); if (mStartDelay > 0 && mListeners != null) { // Listeners were already notified in start() if startDelay is 0; this is // just for delayed animations @@ -1236,13 +1204,14 @@ public class ValueAnimator extends Animator { /** * Return the number of animations currently running. * - * Used by StrictMode internally to annotate violations. Only - * called on the main thread. + * Used by StrictMode internally to annotate violations. + * May be called on arbitrary threads! * * @hide */ public static int getCurrentAnimationsCount() { - return sAnimations.get().size(); + AnimationHandler handler = sAnimationHandler.get(); + return handler != null ? handler.mAnimations.size() : 0; } /** @@ -1252,9 +1221,21 @@ public class ValueAnimator extends Animator { * @hide */ public static void clearAllAnimations() { - sAnimations.get().clear(); - sPendingAnimations.get().clear(); - sDelayedAnims.get().clear(); + AnimationHandler handler = sAnimationHandler.get(); + if (handler != null) { + handler.mAnimations.clear(); + handler.mPendingAnimations.clear(); + handler.mDelayedAnims.clear(); + } + } + + private AnimationHandler getOrCreateAnimationHandler() { + AnimationHandler handler = sAnimationHandler.get(); + if (handler == null) { + handler = new AnimationHandler(); + sAnimationHandler.set(handler); + } + return handler; } @Override diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 4fe9cef..9661b9e 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1442,9 +1442,10 @@ public class ActivityManager { public int getLauncherLargeIconDensity() { final Resources res = mContext.getResources(); final int density = res.getDisplayMetrics().densityDpi; + final int sw = res.getConfiguration().smallestScreenWidthDp; - if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) - != Configuration.SCREENLAYOUT_SIZE_XLARGE) { + if (sw < 600) { + // Smaller than approx 7" tablets, use the regular icon size. return density; } @@ -1456,9 +1457,13 @@ public class ActivityManager { case DisplayMetrics.DENSITY_HIGH: return DisplayMetrics.DENSITY_XHIGH; case DisplayMetrics.DENSITY_XHIGH: - return DisplayMetrics.DENSITY_MEDIUM * 2; + return DisplayMetrics.DENSITY_XXHIGH; + case DisplayMetrics.DENSITY_XXHIGH: + return DisplayMetrics.DENSITY_XHIGH * 2; default: - return density; + // The density is some abnormal value. Return some other + // abnormal value that is a reasonable scaling of it. + return (int)(density*1.5f); } } @@ -1471,9 +1476,10 @@ public class ActivityManager { public int getLauncherLargeIconSize() { final Resources res = mContext.getResources(); final int size = res.getDimensionPixelSize(android.R.dimen.app_icon_size); + final int sw = res.getConfiguration().smallestScreenWidthDp; - if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) - != Configuration.SCREENLAYOUT_SIZE_XLARGE) { + if (sw < 600) { + // Smaller than approx 7" tablets, use the regular icon size. return size; } @@ -1487,9 +1493,13 @@ public class ActivityManager { case DisplayMetrics.DENSITY_HIGH: return (size * DisplayMetrics.DENSITY_XHIGH) / DisplayMetrics.DENSITY_HIGH; case DisplayMetrics.DENSITY_XHIGH: - return (size * DisplayMetrics.DENSITY_MEDIUM * 2) / DisplayMetrics.DENSITY_XHIGH; + return (size * DisplayMetrics.DENSITY_XXHIGH) / DisplayMetrics.DENSITY_XHIGH; + case DisplayMetrics.DENSITY_XXHIGH: + return (size * DisplayMetrics.DENSITY_XHIGH*2) / DisplayMetrics.DENSITY_XXHIGH; default: - return size; + // The density is some abnormal value. Return some other + // abnormal value that is a reasonable scaling of it. + return (int)(size*1.5f); } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 0c761fc..3c5f53a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -65,6 +65,7 @@ import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.LogPrinter; +import android.util.PrintWriterPrinter; import android.util.Slog; import android.view.Display; import android.view.HardwareRenderer; @@ -485,7 +486,6 @@ public final class ActivityThread { private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%21s %8d"; private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d"; - private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d"; private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s"; // Formatting for checkin service - update version if row format changes @@ -813,6 +813,19 @@ public final class ActivityThread { } } + public void dumpProvider(FileDescriptor fd, IBinder providertoken, + String[] args) { + DumpComponentInfo data = new DumpComponentInfo(); + try { + data.fd = ParcelFileDescriptor.dup(fd); + data.token = providertoken; + data.args = args; + queueOrSendMessage(H.DUMP_PROVIDER, data); + } catch (IOException e) { + Slog.w(TAG, "dumpProvider failed", e); + } + } + @Override public Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, boolean checkin, boolean all, String[] args) { @@ -853,7 +866,6 @@ public final class ActivityThread { int binderProxyObjectCount = Debug.getBinderProxyObjectCount(); int binderDeathObjectCount = Debug.getBinderDeathObjectCount(); long openSslSocketCount = Debug.countInstancesOfClass(OpenSSLSocketImpl.class); - long sqliteAllocated = SQLiteDebug.getHeapAllocatedSize() / 1024; SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo(); // For checkin, we print one long comma-separated list of values @@ -921,9 +933,9 @@ public final class ActivityThread { pw.print(openSslSocketCount); pw.print(','); // SQL - pw.print(sqliteAllocated); pw.print(','); pw.print(stats.memoryUsed / 1024); pw.print(','); - pw.print(stats.pageCacheOverflo / 1024); pw.print(','); + pw.print(stats.memoryUsed / 1024); pw.print(','); + pw.print(stats.pageCacheOverflow / 1024); pw.print(','); pw.print(stats.largestMemAlloc / 1024); for (int i = 0; i < stats.dbStats.size(); i++) { DbStats dbStats = stats.dbStats.get(i); @@ -989,10 +1001,9 @@ public final class ActivityThread { // SQLite mem info pw.println(" "); pw.println(" SQL"); - printRow(pw, TWO_COUNT_COLUMNS_DB, "heap:", sqliteAllocated, "MEMORY_USED:", - stats.memoryUsed / 1024); - printRow(pw, TWO_COUNT_COLUMNS_DB, "PAGECACHE_OVERFLOW:", - stats.pageCacheOverflo / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024); + printRow(pw, ONE_COUNT_COLUMN, "MEMORY_USED:", stats.memoryUsed / 1024); + printRow(pw, TWO_COUNT_COLUMNS, "PAGECACHE_OVERFLOW:", + stats.pageCacheOverflow / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024); pw.println(" "); int N = stats.dbStats.size(); if (N > 0) { @@ -1026,6 +1037,14 @@ public final class ActivityThread { WindowManagerImpl.getDefault().dumpGfxInfo(fd); } + @Override + public void dumpDbInfo(FileDescriptor fd, String[] args) { + PrintWriter pw = new PrintWriter(new FileOutputStream(fd)); + PrintWriterPrinter printer = new PrintWriterPrinter(pw); + SQLiteDebug.dump(printer, args); + pw.flush(); + } + private void printRow(PrintWriter pw, String format, Object...objs) { pw.println(String.format(format, objs)); } @@ -1044,6 +1063,7 @@ public final class ActivityThread { public void scheduleTrimMemory(int level) { queueOrSendMessage(H.TRIM_MEMORY, null, level); } + } private class H extends Handler { @@ -1088,6 +1108,7 @@ public final class ActivityThread { public static final int SET_CORE_SETTINGS = 138; public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139; public static final int TRIM_MEMORY = 140; + public static final int DUMP_PROVIDER = 141; String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { @@ -1132,6 +1153,7 @@ public final class ActivityThread { case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS"; case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO"; case TRIM_MEMORY: return "TRIM_MEMORY"; + case DUMP_PROVIDER: return "DUMP_PROVIDER"; } } return "(unknown)"; @@ -1264,6 +1286,9 @@ public final class ActivityThread { case DUMP_ACTIVITY: handleDumpActivity((DumpComponentInfo)msg.obj); break; + case DUMP_PROVIDER: + handleDumpProvider((DumpComponentInfo)msg.obj); + break; case SLEEPING: handleSleeping((IBinder)msg.obj, msg.arg1 != 0); break; @@ -2347,6 +2372,19 @@ public final class ActivityThread { } } + private void handleDumpProvider(DumpComponentInfo info) { + ProviderClientRecord r = mLocalProviders.get(info.token); + if (r != null && r.mLocalProvider != null) { + PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd.getFileDescriptor())); + r.mLocalProvider.dump(info.fd.getFileDescriptor(), pw, info.args); + pw.flush(); + try { + info.fd.close(); + } catch (IOException e) { + } + } + } + private void handleServiceArgs(ServiceArgsData data) { Service s = mServices.get(data.token); if (s != null) { @@ -3703,7 +3741,6 @@ public final class ActivityThread { } final void handleTrimMemory(int level) { - WindowManagerImpl.getDefault().trimMemory(level); ArrayList<ComponentCallbacks2> callbacks; synchronized (mPackages) { @@ -3714,6 +3751,7 @@ public final class ActivityThread { for (int i=0; i<N; i++) { callbacks.get(i).onTrimMemory(level); } + WindowManagerImpl.getDefault().trimMemory(level); } private void setupGraphicsSupport(LoadedApk info) { @@ -3766,7 +3804,7 @@ public final class ActivityThread { // implementation to use the pool executor. Normally, we use the // serialized executor as the default. This has to happen in the // main thread so the main looper is set right. - if (data.appInfo.targetSdkVersion <= 12) { + if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -4375,7 +4413,7 @@ public final class ActivityThread { }); } - public static final ActivityThread systemMain() { + public static ActivityThread systemMain() { HardwareRenderer.disable(true); ActivityThread thread = new ActivityThread(); thread.attach(true); @@ -4416,6 +4454,8 @@ public final class ActivityThread { ActivityThread thread = new ActivityThread(); thread.attach(false); + AsyncTask.init(); + if (false) { Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 9082003..2fe682d 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -16,10 +16,8 @@ package android.app; -import android.content.Context; import android.content.Intent; import android.os.RemoteException; -import android.os.ServiceManager; /** * This class provides access to the system alarm services. These allow you @@ -117,8 +115,8 @@ public class AlarmManager * * @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC or * RTC_WAKEUP. - * @param triggerAtTime Time the alarm should go off, using the - * appropriate clock (depending on the alarm type). + * @param triggerAtMillis time in milliseconds that the alarm should go + * off, using the appropriate clock (depending on the alarm type). * @param operation Action to perform when the alarm goes off; * typically comes from {@link PendingIntent#getBroadcast * IntentSender.getBroadcast()}. @@ -134,9 +132,9 @@ public class AlarmManager * @see #RTC * @see #RTC_WAKEUP */ - public void set(int type, long triggerAtTime, PendingIntent operation) { + public void set(int type, long triggerAtMillis, PendingIntent operation) { try { - mService.set(type, triggerAtTime, operation); + mService.set(type, triggerAtMillis, operation); } catch (RemoteException ex) { } } @@ -169,9 +167,10 @@ public class AlarmManager * * @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or * RTC_WAKEUP. - * @param triggerAtTime Time the alarm should first go off, using the - * appropriate clock (depending on the alarm type). - * @param interval Interval between subsequent repeats of the alarm. + * @param triggerAtMillis time in milliseconds that the alarm should first + * go off, using the appropriate clock (depending on the alarm type). + * @param intervalMillis interval in milliseconds between subsequent repeats + * of the alarm. * @param operation Action to perform when the alarm goes off; * typically comes from {@link PendingIntent#getBroadcast * IntentSender.getBroadcast()}. @@ -187,10 +186,10 @@ public class AlarmManager * @see #RTC * @see #RTC_WAKEUP */ - public void setRepeating(int type, long triggerAtTime, long interval, - PendingIntent operation) { + public void setRepeating(int type, long triggerAtMillis, + long intervalMillis, PendingIntent operation) { try { - mService.setRepeating(type, triggerAtTime, interval, operation); + mService.setRepeating(type, triggerAtMillis, intervalMillis, operation); } catch (RemoteException ex) { } } @@ -219,20 +218,20 @@ public class AlarmManager * requested, the time between any two successive firings of the alarm * may vary. If your application demands very low jitter, use * {@link #setRepeating} instead. - * + * * @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or * RTC_WAKEUP. - * @param triggerAtTime Time the alarm should first go off, using the - * appropriate clock (depending on the alarm type). This - * is inexact: the alarm will not fire before this time, - * but there may be a delay of almost an entire alarm - * interval before the first invocation of the alarm. - * @param interval Interval between subsequent repeats of the alarm. If - * this is one of INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, - * INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY then the - * alarm will be phase-aligned with other alarms to reduce - * the number of wakeups. Otherwise, the alarm will be set - * as though the application had called {@link #setRepeating}. + * @param triggerAtMillis time in milliseconds that the alarm should first + * go off, using the appropriate clock (depending on the alarm type). This + * is inexact: the alarm will not fire before this time, but there may be a + * delay of almost an entire alarm interval before the first invocation of + * the alarm. + * @param intervalMillis interval in milliseconds between subsequent repeats + * of the alarm. If this is one of INTERVAL_FIFTEEN_MINUTES, + * INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY + * then the alarm will be phase-aligned with other alarms to reduce the + * number of wakeups. Otherwise, the alarm will be set as though the + * application had called {@link #setRepeating}. * @param operation Action to perform when the alarm goes off; * typically comes from {@link PendingIntent#getBroadcast * IntentSender.getBroadcast()}. @@ -253,10 +252,10 @@ public class AlarmManager * @see #INTERVAL_HALF_DAY * @see #INTERVAL_DAY */ - public void setInexactRepeating(int type, long triggerAtTime, long interval, - PendingIntent operation) { + public void setInexactRepeating(int type, long triggerAtMillis, + long intervalMillis, PendingIntent operation) { try { - mService.setInexactRepeating(type, triggerAtTime, interval, operation); + mService.setInexactRepeating(type, triggerAtMillis, intervalMillis, operation); } catch (RemoteException ex) { } } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index c4a4fea..e75d7b4 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -352,6 +352,21 @@ public abstract class ApplicationThreadNative extends Binder return true; } + case DUMP_PROVIDER_TRANSACTION: { + data.enforceInterface(IApplicationThread.descriptor); + ParcelFileDescriptor fd = data.readFileDescriptor(); + final IBinder service = data.readStrongBinder(); + final String[] args = data.readStringArray(); + if (fd != null) { + dumpProvider(fd.getFileDescriptor(), service, args); + try { + fd.close(); + } catch (IOException e) { + } + } + return true; + } + case SCHEDULE_REGISTERED_RECEIVER_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); IIntentReceiver receiver = IIntentReceiver.Stub.asInterface( @@ -539,6 +554,26 @@ public abstract class ApplicationThreadNative extends Binder reply.writeNoException(); return true; } + + case DUMP_DB_INFO_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + ParcelFileDescriptor fd = data.readFileDescriptor(); + String[] args = data.readStringArray(); + if (fd != null) { + try { + dumpDbInfo(fd.getFileDescriptor(), args); + } finally { + try { + fd.close(); + } catch (IOException e) { + // swallowed, not propagated back to the caller + } + } + } + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -931,6 +966,17 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + public void dumpProvider(FileDescriptor fd, IBinder token, String[] args) + throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeFileDescriptor(fd); + data.writeStrongBinder(token); + data.writeStringArray(args); + mRemote.transact(DUMP_PROVIDER_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } + public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, String dataStr, Bundle extras, boolean ordered, boolean sticky) throws RemoteException { @@ -1105,4 +1151,13 @@ class ApplicationThreadProxy implements IApplicationThread { mRemote.transact(DUMP_GFX_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } + + public void dumpDbInfo(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + mRemote.transact(DUMP_DB_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 1253fe7..6ad1736 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -102,6 +102,8 @@ public interface IApplicationThread extends IInterface { void processInBackground() throws RemoteException; void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args) throws RemoteException; + void dumpProvider(FileDescriptor fd, IBinder servicetoken, String[] args) + throws RemoteException; void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky) throws RemoteException; @@ -125,6 +127,7 @@ public interface IApplicationThread extends IInterface { Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, boolean checkin, boolean all, String[] args) throws RemoteException; void dumpGfxInfo(FileDescriptor fd, String[] args) throws RemoteException; + void dumpDbInfo(FileDescriptor fd, String[] args) throws RemoteException; String descriptor = "android.app.IApplicationThread"; @@ -171,4 +174,6 @@ public interface IApplicationThread extends IInterface { int SCHEDULE_TRIM_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+41; int DUMP_MEM_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+42; int DUMP_GFX_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+43; + int DUMP_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+44; + int DUMP_DB_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+45; } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index d7f5c55..c037ffb 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -24,14 +24,14 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; -import android.os.PerformanceCollector; -import android.os.RemoteException; import android.os.Debug; import android.os.IBinder; import android.os.MessageQueue; +import android.os.PerformanceCollector; import android.os.Process; -import android.os.SystemClock; +import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.util.AndroidRuntimeException; import android.util.Log; import android.view.IWindowManager; @@ -40,7 +40,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.Window; -import android.view.inputmethod.InputMethodManager; import java.io.File; import java.util.ArrayList; @@ -834,16 +833,21 @@ public class Instrumentation { return; } KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - + KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray()); - + if (events != null) { for (int i = 0; i < events.length; i++) { - sendKeySync(events[i]); + // We have to change the time of an event before injecting it because + // all KeyEvents returned by KeyCharacterMap.getEvents() have the same + // time stamp and the system rejects too old events. Hence, it is + // possible for an event to become stale before it is injected if it + // takes too long to inject the preceding ones. + sendKeySync(KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(), 0)); } - } + } } - + /** * Send a key event to the currently focused window/view and wait for it to * be processed. Finished at some point after the recipient has returned diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 8fa95b4..d04e9db 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -551,7 +551,6 @@ public class SearchDialog extends Dialog { try { // If the intent was created from a suggestion, it will always have an explicit // component here. - Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toUri(0)); getContext().startActivity(intent); // If the search switches to a different activity, // SearchDialogWrapper#performActivityResuming diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 5b8addf..dd9f337 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -56,6 +56,11 @@ public class StatusBarManager { | DISABLE_NOTIFICATION_ALERTS | DISABLE_NOTIFICATION_TICKER | DISABLE_SYSTEM_INFO | DISABLE_RECENT | DISABLE_HOME | DISABLE_BACK | DISABLE_CLOCK; + public static final int NAVIGATION_HINT_BACK_NOP = 1 << 0; + public static final int NAVIGATION_HINT_HOME_NOP = 1 << 1; + public static final int NAVIGATION_HINT_RECENT_NOP = 1 << 2; + public static final int NAVIGATION_HINT_BACK_ALT = 1 << 3; + private Context mContext; private IStatusBarService mService; private IBinder mToken = new Binder(); diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 71f6445..0c22740 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -168,7 +168,7 @@ public class UiModeManager { * {@link Configuration#UI_MODE_TYPE_NORMAL Configuration.UI_MODE_TYPE_NORMAL}, * {@link Configuration#UI_MODE_TYPE_DESK Configuration.UI_MODE_TYPE_DESK}, or * {@link Configuration#UI_MODE_TYPE_CAR Configuration.UI_MODE_TYPE_CAR}, or - * {@link Configuration#UI_MODE_TYPE_TELEVISION Configuration.UI_MODE_TYPE_TV}. + * {@link Configuration#UI_MODE_TYPE_TELEVISION Configuration.UI_MODE_TYPE_APPLIANCE}. */ public int getCurrentModeType() { if (mService != null) { diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index b1c1f30..c1e28b0 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -213,10 +213,6 @@ public class WallpaperManager { mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER); } - public Handler getHandler() { - return mHandler; - } - public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { synchronized (this) { if (mWallpaper != null) { @@ -623,24 +619,14 @@ public class WallpaperManager { * @param yOffset The offset along the Y dimension, from 0 to 1. */ public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) { - final IBinder fWindowToken = windowToken; - final float fXOffset = xOffset; - final float fYOffset = yOffset; - sGlobals.getHandler().post(new Runnable() { - public void run() { - try { - //Log.v(TAG, "Sending new wallpaper offsets from app..."); - ViewRootImpl.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( - fWindowToken, fXOffset, fYOffset, mWallpaperXStep, mWallpaperYStep); - //Log.v(TAG, "...app returning after sending offsets!"); - } catch (RemoteException e) { - // Ignore. - } catch (IllegalArgumentException e) { - // Since this is being posted, it's possible that this windowToken is no longer - // valid, for example, if setWallpaperOffsets is called just before rotation. - } - } - }); + try { + //Log.v(TAG, "Sending new wallpaper offsets from app..."); + ViewRootImpl.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep); + //Log.v(TAG, "...app returning after sending offsets!"); + } catch (RemoteException e) { + // Ignore. + } } /** diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 899816c..600ce6f 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -405,6 +405,25 @@ public final class BluetoothAdapter { } /** + * Get a {@link BluetoothDevice} object for the given Bluetooth hardware + * address. + * <p>Valid Bluetooth hardware addresses must be 6 bytes. This method + * expects the address in network byte order (MSB first). + * <p>A {@link BluetoothDevice} will always be returned for a valid + * hardware address, even if this adapter has never seen that device. + * + * @param address Bluetooth MAC address (6 bytes) + * @throws IllegalArgumentException if address is invalid + */ + public BluetoothDevice getRemoteDevice(byte[] address) { + if (address == null || address.length != 6) { + throw new IllegalArgumentException("Bluetooth address must have 6 bytes"); + } + return new BluetoothDevice(String.format("%02X:%02X:%02X:%02X:%02X:%02X", + address[0], address[1], address[2], address[3], address[4], address[5])); + } + + /** * Return true if Bluetooth is currently enabled and ready for use. * <p>Equivalent to: * <code>getBluetoothState() == STATE_ON</code> @@ -1287,7 +1306,7 @@ public final class BluetoothAdapter { } /** - * Validate a Bluetooth address, such as "00:43:A8:23:10:F0" + * Validate a String Bluetooth address, such as "00:43:A8:23:10:F0" * <p>Alphabetic characters must be uppercase to be valid. * * @param address Bluetooth address as string diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index 0b54396..944ca6b 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -173,6 +173,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled); if (cancelled) { mCancellingTask = mTask; + onCancelLoadInBackground(); } mTask = null; return cancelled; @@ -256,6 +257,25 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { } /** + * Override this method to try to abort the computation currently taking + * place on a background thread. + * + * Note that when this method is called, it is possible that {@link #loadInBackground} + * has not started yet or has already completed. + */ + protected void onCancelLoadInBackground() { + } + + /** + * Returns true if the current execution of {@link #loadInBackground()} is being canceled. + * + * @return True if the current execution of {@link #loadInBackground()} is being canceled. + */ + protected boolean isLoadInBackgroundCanceled() { + return mCancellingTask != null; + } + + /** * Locks the current thread until the loader completes the current load * operation. Returns immediately if there is no load operation running. * Should not be called from the UI thread: calling it from the UI diff --git a/core/java/android/content/CancelationSignal.java b/core/java/android/content/CancelationSignal.java new file mode 100644 index 0000000..58cf59d --- /dev/null +++ b/core/java/android/content/CancelationSignal.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 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 android.content; + +import android.os.RemoteException; + +/** + * Provides the ability to cancel an operation in progress. + */ +public final class CancelationSignal { + private boolean mIsCanceled; + private OnCancelListener mOnCancelListener; + private ICancelationSignal mRemote; + + /** + * Creates a cancelation signal, initially not canceled. + */ + public CancelationSignal() { + } + + /** + * Returns true if the operation has been canceled. + * + * @return True if the operation has been canceled. + */ + public boolean isCanceled() { + synchronized (this) { + return mIsCanceled; + } + } + + /** + * Throws {@link OperationCanceledException} if the operation has been canceled. + * + * @throws OperationCanceledException if the operation has been canceled. + */ + public void throwIfCanceled() { + if (isCanceled()) { + throw new OperationCanceledException(); + } + } + + /** + * Cancels the operation and signals the cancelation listener. + * If the operation has not yet started, then it will be canceled as soon as it does. + */ + public void cancel() { + synchronized (this) { + if (!mIsCanceled) { + mIsCanceled = true; + if (mOnCancelListener != null) { + mOnCancelListener.onCancel(); + } + if (mRemote != null) { + try { + mRemote.cancel(); + } catch (RemoteException ex) { + } + } + } + } + } + + /** + * Sets the cancelation listener to be called when canceled. + * If {@link CancelationSignal#cancel} has already been called, then the provided + * listener is invoked immediately. + * + * The listener is called while holding the cancelation signal's lock which is + * also held while registering or unregistering the listener. Because of the lock, + * it is not possible for the listener to run after it has been unregistered. + * This design choice makes it easier for clients of {@link CancelationSignal} to + * prevent race conditions related to listener registration and unregistration. + * + * @param listener The cancelation listener, or null to remove the current listener. + */ + public void setOnCancelListener(OnCancelListener listener) { + synchronized (this) { + mOnCancelListener = listener; + if (mIsCanceled && listener != null) { + listener.onCancel(); + } + } + } + + /** + * Sets the remote transport. + * + * @param remote The remote transport, or null to remove. + * + * @hide + */ + public void setRemote(ICancelationSignal remote) { + synchronized (this) { + mRemote = remote; + if (mIsCanceled && remote != null) { + try { + remote.cancel(); + } catch (RemoteException ex) { + } + } + } + } + + /** + * Creates a transport that can be returned back to the caller of + * a Binder function and subsequently used to dispatch a cancelation signal. + * + * @return The new cancelation signal transport. + * + * @hide + */ + public static ICancelationSignal createTransport() { + return new Transport(); + } + + /** + * Given a locally created transport, returns its associated cancelation signal. + * + * @param transport The locally created transport, or null if none. + * @return The associated cancelation signal, or null if none. + * + * @hide + */ + public static CancelationSignal fromTransport(ICancelationSignal transport) { + if (transport instanceof Transport) { + return ((Transport)transport).mCancelationSignal; + } + return null; + } + + /** + * Listens for cancelation. + */ + public interface OnCancelListener { + /** + * Called when {@link CancelationSignal#cancel} is invoked. + */ + void onCancel(); + } + + private static final class Transport extends ICancelationSignal.Stub { + final CancelationSignal mCancelationSignal = new CancelationSignal(); + + @Override + public void cancel() throws RemoteException { + mCancelationSignal.cancel(); + } + } +} diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 092a0c8..adbeb6a 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -29,11 +29,14 @@ import android.os.Binder; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.Process; +import android.os.RemoteException; import android.util.Log; import java.io.File; +import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; /** @@ -172,28 +175,33 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return getContentProvider().getClass().getName(); } + @Override public Cursor query(Uri uri, String[] projection, - String selection, String[] selectionArgs, String sortOrder) { + String selection, String[] selectionArgs, String sortOrder, + ICancelationSignal cancelationSignal) { enforceReadPermission(uri); - return ContentProvider.this.query(uri, projection, selection, - selectionArgs, sortOrder); + return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder, + CancelationSignal.fromTransport(cancelationSignal)); } + @Override public String getType(Uri uri) { return ContentProvider.this.getType(uri); } - + @Override public Uri insert(Uri uri, ContentValues initialValues) { enforceWritePermission(uri); return ContentProvider.this.insert(uri, initialValues); } + @Override public int bulkInsert(Uri uri, ContentValues[] initialValues) { enforceWritePermission(uri); return ContentProvider.this.bulkInsert(uri, initialValues); } + @Override public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { for (ContentProviderOperation operation : operations) { @@ -208,17 +216,20 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.applyBatch(operations); } + @Override public int delete(Uri uri, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.delete(uri, selection, selectionArgs); } + @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.update(uri, values, selection, selectionArgs); } + @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); @@ -226,6 +237,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openFile(uri, mode); } + @Override public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); @@ -233,6 +245,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openAssetFile(uri, mode); } + @Override public Bundle call(String method, String arg, Bundle extras) { return ContentProvider.this.call(method, arg, extras); } @@ -249,6 +262,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts); } + @Override + public ICancelationSignal createCancelationSignal() throws RemoteException { + return CancelationSignal.createTransport(); + } + private void enforceReadPermission(Uri uri) { final int uid = Binder.getCallingUid(); if (uid == mMyUid) { @@ -539,6 +557,75 @@ public abstract class ContentProvider implements ComponentCallbacks2 { String selection, String[] selectionArgs, String sortOrder); /** + * Implement this to handle query requests from clients with support for cancelation. + * This method can be called from multiple threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes + * and Threads</a>. + * <p> + * Example client call:<p> + * <pre>// Request a specific record. + * Cursor managedCursor = managedQuery( + ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2), + projection, // Which columns to return. + null, // WHERE clause. + null, // WHERE clause value substitution + People.NAME + " ASC"); // Sort order.</pre> + * Example implementation:<p> + * <pre>// SQLiteQueryBuilder is a helper class that creates the + // proper SQL syntax for us. + SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); + + // Set the table we're querying. + qBuilder.setTables(DATABASE_TABLE_NAME); + + // If the query ends in a specific record number, we're + // being asked for a specific record, so set the + // WHERE clause in our query. + if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){ + qBuilder.appendWhere("_id=" + uri.getPathLeafId()); + } + + // Make the query. + Cursor c = qBuilder.query(mDb, + projection, + selection, + selectionArgs, + groupBy, + having, + sortOrder); + c.setNotificationUri(getContext().getContentResolver(), uri); + return c;</pre> + * <p> + * If you implement this method then you must also implement the version of + * {@link #query(Uri, String[], String, String[], String)} that does not take a cancelation + * provider to ensure correct operation on older versions of the Android Framework in + * which the cancelation signal overload was not available. + * + * @param uri The URI to query. This will be the full URI sent by the client; + * if the client is requesting a specific record, the URI will end in a record number + * that the implementation should parse and add to a WHERE or HAVING clause, specifying + * that _id value. + * @param projection The list of columns to put into the cursor. If + * null all columns are included. + * @param selection A selection criteria to apply when filtering rows. + * If null then all rows are included. + * @param selectionArgs You may include ?s in selection, which will be replaced by + * the values from selectionArgs, in order that they appear in the selection. + * The values will be bound as Strings. + * @param sortOrder How the rows in the cursor should be sorted. + * If null then the provider is free to define the sort order. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return a Cursor or null. + */ + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancelationSignal cancelationSignal) { + return query(uri, projection, selection, selectionArgs, sortOrder); + } + + /** * Implement this to handle requests for the MIME type of the data at the * given URI. The returned MIME type should start with * <code>vnd.android.cursor.item</code> for a single record, @@ -1013,4 +1100,19 @@ public abstract class ContentProvider implements ComponentCallbacks2 { Log.w(TAG, "implement ContentProvider shutdown() to make sure all database " + "connections are gracefully shutdown"); } + + /** + * Print the Provider's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity provider <provider_component_name>". + * + * @param prefix Desired prefix to prepend at each line of output. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + * @hide + */ + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + writer.println("nothing to dump"); + } } diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 0540109..9a1fa65 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -47,7 +47,20 @@ public class ContentProviderClient { /** See {@link ContentProvider#query ContentProvider.query} */ public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException { - return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder); + return query(url, projection, selection, selectionArgs, sortOrder, null); + } + + /** See {@link ContentProvider#query ContentProvider.query} */ + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancelationSignal cancelationSignal) + throws RemoteException { + ICancelationSignal remoteCancelationSignal = null; + if (cancelationSignal != null) { + remoteCancelationSignal = mContentProvider.createCancelationSignal(); + cancelationSignal.setRemote(remoteCancelationSignal); + } + return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder, + remoteCancelationSignal); } /** See {@link ContentProvider#getType ContentProvider.getType} */ diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index b089bf2..e0e277a 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -21,7 +21,6 @@ import android.database.BulkCursorNative; import android.database.BulkCursorToCursorAdaptor; import android.database.Cursor; import android.database.CursorToBulkCursorAdaptor; -import android.database.CursorWindow; import android.database.DatabaseUtils; import android.database.IBulkCursor; import android.database.IContentObserver; @@ -41,8 +40,6 @@ import java.util.ArrayList; * {@hide} */ abstract public class ContentProviderNative extends Binder implements IContentProvider { - private static final String TAG = "ContentProvider"; - public ContentProviderNative() { attachInterface(this, descriptor); @@ -108,8 +105,11 @@ abstract public class ContentProviderNative extends Binder implements IContentPr String sortOrder = data.readString(); IContentObserver observer = IContentObserver.Stub.asInterface( data.readStrongBinder()); + ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface( + data.readStrongBinder()); - Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder); + Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder, + cancelationSignal); if (cursor != null) { CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor( cursor, observer, getProviderName()); @@ -295,6 +295,16 @@ abstract public class ContentProviderNative extends Binder implements IContentPr } return true; } + + case CREATE_CANCELATION_SIGNAL_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + + ICancelationSignal cancelationSignal = createCancelationSignal(); + reply.writeNoException(); + reply.writeStrongBinder(cancelationSignal.asBinder()); + return true; + } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -324,7 +334,8 @@ final class ContentProviderProxy implements IContentProvider } public Cursor query(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder) throws RemoteException { + String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal) + throws RemoteException { BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -352,6 +363,7 @@ final class ContentProviderProxy implements IContentProvider } data.writeString(sortOrder); data.writeStrongBinder(adaptor.getObserver().asBinder()); + data.writeStrongBinder(cancelationSignal != null ? cancelationSignal.asBinder() : null); mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0); @@ -620,5 +632,24 @@ final class ContentProviderProxy implements IContentProvider } } + public ICancelationSignal createCancelationSignal() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + data.writeInterfaceToken(IContentProvider.descriptor); + + mRemote.transact(IContentProvider.CREATE_CANCELATION_SIGNAL_TRANSACTION, + data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface( + reply.readStrongBinder()); + return cancelationSignal; + } finally { + data.recycle(); + reply.recycle(); + } + } + private IBinder mRemote; } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index cc3219b..e79475a 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -22,6 +22,7 @@ import android.accounts.Account; import android.app.ActivityManagerNative; import android.app.ActivityThread; import android.app.AppGlobals; +import android.content.ContentProvider.Transport; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -302,13 +303,62 @@ public abstract class ContentResolver { */ public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return query(uri, projection, selection, selectionArgs, sortOrder, null); + } + + /** + * <p> + * Query the given URI, returning a {@link Cursor} over the result set. + * </p> + * <p> + * For best performance, the caller should follow these guidelines: + * <ul> + * <li>Provide an explicit projection, to prevent + * reading data from storage that aren't going to be used.</li> + * <li>Use question mark parameter markers such as 'phone=?' instead of + * explicit values in the {@code selection} parameter, so that queries + * that differ only by those values will be recognized as the same + * for caching purposes.</li> + * </ul> + * </p> + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param projection A list of which columns to return. Passing null will + * return all columns, which is inefficient. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that they + * appear in the selection. The values will be bound as Strings. + * @param sortOrder How to order the rows, formatted as an SQL ORDER BY + * clause (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A Cursor object, which is positioned before the first entry, or null + * @see Cursor + */ + public final Cursor query(final Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancelationSignal cancelationSignal) { IContentProvider provider = acquireProvider(uri); if (provider == null) { return null; } try { long startTime = SystemClock.uptimeMillis(); - Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder); + + ICancelationSignal remoteCancelationSignal = null; + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + remoteCancelationSignal = provider.createCancelationSignal(); + cancelationSignal.setRemote(remoteCancelationSignal); + } + Cursor qCursor = provider.query(uri, projection, + selection, selectionArgs, sortOrder, remoteCancelationSignal); if (qCursor == null) { releaseProvider(provider); return null; @@ -1034,8 +1084,11 @@ public abstract class ContentResolver { * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. * By default, CursorAdapter objects will get this notification. * - * @param uri - * @param observer The observer that originated the change, may be <code>null</null> + * @param uri The uri of the content that was changed. + * @param observer The observer that originated the change, may be <code>null</null>. + * The observer that originated the change will only receive the notification if it + * has requested to receive self-change notifications by implementing + * {@link ContentObserver#deliverSelfNotifications()} to return true. */ public void notifyChange(Uri uri, ContentObserver observer) { notifyChange(uri, observer, true /* sync to network */); @@ -1046,8 +1099,11 @@ public abstract class ContentResolver { * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. * By default, CursorAdapter objects will get this notification. * - * @param uri - * @param observer The observer that originated the change, may be <code>null</null> + * @param uri The uri of the content that was changed. + * @param observer The observer that originated the change, may be <code>null</null>. + * The observer that originated the change will only receive the notification if it + * has requested to receive self-change notifications by implementing + * {@link ContentObserver#deliverSelfNotifications()} to return true. * @param syncToNetwork If true, attempt to sync the change to the network. */ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 0e83dc0..fc4c262 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -176,7 +176,7 @@ public final class ContentService extends IContentService.Stub { for (int i=0; i<numCalls; i++) { ObserverCall oc = calls.get(i); try { - oc.mObserver.onChange(oc.mSelfNotify); + oc.mObserver.onChange(oc.mSelfChange, uri); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri); } @@ -218,13 +218,12 @@ public final class ContentService extends IContentService.Stub { public static final class ObserverCall { final ObserverNode mNode; final IContentObserver mObserver; - final boolean mSelfNotify; + final boolean mSelfChange; - ObserverCall(ObserverNode node, IContentObserver observer, - boolean selfNotify) { + ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange) { mNode = node; mObserver = observer; - mSelfNotify = selfNotify; + mSelfChange = selfChange; } } @@ -668,7 +667,7 @@ public final class ContentService extends IContentService.Stub { } private void collectMyObserversLocked(boolean leaf, IContentObserver observer, - boolean selfNotify, ArrayList<ObserverCall> calls) { + boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) { int N = mObservers.size(); IBinder observerBinder = observer == null ? null : observer.asBinder(); for (int i = 0; i < N; i++) { @@ -676,28 +675,29 @@ public final class ContentService extends IContentService.Stub { // Don't notify the observer if it sent the notification and isn't interesed // in self notifications - if (entry.observer.asBinder() == observerBinder && !selfNotify) { + boolean selfChange = (entry.observer.asBinder() == observerBinder); + if (selfChange && !observerWantsSelfNotifications) { continue; } // Make sure the observer is interested in the notification if (leaf || (!leaf && entry.notifyForDescendents)) { - calls.add(new ObserverCall(this, entry.observer, selfNotify)); + calls.add(new ObserverCall(this, entry.observer, selfChange)); } } } public void collectObserversLocked(Uri uri, int index, IContentObserver observer, - boolean selfNotify, ArrayList<ObserverCall> calls) { + boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) { String segment = null; int segmentCount = countUriSegments(uri); if (index >= segmentCount) { // This is the leaf node, notify all observers - collectMyObserversLocked(true, observer, selfNotify, calls); + collectMyObserversLocked(true, observer, observerWantsSelfNotifications, calls); } else if (index < segmentCount){ segment = getUriSegment(uri, index); // Notify any observers at this level who are interested in descendents - collectMyObserversLocked(false, observer, selfNotify, calls); + collectMyObserversLocked(false, observer, observerWantsSelfNotifications, calls); } int N = mChildren.size(); @@ -705,7 +705,8 @@ public final class ContentService extends IContentService.Stub { ObserverNode node = mChildren.get(i); if (segment == null || node.mName.equals(segment)) { // We found the child, - node.collectObserversLocked(uri, index + 1, observer, selfNotify, calls); + node.collectObserversLocked(uri, index + 1, + observer, observerWantsSelfNotifications, calls); if (segment != null) { break; } diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 7af535b..6e4aca8 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -19,7 +19,6 @@ package android.content; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; -import android.os.AsyncTask; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -49,18 +48,42 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { String mSortOrder; Cursor mCursor; + CancelationSignal mCancelationSignal; /* Runs on a worker thread */ @Override public Cursor loadInBackground() { - Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, - mSelectionArgs, mSortOrder); - if (cursor != null) { - // Ensure the cursor window is filled - cursor.getCount(); - registerContentObserver(cursor, mObserver); + synchronized (this) { + if (isLoadInBackgroundCanceled()) { + throw new OperationCanceledException(); + } + mCancelationSignal = new CancelationSignal(); + } + try { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder, mCancelationSignal); + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + registerContentObserver(cursor, mObserver); + } + return cursor; + } finally { + synchronized (this) { + mCancelationSignal = null; + } + } + } + + @Override + protected void onCancelLoadInBackground() { + super.onCancelLoadInBackground(); + + synchronized (this) { + if (mCancelationSignal != null) { + mCancelationSignal.cancel(); + } } - return cursor; } /** diff --git a/core/java/android/nfc/LlcpPacket.aidl b/core/java/android/content/ICancelationSignal.aidl index 80f424d..3f5a24d 100644 --- a/core/java/android/nfc/LlcpPacket.aidl +++ b/core/java/android/content/ICancelationSignal.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2012 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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package android.nfc; +package android.content; /** * @hide */ -parcelable LlcpPacket;
\ No newline at end of file +interface ICancelationSignal { + oneway void cancel(); +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 2a67ff8..f52157f 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -34,7 +34,8 @@ import java.util.ArrayList; */ public interface IContentProvider extends IInterface { public Cursor query(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder) throws RemoteException; + String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal) + throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; @@ -50,6 +51,7 @@ public interface IContentProvider extends IInterface { public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws RemoteException, OperationApplicationException; public Bundle call(String method, String arg, Bundle extras) throws RemoteException; + public ICancelationSignal createCancelationSignal() throws RemoteException; // Data interchange. public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException; @@ -71,4 +73,5 @@ public interface IContentProvider extends IInterface { static final int CALL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 20; static final int GET_STREAM_TYPES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 21; static final int OPEN_TYPED_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 22; + static final int CREATE_CANCELATION_SIGNAL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 23; } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e3b1f54..fbc1b2b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -43,6 +43,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; import java.util.Set; /** @@ -4420,22 +4421,24 @@ public class Intent implements Parcelable, Cloneable { /** * Set the data this intent is operating on. This method automatically - * clears any type that was previously set by {@link #setType}. + * clears any type that was previously set by {@link #setType} or + * {@link #setTypeAndNormalize}. * - * <p><em>Note: scheme and host name matching in the Android framework is - * case-sensitive, unlike the formal RFC. As a result, - * you should always ensure that you write your Uri with these elements - * using lower case letters, and normalize any Uris you receive from - * outside of Android to ensure the scheme and host is lower case.</em></p> + * <p><em>Note: scheme matching in the Android framework is + * case-sensitive, unlike the formal RFC. As a result, + * you should always write your Uri with a lower case scheme, + * or use {@link Uri#normalize} or + * {@link #setDataAndNormalize} + * to ensure that the scheme is converted to lower case.</em> * - * @param data The URI of the data this intent is now targeting. + * @param data The Uri of the data this intent is now targeting. * * @return Returns the same Intent object, for chaining multiple calls * into a single statement. * * @see #getData - * @see #setType - * @see #setDataAndType + * @see #setDataAndNormalize + * @see android.net.Intent#normalize */ public Intent setData(Uri data) { mData = data; @@ -4444,16 +4447,45 @@ public class Intent implements Parcelable, Cloneable { } /** - * Set an explicit MIME data type. This is used to create intents that - * only specify a type and not data, for example to indicate the type of - * data to return. This method automatically clears any data that was - * previously set by {@link #setData}. + * Normalize and set the data this intent is operating on. + * + * <p>This method automatically clears any type that was + * previously set (for example, by {@link #setType}). + * + * <p>The data Uri is normalized using + * {@link android.net.Uri#normalize} before it is set, + * so really this is just a convenience method for + * <pre> + * setData(data.normalize()) + * </pre> + * + * @param data The Uri of the data this intent is now targeting. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getData + * @see #setType + * @see android.net.Uri#normalize + */ + public Intent setDataAndNormalize(Uri data) { + return setData(data.normalize()); + } + + /** + * Set an explicit MIME data type. + * + * <p>This is used to create intents that only specify a type and not data, + * for example to indicate the type of data to return. + * + * <p>This method automatically clears any data that was + * previously set (for example by {@link #setData}). * * <p><em>Note: MIME type matching in the Android framework is * case-sensitive, unlike formal RFC MIME types. As a result, * you should always write your MIME types with lower case letters, - * and any MIME types you receive from outside of Android should be - * converted to lower case before supplying them here.</em></p> + * or use {@link #normalizeMimeType} or {@link #setTypeAndNormalize} + * to ensure that it is converted to lower case.</em> * * @param type The MIME type of the data being handled by this intent. * @@ -4461,8 +4493,9 @@ public class Intent implements Parcelable, Cloneable { * into a single statement. * * @see #getType - * @see #setData + * @see #setTypeAndNormalize * @see #setDataAndType + * @see #normalizeMimeType */ public Intent setType(String type) { mData = null; @@ -4471,26 +4504,58 @@ public class Intent implements Parcelable, Cloneable { } /** + * Normalize and set an explicit MIME data type. + * + * <p>This is used to create intents that only specify a type and not data, + * for example to indicate the type of data to return. + * + * <p>This method automatically clears any data that was + * previously set (for example by {@link #setData}). + * + * <p>The MIME type is normalized using + * {@link #normalizeMimeType} before it is set, + * so really this is just a convenience method for + * <pre> + * setType(Intent.normalizeMimeType(type)) + * </pre> + * + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getType + * @see #setData + * @see #normalizeMimeType + */ + public Intent setTypeAndNormalize(String type) { + return setType(normalizeMimeType(type)); + } + + /** * (Usually optional) Set the data for the intent along with an explicit * MIME data type. This method should very rarely be used -- it allows you * to override the MIME type that would ordinarily be inferred from the * data with your own type given here. * - * <p><em>Note: MIME type, Uri scheme, and host name matching in the + * <p><em>Note: MIME type and Uri scheme matching in the * Android framework is case-sensitive, unlike the formal RFC definitions. * As a result, you should always write these elements with lower case letters, - * and normalize any MIME types or Uris you receive from - * outside of Android to ensure these elements are lower case before - * supplying them here.</em></p> + * or use {@link #normalizeMimeType} or {@link android.net.Uri#normalize} or + * {@link #setDataAndTypeAndNormalize} + * to ensure that they are converted to lower case.</em> * - * @param data The URI of the data this intent is now targeting. + * @param data The Uri of the data this intent is now targeting. * @param type The MIME type of the data being handled by this intent. * * @return Returns the same Intent object, for chaining multiple calls * into a single statement. * - * @see #setData * @see #setType + * @see #setData + * @see #normalizeMimeType + * @see android.net.Uri#normalize + * @see #setDataAndTypeAndNormalize */ public Intent setDataAndType(Uri data, String type) { mData = data; @@ -4499,6 +4564,35 @@ public class Intent implements Parcelable, Cloneable { } /** + * (Usually optional) Normalize and set both the data Uri and an explicit + * MIME data type. This method should very rarely be used -- it allows you + * to override the MIME type that would ordinarily be inferred from the + * data with your own type given here. + * + * <p>The data Uri and the MIME type are normalize using + * {@link android.net.Uri#normalize} and {@link #normalizeMimeType} + * before they are set, so really this is just a convenience method for + * <pre> + * setDataAndType(data.normalize(), Intent.normalizeMimeType(type)) + * </pre> + * + * @param data The Uri of the data this intent is now targeting. + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setType + * @see #setData + * @see #setDataAndType + * @see #normalizeMimeType + * @see android.net.Uri#normalize + */ + public Intent setDataAndTypeAndNormalize(Uri data, String type) { + return setDataAndType(data.normalize(), normalizeMimeType(type)); + } + + /** * Add a new category to the intent. Categories provide additional detail * about the action the intent is perform. When resolving an intent, only * activities that provide <em>all</em> of the requested categories will be @@ -5566,7 +5660,7 @@ public class Intent implements Parcelable, Cloneable { * * <ul> * <li> action, as set by {@link #setAction}. - * <li> data URI and MIME type, as set by {@link #setData(Uri)}, + * <li> data Uri and MIME type, as set by {@link #setData(Uri)}, * {@link #setType(String)}, or {@link #setDataAndType(Uri, String)}. * <li> categories, as set by {@link #addCategory}. * <li> package, as set by {@link #setPackage}. @@ -6229,4 +6323,38 @@ public class Intent implements Parcelable, Cloneable { return intent; } + + /** + * Normalize a MIME data type. + * + * <p>A normalized MIME type has white-space trimmed, + * content-type parameters removed, and is lower-case. + * This aligns the type with Android best practices for + * intent filtering. + * + * <p>For example, "text/plain; charset=utf-8" becomes "text/plain". + * "text/x-vCard" becomes "text/x-vcard". + * + * <p>All MIME types received from outside Android (such as user input, + * or external sources like Bluetooth, NFC, or the Internet) should + * be normalized before they are used to create an Intent. + * + * @param type MIME data type to normalize + * @return normalized MIME data type, or null if the input was null + * @see {@link #setType} + * @see {@link #setTypeAndNormalize} + */ + public static String normalizeMimeType(String type) { + if (type == null) { + return null; + } + + type = type.trim().toLowerCase(Locale.US); + + final int semicolonIndex = type.indexOf(';'); + if (semicolonIndex != -1) { + type = type.substring(0, semicolonIndex); + } + return type; + } } diff --git a/core/java/android/content/OperationCanceledException.java b/core/java/android/content/OperationCanceledException.java new file mode 100644 index 0000000..24afcfa --- /dev/null +++ b/core/java/android/content/OperationCanceledException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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 android.content; + +/** + * An exception type that is thrown when an operation in progress is canceled. + * + * @see CancelationSignal + */ +public class OperationCanceledException extends RuntimeException { + public OperationCanceledException() { + this(null); + } + + public OperationCanceledException(String message) { + super(message != null ? message : "The operation has been canceled."); + } +} diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 3c4e545..ba24036 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -1034,6 +1034,7 @@ public class SyncManager implements OnAccountsUpdateListener { protected void dumpSyncState(PrintWriter pw) { pw.print("data connected: "); pw.println(mDataConnectionIsConnected); + pw.print("auto sync: "); pw.println(mSyncStorageEngine.getMasterSyncAutomatically()); pw.print("memory low: "); pw.println(mStorageIsLow); final Account[] accounts = mAccounts; @@ -1272,57 +1273,17 @@ public class SyncManager implements OnAccountsUpdateListener { } - pw.println(); - pw.printf("Detailed Statistics (Recent history): %d (# of times) %ds (sync time)\n", - totalTimes, totalElapsedTime / 1000); - - final List<AuthoritySyncStats> sortedAuthorities = - new ArrayList<AuthoritySyncStats>(authorityMap.values()); - Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() { - @Override - public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) { - // reverse order - int compare = Integer.compare(rhs.times, lhs.times); - if (compare == 0) { - compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime); - } - return compare; - } - }); - - final int maxLength = Math.max(maxAuthority, maxAccount + 3); - final int padLength = 2 + 2 + maxLength + 2 + 10 + 11; - final char chars[] = new char[padLength]; - Arrays.fill(chars, '-'); - final String separator = new String(chars); - - final String authorityFormat = String.format(" %%-%ds: %%-9s %%-11s\n", maxLength + 2); - final String accountFormat = String.format(" %%-%ds: %%-9s %%-11s\n", maxLength); - - pw.println(separator); - for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) { - String name = authoritySyncStats.name; - long elapsedTime; - int times; - String timeStr; - String timesStr; - - elapsedTime = authoritySyncStats.elapsedTime; - times = authoritySyncStats.times; - timeStr = String.format("%ds/%d%%", - elapsedTime / 1000, - elapsedTime * 100 / totalElapsedTime); - timesStr = String.format("%d/%d%%", - times, - times * 100 / totalTimes); - pw.printf(authorityFormat, name, timesStr, timeStr); - - final List<AccountSyncStats> sortedAccounts = - new ArrayList<AccountSyncStats>( - authoritySyncStats.accountMap.values()); - Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() { + if (totalElapsedTime > 0) { + pw.println(); + pw.printf("Detailed Statistics (Recent history): " + + "%d (# of times) %ds (sync time)\n", + totalTimes, totalElapsedTime / 1000); + + final List<AuthoritySyncStats> sortedAuthorities = + new ArrayList<AuthoritySyncStats>(authorityMap.values()); + Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() { @Override - public int compare(AccountSyncStats lhs, AccountSyncStats rhs) { + public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) { // reverse order int compare = Integer.compare(rhs.times, lhs.times); if (compare == 0) { @@ -1331,18 +1292,63 @@ public class SyncManager implements OnAccountsUpdateListener { return compare; } }); - for (AccountSyncStats stats: sortedAccounts) { - elapsedTime = stats.elapsedTime; - times = stats.times; + + final int maxLength = Math.max(maxAuthority, maxAccount + 3); + final int padLength = 2 + 2 + maxLength + 2 + 10 + 11; + final char chars[] = new char[padLength]; + Arrays.fill(chars, '-'); + final String separator = new String(chars); + + final String authorityFormat = + String.format(" %%-%ds: %%-9s %%-11s\n", maxLength + 2); + final String accountFormat = + String.format(" %%-%ds: %%-9s %%-11s\n", maxLength); + + pw.println(separator); + for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) { + String name = authoritySyncStats.name; + long elapsedTime; + int times; + String timeStr; + String timesStr; + + elapsedTime = authoritySyncStats.elapsedTime; + times = authoritySyncStats.times; timeStr = String.format("%ds/%d%%", elapsedTime / 1000, elapsedTime * 100 / totalElapsedTime); timesStr = String.format("%d/%d%%", times, times * 100 / totalTimes); - pw.printf(accountFormat, stats.name, timesStr, timeStr); + pw.printf(authorityFormat, name, timesStr, timeStr); + + final List<AccountSyncStats> sortedAccounts = + new ArrayList<AccountSyncStats>( + authoritySyncStats.accountMap.values()); + Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() { + @Override + public int compare(AccountSyncStats lhs, AccountSyncStats rhs) { + // reverse order + int compare = Integer.compare(rhs.times, lhs.times); + if (compare == 0) { + compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime); + } + return compare; + } + }); + for (AccountSyncStats stats: sortedAccounts) { + elapsedTime = stats.elapsedTime; + times = stats.times; + timeStr = String.format("%ds/%d%%", + elapsedTime / 1000, + elapsedTime * 100 / totalElapsedTime); + timesStr = String.format("%d/%d%%", + times, + times * 100 / totalTimes); + pw.printf(accountFormat, stats.name, timesStr, timeStr); + } + pw.println(separator); } - pw.println(separator); } pw.println(); diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index bcd599b..e3749b4 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -34,8 +34,8 @@ import java.util.Comparator; */ public class ResolveInfo implements Parcelable { /** - * The activity that corresponds to this resolution match, if this - * resolution is for an activity. One and only one of this and + * The activity or broadcast receiver that corresponds to this resolution match, + * if this resolution is for an activity or broadcast receiver. One and only one of this and * serviceInfo must be non-null. */ public ActivityInfo activityInfo; diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 5c3a17a..6015668 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -228,6 +228,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration public static final int UI_MODE_TYPE_DESK = 0x02; public static final int UI_MODE_TYPE_CAR = 0x03; public static final int UI_MODE_TYPE_TELEVISION = 0x04; + public static final int UI_MODE_TYPE_APPLIANCE = 0x05; public static final int UI_MODE_NIGHT_MASK = 0x30; public static final int UI_MODE_NIGHT_UNDEFINED = 0x00; @@ -239,7 +240,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration * <p>The {@link #UI_MODE_TYPE_MASK} bits define the overall ui mode of the * device. They may be one of {@link #UI_MODE_TYPE_UNDEFINED}, * {@link #UI_MODE_TYPE_NORMAL}, {@link #UI_MODE_TYPE_DESK}, - * or {@link #UI_MODE_TYPE_CAR}. + * {@link #UI_MODE_TYPE_CAR}, {@link #UI_MODE_TYPE_TELEVISION}, or + * {@link #UI_MODE_TYPE_APPLIANCE}. * * <p>The {@link #UI_MODE_NIGHT_MASK} defines whether the screen * is in a special mode. They may be one of {@link #UI_MODE_NIGHT_UNDEFINED}, @@ -391,6 +393,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration case UI_MODE_TYPE_DESK: sb.append(" desk"); break; case UI_MODE_TYPE_CAR: sb.append(" car"); break; case UI_MODE_TYPE_TELEVISION: sb.append(" television"); break; + case UI_MODE_TYPE_APPLIANCE: sb.append(" appliance"); break; default: sb.append(" uimode="); sb.append(uiMode&UI_MODE_TYPE_MASK); break; } switch ((uiMode&UI_MODE_NIGHT_MASK)) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index b6b6a8d..2af58be 100755 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -1887,8 +1887,7 @@ public class Resources { if (cs != null) { dr = cs.newDrawable(this); } else { - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && - value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + if (isColorDrawable) { dr = new ColorDrawable(value.data); } diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 74fef29..b28ed8d 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -284,23 +284,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } - /** - * This is hidden until the data set change model has been re-evaluated. - * @hide - */ - protected void notifyDataSetChange() { - mDataSetObservable.notifyChanged(); - } - - /** - * This is hidden until the data set change model has been re-evaluated. - * @hide - */ - protected DataSetObservable getDataSetObservable() { - return mDataSetObservable; - - } - public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); } @@ -317,7 +300,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { */ protected void onChange(boolean selfChange) { synchronized (mSelfObserverLock) { - mContentObservable.dispatchChange(selfChange); + mContentObservable.dispatchChange(selfChange, null); if (mNotifyUri != null && selfChange) { mContentResolver.notifyChange(mNotifyUri, mSelfObserver); } diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index 20a9c67..67cf0f8 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -180,13 +180,13 @@ final class BulkCursorProxy implements IBulkCursor { return mRemote; } - public CursorWindow getWindow(int startPos) throws RemoteException + public CursorWindow getWindow(int position) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IBulkCursor.descriptor); - data.writeInt(startPos); + data.writeInt(position); mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); diff --git a/core/java/android/database/ContentObservable.java b/core/java/android/database/ContentObservable.java index 8d7b7c5..7692bb3 100644 --- a/core/java/android/database/ContentObservable.java +++ b/core/java/android/database/ContentObservable.java @@ -16,40 +16,75 @@ package android.database; +import android.net.Uri; + /** - * A specialization of Observable for ContentObserver that provides methods for - * invoking the various callback methods of ContentObserver. + * A specialization of {@link Observable} for {@link ContentObserver} + * that provides methods for sending notifications to a list of + * {@link ContentObserver} objects. */ public class ContentObservable extends Observable<ContentObserver> { - + // Even though the generic method defined in Observable would be perfectly + // fine on its own, we can't delete this overridden method because it would + // potentially break binary compatibility with existing applications. @Override public void registerObserver(ContentObserver observer) { super.registerObserver(observer); } /** - * invokes dispatchUpdate on each observer, unless the observer doesn't want - * self-notifications and the update is from a self-notification - * @param selfChange + * Invokes {@link ContentObserver#dispatchChange(boolean)} on each observer. + * <p> + * If <code>selfChange</code> is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + * </p> + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead. */ + @Deprecated public void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + + /** + * Invokes {@link ContentObserver#dispatchChange(boolean, Uri)} on each observer. + * Includes the changed content Uri when available. + * <p> + * If <code>selfChange</code> is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public void dispatchChange(boolean selfChange, Uri uri) { synchronized(mObservers) { for (ContentObserver observer : mObservers) { if (!selfChange || observer.deliverSelfNotifications()) { - observer.dispatchChange(selfChange); + observer.dispatchChange(selfChange, uri); } } } } /** - * invokes onChange on each observer - * @param selfChange + * Invokes {@link ContentObserver#onChange} on each observer. + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange} instead. */ + @Deprecated public void notifyChange(boolean selfChange) { synchronized(mObservers) { for (ContentObserver observer : mObservers) { - observer.onChange(selfChange); + observer.onChange(selfChange, null); } } } diff --git a/core/java/android/database/ContentObserver.java b/core/java/android/database/ContentObserver.java index 3b829a3..e4fbc28 100644 --- a/core/java/android/database/ContentObserver.java +++ b/core/java/android/database/ContentObserver.java @@ -16,65 +16,23 @@ package android.database; +import android.net.Uri; import android.os.Handler; /** - * Receives call backs for changes to content. Must be implemented by objects which are added - * to a {@link ContentObservable}. + * Receives call backs for changes to content. + * Must be implemented by objects which are added to a {@link ContentObservable}. */ public abstract class ContentObserver { + private final Object mLock = new Object(); + private Transport mTransport; // guarded by mLock - private Transport mTransport; - - // Protects mTransport - private Object lock = new Object(); - - /* package */ Handler mHandler; - - private final class NotificationRunnable implements Runnable { - - private boolean mSelf; - - public NotificationRunnable(boolean self) { - mSelf = self; - } - - public void run() { - ContentObserver.this.onChange(mSelf); - } - } - - private static final class Transport extends IContentObserver.Stub { - ContentObserver mContentObserver; - - public Transport(ContentObserver contentObserver) { - mContentObserver = contentObserver; - } - - public boolean deliverSelfNotifications() { - ContentObserver contentObserver = mContentObserver; - if (contentObserver != null) { - return contentObserver.deliverSelfNotifications(); - } - return false; - } - - public void onChange(boolean selfChange) { - ContentObserver contentObserver = mContentObserver; - if (contentObserver != null) { - contentObserver.dispatchChange(selfChange); - } - } - - public void releaseContentObserver() { - mContentObserver = null; - } - } + Handler mHandler; /** - * onChange() will happen on the provider Handler. + * Creates a content observer. * - * @param handler The handler to run {@link #onChange} on. + * @param handler The handler to run {@link #onChange} on, or null if none. */ public ContentObserver(Handler handler) { mHandler = handler; @@ -86,7 +44,7 @@ public abstract class ContentObserver { * {@hide} */ public IContentObserver getContentObserver() { - synchronized(lock) { + synchronized (mLock) { if (mTransport == null) { mTransport = new Transport(this); } @@ -101,8 +59,8 @@ public abstract class ContentObserver { * {@hide} */ public IContentObserver releaseContentObserver() { - synchronized(lock) { - Transport oldTransport = mTransport; + synchronized (mLock) { + final Transport oldTransport = mTransport; if (oldTransport != null) { oldTransport.releaseContentObserver(); mTransport = null; @@ -112,27 +70,134 @@ public abstract class ContentObserver { } /** - * Returns true if this observer is interested in notifications for changes - * made through the cursor the observer is registered with. + * Returns true if this observer is interested receiving self-change notifications. + * + * Subclasses should override this method to indicate whether the observer + * is interested in receiving notifications for changes that it made to the + * content itself. + * + * @return True if self-change notifications should be delivered to the observer. */ public boolean deliverSelfNotifications() { return false; } /** - * This method is called when a change occurs to the cursor that - * is being observed. - * - * @param selfChange true if the update was caused by a call to <code>commit</code> on the - * cursor that is being observed. + * This method is called when a content change occurs. + * <p> + * Subclasses should override this method to handle content changes. + * </p> + * + * @param selfChange True if this is a self-change notification. */ - public void onChange(boolean selfChange) {} + public void onChange(boolean selfChange) { + // Do nothing. Subclass should override. + } + /** + * This method is called when a content change occurs. + * Includes the changed content Uri when available. + * <p> + * Subclasses should override this method to handle content changes. + * To ensure correct operation on older versions of the framework that + * did not provide a Uri argument, applications should also implement + * the {@link #onChange(boolean)} overload of this method whenever they + * implement the {@link #onChange(boolean, Uri)} overload. + * </p><p> + * Example implementation: + * <pre><code> + * // Implement the onChange(boolean) method to delegate the change notification to + * // the onChange(boolean, Uri) method to ensure correct operation on older versions + * // of the framework that did not have the onChange(boolean, Uri) method. + * {@literal @Override} + * public void onChange(boolean selfChange) { + * onChange(selfChange, null); + * } + * + * // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument. + * {@literal @Override} + * public void onChange(boolean selfChange, Uri uri) { + * // Handle change. + * } + * </code></pre> + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public void onChange(boolean selfChange, Uri uri) { + onChange(selfChange); + } + + /** + * Dispatches a change notification to the observer. + * <p> + * If a {@link Handler} was supplied to the {@link ContentObserver} constructor, + * then a call to the {@link #onChange} method is posted to the handler's message queue. + * Otherwise, the {@link #onChange} method is invoked immediately on this thread. + * </p> + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead. + */ + @Deprecated public final void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + + /** + * Dispatches a change notification to the observer. + * Includes the changed content Uri when available. + * <p> + * If a {@link Handler} was supplied to the {@link ContentObserver} constructor, + * then a call to the {@link #onChange} method is posted to the handler's message queue. + * Otherwise, the {@link #onChange} method is invoked immediately on this thread. + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public final void dispatchChange(boolean selfChange, Uri uri) { if (mHandler == null) { - onChange(selfChange); + onChange(selfChange, uri); } else { - mHandler.post(new NotificationRunnable(selfChange)); + mHandler.post(new NotificationRunnable(selfChange, uri)); + } + } + + private final class NotificationRunnable implements Runnable { + private final boolean mSelfChange; + private final Uri mUri; + + public NotificationRunnable(boolean selfChange, Uri uri) { + mSelfChange = selfChange; + mUri = uri; + } + + @Override + public void run() { + ContentObserver.this.onChange(mSelfChange, mUri); + } + } + + private static final class Transport extends IContentObserver.Stub { + private ContentObserver mContentObserver; + + public Transport(ContentObserver contentObserver) { + mContentObserver = contentObserver; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + ContentObserver contentObserver = mContentObserver; + if (contentObserver != null) { + contentObserver.dispatchChange(selfChange, uri); + } + } + + public void releaseContentObserver() { + mContentObserver = null; } } } diff --git a/core/java/android/database/CrossProcessCursorWrapper.java b/core/java/android/database/CrossProcessCursorWrapper.java index 8c250b8..1b77cb9 100644 --- a/core/java/android/database/CrossProcessCursorWrapper.java +++ b/core/java/android/database/CrossProcessCursorWrapper.java @@ -24,10 +24,10 @@ import android.database.CursorWrapper; /** * Cursor wrapper that implements {@link CrossProcessCursor}. * <p> - * If the wrapper cursor implemented {@link CrossProcessCursor}, then delegates - * {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it. Otherwise, - * provides default implementations of these methods that traverse the contents - * of the cursor similar to {@link AbstractCursor#fillWindow}. + * If the wrapped cursor implements {@link CrossProcessCursor}, then the wrapper + * delegates {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it. + * Otherwise, the wrapper provides default implementations of these methods that + * traverse the contents of the cursor similar to {@link AbstractCursor#fillWindow}. * </p><p> * This wrapper can be used to adapt an ordinary {@link Cursor} into a * {@link CrossProcessCursor}. diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index a9a71cf..59ec89d 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -341,6 +341,7 @@ public interface Cursor { * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. * Inactive Cursors use fewer resources than active Cursors. * Calling {@link #requery} will make the cursor active again. + * @deprecated Since {@link #requery()} is deprecated, so too is this. */ void deactivate(); diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 215035d..167278a 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -16,6 +16,7 @@ package android.database; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -78,9 +79,9 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } @Override - public void onChange(boolean selfChange) { + public void onChange(boolean selfChange, Uri uri) { try { - mRemote.onChange(selfChange); + mRemote.onChange(selfChange, uri); } catch (RemoteException ex) { // Do nothing, the far side is dead } @@ -132,11 +133,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } @Override - public CursorWindow getWindow(int startPos) { + public CursorWindow getWindow(int position) { synchronized (mLock) { throwIfCursorIsClosed(); - if (!mCursor.moveToPosition(startPos)) { + if (!mCursor.moveToPosition(position)) { closeFilledWindowLocked(); return null; } @@ -149,12 +150,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative if (window == null) { mFilledWindow = new CursorWindow(mProviderName); window = mFilledWindow; - mCursor.fillWindow(startPos, window); - } else if (startPos < window.getStartPosition() - || startPos >= window.getStartPosition() + window.getNumRows()) { + } else if (position < window.getStartPosition() + || position >= window.getStartPosition() + window.getNumRows()) { window.clear(); - mCursor.fillWindow(startPos, window); } + mCursor.fillWindow(position, window); } // Acquire a reference before returning from this RPC. diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index e9675e8..85f570c 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -98,8 +98,8 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { */ public CursorWindow(String name) { mStartPos = 0; - mName = name; - mWindowPtr = nativeCreate(name, sCursorWindowSize); + mName = name != null && name.length() != 0 ? name : "<unnamed>"; + mWindowPtr = nativeCreate(mName, sCursorWindowSize); if (mWindowPtr == 0) { throw new CursorWindowAllocationException("Cursor window allocation of " + (sCursorWindowSize / 1024) + " kb failed. " + printStats()); @@ -161,7 +161,7 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { } /** - * Gets the name of this cursor window. + * Gets the name of this cursor window, never null. * @hide */ public String getName() { diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java index 51c72c1..ca77a13 100644 --- a/core/java/android/database/DataSetObservable.java +++ b/core/java/android/database/DataSetObservable.java @@ -17,13 +17,15 @@ package android.database; /** - * A specialization of Observable for DataSetObserver that provides methods for - * invoking the various callback methods of DataSetObserver. + * A specialization of {@link Observable} for {@link DataSetObserver} + * that provides methods for sending notifications to a list of + * {@link DataSetObserver} objects. */ public class DataSetObservable extends Observable<DataSetObserver> { /** - * Invokes onChanged on each observer. Called when the data set being observed has - * changed, and which when read contains the new state of the data. + * Invokes {@link DataSetObserver#onChanged} on each observer. + * Called when the contents of the data set have changed. The recipient + * will obtain the new contents the next time it queries the data set. */ public void notifyChanged() { synchronized(mObservers) { @@ -38,8 +40,9 @@ public class DataSetObservable extends Observable<DataSetObserver> { } /** - * Invokes onInvalidated on each observer. Called when the data set being monitored - * has changed such that it is no longer valid. + * Invokes {@link DataSetObserver#onInvalidated} on each observer. + * Called when the data set is no longer valid and cannot be queried again, + * such as when the data set has been closed. */ public void notifyInvalidated() { synchronized (mObservers) { diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 01bcdf7..b69d9bf 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -727,6 +727,32 @@ public class DatabaseUtils { } /** + * Picks a start position for {@link Cursor#fillWindow} such that the + * window will contain the requested row and a useful range of rows + * around it. + * + * When the data set is too large to fit in a cursor window, seeking the + * cursor can become a very expensive operation since we have to run the + * query again when we move outside the bounds of the current window. + * + * We try to choose a start position for the cursor window such that + * 1/3 of the window's capacity is used to hold rows before the requested + * position and 2/3 of the window's capacity is used to hold rows after the + * requested position. + * + * @param cursorPosition The row index of the row we want to get. + * @param cursorWindowCapacity The estimated number of rows that can fit in + * a cursor window, or 0 if unknown. + * @return The recommended start position, always less than or equal to + * the requested row. + * @hide + */ + public static int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + + /** * Query the table for the number of rows in the table. * @param db the database the table is in * @param table the name of the table to query diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 7c96797..0f4500a 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -30,11 +30,17 @@ import android.os.RemoteException; */ public interface IBulkCursor extends IInterface { /** - * Returns a BulkCursorWindow, which either has a reference to a shared - * memory segment with the rows, or an array of JSON strings. + * Gets a cursor window that contains the specified position. + * The window will contain a range of rows around the specified position. */ - public CursorWindow getWindow(int startPos) throws RemoteException; + public CursorWindow getWindow(int position) throws RemoteException; + /** + * Notifies the cursor that the position has changed. + * Only called when {@link #getWantsAllOnMoveCalls()} returns true. + * + * @param position The new position + */ public void onMove(int position) throws RemoteException; /** diff --git a/core/java/android/database/IContentObserver.aidl b/core/java/android/database/IContentObserver.aidl index ac2f975..13aff05 100755 --- a/core/java/android/database/IContentObserver.aidl +++ b/core/java/android/database/IContentObserver.aidl @@ -17,6 +17,8 @@ package android.database; +import android.net.Uri; + /** * @hide */ @@ -27,5 +29,5 @@ interface IContentObserver * observed. selfUpdate is true if the update was caused by a call to * commit on the cursor that is being observed. */ - oneway void onChange(boolean selfUpdate); + oneway void onChange(boolean selfUpdate, in Uri uri); } diff --git a/core/java/android/database/Observable.java b/core/java/android/database/Observable.java index b6fecab..aff32db 100644 --- a/core/java/android/database/Observable.java +++ b/core/java/android/database/Observable.java @@ -19,7 +19,12 @@ package android.database; import java.util.ArrayList; /** - * Provides methods for (un)registering arbitrary observers in an ArrayList. + * Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}. + * + * This abstract class is intended to be subclassed and specialized to maintain + * a registry of observers of specific types and dispatch notifications to them. + * + * @param T The observer type. */ public abstract class Observable<T> { /** @@ -66,13 +71,13 @@ public abstract class Observable<T> { mObservers.remove(index); } } - + /** - * Remove all registered observer + * Remove all registered observers. */ public void unregisterAll() { synchronized(mObservers) { mObservers.clear(); - } + } } } diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java deleted file mode 100644 index 39a9d23..0000000 --- a/core/java/android/database/sqlite/DatabaseConnectionPool.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (C) 20010 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 android.database.sqlite; - -import android.content.res.Resources; -import android.os.SystemClock; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Random; - -/** - * A connection pool to be used by readers. - * Note that each connection can be used by only one reader at a time. - */ -/* package */ class DatabaseConnectionPool { - - private static final String TAG = "DatabaseConnectionPool"; - - /** The default connection pool size. */ - private volatile int mMaxPoolSize = - Resources.getSystem().getInteger(com.android.internal.R.integer.db_connection_pool_size); - - /** The connection pool objects are stored in this member. - * TODO: revisit this data struct as the number of pooled connections increase beyond - * single-digit values. - */ - private final ArrayList<PoolObj> mPool = new ArrayList<PoolObj>(mMaxPoolSize); - - /** the main database connection to which this connection pool is attached */ - private final SQLiteDatabase mParentDbObj; - - /** Random number generator used to pick a free connection out of the pool */ - private Random rand; // lazily initialized - - /* package */ DatabaseConnectionPool(SQLiteDatabase db) { - this.mParentDbObj = db; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Max Pool Size: " + mMaxPoolSize); - } - } - - /** - * close all database connections in the pool - even if they are in use! - */ - /* package */ synchronized void close() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Closing the connection pool on " + mParentDbObj.getPath() + toString()); - } - for (int i = mPool.size() - 1; i >= 0; i--) { - mPool.get(i).mDb.close(); - } - mPool.clear(); - } - - /** - * get a free connection from the pool - * - * @param sql if not null, try to find a connection inthe pool which already has cached - * the compiled statement for this sql. - * @return the Database connection that the caller can use - */ - /* package */ synchronized SQLiteDatabase get(String sql) { - SQLiteDatabase db = null; - PoolObj poolObj = null; - int poolSize = mPool.size(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert sql != null; - doAsserts(); - } - if (getFreePoolSize() == 0) { - // no free ( = available) connections - if (mMaxPoolSize == poolSize) { - // maxed out. can't open any more connections. - // let the caller wait on one of the pooled connections - // preferably a connection caching the pre-compiled statement of the given SQL - if (mMaxPoolSize == 1) { - poolObj = mPool.get(0); - } else { - for (int i = 0; i < mMaxPoolSize; i++) { - if (mPool.get(i).mDb.isInStatementCache(sql)) { - poolObj = mPool.get(i); - break; - } - } - if (poolObj == null) { - // there are no database connections with the given SQL pre-compiled. - // ok to return any of the connections. - if (rand == null) { - rand = new Random(SystemClock.elapsedRealtime()); - } - poolObj = mPool.get(rand.nextInt(mMaxPoolSize)); - } - } - db = poolObj.mDb; - } else { - // create a new connection and add it to the pool, since we haven't reached - // max pool size allowed - db = mParentDbObj.createPoolConnection((short)(poolSize + 1)); - poolObj = new PoolObj(db); - mPool.add(poolSize, poolObj); - } - } else { - // there are free connections available. pick one - // preferably a connection caching the pre-compiled statement of the given SQL - for (int i = 0; i < poolSize; i++) { - if (mPool.get(i).isFree() && mPool.get(i).mDb.isInStatementCache(sql)) { - poolObj = mPool.get(i); - break; - } - } - if (poolObj == null) { - // didn't find a free database connection with the given SQL already - // pre-compiled. return a free connection (this means, the same SQL could be - // pre-compiled on more than one database connection. potential wasted memory.) - for (int i = 0; i < poolSize; i++) { - if (mPool.get(i).isFree()) { - poolObj = mPool.get(i); - break; - } - } - } - db = poolObj.mDb; - } - - assert poolObj != null; - assert poolObj.mDb == db; - - poolObj.acquire(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "END get-connection: " + toString() + poolObj.toString()); - } - return db; - // TODO if a thread acquires a connection and dies without releasing the connection, then - // there could be a connection leak. - } - - /** - * release the given database connection back to the pool. - * @param db the connection to be released - */ - /* package */ synchronized void release(SQLiteDatabase db) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert db.mConnectionNum > 0; - doAsserts(); - assert mPool.get(db.mConnectionNum - 1).mDb == db; - } - - PoolObj poolObj = mPool.get(db.mConnectionNum - 1); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "BEGIN release-conn: " + toString() + poolObj.toString()); - } - - if (poolObj.isFree()) { - throw new IllegalStateException("Releasing object already freed: " + - db.mConnectionNum); - } - - poolObj.release(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "END release-conn: " + toString() + poolObj.toString()); - } - } - - /** - * Returns a list of all database connections in the pool (both free and busy connections). - * This method is used when "adb bugreport" is done. - */ - /* package */ synchronized ArrayList<SQLiteDatabase> getConnectionList() { - ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>(); - for (int i = mPool.size() - 1; i >= 0; i--) { - list.add(mPool.get(i).mDb); - } - return list; - } - - /** - * package level access for testing purposes only. otherwise, private should be sufficient. - */ - /* package */ int getFreePoolSize() { - int count = 0; - for (int i = mPool.size() - 1; i >= 0; i--) { - if (mPool.get(i).isFree()) { - count++; - } - } - return count++; - } - - /** - * only for testing purposes - */ - /* package */ ArrayList<PoolObj> getPool() { - return mPool; - } - - @Override - public String toString() { - StringBuilder buff = new StringBuilder(); - buff.append("db: "); - buff.append(mParentDbObj.getPath()); - buff.append(", totalsize = "); - buff.append(mPool.size()); - buff.append(", #free = "); - buff.append(getFreePoolSize()); - buff.append(", maxpoolsize = "); - buff.append(mMaxPoolSize); - for (PoolObj p : mPool) { - buff.append("\n"); - buff.append(p.toString()); - } - return buff.toString(); - } - - private void doAsserts() { - for (int i = 0; i < mPool.size(); i++) { - mPool.get(i).verify(); - assert mPool.get(i).mDb.mConnectionNum == (i + 1); - } - } - - /** only used for testing purposes. */ - /* package */ synchronized void setMaxPoolSize(int size) { - mMaxPoolSize = size; - } - - /** only used for testing purposes. */ - /* package */ synchronized int getMaxPoolSize() { - return mMaxPoolSize; - } - - /** only used for testing purposes. */ - /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) { - return mPool.get(db.mConnectionNum - 1).isFree(); - } - - /** only used for testing purposes. */ - /* package */ int getSize() { - return mPool.size(); - } - - /** - * represents objects in the connection pool. - * package-level access for testing purposes only. - */ - /* package */ static class PoolObj { - - private final SQLiteDatabase mDb; - private boolean mFreeBusyFlag = FREE; - private static final boolean FREE = true; - private static final boolean BUSY = false; - - /** the number of threads holding this connection */ - // @GuardedBy("this") - private int mNumHolders = 0; - - /** contains the threadIds of the threads holding this connection. - * used for debugging purposes only. - */ - // @GuardedBy("this") - private HashSet<Long> mHolderIds = new HashSet<Long>(); - - public PoolObj(SQLiteDatabase db) { - mDb = db; - } - - private synchronized void acquire() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert isFree(); - long id = Thread.currentThread().getId(); - assert !mHolderIds.contains(id); - mHolderIds.add(id); - } - - mNumHolders++; - mFreeBusyFlag = BUSY; - } - - private synchronized void release() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - long id = Thread.currentThread().getId(); - assert mHolderIds.size() == mNumHolders; - assert mHolderIds.contains(id); - mHolderIds.remove(id); - } - - mNumHolders--; - if (mNumHolders == 0) { - mFreeBusyFlag = FREE; - } - } - - private synchronized boolean isFree() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - verify(); - } - return (mFreeBusyFlag == FREE); - } - - private synchronized void verify() { - if (mFreeBusyFlag == FREE) { - assert mNumHolders == 0; - } else { - assert mNumHolders > 0; - } - } - - /** - * only for testing purposes - */ - /* package */ synchronized int getNumHolders() { - return mNumHolders; - } - - @Override - public String toString() { - StringBuilder buff = new StringBuilder(); - buff.append(", conn # "); - buff.append(mDb.mConnectionNum); - buff.append(", mCountHolders = "); - synchronized(this) { - buff.append(mNumHolders); - buff.append(", freeBusyFlag = "); - buff.append(mFreeBusyFlag); - for (Long l : mHolderIds) { - buff.append(", id = " + l); - } - } - return buff.toString(); - } - } -} diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java index 01e9fb3..7e91a7b 100644 --- a/core/java/android/database/sqlite/SQLiteClosable.java +++ b/core/java/android/database/sqlite/SQLiteClosable.java @@ -16,8 +16,6 @@ package android.database.sqlite; -import android.database.CursorWindow; - /** * An object created from a SQLiteDatabase that can be closed. */ @@ -31,7 +29,7 @@ public abstract class SQLiteClosable { synchronized(this) { if (mReferenceCount <= 0) { throw new IllegalStateException( - "attempt to re-open an already-closed object: " + getObjInfo()); + "attempt to re-open an already-closed object: " + this); } mReferenceCount++; } @@ -56,22 +54,4 @@ public abstract class SQLiteClosable { onAllReferencesReleasedFromContainer(); } } - - private String getObjInfo() { - StringBuilder buff = new StringBuilder(); - buff.append(this.getClass().getName()); - buff.append(" ("); - if (this instanceof SQLiteDatabase) { - buff.append("database = "); - buff.append(((SQLiteDatabase)this).getPath()); - } else if (this instanceof SQLiteProgram) { - buff.append("mSql = "); - buff.append(((SQLiteProgram)this).mSql); - } else if (this instanceof CursorWindow) { - buff.append("mStartPos = "); - buff.append(((CursorWindow)this).getStartPosition()); - } - buff.append(") "); - return buff.toString(); - } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java deleted file mode 100644 index dafbc79..0000000 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2009 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 android.database.sqlite; - -import android.os.StrictMode; -import android.util.Log; - -/** - * This class encapsulates compilation of sql statement and release of the compiled statement obj. - * Once a sql statement is compiled, it is cached in {@link SQLiteDatabase} - * and it is released in one of the 2 following ways - * 1. when {@link SQLiteDatabase} object is closed. - * 2. if this is not cached in {@link SQLiteDatabase}, {@link android.database.Cursor#close()} - * releaases this obj. - */ -/* package */ class SQLiteCompiledSql { - - private static final String TAG = "SQLiteCompiledSql"; - - /** The database this program is compiled against. */ - /* package */ final SQLiteDatabase mDatabase; - - /** - * Native linkage, do not modify. This comes from the database. - */ - /* package */ final int nHandle; - - /** - * Native linkage, do not modify. When non-0 this holds a reference to a valid - * sqlite3_statement object. It is only updated by the native code, but may be - * checked in this class when the database lock is held to determine if there - * is a valid native-side program or not. - */ - /* package */ int nStatement = 0; - - /** the following are for debugging purposes */ - private String mSqlStmt = null; - private final Throwable mStackTrace; - - /** when in cache and is in use, this member is set */ - private boolean mInUse = false; - - /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { - db.verifyDbIsOpen(); - db.verifyLockOwner(); - mDatabase = db; - mSqlStmt = sql; - if (StrictMode.vmSqliteObjectLeaksEnabled()) { - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - } else { - mStackTrace = null; - } - nHandle = db.mNativeHandle; - native_compile(sql); - } - - /* package */ void releaseSqlStatement() { - // Note that native_finalize() checks to make sure that nStatement is - // non-null before destroying it. - if (nStatement != 0) { - mDatabase.finalizeStatementLater(nStatement); - nStatement = 0; - } - } - - /** - * returns true if acquire() succeeds. false otherwise. - */ - /* package */ synchronized boolean acquire() { - if (mInUse) { - // it is already in use. - return false; - } - mInUse = true; - return true; - } - - /* package */ synchronized void release() { - mInUse = false; - } - - /* package */ synchronized void releaseIfNotInUse() { - // if it is not in use, release its memory from the database - if (!mInUse) { - releaseSqlStatement(); - } - } - - /** - * Make sure that the native resource is cleaned up. - */ - @Override - protected void finalize() throws Throwable { - try { - if (nStatement == 0) return; - // don't worry about finalizing this object if it is ALREADY in the - // queue of statements to be finalized later - if (mDatabase.isInQueueOfStatementsToBeFinalized(nStatement)) { - return; - } - // finalizer should NEVER get called - // but if the database itself is not closed and is GC'ed, then - // all sub-objects attached to the database could end up getting GC'ed too. - // in that case, don't print any warning. - if (mInUse && mStackTrace != null) { - int len = mSqlStmt.length(); - StrictMode.onSqliteObjectLeaked( - "Releasing statement in a finalizer. Please ensure " + - "that you explicitly call close() on your cursor: " + - mSqlStmt.substring(0, (len > 1000) ? 1000 : len), - mStackTrace); - } - releaseSqlStatement(); - } finally { - super.finalize(); - } - } - - @Override public String toString() { - synchronized(this) { - StringBuilder buff = new StringBuilder(); - buff.append(" nStatement="); - buff.append(nStatement); - buff.append(", mInUse="); - buff.append(mInUse); - buff.append(", db="); - buff.append(mDatabase.getPath()); - buff.append(", db_connectionNum="); - buff.append(mDatabase.mConnectionNum); - buff.append(", sql="); - int len = mSqlStmt.length(); - buff.append(mSqlStmt.substring(0, (len > 100) ? 100 : len)); - return buff.toString(); - } - } - - /** - * Compiles SQL into a SQLite program. - * - * <P>The database lock must be held when calling this method. - * @param sql The SQL to compile. - */ - private final native void native_compile(String sql); -} diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java new file mode 100644 index 0000000..710bd53 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -0,0 +1,1315 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +import dalvik.system.BlockGuard; +import dalvik.system.CloseGuard; + +import android.content.CancelationSignal; +import android.content.OperationCanceledException; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import android.util.LruCache; +import android.util.Printer; + +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Represents a SQLite database connection. + * Each connection wraps an instance of a native <code>sqlite3</code> object. + * <p> + * When database connection pooling is enabled, there can be multiple active + * connections to the same database. Otherwise there is typically only one + * connection per database. + * </p><p> + * When the SQLite WAL feature is enabled, multiple readers and one writer + * can concurrently access the database. Without WAL, readers and writers + * are mutually exclusive. + * </p> + * + * <h2>Ownership and concurrency guarantees</h2> + * <p> + * Connection objects are not thread-safe. They are acquired as needed to + * perform a database operation and are then returned to the pool. At any + * given time, a connection is either owned and used by a {@link SQLiteSession} + * object or the {@link SQLiteConnectionPool}. Those classes are + * responsible for serializing operations to guard against concurrent + * use of a connection. + * </p><p> + * The guarantee of having a single owner allows this class to be implemented + * without locks and greatly simplifies resource management. + * </p> + * + * <h2>Encapsulation guarantees</h2> + * <p> + * The connection object object owns *all* of the SQLite related native + * objects that are associated with the connection. What's more, there are + * no other objects in the system that are capable of obtaining handles to + * those native objects. Consequently, when the connection is closed, we do + * not have to worry about what other components might have references to + * its associated SQLite state -- there are none. + * </p><p> + * Encapsulation is what ensures that the connection object's + * lifecycle does not become a tortured mess of finalizers and reference + * queues. + * </p> + * + * <h2>Reentrance</h2> + * <p> + * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + * </p> + * + * @hide + */ +public final class SQLiteConnection implements CancelationSignal.OnCancelListener { + private static final String TAG = "SQLiteConnection"; + private static final boolean DEBUG = false; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*"); + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final SQLiteConnectionPool mPool; + private final SQLiteDatabaseConfiguration mConfiguration; + private final int mConnectionId; + private final boolean mIsPrimaryConnection; + private final PreparedStatementCache mPreparedStatementCache; + private PreparedStatement mPreparedStatementPool; + + // The recent operations log. + private final OperationLog mRecentOperations = new OperationLog(); + + // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) + private int mConnectionPtr; + + private boolean mOnlyAllowReadOnlyOperations; + + // The number of times attachCancelationSignal has been called. + // Because SQLite statement execution can be re-entrant, we keep track of how many + // times we have attempted to attach a cancelation signal to the connection so that + // we can ensure that we detach the signal at the right time. + private int mCancelationSignalAttachCount; + + private static native int nativeOpen(String path, int openFlags, String label, + boolean enableTrace, boolean enableProfile); + private static native void nativeClose(int connectionPtr); + private static native void nativeRegisterCustomFunction(int connectionPtr, + SQLiteCustomFunction function); + private static native void nativeSetLocale(int connectionPtr, String locale); + private static native int nativePrepareStatement(int connectionPtr, String sql); + private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr); + private static native int nativeGetParameterCount(int connectionPtr, int statementPtr); + private static native boolean nativeIsReadOnly(int connectionPtr, int statementPtr); + private static native int nativeGetColumnCount(int connectionPtr, int statementPtr); + private static native String nativeGetColumnName(int connectionPtr, int statementPtr, + int index); + private static native void nativeBindNull(int connectionPtr, int statementPtr, + int index); + private static native void nativeBindLong(int connectionPtr, int statementPtr, + int index, long value); + private static native void nativeBindDouble(int connectionPtr, int statementPtr, + int index, double value); + private static native void nativeBindString(int connectionPtr, int statementPtr, + int index, String value); + private static native void nativeBindBlob(int connectionPtr, int statementPtr, + int index, byte[] value); + private static native void nativeResetStatementAndClearBindings( + int connectionPtr, int statementPtr); + private static native void nativeExecute(int connectionPtr, int statementPtr); + private static native long nativeExecuteForLong(int connectionPtr, int statementPtr); + private static native String nativeExecuteForString(int connectionPtr, int statementPtr); + private static native int nativeExecuteForBlobFileDescriptor( + int connectionPtr, int statementPtr); + private static native int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr); + private static native long nativeExecuteForLastInsertedRowId( + int connectionPtr, int statementPtr); + private static native long nativeExecuteForCursorWindow( + int connectionPtr, int statementPtr, int windowPtr, + int startPos, int requiredPos, boolean countAllRows); + private static native int nativeGetDbLookaside(int connectionPtr); + private static native void nativeCancel(int connectionPtr); + private static native void nativeResetCancel(int connectionPtr, boolean cancelable); + + private SQLiteConnection(SQLiteConnectionPool pool, + SQLiteDatabaseConfiguration configuration, + int connectionId, boolean primaryConnection) { + mPool = pool; + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + mConnectionId = connectionId; + mIsPrimaryConnection = primaryConnection; + mPreparedStatementCache = new PreparedStatementCache( + mConfiguration.maxSqlCacheSize); + mCloseGuard.open("close"); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mPool != null && mConnectionPtr != 0) { + mPool.onConnectionLeaked(); + } + + dispose(true); + } finally { + super.finalize(); + } + } + + // Called by SQLiteConnectionPool only. + static SQLiteConnection open(SQLiteConnectionPool pool, + SQLiteDatabaseConfiguration configuration, + int connectionId, boolean primaryConnection) { + SQLiteConnection connection = new SQLiteConnection(pool, configuration, + connectionId, primaryConnection); + try { + connection.open(); + return connection; + } catch (SQLiteException ex) { + connection.dispose(false); + throw ex; + } + } + + // Called by SQLiteConnectionPool only. + // Closes the database closes and releases all of its associated resources. + // Do not call methods on the connection after it is closed. It will probably crash. + void close() { + dispose(false); + } + + private void open() { + mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, + mConfiguration.label, + SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); + + setLocaleFromConfiguration(); + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (mConnectionPtr != 0) { + final int cookie = mRecentOperations.beginOperation("close", null, null); + try { + mPreparedStatementCache.evictAll(); + nativeClose(mConnectionPtr); + mConnectionPtr = 0; + } finally { + mRecentOperations.endOperation(cookie); + } + } + } + + private void setLocaleFromConfiguration() { + nativeSetLocale(mConnectionPtr, mConfiguration.locale.toString()); + } + + // Called by SQLiteConnectionPool only. + void reconfigure(SQLiteDatabaseConfiguration configuration) { + // Register custom functions. + final int functionCount = configuration.customFunctions.size(); + for (int i = 0; i < functionCount; i++) { + SQLiteCustomFunction function = configuration.customFunctions.get(i); + if (!mConfiguration.customFunctions.contains(function)) { + nativeRegisterCustomFunction(mConnectionPtr, function); + } + } + + // Remember whether locale has changed. + boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); + + // Update configuration parameters. + mConfiguration.updateParametersFrom(configuration); + + // Update prepared statement cache size. + mPreparedStatementCache.resize(configuration.maxSqlCacheSize); + + // Update locale. + if (localeChanged) { + setLocaleFromConfiguration(); + } + } + + // Called by SQLiteConnectionPool only. + // When set to true, executing write operations will throw SQLiteException. + // Preparing statements that might write is ok, just don't execute them. + void setOnlyAllowReadOnlyOperations(boolean readOnly) { + mOnlyAllowReadOnlyOperations = readOnly; + } + + // Called by SQLiteConnectionPool only. + // Returns true if the prepared statement cache contains the specified SQL. + boolean isPreparedStatementInCache(String sql) { + return mPreparedStatementCache.get(sql) != null; + } + + /** + * Gets the unique id of this connection. + * @return The connection id. + */ + public int getConnectionId() { + return mConnectionId; + } + + /** + * Returns true if this is the primary database connection. + * @return True if this is the primary database connection. + */ + public boolean isPrimaryConnection() { + return mIsPrimaryConnection; + } + + /** + * Prepares a statement for execution but does not bind its parameters or execute it. + * <p> + * This method can be used to check for syntax errors during compilation + * prior to execution of the statement. If the {@code outStatementInfo} argument + * is not null, the provided {@link SQLiteStatementInfo} object is populated + * with information about the statement. + * </p><p> + * A prepared statement makes no reference to the arguments that may eventually + * be bound to it, consequently it it possible to cache certain prepared statements + * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable, + * then it will be stored in the cache for later. + * </p><p> + * To take advantage of this behavior as an optimization, the connection pool + * provides a method to acquire a connection that already has a given SQL statement + * in its prepared statement cache so that it is ready for execution. + * </p> + * + * @param sql The SQL statement to prepare. + * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate + * with information about the statement, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error. + */ + public void prepare(String sql, SQLiteStatementInfo outStatementInfo) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("prepare", sql, null); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + if (outStatementInfo != null) { + outStatementInfo.numParameters = statement.mNumParameters; + outStatementInfo.readOnly = statement.mReadOnly; + + final int columnCount = nativeGetColumnCount( + mConnectionPtr, statement.mStatementPtr); + if (columnCount == 0) { + outStatementInfo.columnNames = EMPTY_STRING_ARRAY; + } else { + outStatementInfo.columnNames = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + outStatementInfo.columnNames[i] = nativeGetColumnName( + mConnectionPtr, statement.mStatementPtr, i); + } + } + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that does not return a result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public void execute(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + nativeExecute(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single <code>long</code> result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a <code>long</code>, or zero if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLong(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single {@link String} result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a <code>String</code>, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public String executeForString(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single BLOB result as a + * file descriptor to a shared memory region. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The file descriptor for a shared memory region that contains + * the value of the first column in the first row of the result set as a BLOB, + * or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + int fd = nativeExecuteForBlobFileDescriptor( + mConnectionPtr, statement.mStatementPtr); + return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null; + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a count of the number of rows + * that were changed. Use for UPDATE or DELETE SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were changed. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForChangedRowCount( + mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns the row id of the last row inserted + * by the statement. Use for INSERT SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The row id of the last row that was inserted, or 0 if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForLastInsertedRowId( + mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement and populates the specified {@link CursorWindow} + * with a range of results. Returns the number of rows that were counted + * during query execution. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param window The cursor window to clear and fill. + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled + * so that it does. Must be greater than or equal to <code>startPos</code>. + * @param countAllRows True to count all rows that the query would return + * regagless of whether they fit in the window. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were counted during query execution. Might + * not be all rows in the result set unless <code>countAllRows</code> is true. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + if (window == null) { + throw new IllegalArgumentException("window must not be null."); + } + + int actualPos = -1; + int countedRows = -1; + int filledRows = -1; + final int cookie = mRecentOperations.beginOperation("executeForCursorWindow", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancelationSignal(cancelationSignal); + try { + final long result = nativeExecuteForCursorWindow( + mConnectionPtr, statement.mStatementPtr, window.mWindowPtr, + startPos, requiredPos, countAllRows); + actualPos = (int)(result >> 32); + countedRows = (int)result; + filledRows = window.getNumRows(); + window.setStartPosition(actualPos); + return countedRows; + } finally { + detachCancelationSignal(cancelationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + if (mRecentOperations.endOperationDeferLog(cookie)) { + mRecentOperations.logOperation(cookie, "window='" + window + + "', startPos=" + startPos + + ", actualPos=" + actualPos + + ", filledRows=" + filledRows + + ", countedRows=" + countedRows); + } + } + } + + private PreparedStatement acquirePreparedStatement(String sql) { + PreparedStatement statement = mPreparedStatementCache.get(sql); + boolean skipCache = false; + if (statement != null) { + if (!statement.mInUse) { + return statement; + } + // The statement is already in the cache but is in use (this statement appears + // to be not only re-entrant but recursive!). So prepare a new copy of the + // statement but do not cache it. + skipCache = true; + } + + final int statementPtr = nativePrepareStatement(mConnectionPtr, sql); + try { + final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr); + final int type = DatabaseUtils.getSqlStatementType(sql); + final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); + statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly); + if (!skipCache && isCacheable(type)) { + mPreparedStatementCache.put(sql, statement); + statement.mInCache = true; + } + } catch (RuntimeException ex) { + // Finalize the statement if an exception occurred and we did not add + // it to the cache. If it is already in the cache, then leave it there. + if (statement == null || !statement.mInCache) { + nativeFinalizeStatement(mConnectionPtr, statementPtr); + } + throw ex; + } + statement.mInUse = true; + return statement; + } + + private void releasePreparedStatement(PreparedStatement statement) { + statement.mInUse = false; + if (statement.mInCache) { + try { + nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr); + } catch (SQLiteException ex) { + // The statement could not be reset due to an error. Remove it from the cache. + // When remove() is called, the cache will invoke its entryRemoved() callback, + // which will in turn call finalizePreparedStatement() to finalize and + // recycle the statement. + if (DEBUG) { + Log.d(TAG, "Could not reset prepared statement due to an exception. " + + "Removing it from the cache. SQL: " + + trimSqlForDisplay(statement.mSql), ex); + } + + mPreparedStatementCache.remove(statement.mSql); + } + } else { + finalizePreparedStatement(statement); + } + } + + private void finalizePreparedStatement(PreparedStatement statement) { + nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); + recyclePreparedStatement(statement); + } + + private void attachCancelationSignal(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + + mCancelationSignalAttachCount += 1; + if (mCancelationSignalAttachCount == 1) { + // Reset cancelation flag before executing the statement. + nativeResetCancel(mConnectionPtr, true /*cancelable*/); + + // After this point, onCancel() may be called concurrently. + cancelationSignal.setOnCancelListener(this); + } + } + } + + private void detachCancelationSignal(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + assert mCancelationSignalAttachCount > 0; + + mCancelationSignalAttachCount -= 1; + if (mCancelationSignalAttachCount == 0) { + // After this point, onCancel() cannot be called concurrently. + cancelationSignal.setOnCancelListener(null); + + // Reset cancelation flag after executing the statement. + nativeResetCancel(mConnectionPtr, false /*cancelable*/); + } + } + } + + // CancelationSignal.OnCancelationListener callback. + // This method may be called on a different thread than the executing statement. + // However, it will only be called between calls to attachCancelationSignal and + // detachCancelationSignal, while a statement is executing. We can safely assume + // that the SQLite connection is still alive. + @Override + public void onCancel() { + nativeCancel(mConnectionPtr); + } + + private void bindArguments(PreparedStatement statement, Object[] bindArgs) { + final int count = bindArgs != null ? bindArgs.length : 0; + if (count != statement.mNumParameters) { + throw new SQLiteBindOrColumnIndexOutOfRangeException( + "Expected " + statement.mNumParameters + " bind arguments but " + + bindArgs.length + " were provided."); + } + if (count == 0) { + return; + } + + final int statementPtr = statement.mStatementPtr; + for (int i = 0; i < count; i++) { + final Object arg = bindArgs[i]; + switch (DatabaseUtils.getTypeOfObject(arg)) { + case Cursor.FIELD_TYPE_NULL: + nativeBindNull(mConnectionPtr, statementPtr, i + 1); + break; + case Cursor.FIELD_TYPE_INTEGER: + nativeBindLong(mConnectionPtr, statementPtr, i + 1, + ((Number)arg).longValue()); + break; + case Cursor.FIELD_TYPE_FLOAT: + nativeBindDouble(mConnectionPtr, statementPtr, i + 1, + ((Number)arg).doubleValue()); + break; + case Cursor.FIELD_TYPE_BLOB: + nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg); + break; + case Cursor.FIELD_TYPE_STRING: + default: + if (arg instanceof Boolean) { + // Provide compatibility with legacy applications which may pass + // Boolean values in bind args. + nativeBindLong(mConnectionPtr, statementPtr, i + 1, + ((Boolean)arg).booleanValue() ? 1 : 0); + } else { + nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString()); + } + break; + } + } + } + + private void throwIfStatementForbidden(PreparedStatement statement) { + if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) { + throw new SQLiteException("Cannot execute this statement because it " + + "might modify the database but the connection is read-only."); + } + } + + private static boolean isCacheable(int statementType) { + if (statementType == DatabaseUtils.STATEMENT_UPDATE + || statementType == DatabaseUtils.STATEMENT_SELECT) { + return true; + } + return false; + } + + private void applyBlockGuardPolicy(PreparedStatement statement) { + if (!mConfiguration.isInMemoryDb()) { + if (statement.mReadOnly) { + BlockGuard.getThreadPolicy().onReadFromDisk(); + } else { + BlockGuard.getThreadPolicy().onWriteToDisk(); + } + } + } + + /** + * Dumps debugging information about this connection. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + public void dump(Printer printer, boolean verbose) { + dumpUnsafe(printer, verbose); + } + + /** + * Dumps debugging information about this connection, in the case where the + * caller might not actually own the connection. + * + * This function is written so that it may be called by a thread that does not + * own the connection. We need to be very careful because the connection state is + * not synchronized. + * + * At worst, the method may return stale or slightly wrong data, however + * it should not crash. This is ok as it is only used for diagnostic purposes. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + void dumpUnsafe(Printer printer, boolean verbose) { + printer.println("Connection #" + mConnectionId + ":"); + if (verbose) { + printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr)); + } + printer.println(" isPrimaryConnection: " + mIsPrimaryConnection); + printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations); + + mRecentOperations.dump(printer); + + if (verbose) { + mPreparedStatementCache.dump(printer); + } + } + + /** + * Describes the currently executing operation, in the case where the + * caller might not actually own the connection. + * + * This function is written so that it may be called by a thread that does not + * own the connection. We need to be very careful because the connection state is + * not synchronized. + * + * At worst, the method may return stale or slightly wrong data, however + * it should not crash. This is ok as it is only used for diagnostic purposes. + * + * @return A description of the current operation including how long it has been running, + * or null if none. + */ + String describeCurrentOperationUnsafe() { + return mRecentOperations.describeCurrentOperation(); + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + void collectDbStats(ArrayList<DbStats> dbStatsList) { + // Get information about the main database. + int lookaside = nativeGetDbLookaside(mConnectionPtr); + long pageCount = 0; + long pageSize = 0; + try { + pageCount = executeForLong("PRAGMA page_count;", null, null); + pageSize = executeForLong("PRAGMA page_size;", null, null); + } catch (SQLiteException ex) { + // Ignore. + } + dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize)); + + // Get information about attached databases. + // We ignore the first row in the database list because it corresponds to + // the main database which we have already described. + CursorWindow window = new CursorWindow("collectDbStats"); + try { + executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null); + for (int i = 1; i < window.getNumRows(); i++) { + String name = window.getString(i, 1); + String path = window.getString(i, 2); + pageCount = 0; + pageSize = 0; + try { + pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null); + pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null); + } catch (SQLiteException ex) { + // Ignore. + } + String label = " (attached) " + name; + if (!path.isEmpty()) { + label += ": " + path; + } + dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0)); + } + } catch (SQLiteException ex) { + // Ignore. + } finally { + window.close(); + } + } + + /** + * Collects statistics about database connection memory usage, in the case where the + * caller might not actually own the connection. + * + * @return The statistics object, never null. + */ + void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) { + dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0)); + } + + private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) { + // The prepared statement cache is thread-safe so we can access its statistics + // even if we do not own the database connection. + String label = mConfiguration.path; + if (!mIsPrimaryConnection) { + label += " (" + mConnectionId + ")"; + } + return new DbStats(label, pageCount, pageSize, lookaside, + mPreparedStatementCache.hitCount(), + mPreparedStatementCache.missCount(), + mPreparedStatementCache.size()); + } + + @Override + public String toString() { + return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")"; + } + + private PreparedStatement obtainPreparedStatement(String sql, int statementPtr, + int numParameters, int type, boolean readOnly) { + PreparedStatement statement = mPreparedStatementPool; + if (statement != null) { + mPreparedStatementPool = statement.mPoolNext; + statement.mPoolNext = null; + statement.mInCache = false; + } else { + statement = new PreparedStatement(); + } + statement.mSql = sql; + statement.mStatementPtr = statementPtr; + statement.mNumParameters = numParameters; + statement.mType = type; + statement.mReadOnly = readOnly; + return statement; + } + + private void recyclePreparedStatement(PreparedStatement statement) { + statement.mSql = null; + statement.mPoolNext = mPreparedStatementPool; + mPreparedStatementPool = statement; + } + + private static String trimSqlForDisplay(String sql) { + return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" "); + } + + /** + * Holder type for a prepared statement. + * + * Although this object holds a pointer to a native statement object, it + * does not have a finalizer. This is deliberate. The {@link SQLiteConnection} + * owns the statement object and will take care of freeing it when needed. + * In particular, closing the connection requires a guarantee of deterministic + * resource disposal because all native statement objects must be freed before + * the native database object can be closed. So no finalizers here. + */ + private static final class PreparedStatement { + // Next item in pool. + public PreparedStatement mPoolNext; + + // The SQL from which the statement was prepared. + public String mSql; + + // The native sqlite3_stmt object pointer. + // Lifetime is managed explicitly by the connection. + public int mStatementPtr; + + // The number of parameters that the prepared statement has. + public int mNumParameters; + + // The statement type. + public int mType; + + // True if the statement is read-only. + public boolean mReadOnly; + + // True if the statement is in the cache. + public boolean mInCache; + + // True if the statement is in use (currently executing). + // We need this flag because due to the use of custom functions in triggers, it's + // possible for SQLite calls to be re-entrant. Consequently we need to prevent + // in use statements from being finalized until they are no longer in use. + public boolean mInUse; + } + + private final class PreparedStatementCache + extends LruCache<String, PreparedStatement> { + public PreparedStatementCache(int size) { + super(size); + } + + @Override + protected void entryRemoved(boolean evicted, String key, + PreparedStatement oldValue, PreparedStatement newValue) { + oldValue.mInCache = false; + if (!oldValue.mInUse) { + finalizePreparedStatement(oldValue); + } + } + + public void dump(Printer printer) { + printer.println(" Prepared statement cache:"); + Map<String, PreparedStatement> cache = snapshot(); + if (!cache.isEmpty()) { + int i = 0; + for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) { + PreparedStatement statement = entry.getValue(); + if (statement.mInCache) { // might be false due to a race with entryRemoved + String sql = entry.getKey(); + printer.println(" " + i + ": statementPtr=0x" + + Integer.toHexString(statement.mStatementPtr) + + ", numParameters=" + statement.mNumParameters + + ", type=" + statement.mType + + ", readOnly=" + statement.mReadOnly + + ", sql=\"" + trimSqlForDisplay(sql) + "\""); + } + i += 1; + } + } else { + printer.println(" <none>"); + } + } + } + + private static final class OperationLog { + private static final int MAX_RECENT_OPERATIONS = 20; + private static final int COOKIE_GENERATION_SHIFT = 8; + private static final int COOKIE_INDEX_MASK = 0xff; + + private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; + private int mIndex; + private int mGeneration; + + public int beginOperation(String kind, String sql, Object[] bindArgs) { + synchronized (mOperations) { + final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS; + Operation operation = mOperations[index]; + if (operation == null) { + operation = new Operation(); + mOperations[index] = operation; + } else { + operation.mFinished = false; + operation.mException = null; + if (operation.mBindArgs != null) { + operation.mBindArgs.clear(); + } + } + operation.mStartTime = System.currentTimeMillis(); + operation.mKind = kind; + operation.mSql = sql; + if (bindArgs != null) { + if (operation.mBindArgs == null) { + operation.mBindArgs = new ArrayList<Object>(); + } else { + operation.mBindArgs.clear(); + } + for (int i = 0; i < bindArgs.length; i++) { + final Object arg = bindArgs[i]; + if (arg != null && arg instanceof byte[]) { + // Don't hold onto the real byte array longer than necessary. + operation.mBindArgs.add(EMPTY_BYTE_ARRAY); + } else { + operation.mBindArgs.add(arg); + } + } + } + operation.mCookie = newOperationCookieLocked(index); + mIndex = index; + return operation.mCookie; + } + } + + public void failOperation(int cookie, Exception ex) { + synchronized (mOperations) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mException = ex; + } + } + } + + public void endOperation(int cookie) { + synchronized (mOperations) { + if (endOperationDeferLogLocked(cookie)) { + logOperationLocked(cookie, null); + } + } + } + + public boolean endOperationDeferLog(int cookie) { + synchronized (mOperations) { + return endOperationDeferLogLocked(cookie); + } + } + + public void logOperation(int cookie, String detail) { + synchronized (mOperations) { + logOperationLocked(cookie, detail); + } + } + + private boolean endOperationDeferLogLocked(int cookie) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mEndTime = System.currentTimeMillis(); + operation.mFinished = true; + return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( + operation.mEndTime - operation.mStartTime); + } + return false; + } + + private void logOperationLocked(int cookie, String detail) { + final Operation operation = getOperationLocked(cookie); + StringBuilder msg = new StringBuilder(); + operation.describe(msg); + if (detail != null) { + msg.append(", ").append(detail); + } + Log.d(TAG, msg.toString()); + } + + private int newOperationCookieLocked(int index) { + final int generation = mGeneration++; + return generation << COOKIE_GENERATION_SHIFT | index; + } + + private Operation getOperationLocked(int cookie) { + final int index = cookie & COOKIE_INDEX_MASK; + final Operation operation = mOperations[index]; + return operation.mCookie == cookie ? operation : null; + } + + public String describeCurrentOperation() { + synchronized (mOperations) { + final Operation operation = mOperations[mIndex]; + if (operation != null && !operation.mFinished) { + StringBuilder msg = new StringBuilder(); + operation.describe(msg); + return msg.toString(); + } + return null; + } + } + + public void dump(Printer printer) { + synchronized (mOperations) { + printer.println(" Most recently executed operations:"); + int index = mIndex; + Operation operation = mOperations[index]; + if (operation != null) { + int n = 0; + do { + StringBuilder msg = new StringBuilder(); + msg.append(" ").append(n).append(": ["); + msg.append(operation.getFormattedStartTime()); + msg.append("] "); + operation.describe(msg); + printer.println(msg.toString()); + + if (index > 0) { + index -= 1; + } else { + index = MAX_RECENT_OPERATIONS - 1; + } + n += 1; + operation = mOperations[index]; + } while (operation != null && n < MAX_RECENT_OPERATIONS); + } else { + printer.println(" <none>"); + } + } + } + } + + private static final class Operation { + private static final SimpleDateFormat sDateFormat = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + public long mStartTime; + public long mEndTime; + public String mKind; + public String mSql; + public ArrayList<Object> mBindArgs; + public boolean mFinished; + public Exception mException; + public int mCookie; + + public void describe(StringBuilder msg) { + msg.append(mKind); + if (mFinished) { + msg.append(" took ").append(mEndTime - mStartTime).append("ms"); + } else { + msg.append(" started ").append(System.currentTimeMillis() - mStartTime) + .append("ms ago"); + } + msg.append(" - ").append(getStatus()); + if (mSql != null) { + msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\""); + } + if (mBindArgs != null && mBindArgs.size() != 0) { + msg.append(", bindArgs=["); + final int count = mBindArgs.size(); + for (int i = 0; i < count; i++) { + final Object arg = mBindArgs.get(i); + if (i != 0) { + msg.append(", "); + } + if (arg == null) { + msg.append("null"); + } else if (arg instanceof byte[]) { + msg.append("<byte[]>"); + } else if (arg instanceof String) { + msg.append("\"").append((String)arg).append("\""); + } else { + msg.append(arg); + } + } + msg.append("]"); + } + if (mException != null) { + msg.append(", exception=\"").append(mException.getMessage()).append("\""); + } + } + + private String getStatus() { + if (!mFinished) { + return "running"; + } + return mException != null ? "failed" : "succeeded"; + } + + private String getFormattedStartTime() { + return sDateFormat.format(new Date(mStartTime)); + } + } +} diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java new file mode 100644 index 0000000..d335738 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -0,0 +1,970 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +import dalvik.system.CloseGuard; + +import android.content.CancelationSignal; +import android.content.OperationCanceledException; +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.SystemClock; +import android.util.Log; +import android.util.PrefixPrinter; +import android.util.Printer; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +/** + * Maintains a pool of active SQLite database connections. + * <p> + * At any given time, a connection is either owned by the pool, or it has been + * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is + * finished with the connection it is using, it must return the connection + * back to the pool. + * </p><p> + * The pool holds strong references to the connections it owns. However, + * it only holds <em>weak references</em> to the connections that sessions + * have acquired from it. Using weak references in the latter case ensures + * that the connection pool can detect when connections have been improperly + * abandoned so that it can create new connections to replace them if needed. + * </p><p> + * The connection pool is thread-safe (but the connections themselves are not). + * </p> + * + * <h2>Exception safety</h2> + * <p> + * This code attempts to maintain the invariant that opened connections are + * always owned. Unfortunately that means it needs to handle exceptions + * all over to ensure that broken connections get cleaned up. Most + * operations invokving SQLite can throw {@link SQLiteException} or other + * runtime exceptions. This is a bit of a pain to deal with because the compiler + * cannot help us catch missing exception handling code. + * </p><p> + * The general rule for this file: If we are making calls out to + * {@link SQLiteConnection} then we must be prepared to handle any + * runtime exceptions it might throw at us. Note that out-of-memory + * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves + * handling out of memory because it is hard to do anything at all sensible then + * and most likely the VM is about to crash. + * </p> + * + * @hide + */ +public final class SQLiteConnectionPool implements Closeable { + private static final String TAG = "SQLiteConnectionPool"; + + // Amount of time to wait in milliseconds before unblocking acquireConnection + // and logging a message about the connection pool being busy. + private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final Object mLock = new Object(); + private final AtomicBoolean mConnectionLeaked = new AtomicBoolean(); + private final SQLiteDatabaseConfiguration mConfiguration; + private boolean mIsOpen; + private int mNextConnectionId; + + private ConnectionWaiter mConnectionWaiterPool; + private ConnectionWaiter mConnectionWaiterQueue; + + // Strong references to all available connections. + private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections = + new ArrayList<SQLiteConnection>(); + private SQLiteConnection mAvailablePrimaryConnection; + + // Weak references to all acquired connections. The associated value + // is a boolean that indicates whether the connection must be reconfigured + // before being returned to the available connection list. + // For example, the prepared statement cache size may have changed and + // need to be updated. + private final WeakHashMap<SQLiteConnection, Boolean> mAcquiredConnections = + new WeakHashMap<SQLiteConnection, Boolean>(); + + /** + * Connection flag: Read-only. + * <p> + * This flag indicates that the connection will only be used to + * perform read-only operations. + * </p> + */ + public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0; + + /** + * Connection flag: Primary connection affinity. + * <p> + * This flag indicates that the primary connection is required. + * This flag helps support legacy applications that expect most data modifying + * operations to be serialized by locking the primary database connection. + * Setting this flag essentially implements the old "db lock" concept by preventing + * an operation from being performed until it can obtain exclusive access to + * the primary connection. + * </p> + */ + public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1; + + /** + * Connection flag: Connection is being used interactively. + * <p> + * This flag indicates that the connection is needed by the UI thread. + * The connection pool can use this flag to elevate the priority + * of the database connection request. + * </p> + */ + public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2; + + private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) { + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } + } + + /** + * Opens a connection pool for the specified database. + * + * @param configuration The database configuration. + * @return The connection pool. + * + * @throws SQLiteException if a database error occurs. + */ + public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + // Create the pool. + SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration); + pool.open(); // might throw + return pool; + } + + // Might throw + private void open() { + // Open the primary connection. + // This might throw if the database is corrupt. + mAvailablePrimaryConnection = openConnectionLocked( + true /*primaryConnection*/); // might throw + + // Mark the pool as being open for business. + mIsOpen = true; + mCloseGuard.open("close"); + } + + /** + * Closes the connection pool. + * <p> + * When the connection pool is closed, it will refuse all further requests + * to acquire connections. All connections that are currently available in + * the pool are closed immediately. Any connections that are still in use + * will be closed as soon as they are returned to the pool. + * </p> + * + * @throws IllegalStateException if the pool has been closed. + */ + public void close() { + dispose(false); + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (!finalized) { + // Close all connections. We don't need (or want) to do this + // when finalized because we don't know what state the connections + // themselves will be in. The finalizer is really just here for CloseGuard. + // The connections will take care of themselves when their own finalizers run. + synchronized (mLock) { + throwIfClosedLocked(); + + mIsOpen = false; + + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i)); + } + mAvailableNonPrimaryConnections.clear(); + + if (mAvailablePrimaryConnection != null) { + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + + final int pendingCount = mAcquiredConnections.size(); + if (pendingCount != 0) { + Log.i(TAG, "The connection pool for " + mConfiguration.label + + " has been closed but there are still " + + pendingCount + " connections in use. They will be closed " + + "as they are released back to the pool."); + } + + wakeConnectionWaitersLocked(); + } + } + } + + /** + * Reconfigures the database configuration of the connection pool and all of its + * connections. + * <p> + * Configuration changes are propagated down to connections immediately if + * they are available or as soon as they are released. This includes changes + * that affect the size of the pool. + * </p> + * + * @param configuration The new configuration. + * + * @throws IllegalStateException if the pool has been closed. + */ + public void reconfigure(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + synchronized (mLock) { + throwIfClosedLocked(); + + final boolean poolSizeChanged = mConfiguration.maxConnectionPoolSize + != configuration.maxConnectionPoolSize; + mConfiguration.updateParametersFrom(configuration); + + if (poolSizeChanged) { + int availableCount = mAvailableNonPrimaryConnections.size(); + while (availableCount-- > mConfiguration.maxConnectionPoolSize - 1) { + SQLiteConnection connection = + mAvailableNonPrimaryConnections.remove(availableCount); + closeConnectionAndLogExceptionsLocked(connection); + } + } + + reconfigureAllConnectionsLocked(); + + wakeConnectionWaitersLocked(); + } + } + + /** + * Acquires a connection from the pool. + * <p> + * The caller must call {@link #releaseConnection} to release the connection + * back to the pool when it is finished. Failure to do so will result + * in much unpleasantness. + * </p> + * + * @param sql If not null, try to find a connection that already has + * the specified SQL statement in its prepared statement cache. + * @param connectionFlags The connection request flags. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The connection that was acquired, never null. + * + * @throws IllegalStateException if the pool has been closed. + * @throws SQLiteException if a database error occurs. + * @throws OperationCanceledException if the operation was canceled. + */ + public SQLiteConnection acquireConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { + return waitForConnection(sql, connectionFlags, cancelationSignal); + } + + /** + * Releases a connection back to the pool. + * <p> + * It is ok to call this method after the pool has closed, to release + * connections that were still in use at the time of closure. + * </p> + * + * @param connection The connection to release. Must not be null. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public void releaseConnection(SQLiteConnection connection) { + synchronized (mLock) { + Boolean mustReconfigure = mAcquiredConnections.remove(connection); + if (mustReconfigure == null) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + closeConnectionAndLogExceptionsLocked(connection); + } else if (connection.isPrimaryConnection()) { + assert mAvailablePrimaryConnection == null; + try { + if (mustReconfigure == Boolean.TRUE) { + connection.reconfigure(mConfiguration); // might throw + } + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + connection = null; + } + if (connection != null) { + mAvailablePrimaryConnection = connection; + } + wakeConnectionWaitersLocked(); + } else if (mAvailableNonPrimaryConnections.size() >= + mConfiguration.maxConnectionPoolSize - 1) { + closeConnectionAndLogExceptionsLocked(connection); + } else { + try { + if (mustReconfigure == Boolean.TRUE) { + connection.reconfigure(mConfiguration); // might throw + } + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released non-primary connection, " + + "closing it: " + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + connection = null; + } + if (connection != null) { + mAvailableNonPrimaryConnections.add(connection); + } + wakeConnectionWaitersLocked(); + } + } + } + + /** + * Returns true if the session should yield the connection due to + * contention over available database connections. + * + * @param connection The connection owned by the session. + * @param connectionFlags The connection request flags. + * @return True if the session should yield its connection. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) { + synchronized (mLock) { + if (!mAcquiredConnections.containsKey(connection)) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + return false; + } + + return isSessionBlockingImportantConnectionWaitersLocked( + connection.isPrimaryConnection(), connectionFlags); + } + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + public void collectDbStats(ArrayList<DbStats> dbStatsList) { + synchronized (mLock) { + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAvailableNonPrimaryConnections) { + connection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + connection.collectDbStatsUnsafe(dbStatsList); + } + } + } + + // Might throw. + private SQLiteConnection openConnectionLocked(boolean primaryConnection) { + final int connectionId = mNextConnectionId++; + return SQLiteConnection.open(this, mConfiguration, + connectionId, primaryConnection); // might throw + } + + void onConnectionLeaked() { + // This code is running inside of the SQLiteConnection finalizer. + // + // We don't know whether it is just the connection that has been finalized (and leaked) + // or whether the connection pool has also been or is about to be finalized. + // Consequently, it would be a bad idea to try to grab any locks or to + // do any significant work here. So we do the simplest possible thing and + // set a flag. waitForConnection() periodically checks this flag (when it + // times out) so that it can recover from leaked connections and wake + // itself or other threads up if necessary. + // + // You might still wonder why we don't try to do more to wake up the waiters + // immediately. First, as explained above, it would be hard to do safely + // unless we started an extra Thread to function as a reference queue. Second, + // this is never supposed to happen in normal operation. Third, there is no + // guarantee that the GC will actually detect the leak in a timely manner so + // it's not all that important that we recover from the leak in a timely manner + // either. Fourth, if a badly behaved application finds itself hung waiting for + // several seconds while waiting for a leaked connection to be detected and recreated, + // then perhaps its authors will have added incentive to fix the problem! + + Log.w(TAG, "A SQLiteConnection object for database '" + + mConfiguration.label + "' was leaked! Please fix your application " + + "to end transactions in progress properly and to close the database " + + "when it is no longer needed."); + + mConnectionLeaked.set(true); + } + + // Can't throw. + private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) { + try { + connection.close(); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to close connection, its fate is now in the hands " + + "of the merciful GC: " + connection, ex); + } + } + + // Can't throw. + private void reconfigureAllConnectionsLocked() { + boolean wake = false; + if (mAvailablePrimaryConnection != null) { + try { + mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " + + mAvailablePrimaryConnection, ex); + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + wake = true; + } + } + + int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i); + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + mAvailableNonPrimaryConnections.remove(i--); + count -= 1; + wake = true; + } + } + + if (!mAcquiredConnections.isEmpty()) { + ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>( + mAcquiredConnections.size()); + for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) { + if (entry.getValue() != Boolean.TRUE) { + keysToUpdate.add(entry.getKey()); + } + } + final int updateCount = keysToUpdate.size(); + for (int i = 0; i < updateCount; i++) { + mAcquiredConnections.put(keysToUpdate.get(i), Boolean.TRUE); + } + } + + if (wake) { + wakeConnectionWaitersLocked(); + } + } + + // Might throw. + private SQLiteConnection waitForConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { + final boolean wantPrimaryConnection = + (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; + + final ConnectionWaiter waiter; + synchronized (mLock) { + throwIfClosedLocked(); + + // Abort if canceled. + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + // Try to acquire a connection. + SQLiteConnection connection = null; + if (!wantPrimaryConnection) { + connection = tryAcquireNonPrimaryConnectionLocked( + sql, connectionFlags); // might throw + } + if (connection == null) { + connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw + } + if (connection != null) { + return connection; + } + + // No connections available. Enqueue a waiter in priority order. + final int priority = getPriority(connectionFlags); + final long startTime = SystemClock.uptimeMillis(); + waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, + priority, wantPrimaryConnection, sql, connectionFlags); + ConnectionWaiter predecessor = null; + ConnectionWaiter successor = mConnectionWaiterQueue; + while (successor != null) { + if (priority > successor.mPriority) { + waiter.mNext = successor; + break; + } + predecessor = successor; + successor = successor.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter; + } else { + mConnectionWaiterQueue = waiter; + } + + if (cancelationSignal != null) { + final int nonce = waiter.mNonce; + cancelationSignal.setOnCancelListener(new CancelationSignal.OnCancelListener() { + @Override + public void onCancel() { + synchronized (mLock) { + cancelConnectionWaiterLocked(waiter, nonce); + } + } + }); + } + } + + // Park the thread until a connection is assigned or the pool is closed. + // Rethrow an exception from the wait, if we got one. + long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis; + for (;;) { + // Detect and recover from connection leaks. + if (mConnectionLeaked.compareAndSet(true, false)) { + synchronized (mLock) { + wakeConnectionWaitersLocked(); + } + } + + // Wait to be unparked (may already have happened), a timeout, or interruption. + LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L); + + // Clear the interrupted flag, just in case. + Thread.interrupted(); + + // Check whether we are done waiting yet. + synchronized (mLock) { + throwIfClosedLocked(); + + final SQLiteConnection connection = waiter.mAssignedConnection; + final RuntimeException ex = waiter.mException; + if (connection != null || ex != null) { + if (cancelationSignal != null) { + cancelationSignal.setOnCancelListener(null); + } + recycleConnectionWaiterLocked(waiter); + if (connection != null) { + return connection; + } + throw ex; // rethrow! + } + + final long now = SystemClock.uptimeMillis(); + if (now < nextBusyTimeoutTime) { + busyTimeoutMillis = now - nextBusyTimeoutTime; + } else { + logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); + busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + nextBusyTimeoutTime = now + busyTimeoutMillis; + } + } + } + } + + // Can't throw. + private void cancelConnectionWaiterLocked(ConnectionWaiter waiter, int nonce) { + if (waiter.mNonce != nonce) { + // Waiter already removed and recycled. + return; + } + + if (waiter.mAssignedConnection != null || waiter.mException != null) { + // Waiter is done waiting but has not woken up yet. + return; + } + + // Waiter must still be waiting. Dequeue it. + ConnectionWaiter predecessor = null; + ConnectionWaiter current = mConnectionWaiterQueue; + while (current != waiter) { + assert current != null; + predecessor = current; + current = current.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter.mNext; + } else { + mConnectionWaiterQueue = waiter.mNext; + } + + // Send the waiter an exception and unpark it. + waiter.mException = new OperationCanceledException(); + LockSupport.unpark(waiter.mThread); + + // Check whether removing this waiter will enable other waiters to make progress. + wakeConnectionWaitersLocked(); + } + + // Can't throw. + private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { + final Thread thread = Thread.currentThread(); + StringBuilder msg = new StringBuilder(); + msg.append("The connection pool for database '").append(mConfiguration.label); + msg.append("' has been unable to grant a connection to thread "); + msg.append(thread.getId()).append(" (").append(thread.getName()).append(") "); + msg.append("with flags 0x").append(Integer.toHexString(connectionFlags)); + msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n"); + + ArrayList<String> requests = new ArrayList<String>(); + int activeConnections = 0; + int idleConnections = 0; + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + String description = connection.describeCurrentOperationUnsafe(); + if (description != null) { + requests.add(description); + activeConnections += 1; + } else { + idleConnections += 1; + } + } + } + int availableConnections = mAvailableNonPrimaryConnections.size(); + if (mAvailablePrimaryConnection != null) { + availableConnections += 1; + } + + msg.append("Connections: ").append(activeConnections).append(" active, "); + msg.append(idleConnections).append(" idle, "); + msg.append(availableConnections).append(" available.\n"); + + if (!requests.isEmpty()) { + msg.append("\nRequests in progress:\n"); + for (String request : requests) { + msg.append(" ").append(request).append("\n"); + } + } + + Log.w(TAG, msg.toString()); + } + + // Can't throw. + private void wakeConnectionWaitersLocked() { + // Unpark all waiters that have requests that we can fulfill. + // This method is designed to not throw runtime exceptions, although we might send + // a waiter an exception for it to rethrow. + ConnectionWaiter predecessor = null; + ConnectionWaiter waiter = mConnectionWaiterQueue; + boolean primaryConnectionNotAvailable = false; + boolean nonPrimaryConnectionNotAvailable = false; + while (waiter != null) { + boolean unpark = false; + if (!mIsOpen) { + unpark = true; + } else { + try { + SQLiteConnection connection = null; + if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) { + connection = tryAcquireNonPrimaryConnectionLocked( + waiter.mSql, waiter.mConnectionFlags); // might throw + if (connection == null) { + nonPrimaryConnectionNotAvailable = true; + } + } + if (connection == null && !primaryConnectionNotAvailable) { + connection = tryAcquirePrimaryConnectionLocked( + waiter.mConnectionFlags); // might throw + if (connection == null) { + primaryConnectionNotAvailable = true; + } + } + if (connection != null) { + waiter.mAssignedConnection = connection; + unpark = true; + } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) { + // There are no connections available and the pool is still open. + // We cannot fulfill any more connection requests, so stop here. + break; + } + } catch (RuntimeException ex) { + // Let the waiter handle the exception from acquiring a connection. + waiter.mException = ex; + unpark = true; + } + } + + final ConnectionWaiter successor = waiter.mNext; + if (unpark) { + if (predecessor != null) { + predecessor.mNext = successor; + } else { + mConnectionWaiterQueue = successor; + } + waiter.mNext = null; + + LockSupport.unpark(waiter.mThread); + } else { + predecessor = waiter; + } + waiter = successor; + } + } + + // Might throw. + private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) { + // If the primary connection is available, acquire it now. + SQLiteConnection connection = mAvailablePrimaryConnection; + if (connection != null) { + mAvailablePrimaryConnection = null; + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Make sure that the primary connection actually exists and has just been acquired. + for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) { + if (acquiredConnection.isPrimaryConnection()) { + return null; + } + } + + // Uhoh. No primary connection! Either this is the first time we asked + // for it, or maybe it leaked? + connection = openConnectionLocked(true /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private SQLiteConnection tryAcquireNonPrimaryConnectionLocked( + String sql, int connectionFlags) { + // Try to acquire the next connection in the queue. + SQLiteConnection connection; + final int availableCount = mAvailableNonPrimaryConnections.size(); + if (availableCount > 1 && sql != null) { + // If we have a choice, then prefer a connection that has the + // prepared statement in its cache. + for (int i = 0; i < availableCount; i++) { + connection = mAvailableNonPrimaryConnections.get(i); + if (connection.isPreparedStatementInCache(sql)) { + mAvailableNonPrimaryConnections.remove(i); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + } + } + if (availableCount > 0) { + // Otherwise, just grab the next one. + connection = mAvailableNonPrimaryConnections.remove(availableCount - 1); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Expand the pool if needed. + int openConnections = mAcquiredConnections.size(); + if (mAvailablePrimaryConnection != null) { + openConnections += 1; + } + if (openConnections >= mConfiguration.maxConnectionPoolSize) { + return null; + } + connection = openConnectionLocked(false /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) { + try { + final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0; + connection.setOnlyAllowReadOnlyOperations(readOnly); + + mAcquiredConnections.put(connection, Boolean.FALSE); + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " + + connection +", connectionFlags=" + connectionFlags); + closeConnectionAndLogExceptionsLocked(connection); + throw ex; // rethrow! + } + } + + private boolean isSessionBlockingImportantConnectionWaitersLocked( + boolean holdingPrimaryConnection, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterQueue; + if (waiter != null) { + final int priority = getPriority(connectionFlags); + do { + // Only worry about blocked connections that have same or lower priority. + if (priority > waiter.mPriority) { + break; + } + + // If we are holding the primary connection then we are blocking the waiter. + // Likewise, if we are holding a non-primary connection and the waiter + // would accept a non-primary connection, then we are blocking the waier. + if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) { + return true; + } + + waiter = waiter.mNext; + } while (waiter != null); + } + return false; + } + + private static int getPriority(int connectionFlags) { + return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0; + } + + private void throwIfClosedLocked() { + if (!mIsOpen) { + throw new IllegalStateException("Cannot perform this operation " + + "because the connection pool have been closed."); + } + } + + private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime, + int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterPool; + if (waiter != null) { + mConnectionWaiterPool = waiter.mNext; + waiter.mNext = null; + } else { + waiter = new ConnectionWaiter(); + } + waiter.mThread = thread; + waiter.mStartTime = startTime; + waiter.mPriority = priority; + waiter.mWantPrimaryConnection = wantPrimaryConnection; + waiter.mSql = sql; + waiter.mConnectionFlags = connectionFlags; + return waiter; + } + + private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) { + waiter.mNext = mConnectionWaiterPool; + waiter.mThread = null; + waiter.mSql = null; + waiter.mAssignedConnection = null; + waiter.mException = null; + waiter.mNonce += 1; + mConnectionWaiterPool = waiter; + } + + /** + * Dumps debugging information about this connection pool. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + public void dump(Printer printer, boolean verbose) { + Printer indentedPrinter = PrefixPrinter.create(printer, " "); + synchronized (mLock) { + printer.println("Connection pool for " + mConfiguration.path + ":"); + printer.println(" Open: " + mIsOpen); + printer.println(" Max connections: " + mConfiguration.maxConnectionPoolSize); + + printer.println(" Available primary connection:"); + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.dump(indentedPrinter, verbose); + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Available non-primary connections:"); + if (!mAvailableNonPrimaryConnections.isEmpty()) { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose); + } + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Acquired connections:"); + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry<SQLiteConnection, Boolean> entry : + mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + connection.dumpUnsafe(indentedPrinter, verbose); + indentedPrinter.println(" Pending reconfiguration: " + entry.getValue()); + } + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Connection waiters:"); + if (mConnectionWaiterQueue != null) { + int i = 0; + final long now = SystemClock.uptimeMillis(); + for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null; + waiter = waiter.mNext, i++) { + indentedPrinter.println(i + ": waited for " + + ((now - waiter.mStartTime) * 0.001f) + + " ms - thread=" + waiter.mThread + + ", priority=" + waiter.mPriority + + ", sql='" + waiter.mSql + "'"); + } + } else { + indentedPrinter.println("<none>"); + } + } + } + + @Override + public String toString() { + return "SQLiteConnectionPool: " + mConfiguration.path; + } + + private static final class ConnectionWaiter { + public ConnectionWaiter mNext; + public Thread mThread; + public long mStartTime; + public int mPriority; + public boolean mWantPrimaryConnection; + public String mSql; + public int mConnectionFlags; + public SQLiteConnection mAssignedConnection; + public RuntimeException mException; + public int mNonce; + } +} diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c24acd4..82bb23e 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -18,6 +18,7 @@ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; +import android.database.DatabaseUtils; import android.os.StrictMode; import android.util.Log; @@ -42,13 +43,16 @@ public class SQLiteCursor extends AbstractWindowedCursor { private final String[] mColumns; /** The query object for the cursor */ - private SQLiteQuery mQuery; + private final SQLiteQuery mQuery; /** The compiled query this cursor came from */ private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ - private volatile int mCount = NO_COUNT; + private int mCount = NO_COUNT; + + /** The number of rows that can fit in the cursor window, 0 if unknown */ + private int mCursorWindowCapacity; /** A mapping of column names to column indices, to speed up lookups */ private Map<String, Integer> mColumnNameMap; @@ -61,8 +65,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from - * {@code FROM} onward would be in the params argument. This constructor - * has package scope. + * {@code FROM} onward would be in the params argument. * * @param db a reference to a Database object that is already constructed * and opened. This param is not used any longer @@ -82,8 +85,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from - * {@code FROM} onward would be in the params argument. This constructor - * has package scope. + * {@code FROM} onward would be in the params argument. * * @param editTable the name of the table used for this query * @param query the {@link SQLiteQuery} object associated with this cursor object. @@ -92,9 +94,6 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (query == null) { throw new IllegalArgumentException("query object cannot be null"); } - if (query.mDatabase == null) { - throw new IllegalArgumentException("query.mDatabase cannot be null"); - } if (StrictMode.vmSqliteObjectLeaksEnabled()) { mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); } else { @@ -105,38 +104,21 @@ public class SQLiteCursor extends AbstractWindowedCursor { mColumnNameMap = null; mQuery = query; - query.mDatabase.lock(query.mSql); - try { - // Setup the list of columns - int columnCount = mQuery.columnCountLocked(); - mColumns = new String[columnCount]; - - // Read in all column names - for (int i = 0; i < columnCount; i++) { - String columnName = mQuery.columnNameLocked(i); - mColumns[i] = columnName; - if (false) { - Log.v("DatabaseWindow", "mColumns[" + i + "] is " - + mColumns[i]); - } - - // Make note of the row ID column index for quick access to it - if ("_id".equals(columnName)) { - mRowIdColumnIndex = i; - } + mColumns = query.getColumnNames(); + for (int i = 0; i < mColumns.length; i++) { + // Make note of the row ID column index for quick access to it + if ("_id".equals(mColumns[i])) { + mRowIdColumnIndex = i; } - } finally { - query.mDatabase.unlock(); } } /** + * Get the database that this cursor is associated with. * @return the SQLiteDatabase that this cursor is associated with. */ public SQLiteDatabase getDatabase() { - synchronized (this) { - return mQuery.mDatabase; - } + return mQuery.getDatabase(); } @Override @@ -158,25 +140,23 @@ public class SQLiteCursor extends AbstractWindowedCursor { return mCount; } - private void fillWindow(int startPos) { + private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); - mWindow.setStartPosition(startPos); - int count = getQuery().fillWindow(mWindow); - if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 + + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + count); + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); } - mCount = count; - } else if (mCount <= 0) { - throw new IllegalStateException("Row count should never be zero or negative " - + "when the start position is non-zero"); + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + mQuery.fillWindow(mWindow, startPos, requiredPos, false); } } - private synchronized SQLiteQuery getQuery() { - return mQuery; - } - @Override public int getColumnIndex(String columnName) { // Create mColumnNameMap on demand @@ -231,75 +211,28 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (isClosed()) { return false; } - long timeStart = 0; - if (false) { - timeStart = System.currentTimeMillis(); - } synchronized (this) { + if (!mQuery.getDatabase().isOpen()) { + return false; + } + if (mWindow != null) { mWindow.clear(); } mPos = -1; - SQLiteDatabase db = null; - try { - db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - if (!db.equals(mQuery.mDatabase)) { - // since we need to use a different database connection handle, - // re-compile the query - try { - db.lock(mQuery.mSql); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - try { - // close the old mQuery object and open a new one - mQuery.close(); - mQuery = new SQLiteQuery(db, mQuery); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } finally { - db.unlock(); - } - } - // This one will recreate the temp table, and get its count - mDriver.cursorRequeried(this); mCount = NO_COUNT; - try { - mQuery.requery(); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - } - if (false) { - Log.v("DatabaseWindow", "closing window in requery()"); - Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); + mDriver.cursorRequeried(this); } - boolean result = false; try { - result = super.requery(); + return super.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); + return false; } - if (false) { - long timeEnd = System.currentTimeMillis(); - Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); - } - return result; } @Override @@ -324,20 +257,16 @@ public class SQLiteCursor extends AbstractWindowedCursor { // if the cursor hasn't been closed yet, close it first if (mWindow != null) { if (mStackTrace != null) { - int len = mQuery.mSql.length(); + String sql = mQuery.getSql(); + int len = sql.length(); StrictMode.onSqliteObjectLeaked( "Finalizing a Cursor that has not been deactivated or closed. " + - "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + - ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len), + "database = " + mQuery.getDatabase().getLabel() + + ", table = " + mEditTable + + ", query = " + sql.substring(0, (len > 1000) ? 1000 : len), mStackTrace); } close(); - SQLiteDebug.notifyActiveCursorFinalized(); - } else { - if (false) { - Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + - ", table = " + mEditTable + ", query = " + mQuery.mSql); - } } } finally { super.finalize(); diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java index b3963f9..ad2cdd2 100644 --- a/core/java/android/database/sqlite/SQLiteCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java @@ -39,7 +39,7 @@ public interface SQLiteCursorDriver { void cursorDeactivated(); /** - * Called by a SQLiteCursor when it is requeryed. + * Called by a SQLiteCursor when it is requeried. */ void cursorRequeried(Cursor cursor); diff --git a/core/java/android/database/sqlite/SQLiteCustomFunction.java b/core/java/android/database/sqlite/SQLiteCustomFunction.java new file mode 100644 index 0000000..02f3284 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCustomFunction.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +/** + * Describes a custom SQL function. + * + * @hide + */ +public final class SQLiteCustomFunction { + public final String name; + public final int numArgs; + public final SQLiteDatabase.CustomFunction callback; + + /** + * Create custom function. + * + * @param name The name of the sqlite3 function. + * @param numArgs The number of arguments for the function, or -1 to + * support any number of arguments. + * @param callback The callback to invoke when the function is executed. + */ + public SQLiteCustomFunction(String name, int numArgs, + SQLiteDatabase.CustomFunction callback) { + if (name == null) { + throw new IllegalArgumentException("name must not be null."); + } + + this.name = name; + this.numArgs = numArgs; + this.callback = callback; + } + + // Called from native. + @SuppressWarnings("unused") + private void dispatchCallback(String[] args) { + callback.callback(args); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index f990be6..7db7bfb 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,8 +16,9 @@ package android.database.sqlite; -import android.app.AppGlobals; +import android.content.CancelationSignal; import android.content.ContentValues; +import android.content.OperationCanceledException; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseErrorHandler; @@ -25,61 +26,117 @@ import android.database.DatabaseUtils; import android.database.DefaultDatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDebug.DbStats; -import android.os.Debug; -import android.os.StatFs; -import android.os.SystemClock; -import android.os.SystemProperties; +import android.os.Looper; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import android.util.LruCache; import android.util.Pair; -import dalvik.system.BlockGuard; +import android.util.Printer; + +import dalvik.system.CloseGuard; + import java.io.File; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Random; import java.util.WeakHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Pattern; /** * Exposes methods to manage a SQLite database. - * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and + * + * <p> + * SQLiteDatabase has methods to create, delete, execute SQL commands, and * perform other common database management tasks. - * <p>See the Notepad sample application in the SDK for an example of creating + * </p><p> + * See the Notepad sample application in the SDK for an example of creating * and managing a database. - * <p> Database names must be unique within an application, not across all - * applications. + * </p><p> + * Database names must be unique within an application, not across all applications. + * </p> * * <h3>Localized Collation - ORDER BY</h3> - * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies - * two more, <code>LOCALIZED</code>, which changes with the system's current locale - * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which - * is the Unicode Collation Algorithm and not tailored to the current locale. + * <p> + * In addition to SQLite's default <code>BINARY</code> collator, Android supplies + * two more, <code>LOCALIZED</code>, which changes with the system's current locale, + * and <code>UNICODE</code>, which is the Unicode Collation Algorithm and not tailored + * to the current locale. + * </p> */ public class SQLiteDatabase extends SQLiteClosable { private static final String TAG = "SQLiteDatabase"; - private static final boolean ENABLE_DB_SAMPLE = false; // true to enable stats in event log - private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_DB_CORRUPT = 75004; - /** - * Algorithms used in ON CONFLICT clause - * http://www.sqlite.org/lang_conflict.html - */ - /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, + // Stores reference to all databases opened in the current process. + // (The referent Object is not used at this time.) + // INVARIANT: Guarded by sActiveDatabases. + private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = + new WeakHashMap<SQLiteDatabase, Object>(); + + // Thread-local for database sessions that belong to this database. + // Each thread has its own database session. + // INVARIANT: Immutable. + private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() { + @Override + protected SQLiteSession initialValue() { + return createSession(); + } + }; + + // The optional factory to use when creating new Cursors. May be null. + // INVARIANT: Immutable. + private final CursorFactory mCursorFactory; + + // Error handler to be used when SQLite returns corruption errors. + // INVARIANT: Immutable. + private final DatabaseErrorHandler mErrorHandler; + + // Shared database state lock. + // This lock guards all of the shared state of the database, such as its + // configuration, whether it is open or closed, and so on. This lock should + // be held for as little time as possible. + // + // The lock MUST NOT be held while attempting to acquire database connections or + // while executing SQL statements on behalf of the client as it can lead to deadlock. + // + // It is ok to hold the lock while reconfiguring the connection pool or dumping + // statistics because those operations are non-reentrant and do not try to acquire + // connections that might be held by other threads. + // + // Basic rule: grab the lock, access or modify global state, release the lock, then + // do the required SQL work. + private final Object mLock = new Object(); + + // Warns if the database is finalized without being closed properly. + // INVARIANT: Guarded by mLock. + private final CloseGuard mCloseGuardLocked = CloseGuard.get(); + + // The database configuration. + // INVARIANT: Guarded by mLock. + private final SQLiteDatabaseConfiguration mConfigurationLocked; + + // The connection pool for the database, null when closed. + // The pool itself is thread-safe, but the reference to it can only be acquired + // when the lock is held. + // INVARIANT: Guarded by mLock. + private SQLiteConnectionPool mConnectionPoolLocked; + + // True if the database has attached databases. + // INVARIANT: Guarded by mLock. + private boolean mHasAttachedDbsLocked; + + // True if the database is in WAL mode. + // INVARIANT: Guarded by mLock. + private boolean mIsWALEnabledLocked; + + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, * thus ending the current transaction, and the command aborts with a * return code of SQLITE_CONSTRAINT. If no transaction is active * (other than the implied transaction that is created on every command) - * then this algorithm works the same as ABORT. + * then this algorithm works the same as ABORT. */ public static final int CONFLICT_ROLLBACK = 1; @@ -118,14 +175,15 @@ public class SQLiteDatabase extends SQLiteClosable { * violation occurs then the IGNORE algorithm is used. When this conflict * resolution strategy deletes rows in order to satisfy a constraint, * it does not invoke delete triggers on those rows. - * This behavior might change in a future release. + * This behavior might change in a future release. */ public static final int CONFLICT_REPLACE = 5; /** - * use the following when no conflict action is specified. + * Use the following when no conflict action is specified. */ public static final int CONFLICT_NONE = 0; + private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; @@ -146,7 +204,7 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; /** - * Flag for {@link #openDatabase} to open the database for reading and writing. + * Open flag: Flag for {@link #openDatabase} to open the database for reading and writing. * If the disk is full, this may fail even before you actually write anything. * * {@more} Note that the value of this flag is 0, so it is the default. @@ -154,7 +212,7 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int OPEN_READWRITE = 0x00000000; // update native code if changing /** - * Flag for {@link #openDatabase} to open the database for reading only. + * Open flag: Flag for {@link #openDatabase} to open the database for reading only. * This is the only reliable way to open a database if the disk may be full. */ public static final int OPEN_READONLY = 0x00000001; // update native code if changing @@ -162,7 +220,8 @@ public class SQLiteDatabase extends SQLiteClosable { private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing /** - * Flag for {@link #openDatabase} to open the database without support for localized collators. + * Open flag: Flag for {@link #openDatabase} to open the database without support for + * localized collators. * * {@more} This causes the collator <code>LOCALIZED</code> not to be created. * You must be consistent when using this flag to use the setting the database was @@ -171,190 +230,62 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing /** - * Flag for {@link #openDatabase} to create the database file if it does not already exist. + * Open flag: Flag for {@link #openDatabase} to create the database file if it does not + * already exist. */ public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing /** - * Indicates whether the most-recently started transaction has been marked as successful. - */ - private boolean mInnerTransactionIsSuccessful; - - /** - * Valid during the life of a transaction, and indicates whether the entire transaction (the - * outer one and all of the inner ones) so far has been successful. - */ - private boolean mTransactionIsSuccessful; - - /** - * Valid during the life of a transaction. - */ - private SQLiteTransactionListener mTransactionListener; - - /** - * this member is set if {@link #execSQL(String)} is used to begin and end transactions. - */ - private boolean mTransactionUsingExecSql; - - /** Synchronize on this when accessing the database */ - private final DatabaseReentrantLock mLock = new DatabaseReentrantLock(true); - - private long mLockAcquiredWallTime = 0L; - private long mLockAcquiredThreadTime = 0L; - - // limit the frequency of complaints about each database to one within 20 sec - // unless run command adb shell setprop log.tag.Database VERBOSE - private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; - /** If the lock is held this long then a warning will be printed when it is released. */ - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; - private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; - - private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; - - // The pattern we remove from database filenames before - // potentially logging them. - private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); - - private long mLastLockMessageTime = 0L; - - // Things related to query logging/sampling for debugging - // slow/frequent queries during development. Always log queries - // which take (by default) 500ms+; shorter queries are sampled - // accordingly. Commit statements, which are typically slow, are - // logged together with the most recently executed SQL statement, - // for disambiguation. The 500ms value is configurable via a - // SystemProperty, but developers actively debugging database I/O - // should probably use the regular log tunable, - // LOG_SLOW_QUERIES_PROPERTY, defined below. - private static int sQueryLogTimeInMillis = 0; // lazily initialized - private static final int QUERY_LOG_SQL_LENGTH = 64; - private static final String COMMIT_SQL = "COMMIT;"; - private static final String BEGIN_SQL = "BEGIN;"; - private final Random mRandom = new Random(); - /** the last non-commit/rollback sql statement in a transaction */ - // guarded by 'this' - private String mLastSqlStatement = null; - - synchronized String getLastSqlStatement() { - return mLastSqlStatement; - } - - synchronized void setLastSqlStatement(String sql) { - mLastSqlStatement = sql; - } - - /** guarded by {@link #mLock} */ - private long mTransStartTime; - - // String prefix for slow database query EventLog records that show - // lock acquistions of the database. - /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; - - /** Used by native code, do not rename. make it volatile, so it is thread-safe. */ - /* package */ volatile int mNativeHandle = 0; - - /** - * The size, in bytes, of a block on "/data". This corresponds to the Unix - * statfs.f_bsize field. note that this field is lazily initialized. - */ - private static int sBlockSize = 0; - - /** The path for the database file */ - private final String mPath; - - /** The anonymized path for the database file for logging purposes */ - private String mPathForLogs = null; // lazily populated - - /** The flags passed to open/create */ - private final int mFlags; - - /** The optional factory to use when creating new Cursors */ - private final CursorFactory mFactory; - - private final WeakHashMap<SQLiteClosable, Object> mPrograms; - - /** Default statement-cache size per database connection ( = instance of this class) */ - private static final int DEFAULT_SQL_CACHE_SIZE = 25; - - /** - * for each instance of this class, a LRU cache is maintained to store - * the compiled query statement ids returned by sqlite database. - * key = SQL statement with "?" for bind args - * value = {@link SQLiteCompiledSql} - * If an application opens the database and keeps it open during its entire life, then - * there will not be an overhead of compilation of SQL statements by sqlite. - * - * why is this cache NOT static? because sqlite attaches compiledsql statements to the - * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is - * invoked. + * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}. * - * this cache's max size is settable by calling the method - * (@link #setMaxSqlCacheSize(int)}. - */ - // guarded by this - private LruCache<String, SQLiteCompiledSql> mCompiledQueries; - - /** - * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)} - * size of each prepared-statement is between 1K - 6K, depending on the complexity of the - * SQL statement & schema. + * Each prepared-statement is between 1K - 6K, depending on the complexity of the + * SQL statement & schema. A large SQL cache may use a significant amount of memory. */ public static final int MAX_SQL_CACHE_SIZE = 100; - private boolean mCacheFullWarning; - - /** Used to find out where this object was created in case it never got closed. */ - private final Throwable mStackTrace; - - /** stores the list of statement ids that need to be finalized by sqlite */ - private final ArrayList<Integer> mClosedStatementIds = new ArrayList<Integer>(); - - /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors - * Corruption - * */ - private final DatabaseErrorHandler mErrorHandler; - - /** The Database connection pool {@link DatabaseConnectionPool}. - * Visibility is package-private for testing purposes. otherwise, private visibility is enough. - */ - /* package */ volatile DatabaseConnectionPool mConnectionPool = null; - - /** Each database connection handle in the pool is assigned a number 1..N, where N is the - * size of the connection pool. - * The main connection handle to which the pool is attached is assigned a value of 0. - */ - /* package */ final short mConnectionNum; - /** on pooled database connections, this member points to the parent ( = main) - * database connection handle. - * package visibility only for testing purposes - */ - /* package */ SQLiteDatabase mParentConnObj = null; - - private static final String MEMORY_DB_PATH = ":memory:"; - - /** set to true if the database has attached databases */ - private volatile boolean mHasAttachedDbs = false; - - /** stores reference to all databases opened in the current process. */ - private static ArrayList<WeakReference<SQLiteDatabase>> mActiveDatabases = - new ArrayList<WeakReference<SQLiteDatabase>>(); - - synchronized void addSQLiteClosable(SQLiteClosable closable) { - // mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database - // itself. so, there is no need to lock(). - mPrograms.put(closable, null); + private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, + DatabaseErrorHandler errorHandler) { + mCursorFactory = cursorFactory; + mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); + mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); } - synchronized void removeSQLiteClosable(SQLiteClosable closable) { - mPrograms.remove(closable); + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } } @Override protected void onAllReferencesReleased() { - if (isOpen()) { - // close the database which will close all pending statements to be finalized also - close(); + dispose(false); + } + + private void dispose(boolean finalized) { + final SQLiteConnectionPool pool; + synchronized (mLock) { + if (mCloseGuardLocked != null) { + if (finalized) { + mCloseGuardLocked.warnIfOpen(); + } + mCloseGuardLocked.close(); + } + + pool = mConnectionPoolLocked; + mConnectionPoolLocked = null; + } + + if (!finalized) { + synchronized (sActiveDatabases) { + sActiveDatabases.remove(this); + } + + if (pool != null) { + pool.close(); + } } } @@ -364,7 +295,9 @@ public class SQLiteDatabase extends SQLiteClosable { * * @return the number of bytes actually released */ - static public native int releaseMemory(); + public static int releaseMemory() { + return SQLiteGlobal.releaseMemory(); + } /** * Control whether or not the SQLiteDatabase is made thread-safe by using locks @@ -372,159 +305,82 @@ public class SQLiteDatabase extends SQLiteClosable { * DB will only be used by a single thread then you should set this to false. * The default is true. * @param lockingEnabled set to true to enable locks, false otherwise + * + * @deprecated This method now does nothing. Do not use. */ + @Deprecated public void setLockingEnabled(boolean lockingEnabled) { - mLockingEnabled = lockingEnabled; } /** - * If set then the SQLiteDatabase is made thread-safe by using locks - * around critical sections + * Gets a label to use when describing the database in log messages. + * @return The label. */ - private boolean mLockingEnabled = true; - - /* package */ void onCorruption() { - EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - mErrorHandler.onCorruption(this); + String getLabel() { + synchronized (mLock) { + return mConfigurationLocked.label; + } } /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() + * Sends a corruption message to the database error handler. */ - /* package */ void lock(String sql) { - lock(sql, false); - } - - /* pachage */ void lock() { - lock(null, false); - } - - private static final long LOCK_WAIT_PERIOD = 30L; - private void lock(String sql, boolean forced) { - // make sure this method is NOT being called from a 'synchronized' method - if (Thread.holdsLock(this)) { - Log.w(TAG, "don't lock() while in a synchronized method"); - } - verifyDbIsOpen(); - if (!forced && !mLockingEnabled) return; - boolean done = false; - long timeStart = SystemClock.uptimeMillis(); - while (!done) { - try { - // wait for 30sec to acquire the lock - done = mLock.tryLock(LOCK_WAIT_PERIOD, TimeUnit.SECONDS); - if (!done) { - // lock not acquired in NSec. print a message and stacktrace saying the lock - // has not been available for 30sec. - Log.w(TAG, "database lock has not been available for " + LOCK_WAIT_PERIOD + - " sec. Current Owner of the lock is " + mLock.getOwnerDescription() + - ". Continuing to wait in thread: " + Thread.currentThread().getId()); - } - } catch (InterruptedException e) { - // ignore the interruption - } - } - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - // Use elapsed real-time since the CPU may sleep when waiting for IO - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - } - } - if (sql != null) { - if (ENABLE_DB_SAMPLE) { - logTimeStat(sql, timeStart, GET_LOCK_LOG_PREFIX); - } - } - } - private static class DatabaseReentrantLock extends ReentrantLock { - DatabaseReentrantLock(boolean fair) { - super(fair); - } - @Override - public Thread getOwner() { - return super.getOwner(); - } - public String getOwnerDescription() { - Thread t = getOwner(); - return (t== null) ? "none" : String.valueOf(t.getId()); - } + void onCorruption() { + EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel()); + mErrorHandler.onCorruption(this); } /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. + * Gets the {@link SQLiteSession} that belongs to this thread for this database. + * Once a thread has obtained a session, it will continue to obtain the same + * session even after the database has been closed (although the session will not + * be usable). However, a thread that does not already have a session cannot + * obtain one after the database has been closed. + * + * The idea is that threads that have active connections to the database may still + * have work to complete even after the call to {@link #close}. Active database + * connections are not actually disposed until they are released by the threads + * that own them. * - * @see #unlockForced() + * @return The session, never null. + * + * @throws IllegalStateException if the thread does not yet have a session and + * the database is not open. */ - private void lockForced() { - lock(null, true); + SQLiteSession getThreadSession() { + return mThreadSession.get(); // initialValue() throws if database closed } - private void lockForced(String sql) { - lock(sql, true); - } - - /** - * Releases the database lock. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() - */ - /* package */ void unlock() { - if (!mLockingEnabled) return; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } + SQLiteSession createSession() { + final SQLiteConnectionPool pool; + synchronized (mLock) { + throwIfNotOpenLocked(); + pool = mConnectionPoolLocked; } - mLock.unlock(); + return new SQLiteSession(pool); } /** - * Releases the database lock. + * Gets default connection flags that are appropriate for this thread, taking into + * account whether the thread is acting on behalf of the UI. * - * @see #unlockForced() + * @param readOnly True if the connection should be read-only. + * @return The connection flags. */ - private void unlockForced() { - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } + int getThreadDefaultConnectionFlags(boolean readOnly) { + int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY : + SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY; + if (isMainThread()) { + flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE; } - mLock.unlock(); + return flags; } - private void checkLockHoldTime() { - // Use elapsed real-time since the CPU may sleep when waiting for IO - long elapsedTime = SystemClock.elapsedRealtime(); - long lockedTime = elapsedTime - mLockAcquiredWallTime; - if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && - !Log.isLoggable(TAG, Log.VERBOSE) && - (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { - return; - } - if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) { - int threadTime = (int) - ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000); - if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS || - lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) { - mLastLockMessageTime = elapsedTime; - String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was " - + threadTime + "ms"; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) { - Log.d(TAG, msg, new Exception()); - } else { - Log.d(TAG, msg); - } - } - } + private static boolean isMainThread() { + // FIXME: There should be a better way to do this. + // Would also be nice to have something that would work across Binder calls. + Looper looper = Looper.myLooper(); + return looper != null && looper == Looper.getMainLooper(); } /** @@ -636,50 +492,9 @@ public class SQLiteDatabase extends SQLiteClosable { private void beginTransaction(SQLiteTransactionListener transactionListener, boolean exclusive) { - verifyDbIsOpen(); - lockForced(BEGIN_SQL); - boolean ok = false; - try { - // If this thread already had the lock then get out - if (mLock.getHoldCount() > 1) { - if (mInnerTransactionIsSuccessful) { - String msg = "Cannot call beginTransaction between " - + "calling setTransactionSuccessful and endTransaction"; - IllegalStateException e = new IllegalStateException(msg); - Log.e(TAG, "beginTransaction() failed", e); - throw e; - } - ok = true; - return; - } - - // This thread didn't already have the lock, so begin a database - // transaction now. - if (exclusive && mConnectionPool == null) { - execSQL("BEGIN EXCLUSIVE;"); - } else { - execSQL("BEGIN IMMEDIATE;"); - } - mTransStartTime = SystemClock.uptimeMillis(); - mTransactionListener = transactionListener; - mTransactionIsSuccessful = true; - mInnerTransactionIsSuccessful = false; - if (transactionListener != null) { - try { - transactionListener.onBegin(); - } catch (RuntimeException e) { - execSQL("ROLLBACK;"); - throw e; - } - } - ok = true; - } finally { - if (!ok) { - // beginTransaction is called before the try block so we must release the lock in - // the case of failure. - unlockForced(); - } - } + getThreadSession().beginTransaction(exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE : + SQLiteSession.TRANSACTION_MODE_IMMEDIATE, transactionListener, + getThreadDefaultConnectionFlags(false /*readOnly*/), null); } /** @@ -687,68 +502,7 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { - verifyLockOwner(); - try { - if (mInnerTransactionIsSuccessful) { - mInnerTransactionIsSuccessful = false; - } else { - mTransactionIsSuccessful = false; - } - if (mLock.getHoldCount() != 1) { - return; - } - RuntimeException savedException = null; - if (mTransactionListener != null) { - try { - if (mTransactionIsSuccessful) { - mTransactionListener.onCommit(); - } else { - mTransactionListener.onRollback(); - } - } catch (RuntimeException e) { - savedException = e; - mTransactionIsSuccessful = false; - } - } - if (mTransactionIsSuccessful) { - execSQL(COMMIT_SQL); - // if write-ahead logging is used, we have to take care of checkpoint. - // TODO: should applications be given the flexibility of choosing when to - // trigger checkpoint? - // for now, do checkpoint after every COMMIT because that is the fastest - // way to guarantee that readers will see latest data. - // but this is the slowest way to run sqlite with in write-ahead logging mode. - if (this.mConnectionPool != null) { - execSQL("PRAGMA wal_checkpoint;"); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.i(TAG, "PRAGMA wal_Checkpoint done"); - } - } - // log the transaction time to the Eventlog. - if (ENABLE_DB_SAMPLE) { - logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL); - } - } else { - try { - execSQL("ROLLBACK;"); - if (savedException != null) { - throw savedException; - } - } catch (SQLException e) { - if (false) { - Log.d(TAG, "exception during rollback, maybe the DB previously " - + "performed an auto-rollback"); - } - } - } - } finally { - mTransactionListener = null; - unlockForced(); - if (false) { - Log.v(TAG, "unlocked " + Thread.currentThread() - + ", holdCount is " + mLock.getHoldCount()); - } - } + getThreadSession().endTransaction(null); } /** @@ -761,86 +515,46 @@ public class SQLiteDatabase extends SQLiteClosable { * transaction is already marked as successful. */ public void setTransactionSuccessful() { - verifyDbIsOpen(); - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } - if (mInnerTransactionIsSuccessful) { - throw new IllegalStateException( - "setTransactionSuccessful may only be called once per call to beginTransaction"); - } - mInnerTransactionIsSuccessful = true; + getThreadSession().setTransactionSuccessful(); } /** - * return true if there is a transaction pending + * Returns true if the current thread has a transaction pending. + * + * @return True if the current thread is in a transaction. */ public boolean inTransaction() { - return mLock.getHoldCount() > 0 || mTransactionUsingExecSql; - } - - /* package */ synchronized void setTransactionUsingExecSqlFlag() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "found execSQL('begin transaction')"); - } - mTransactionUsingExecSql = true; - } - - /* package */ synchronized void resetTransactionUsingExecSqlFlag() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - if (mTransactionUsingExecSql) { - Log.i(TAG, "found execSQL('commit or end or rollback')"); - } - } - mTransactionUsingExecSql = false; + return getThreadSession().hasTransaction(); } /** - * Returns true if the caller is considered part of the current transaction, if any. + * Returns true if the current thread is holding an active connection to the database. * <p> - * Caller is part of the current transaction if either of the following is true - * <ol> - * <li>If transaction is started by calling beginTransaction() methods AND if the caller is - * in the same thread as the thread that started the transaction. - * </li> - * <li>If the transaction is started by calling {@link #execSQL(String)} like this: - * execSQL("BEGIN transaction"). In this case, every thread in the process is considered - * part of the current transaction.</li> - * </ol> - * - * @return true if the caller is considered part of the current transaction, if any. - */ - /* package */ synchronized boolean amIInTransaction() { - // always do this test on the main database connection - NOT on pooled database connection - // since transactions always occur on the main database connections only. - SQLiteDatabase db = (isPooledConnection()) ? mParentConnObj : this; - boolean b = (!db.inTransaction()) ? false : - db.mTransactionUsingExecSql || db.mLock.isHeldByCurrentThread(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "amIinTransaction: " + b); - } - return b; - } - - /** - * Checks if the database lock is held by this thread. + * The name of this method comes from a time when having an active connection + * to the database meant that the thread was holding an actual lock on the + * database. Nowadays, there is no longer a true "database lock" although threads + * may block if they cannot acquire a database connection to perform a + * particular operation. + * </p> * - * @return true, if this thread is holding the database lock. + * @return True if the current thread is holding an active connection to the database. */ public boolean isDbLockedByCurrentThread() { - return mLock.isHeldByCurrentThread(); + return getThreadSession().hasConnection(); } /** - * Checks if the database is locked by another thread. This is - * just an estimate, since this status can change at any time, - * including after the call is made but before the result has - * been acted upon. + * Always returns false. + * <p> + * There is no longer the concept of a database lock, so this method always returns false. + * </p> * - * @return true, if the database is locked by another thread + * @return False. + * @deprecated Always returns false. Do not use this method. */ + @Deprecated public boolean isDbLockedByOtherThreads() { - return !mLock.isHeldByCurrentThread() && mLock.isLocked(); + return false; } /** @@ -884,46 +598,12 @@ public class SQLiteDatabase extends SQLiteClosable { return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); } - private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { - if (mLock.getQueueLength() == 0) { - // Reset the lock acquire time since we know that the thread was willing to yield - // the lock at this time. - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - return false; - } - setTransactionSuccessful(); - SQLiteTransactionListener transactionListener = mTransactionListener; - endTransaction(); - if (checkFullyYielded) { - if (this.isDbLockedByCurrentThread()) { - throw new IllegalStateException( - "Db locked more than once. yielfIfContended cannot yield"); - } - } - if (sleepAfterYieldDelay > 0) { - // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to - // check if anyone is using the database. If the database is not contended, - // retake the lock and return. - long remainingDelay = sleepAfterYieldDelay; - while (remainingDelay > 0) { - try { - Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? - remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); - } catch (InterruptedException e) { - Thread.interrupted(); - } - remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; - if (mLock.getQueueLength() == 0) { - break; - } - } - } - beginTransactionWithListener(transactionListener); - return true; + private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) { + return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null); } /** + * Deprecated. * @deprecated This method no longer serves any useful purpose and has been deprecated. */ @Deprecated @@ -932,19 +612,6 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Used to allow returning sub-classes of {@link Cursor} when calling query. - */ - public interface CursorFactory { - /** - * See - * {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. - */ - public Cursor newCursor(SQLiteDatabase db, - SQLiteCursorDriver masterQuery, String editTable, - SQLiteQuery query); - } - - /** * Open the database according to the flags {@link #OPEN_READWRITE} * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. * @@ -983,50 +650,9 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { - SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler, - (short) 0 /* the main connection handle */); - - // set sqlite pagesize to mBlockSize - if (sBlockSize == 0) { - // TODO: "/data" should be a static final String constant somewhere. it is hardcoded - // in several places right now. - sBlockSize = new StatFs("/data").getBlockSize(); - } - sqliteDatabase.setPageSize(sBlockSize); - sqliteDatabase.setJournalMode(path, "TRUNCATE"); - - // add this database to the list of databases opened in this process - synchronized(mActiveDatabases) { - mActiveDatabases.add(new WeakReference<SQLiteDatabase>(sqliteDatabase)); - } - return sqliteDatabase; - } - - private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, - DatabaseErrorHandler errorHandler, short connectionNum) { - SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum); - try { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "opening the db : " + path); - } - // Open the database. - db.dbopen(path, flags); - db.setLocale(Locale.getDefault()); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - db.enableSqlTracing(path, connectionNum); - } - if (SQLiteDebug.DEBUG_SQL_TIME) { - db.enableSqlProfiling(path, connectionNum); - } - return db; - } catch (SQLiteDatabaseCorruptException e) { - db.mErrorHandler.onCorruption(db); - return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler); - } catch (SQLiteException e) { - Log.e(TAG, "Failed to open the database. closing it.", e); - db.close(); - throw e; - } + SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler); + db.open(); + return db; } /** @@ -1051,16 +677,46 @@ public class SQLiteDatabase extends SQLiteClosable { return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); } - private void setJournalMode(final String dbPath, final String mode) { - // journal mode can be set only for non-memory databases + private void open() { + try { + try { + openInner(); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + openInner(); + } + + // Disable WAL if it was previously enabled. + setJournalMode("TRUNCATE"); + } catch (SQLiteException ex) { + Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); + close(); + throw ex; + } + } + + private void openInner() { + synchronized (mLock) { + assert mConnectionPoolLocked == null; + mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); + mCloseGuardLocked.open("close"); + } + + synchronized (sActiveDatabases) { + sActiveDatabases.put(this, null); + } + } + + private void setJournalMode(String mode) { + // Journal mode can be set only for non-memory databases // AND can't be set for readonly databases - if (dbPath.equalsIgnoreCase(MEMORY_DB_PATH) || isReadOnly()) { + if (isInMemoryDatabase() || isReadOnly()) { return; } String s = DatabaseUtils.stringForQuery(this, "PRAGMA journal_mode=" + mode, null); if (!s.equalsIgnoreCase(mode)) { - Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + dbPath + - " (on pragma set journal_mode, sqlite returned:" + s); + Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + getLabel() + + " (on pragma set journal_mode, sqlite returned:" + s); } } @@ -1077,159 +733,37 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase create(CursorFactory factory) { // This is a magic string with special meaning for SQLite. - return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY); + return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, + factory, CREATE_IF_NECESSARY); } /** * Close the database. */ public void close() { - if (!isOpen()) { - return; - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum); - } - lock(); - try { - // some other thread could have closed this database while I was waiting for lock. - // check the database state - if (!isOpen()) { - return; - } - closeClosable(); - // finalize ALL statements queued up so far - closePendingStatements(); - releaseCustomFunctions(); - // close this database instance - regardless of its reference count value - closeDatabase(); - if (mConnectionPool != null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert mConnectionPool != null; - Log.i(TAG, mConnectionPool.toString()); - } - mConnectionPool.close(); - } - } finally { - unlock(); - } - } - - private void closeClosable() { - /* deallocate all compiled SQL statement objects from mCompiledQueries cache. - * this should be done before de-referencing all {@link SQLiteClosable} objects - * from this database object because calling - * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database - * to be closed. sqlite doesn't let a database close if there are - * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. - */ - deallocCachedSqlStatements(); - - Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry<SQLiteClosable, Object> entry = iter.next(); - SQLiteClosable program = entry.getKey(); - if (program != null) { - program.onAllReferencesReleasedFromContainer(); - } - } - } - - /** - * package level access for testing purposes - */ - /* package */ void closeDatabase() throws SQLiteException { - try { - dbclose(); - } catch (SQLiteUnfinalizedObjectsException e) { - String msg = e.getMessage(); - String[] tokens = msg.split(",", 2); - int stmtId = Integer.parseInt(tokens[0]); - // get extra info about this statement, if it is still to be released by closeClosable() - Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); - boolean found = false; - while (iter.hasNext()) { - Map.Entry<SQLiteClosable, Object> entry = iter.next(); - SQLiteClosable program = entry.getKey(); - if (program != null && program instanceof SQLiteProgram) { - SQLiteCompiledSql compiledSql = ((SQLiteProgram)program).mCompiledSql; - if (compiledSql.nStatement == stmtId) { - msg = compiledSql.toString(); - found = true; - } - } - } - if (!found) { - // the statement is already released by closeClosable(). is it waiting to be - // finalized? - if (mClosedStatementIds.contains(stmtId)) { - Log.w(TAG, "this shouldn't happen. finalizing the statement now: "); - closePendingStatements(); - // try to close the database again - closeDatabase(); - } - } else { - // the statement is not yet closed. most probably programming error in the app. - throw new SQLiteUnfinalizedObjectsException( - "close() on database: " + getPath() + - " failed due to un-close()d SQL statements: " + msg); - } - } - } - - /** - * Native call to close the database. - */ - private native void dbclose(); - - /** - * A callback interface for a custom sqlite3 function. - * This can be used to create a function that can be called from - * sqlite3 database triggers. - * @hide - */ - public interface CustomFunction { - public void callback(String[] args); + dispose(false); } /** * Registers a CustomFunction callback as a function that can be called from - * sqlite3 database triggers. + * SQLite database triggers. + * * @param name the name of the sqlite3 function * @param numArgs the number of arguments for the function * @param function callback to call when the function is executed * @hide */ public void addCustomFunction(String name, int numArgs, CustomFunction function) { - verifyDbIsOpen(); - synchronized (mCustomFunctions) { - int ref = native_addCustomFunction(name, numArgs, function); - if (ref != 0) { - // save a reference to the function for cleanup later - mCustomFunctions.add(new Integer(ref)); - } else { - throw new SQLiteException("failed to add custom function " + name); - } - } - } + // Create wrapper (also validates arguments). + SQLiteCustomFunction wrapper = new SQLiteCustomFunction(name, numArgs, function); - private void releaseCustomFunctions() { - synchronized (mCustomFunctions) { - for (int i = 0; i < mCustomFunctions.size(); i++) { - Integer function = mCustomFunctions.get(i); - native_releaseCustomFunction(function.intValue()); - } - mCustomFunctions.clear(); + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.customFunctions.add(wrapper); + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } } - // list of CustomFunction references so we can clean up when the database closes - private final ArrayList<Integer> mCustomFunctions = - new ArrayList<Integer>(); - - private native int native_addCustomFunction(String name, int numArgs, CustomFunction function); - private native void native_releaseCustomFunction(int function); - /** * Gets the database version. * @@ -1364,7 +898,7 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. */ public SQLiteStatement compileStatement(String sql) throws SQLException { - verifyDbIsOpen(); + throwIfNotOpen(); // fail fast return new SQLiteStatement(this, sql, null); } @@ -1403,7 +937,48 @@ public class SQLiteDatabase extends SQLiteClosable { String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, - groupBy, having, orderBy, limit); + groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * @see Cursor + */ + public Cursor query(boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancelationSignal cancelationSignal) { + return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, + groupBy, having, orderBy, limit, cancelationSignal); } /** @@ -1442,12 +1017,55 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { - verifyDbIsOpen(); + return queryWithFactory(cursorFactory, distinct, table, columns, selection, + selectionArgs, groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * @see Cursor + */ + public Cursor queryWithFactory(CursorFactory cursorFactory, + boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancelationSignal cancelationSignal) { + throwIfNotOpen(); // fail fast String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); - return rawQueryWithFactory( - cursorFactory, sql, selectionArgs, findEditTable(table)); + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, + findEditTable(table), cancelationSignal); } /** @@ -1535,7 +1153,25 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link Cursor}s are not synchronized, see the documentation for more details. */ public Cursor rawQuery(String sql, String[] selectionArgs) { - return rawQueryWithFactory(null, sql, selectionArgs, null); + return rawQueryWithFactory(null, sql, selectionArgs, null, null); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + CancelationSignal cancelationSignal) { + return rawQueryWithFactory(null, sql, selectionArgs, null, cancelationSignal); } /** @@ -1553,21 +1189,33 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { - verifyDbIsOpen(); - BlockGuard.getThreadPolicy().onReadFromDisk(); + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null); + } - SQLiteDatabase db = getDbConnection(sql); - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); + /** + * Runs the provided SQL and returns a cursor over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param editTable the name of the first table, which is editable + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + */ + public Cursor rawQueryWithFactory( + CursorFactory cursorFactory, String sql, String[] selectionArgs, + String editTable, CancelationSignal cancelationSignal) { + throwIfNotOpen(); // fail fast - Cursor cursor = null; - try { - cursor = driver.query( - cursorFactory != null ? cursorFactory : mFactory, - selectionArgs); - } finally { - releaseDbConnection(db); - } - return cursor; + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable, + cancelationSignal); + return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory, + selectionArgs); } /** @@ -1716,9 +1364,6 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { return statement.executeInsert(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1739,9 +1384,6 @@ public class SQLiteDatabase extends SQLiteClosable { (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1808,9 +1450,6 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1891,266 +1530,107 @@ public class SQLiteDatabase extends SQLiteClosable { private int executeSql(String sql, Object[] bindArgs) throws SQLException { if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { - disableWriteAheadLogging(); - mHasAttachedDbs = true; + boolean disableWal = false; + synchronized (mLock) { + if (!mHasAttachedDbsLocked) { + mHasAttachedDbsLocked = true; + disableWal = true; + } + } + if (disableWal) { + disableWriteAheadLogging(); + } } + SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } } - @Override - protected void finalize() throws Throwable { - try { - if (isOpen()) { - Log.e(TAG, "close() was never explicitly called on database '" + - mPath + "' ", mStackTrace); - closeClosable(); - onAllReferencesReleased(); - releaseCustomFunctions(); - } - } finally { - super.finalize(); - } - } - /** - * Private constructor. + * Returns true if the database is opened as read only. * - * @param path The full path to the database - * @param factory The factory to use when creating cursors, may be NULL. - * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already - * exists, mFlags will be updated appropriately. - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption. may be NULL. - * @param connectionNum 0 for main database connection handle. 1..N for pooled database - * connection handles. + * @return True if database is opened as read only. */ - private SQLiteDatabase(String path, CursorFactory factory, int flags, - DatabaseErrorHandler errorHandler, short connectionNum) { - if (path == null) { - throw new IllegalArgumentException("path should not be null"); + public boolean isReadOnly() { + synchronized (mLock) { + return isReadOnlyLocked(); } - setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE); - mFlags = flags; - mPath = path; - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - mFactory = factory; - mPrograms = new WeakHashMap<SQLiteClosable,Object>(); - // Set the DatabaseErrorHandler to be used when SQLite reports corruption. - // If the caller sets errorHandler = null, then use default errorhandler. - mErrorHandler = (errorHandler == null) ? new DefaultDatabaseErrorHandler() : errorHandler; - mConnectionNum = connectionNum; - /* sqlite soft heap limit http://www.sqlite.org/c3ref/soft_heap_limit64.html - * set it to 4 times the default cursor window size. - * TODO what is an appropriate value, considering the WAL feature which could burn - * a lot of memory with many connections to the database. needs testing to figure out - * optimal value for this. - */ - int limit = Resources.getSystem().getInteger( - com.android.internal.R.integer.config_cursorWindowSize) * 1024 * 4; - native_setSqliteSoftHeapLimit(limit); + } + + private boolean isReadOnlyLocked() { + return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY; } /** - * return whether the DB is opened as read only. - * @return true if DB is opened as read only + * Returns true if the database is in-memory db. + * + * @return True if the database is in-memory. + * @hide */ - public boolean isReadOnly() { - return (mFlags & OPEN_READ_MASK) == OPEN_READONLY; + public boolean isInMemoryDatabase() { + synchronized (mLock) { + return mConfigurationLocked.isInMemoryDb(); + } } /** - * @return true if the DB is currently open (has not been closed) + * Returns true if the database is currently open. + * + * @return True if the database is currently open (has not been closed). */ public boolean isOpen() { - return mNativeHandle != 0; + synchronized (mLock) { + return mConnectionPoolLocked != null; + } } + /** + * Returns true if the new version code is greater than the current database version. + * + * @param newVersion The new version code. + * @return True if the new version code is greater than the current database version. + */ public boolean needUpgrade(int newVersion) { return newVersion > getVersion(); } /** - * Getter for the path to the database file. + * Gets the path to the database file. * - * @return the path to our database file. + * @return The path to the database file. */ public final String getPath() { - return mPath; - } - - /* package */ void logTimeStat(String sql, long beginMillis) { - if (ENABLE_DB_SAMPLE) { - logTimeStat(sql, beginMillis, null); - } - } - - private void logTimeStat(String sql, long beginMillis, String prefix) { - // Sample fast queries in proportion to the time taken. - // Quantize the % first, so the logged sampling probability - // exactly equals the actual sampling rate for this query. - - int samplePercent; - long durationMillis = SystemClock.uptimeMillis() - beginMillis; - if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) { - // The common case is locks being uncontended. Don't log those, - // even at 1%, which is our default below. - return; - } - if (sQueryLogTimeInMillis == 0) { - sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500); - } - if (durationMillis >= sQueryLogTimeInMillis) { - samplePercent = 100; - } else { - samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1; - if (mRandom.nextInt(100) >= samplePercent) return; - } - - // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do - // it here so we avoid allocating in the common case. - if (prefix != null) { - sql = prefix + sql; + synchronized (mLock) { + return mConfigurationLocked.path; } - if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); - - // ActivityThread.currentPackageName() only returns non-null if the - // current thread is an application main thread. This parameter tells - // us whether an event loop is blocked, and if so, which app it is. - // - // Sadly, there's no fast way to determine app name if this is *not* a - // main thread, or when we are invoked via Binder (e.g. ContentProvider). - // Hopefully the full path to the database will be informative enough. - - String blockingPackage = AppGlobals.getInitialPackage(); - if (blockingPackage == null) blockingPackage = ""; - - EventLog.writeEvent( - EVENT_DB_OPERATION, - getPathForLogs(), - sql, - durationMillis, - blockingPackage, - samplePercent); - } - - /** - * Removes email addresses from database filenames before they're - * logged to the EventLog where otherwise apps could potentially - * read them. - */ - private String getPathForLogs() { - if (mPathForLogs != null) { - return mPathForLogs; - } - if (mPath == null) { - return null; - } - if (mPath.indexOf('@') == -1) { - mPathForLogs = mPath; - } else { - mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); - } - return mPathForLogs; } /** * Sets the locale for this database. Does nothing if this database has * the NO_LOCALIZED_COLLATORS flag set or was opened read only. + * + * @param locale The new locale. + * * @throws SQLException if the locale could not be set. The most common reason * for this is that there is no collator available for the locale you requested. * In this case the database remains unchanged. */ public void setLocale(Locale locale) { - lock(); - try { - native_setLocale(locale.toString(), mFlags); - } finally { - unlock(); + if (locale == null) { + throw new IllegalArgumentException("locale must not be null."); } - } - /* package */ void verifyDbIsOpen() { - if (!isOpen()) { - throw new IllegalStateException("database " + getPath() + " (conn# " + - mConnectionNum + ") already closed"); + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.locale = locale; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } } - /* package */ void verifyLockOwner() { - verifyDbIsOpen(); - if (mLockingEnabled && !isDbLockedByCurrentThread()) { - throw new IllegalStateException("Don't have database lock!"); - } - } - - /** - * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the - * cache of compiledQueries attached to 'this'. - * <p> - * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL, - * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current - * mapping is NOT replaced with the new mapping). - */ - /* package */ synchronized void addToCompiledQueries( - String sql, SQLiteCompiledSql compiledStatement) { - // don't insert the new mapping if a mapping already exists - if (mCompiledQueries.get(sql) != null) { - return; - } - - int maxCacheSz = (mConnectionNum == 0) ? mCompiledQueries.maxSize() : - mParentConnObj.mCompiledQueries.maxSize(); - - if (SQLiteDebug.DEBUG_SQL_CACHE) { - boolean printWarning = (mConnectionNum == 0) - ? (!mCacheFullWarning && mCompiledQueries.size() == maxCacheSz) - : (!mParentConnObj.mCacheFullWarning && - mParentConnObj.mCompiledQueries.size() == maxCacheSz); - if (printWarning) { - /* - * cache size is not enough for this app. log a warning. - * chances are it is NOT using ? for bindargs - or cachesize is too small. - */ - Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. "); - mCacheFullWarning = true; - Log.d(TAG, "Here are the SQL statements in Cache of database: " + mPath); - for (String s : mCompiledQueries.snapshot().keySet()) { - Log.d(TAG, "Sql statement in Cache: " + s); - } - } - } - /* add the given SQLiteCompiledSql compiledStatement to cache. - * no need to worry about the cache size - because {@link #mCompiledQueries} - * self-limits its size. - */ - mCompiledQueries.put(sql, compiledStatement); - } - - /** package-level access for testing purposes */ - /* package */ synchronized void deallocCachedSqlStatements() { - for (SQLiteCompiledSql compiledSql : mCompiledQueries.snapshot().values()) { - compiledSql.releaseSqlStatement(); - } - mCompiledQueries.evictAll(); - } - - /** - * From the compiledQueries cache, returns the compiled-statement-id for the given SQL. - * Returns null, if not found in the cache. - */ - /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) { - return mCompiledQueries.get(sql); - } - /** * Sets the maximum size of the prepared-statement cache for this database. * (size of the cache = number of compiled-sql-statements stored in the cache). @@ -2162,113 +1642,19 @@ public class SQLiteDatabase extends SQLiteClosable { * This method is thread-safe. * * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) - * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or - * the value set with previous setMaxSqlCacheSize() call. + * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}. */ public void setMaxSqlCacheSize(int cacheSize) { - synchronized (this) { - LruCache<String, SQLiteCompiledSql> oldCompiledQueries = mCompiledQueries; - if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException( - "expected value between 0 and " + MAX_SQL_CACHE_SIZE); - } else if (oldCompiledQueries != null && cacheSize < oldCompiledQueries.maxSize()) { - throw new IllegalStateException("cannot set cacheSize to a value less than the " - + "value set with previous setMaxSqlCacheSize() call."); - } - mCompiledQueries = new LruCache<String, SQLiteCompiledSql>(cacheSize) { - @Override - protected void entryRemoved(boolean evicted, String key, SQLiteCompiledSql oldValue, - SQLiteCompiledSql newValue) { - verifyLockOwner(); - oldValue.releaseIfNotInUse(); - } - }; - if (oldCompiledQueries != null) { - for (Map.Entry<String, SQLiteCompiledSql> entry - : oldCompiledQueries.snapshot().entrySet()) { - mCompiledQueries.put(entry.getKey(), entry.getValue()); - } - } - } - } - - /* package */ synchronized boolean isInStatementCache(String sql) { - return mCompiledQueries.get(sql) != null; - } - - /* package */ synchronized void releaseCompiledSqlObj( - String sql, SQLiteCompiledSql compiledSql) { - if (mCompiledQueries.get(sql) == compiledSql) { - // it is in cache - reset its inUse flag - compiledSql.release(); - } else { - // it is NOT in cache. finalize it. - compiledSql.releaseSqlStatement(); - } - } - - private synchronized int getCacheHitNum() { - return mCompiledQueries.hitCount(); - } - - private synchronized int getCacheMissNum() { - return mCompiledQueries.missCount(); - } - - private synchronized int getCachesize() { - return mCompiledQueries.size(); - } - - /* package */ void finalizeStatementLater(int id) { - if (!isOpen()) { - // database already closed. this statement will already have been finalized. - return; - } - synchronized(mClosedStatementIds) { - if (mClosedStatementIds.contains(id)) { - // this statement id is already queued up for finalization. - return; - } - mClosedStatementIds.add(id); - } - } - - /* package */ boolean isInQueueOfStatementsToBeFinalized(int id) { - if (!isOpen()) { - // database already closed. this statement will already have been finalized. - // return true so that the caller doesn't have to worry about finalizing this statement. - return true; - } - synchronized(mClosedStatementIds) { - return mClosedStatementIds.contains(id); + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException( + "expected value between 0 and " + MAX_SQL_CACHE_SIZE); } - } - /* package */ void closePendingStatements() { - if (!isOpen()) { - // since this database is already closed, no need to finalize anything. - mClosedStatementIds.clear(); - return; - } - verifyLockOwner(); - /* to minimize synchronization on mClosedStatementIds, make a copy of the list */ - ArrayList<Integer> list = new ArrayList<Integer>(mClosedStatementIds.size()); - synchronized(mClosedStatementIds) { - list.addAll(mClosedStatementIds); - mClosedStatementIds.clear(); + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.maxSqlCacheSize = cacheSize; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } - // finalize all the statements from the copied list - int size = list.size(); - for (int i = 0; i < size; i++) { - native_finalize(list.get(i)); - } - } - - /** - * for testing only - */ - /* package */ ArrayList<Integer> getQueuedUpStmtList() { - return mClosedStatementIds; } /** @@ -2314,37 +1700,43 @@ public class SQLiteDatabase extends SQLiteClosable { * @return true if write-ahead-logging is set. false otherwise */ public boolean enableWriteAheadLogging() { - // make sure the database is not READONLY. WAL doesn't make sense for readonly-databases. - if (isReadOnly()) { - return false; - } - // acquire lock - no that no other thread is enabling WAL at the same time - lock(); - try { - if (mConnectionPool != null) { - // already enabled + synchronized (mLock) { + throwIfNotOpenLocked(); + + if (mIsWALEnabledLocked) { return true; } - if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + + if (isReadOnlyLocked()) { + // WAL doesn't make sense for readonly-databases. + // TODO: True, but connection pooling does still make sense... + return false; + } + + if (mConfigurationLocked.isInMemoryDb()) { Log.i(TAG, "can't enable WAL for memory databases."); return false; } // make sure this database has NO attached databases because sqlite's write-ahead-logging // doesn't work for databases with attached databases - if (mHasAttachedDbs) { + if (mHasAttachedDbsLocked) { if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, - "this database: " + mPath + " has attached databases. can't enable WAL."); + Log.d(TAG, "this database: " + mConfigurationLocked.label + + " has attached databases. can't enable WAL."); } return false; } - mConnectionPool = new DatabaseConnectionPool(this); - setJournalMode(mPath, "WAL"); - return true; - } finally { - unlock(); + + mIsWALEnabledLocked = true; + mConfigurationLocked.maxConnectionPoolSize = Math.max(2, + Resources.getSystem().getInteger( + com.android.internal.R.integer.db_connection_pool_size)); + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } + + setJournalMode("WAL"); + return true; } /** @@ -2352,176 +1744,66 @@ public class SQLiteDatabase extends SQLiteClosable { * @hide */ public void disableWriteAheadLogging() { - // grab database lock so that writeAheadLogging is not disabled from 2 different threads - // at the same time - lock(); - try { - if (mConnectionPool == null) { - return; // already disabled - } - mConnectionPool.close(); - setJournalMode(mPath, "TRUNCATE"); - mConnectionPool = null; - } finally { - unlock(); - } - } + synchronized (mLock) { + throwIfNotOpenLocked(); - /* package */ SQLiteDatabase getDatabaseHandle(String sql) { - if (isPooledConnection()) { - // this is a pooled database connection - // use it if it is open AND if I am not currently part of a transaction - if (isOpen() && !amIInTransaction()) { - // TODO: use another connection from the pool - // if this connection is currently in use by some other thread - // AND if there are free connections in the pool - return this; - } else { - // the pooled connection is not open! could have been closed either due - // to corruption on this or some other connection to the database - // OR, maybe the connection pool is disabled after this connection has been - // allocated to me. try to get some other pooled or main database connection - return getParentDbConnObj().getDbConnection(sql); + if (!mIsWALEnabledLocked) { + return; } - } else { - // this is NOT a pooled connection. can we get one? - return getDbConnection(sql); - } - } - - /* package */ SQLiteDatabase createPoolConnection(short connectionNum) { - SQLiteDatabase db = openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum); - db.mParentConnObj = this; - return db; - } - private synchronized SQLiteDatabase getParentDbConnObj() { - return mParentConnObj; - } + mIsWALEnabledLocked = false; + mConfigurationLocked.maxConnectionPoolSize = 1; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } - private boolean isPooledConnection() { - return this.mConnectionNum > 0; + setJournalMode("TRUNCATE"); } - /* package */ SQLiteDatabase getDbConnection(String sql) { - verifyDbIsOpen(); - // this method should always be called with main database connection handle. - // the only time when it is called with pooled database connection handle is - // corruption occurs while trying to open a pooled database connection handle. - // in that case, simply return 'this' handle - if (isPooledConnection()) { - return this; + /** + * Collect statistics about all open databases in the current process. + * Used by bug report. + */ + static ArrayList<DbStats> getDbStats() { + ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); + for (SQLiteDatabase db : getActiveDatabases()) { + db.collectDbStats(dbStatsList); } + return dbStatsList; + } - // use the current connection handle if - // 1. if the caller is part of the ongoing transaction, if any - // 2. OR, if there is NO connection handle pool setup - if (amIInTransaction() || mConnectionPool == null) { - return this; - } else { - // get a connection handle from the pool - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert mConnectionPool != null; - Log.i(TAG, mConnectionPool.toString()); + private void collectDbStats(ArrayList<DbStats> dbStatsList) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + mConnectionPoolLocked.collectDbStats(dbStatsList); } - return mConnectionPool.get(sql); } } - private void releaseDbConnection(SQLiteDatabase db) { - // ignore this release call if - // 1. the database is closed - // 2. OR, if db is NOT a pooled connection handle - // 3. OR, if the database being released is same as 'this' (this condition means - // that we should always be releasing a pooled connection handle by calling this method - // from the 'main' connection handle - if (!isOpen() || !db.isPooledConnection() || (db == this)) { - return; - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert isPooledConnection(); - assert mConnectionPool != null; - Log.d(TAG, "releaseDbConnection threadid = " + Thread.currentThread().getId() + - ", releasing # " + db.mConnectionNum + ", " + getPath()); + private static ArrayList<SQLiteDatabase> getActiveDatabases() { + ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>(); + synchronized (sActiveDatabases) { + databases.addAll(sActiveDatabases.keySet()); } - mConnectionPool.release(db); + return databases; } /** - * this method is used to collect data about ALL open databases in the current process. - * bugreport is a user of this data. + * Dump detailed information about all open databases in the current process. + * Used by bug report. */ - /* package */ static ArrayList<DbStats> getDbStats() { - ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); - // make a local copy of mActiveDatabases - so that this method is not competing - // for synchronization lock on mActiveDatabases - ArrayList<WeakReference<SQLiteDatabase>> tempList; - synchronized(mActiveDatabases) { - tempList = (ArrayList<WeakReference<SQLiteDatabase>>)mActiveDatabases.clone(); + static void dumpAll(Printer printer, boolean verbose) { + for (SQLiteDatabase db : getActiveDatabases()) { + db.dump(printer, verbose); } - for (WeakReference<SQLiteDatabase> w : tempList) { - SQLiteDatabase db = w.get(); - if (db == null || !db.isOpen()) { - continue; - } + } - try { - // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db - int lookasideUsed = db.native_getDbLookaside(); - - // get the lastnode of the dbname - String path = db.getPath(); - int indx = path.lastIndexOf("/"); - String lastnode = path.substring((indx != -1) ? ++indx : 0); - - // get list of attached dbs and for each db, get its size and pagesize - List<Pair<String, String>> attachedDbs = db.getAttachedDbs(); - if (attachedDbs == null) { - continue; - } - for (int i = 0; i < attachedDbs.size(); i++) { - Pair<String, String> p = attachedDbs.get(i); - long pageCount = DatabaseUtils.longForQuery(db, "PRAGMA " + p.first - + ".page_count;", null); - - // first entry in the attached db list is always the main database - // don't worry about prefixing the dbname with "main" - String dbName; - if (i == 0) { - dbName = lastnode; - } else { - // lookaside is only relevant for the main db - lookasideUsed = 0; - dbName = " (attached) " + p.first; - // if the attached db has a path, attach the lastnode from the path to above - if (p.second.trim().length() > 0) { - int idx = p.second.lastIndexOf("/"); - dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); - } - } - if (pageCount > 0) { - dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed, db.getCacheHitNum(), db.getCacheMissNum(), - db.getCachesize())); - } - } - // if there are pooled connections, return the cache stats for them also. - // while we are trying to query the pooled connections for stats, some other thread - // could be disabling conneciton pool. so, grab a reference to the connection pool. - DatabaseConnectionPool connPool = db.mConnectionPool; - if (connPool != null) { - for (SQLiteDatabase pDb : connPool.getConnectionList()) { - dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") " - + lastnode, 0, 0, 0, pDb.getCacheHitNum(), - pDb.getCacheMissNum(), pDb.getCachesize())); - } - } - } catch (SQLiteException e) { - // ignore. we don't care about exceptions when we are taking adb - // bugreport! + private void dump(Printer printer, boolean verbose) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + printer.println(""); + mConnectionPoolLocked.dump(printer, verbose); } } - return dbStatsList; } /** @@ -2532,23 +1814,27 @@ public class SQLiteDatabase extends SQLiteClosable { * is not open. */ public List<Pair<String, String>> getAttachedDbs() { - if (!isOpen()) { - return null; - } ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); - if (!mHasAttachedDbs) { - // No attached databases. - // There is a small window where attached databases exist but this flag is not set yet. - // This can occur when this thread is in a race condition with another thread - // that is executing the SQL statement: "attach database <blah> as <foo>" - // If this thread is NOT ok with such a race condition (and thus possibly not receive - // the entire list of attached databases), then the caller should ensure that no thread - // is executing any SQL statements while a thread is calling this method. - // Typically, this method is called when 'adb bugreport' is done or the caller wants to - // collect stats on the database and all its attached databases. - attachedDbs.add(new Pair<String, String>("main", mPath)); - return attachedDbs; + synchronized (mLock) { + if (mConnectionPoolLocked == null) { + return null; // not open + } + + if (!mHasAttachedDbsLocked) { + // No attached databases. + // There is a small window where attached databases exist but this flag is not + // set yet. This can occur when this thread is in a race condition with another + // thread that is executing the SQL statement: "attach database <blah> as <foo>" + // If this thread is NOT ok with such a race condition (and thus possibly not + // receivethe entire list of attached databases), then the caller should ensure + // that no thread is executing any SQL statements while a thread is calling this + // method. Typically, this method is called when 'adb bugreport' is done or the + // caller wants to collect stats on the database and all its attached databases. + attachedDbs.add(new Pair<String, String>("main", mConfigurationLocked.path)); + return attachedDbs; + } } + // has attached databases. query sqlite to get the list of attached databases. Cursor c = null; try { @@ -2583,7 +1869,8 @@ public class SQLiteDatabase extends SQLiteClosable { * false otherwise. */ public boolean isDatabaseIntegrityOk() { - verifyDbIsOpen(); + throwIfNotOpen(); // fail fast + List<Pair<String, String>> attachedDbs = null; try { attachedDbs = getAttachedDbs(); @@ -2594,8 +1881,9 @@ public class SQLiteDatabase extends SQLiteClosable { } catch (SQLiteException e) { // can't get attachedDb list. do integrity check on the main database attachedDbs = new ArrayList<Pair<String, String>>(); - attachedDbs.add(new Pair<String, String>("main", this.mPath)); + attachedDbs.add(new Pair<String, String>("main", getPath())); } + for (int i = 0; i < attachedDbs.size(); i++) { Pair<String, String> p = attachedDbs.get(i); SQLiteStatement prog = null; @@ -2615,59 +1903,64 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Native call to open the database. + * Prevent other threads from using the database's primary connection. * - * @param path The full path to the database - */ - private native void dbopen(String path, int flags); - - /** - * Native call to setup tracing of all SQL statements + * This method is only used by {@link SQLiteOpenHelper} when transitioning from + * a readable to a writable database. It should not be used in any other way. * - * @param path the full path to the database - * @param connectionNum connection number: 0 - N, where the main database - * connection handle is numbered 0 and the connection handles in the connection - * pool are numbered 1..N. + * @see #unlockPrimaryConnection() */ - private native void enableSqlTracing(String path, short connectionNum); + void lockPrimaryConnection() { + getThreadSession().beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, + null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY, null); + } /** - * Native call to setup profiling of all SQL statements. - * currently, sqlite's profiling = printing of execution-time - * (wall-clock time) of each of the SQL statements, as they - * are executed. + * Allow other threads to use the database's primary connection. * - * @param path the full path to the database - * @param connectionNum connection number: 0 - N, where the main database - * connection handle is numbered 0 and the connection handles in the connection - * pool are numbered 1..N. + * @see #lockPrimaryConnection() */ - private native void enableSqlProfiling(String path, short connectionNum); + void unlockPrimaryConnection() { + getThreadSession().endTransaction(null); + } - /** - * Native call to set the locale. {@link #lock} must be held when calling - * this method. - * @throws SQLException - */ - private native void native_setLocale(String loc, int flags); + @Override + public String toString() { + return "SQLiteDatabase: " + getPath(); + } - /** - * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here - * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html - * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED - */ - private native int native_getDbLookaside(); + private void throwIfNotOpen() { + synchronized (mConnectionPoolLocked) { + throwIfNotOpenLocked(); + } + } + + private void throwIfNotOpenLocked() { + if (mConnectionPoolLocked == null) { + throw new IllegalStateException("The database '" + mConfigurationLocked.label + + "' is not open."); + } + } /** - * finalizes the given statement id. - * - * @param statementId statement to be finzlied by sqlite + * Used to allow returning sub-classes of {@link Cursor} when calling query. */ - private final native void native_finalize(int statementId); + public interface CursorFactory { + /** + * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. + */ + public Cursor newCursor(SQLiteDatabase db, + SQLiteCursorDriver masterQuery, String editTable, + SQLiteQuery query); + } /** - * set sqlite soft heap limit - * http://www.sqlite.org/c3ref/soft_heap_limit64.html + * A callback interface for a custom sqlite3 function. + * This can be used to create a function that can be called from + * sqlite3 database triggers. + * @hide */ - private native void native_setSqliteSoftHeapLimit(int softHeapLimit); + public interface CustomFunction { + public void callback(String[] args); + } } diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java new file mode 100644 index 0000000..bc79ad3 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Describes how to configure a database. + * <p> + * The purpose of this object is to keep track of all of the little + * configuration settings that are applied to a database after it + * is opened so that they can be applied to all connections in the + * connection pool uniformly. + * </p><p> + * Each connection maintains its own copy of this object so it can + * keep track of which settings have already been applied. + * </p> + * + * @hide + */ +public final class SQLiteDatabaseConfiguration { + // The pattern we use to strip email addresses from database paths + // when constructing a label to use in log messages. + private static final Pattern EMAIL_IN_DB_PATTERN = + Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); + + /** + * Special path used by in-memory databases. + */ + public static final String MEMORY_DB_PATH = ":memory:"; + + /** + * The database path. + */ + public final String path; + + /** + * The flags used to open the database. + */ + public final int openFlags; + + /** + * The label to use to describe the database when it appears in logs. + * This is derived from the path but is stripped to remove PII. + */ + public final String label; + + /** + * The maximum number of connections to retain in the connection pool. + * Must be at least 1. + * + * Default is 1. + */ + public int maxConnectionPoolSize; + + /** + * The maximum size of the prepared statement cache for each database connection. + * Must be non-negative. + * + * Default is 25. + */ + public int maxSqlCacheSize; + + /** + * The database locale. + * + * Default is the value returned by {@link Locale#getDefault()}. + */ + public Locale locale; + + /** + * The custom functions to register. + */ + public final ArrayList<SQLiteCustomFunction> customFunctions = + new ArrayList<SQLiteCustomFunction>(); + + /** + * Creates a database configuration with the required parameters for opening a + * database and default values for all other parameters. + * + * @param path The database path. + * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}. + */ + public SQLiteDatabaseConfiguration(String path, int openFlags) { + if (path == null) { + throw new IllegalArgumentException("path must not be null."); + } + + this.path = path; + this.openFlags = openFlags; + label = stripPathForLogs(path); + + // Set default values for optional parameters. + maxConnectionPoolSize = 1; + maxSqlCacheSize = 25; + locale = Locale.getDefault(); + } + + /** + * Creates a database configuration as a copy of another configuration. + * + * @param other The other configuration. + */ + public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + + this.path = other.path; + this.openFlags = other.openFlags; + this.label = other.label; + updateParametersFrom(other); + } + + /** + * Updates the non-immutable parameters of this configuration object + * from the other configuration object. + * + * @param other The object from which to copy the parameters. + */ + public void updateParametersFrom(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + if (!path.equals(other.path) || openFlags != other.openFlags) { + throw new IllegalArgumentException("other configuration must refer to " + + "the same database."); + } + + maxConnectionPoolSize = other.maxConnectionPoolSize; + maxSqlCacheSize = other.maxSqlCacheSize; + locale = other.locale; + customFunctions.clear(); + customFunctions.addAll(other.customFunctions); + } + + /** + * Returns true if the database is in-memory. + * @return True if the database is in-memory. + */ + public boolean isInMemoryDb() { + return path.equalsIgnoreCase(MEMORY_DB_PATH); + } + + private static String stripPathForLogs(String path) { + if (path.indexOf('@') == -1) { + return path; + } + return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY"); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index cc057e0..204483d 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import android.os.Build; import android.os.SystemProperties; import android.util.Log; +import android.util.Printer; /** * Provides debugging info about all SQLite databases running in the current process. @@ -28,6 +29,14 @@ import android.util.Log; * {@hide} */ public final class SQLiteDebug { + private static native void nativeGetPagerStats(PagerStats stats); + + /** + * Controls the printing of informational SQL log messages. + */ + public static final boolean DEBUG_SQL_LOG = + Log.isLoggable("SQLiteLog", Log.VERBOSE); + /** * Controls the printing of SQL statements as they are executed. */ @@ -42,31 +51,6 @@ public final class SQLiteDebug { Log.isLoggable("SQLiteTime", Log.VERBOSE); /** - * Controls the printing of compiled-sql-statement cache stats. - */ - public static final boolean DEBUG_SQL_CACHE = - Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE); - - /** - * Controls the stack trace reporting of active cursors being - * finalized. - */ - public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = - Log.isLoggable("SQLiteCursorClosing", Log.VERBOSE); - - /** - * Controls the tracking of time spent holding the database lock. - */ - public static final boolean DEBUG_LOCK_TIME_TRACKING = - Log.isLoggable("SQLiteLockTime", Log.VERBOSE); - - /** - * Controls the printing of stack traces when tracking the time spent holding the database lock. - */ - public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE = - Log.isLoggable("SQLiteLockStackTrace", Log.VERBOSE); - - /** * True to enable database performance testing instrumentation. * @hide */ @@ -91,30 +75,9 @@ public final class SQLiteDebug { /** * Contains statistics about the active pagers in the current process. * - * @see #getPagerStats(PagerStats) + * @see #nativeGetPagerStats(PagerStats) */ public static class PagerStats { - /** The total number of bytes in all pagers in the current process - * @deprecated not used any longer - */ - @Deprecated - public long totalBytes; - /** The number of bytes in referenced pages in all pagers in the current process - * @deprecated not used any longer - * */ - @Deprecated - public long referencedBytes; - /** The number of bytes in all database files opened in the current process - * @deprecated not used any longer - */ - @Deprecated - public long databaseBytes; - /** The number of pagers opened in the current process - * @deprecated not used any longer - */ - @Deprecated - public int numPagers; - /** the current amount of memory checked out by sqlite using sqlite3_malloc(). * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html */ @@ -127,7 +90,7 @@ public final class SQLiteDebug { * that overflowed because no space was left in the page cache. * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html */ - public int pageCacheOverflo; + public int pageCacheOverflow; /** records the largest memory allocation request handed to sqlite3. * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html @@ -175,52 +138,24 @@ public final class SQLiteDebug { */ public static PagerStats getDatabaseInfo() { PagerStats stats = new PagerStats(); - getPagerStats(stats); + nativeGetPagerStats(stats); stats.dbStats = SQLiteDatabase.getDbStats(); return stats; } /** - * Gathers statistics about all pagers in the current process. - */ - public static native void getPagerStats(PagerStats stats); - - /** - * Returns the size of the SQLite heap. - * @return The size of the SQLite heap in bytes. - */ - public static native long getHeapSize(); - - /** - * Returns the amount of allocated memory in the SQLite heap. - * @return The allocated size in bytes. - */ - public static native long getHeapAllocatedSize(); - - /** - * Returns the amount of free memory in the SQLite heap. - * @return The freed size in bytes. + * Dumps detailed information about all databases used by the process. + * @param printer The printer for dumping database state. + * @param args Command-line arguments supplied to dumpsys dbinfo */ - public static native long getHeapFreeSize(); - - /** - * Determines the number of dirty belonging to the SQLite - * heap segments of this process. pages[0] returns the number of - * shared pages, pages[1] returns the number of private pages - */ - public static native void getHeapDirtyPages(int[] pages); - - private static int sNumActiveCursorsFinalized = 0; - - /** - * Returns the number of active cursors that have been finalized. This depends on the GC having - * run but is still useful for tests. - */ - public static int getNumActiveCursorsFinalized() { - return sNumActiveCursorsFinalized; - } + public static void dump(Printer printer, String[] args) { + boolean verbose = false; + for (String arg : args) { + if (arg.equals("-v")) { + verbose = true; + } + } - static synchronized void notifyActiveCursorFinalized() { - sNumActiveCursorsFinalized++; + SQLiteDatabase.dumpAll(printer, verbose); } } diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index a5e762e..c490dc6 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -16,6 +16,7 @@ package android.database.sqlite; +import android.content.CancelationSignal; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase.CursorFactory; @@ -25,46 +26,42 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; * @hide */ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { - private String mEditTable; - private SQLiteDatabase mDatabase; - private Cursor mCursor; - private String mSql; + private final SQLiteDatabase mDatabase; + private final String mEditTable; + private final String mSql; + private final CancelationSignal mCancelationSignal; private SQLiteQuery mQuery; - public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) { + public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable, + CancelationSignal cancelationSignal) { mDatabase = db; mEditTable = editTable; mSql = sql; + mCancelationSignal = cancelationSignal; } public Cursor query(CursorFactory factory, String[] selectionArgs) { - // Compile the query - SQLiteQuery query = null; - + final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancelationSignal); + final Cursor cursor; try { - mDatabase.lock(mSql); - mDatabase.closePendingStatements(); - query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); + query.bindAllArgsAsStrings(selectionArgs); - // Create the cursor if (factory == null) { - mCursor = new SQLiteCursor(this, mEditTable, query); + cursor = new SQLiteCursor(this, mEditTable, query); } else { - mCursor = factory.newCursor(mDatabase, this, mEditTable, query); + cursor = factory.newCursor(mDatabase, this, mEditTable, query); } - - mQuery = query; - query = null; - return mCursor; - } finally { - // Make sure this object is cleaned up if something happens - if (query != null) query.close(); - mDatabase.unlock(); + } catch (RuntimeException ex) { + query.close(); + throw ex; } + + mQuery = query; + return cursor; } public void cursorClosed() { - mCursor = null; + // Do nothing } public void setBindArguments(String[] bindArgs) { diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java new file mode 100644 index 0000000..dbefd63 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteGlobal.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +import android.os.StatFs; + +/** + * Provides access to SQLite functions that affect all database connection, + * such as memory management. + * + * The native code associated with SQLiteGlobal is also sets global configuration options + * using sqlite3_config() then calls sqlite3_initialize() to ensure that the SQLite + * library is properly initialized exactly once before any other framework or application + * code has a chance to run. + * + * Verbose SQLite logging is enabled if the "log.tag.SQLiteLog" property is set to "V". + * (per {@link SQLiteDebug#DEBUG_SQL_LOG}). + * + * @hide + */ +public final class SQLiteGlobal { + private static final String TAG = "SQLiteGlobal"; + + private static final Object sLock = new Object(); + private static int sDefaultPageSize; + + private static native int nativeReleaseMemory(); + + private SQLiteGlobal() { + } + + /** + * Attempts to release memory by pruning the SQLite page cache and other + * internal data structures. + * + * @return The number of bytes that were freed. + */ + public static int releaseMemory() { + return nativeReleaseMemory(); + } + + /** + * Gets the default page size to use when creating a database. + */ + public static int getDefaultPageSize() { + synchronized (sLock) { + if (sDefaultPageSize == 0) { + sDefaultPageSize = new StatFs("/data").getBlockSize(); + } + return sDefaultPageSize; + } + } +} diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 56cf948..46d9369 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -81,7 +81,8 @@ public abstract class SQLiteOpenHelper { * @param name of the database file, or null for an in-memory database * @param factory to use for creating cursor objects, or null for the default * @param version number of the database (starting at 1); if the database is older, - * {@link #onUpgrade} will be used to upgrade the database + * {@link #onUpgrade} will be used to upgrade the database; if the database is + * newer, {@link #onDowngrade} will be used to downgrade the database * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database * corruption. */ @@ -100,7 +101,7 @@ public abstract class SQLiteOpenHelper { } /** - * Return the name of the SQLite database being opened, as given tp + * Return the name of the SQLite database being opened, as given to * the constructor. */ public String getDatabaseName() { @@ -143,12 +144,14 @@ public abstract class SQLiteOpenHelper { // If we have a read-only database open, someone could be using it // (though they shouldn't), which would cause a lock to be held on // the file, and our attempts to open the database read-write would - // fail waiting for the file lock. To prevent that, we acquire the - // lock on the read-only database, which shuts out other users. + // fail waiting for the file lock. To prevent that, we acquire a lock + // on the read-only database, which shuts out other users. boolean success = false; SQLiteDatabase db = null; - if (mDatabase != null) mDatabase.lock(); + if (mDatabase != null) { + mDatabase.lockPrimaryConnection(); + } try { mIsInitializing = true; if (mName == null) { @@ -185,11 +188,13 @@ public abstract class SQLiteOpenHelper { if (success) { if (mDatabase != null) { try { mDatabase.close(); } catch (Exception e) { } - mDatabase.unlock(); + mDatabase.unlockPrimaryConnection(); } mDatabase = db; } else { - if (mDatabase != null) mDatabase.unlock(); + if (mDatabase != null) { + mDatabase.unlockPrimaryConnection(); + } if (db != null) db.close(); } } @@ -293,7 +298,7 @@ public abstract class SQLiteOpenHelper { public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); /** - * Called when the database needs to be downgraded. This is stricly similar to + * Called when the database needs to be downgraded. This is strictly similar to * onUpgrade() method, but is called whenever current version is newer than requested one. * However, this method is not abstract, so it is not mandatory for a customer to * implement it. If not overridden, default implementation will reject downgrade and diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 89552dc..f3da2a6 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -16,241 +16,108 @@ package android.database.sqlite; +import android.content.CancelationSignal; import android.database.DatabaseUtils; -import android.database.Cursor; -import java.util.HashMap; +import java.util.Arrays; /** * A base class for compiled SQLite programs. - *<p> - * SQLiteProgram is NOT internally synchronized so code using a SQLiteProgram from multiple - * threads should perform its own synchronization when using the SQLiteProgram. + * <p> + * This class is not thread-safe. + * </p> */ public abstract class SQLiteProgram extends SQLiteClosable { + private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final String TAG = "SQLiteProgram"; + private final SQLiteDatabase mDatabase; + private final String mSql; + private final boolean mReadOnly; + private final String[] mColumnNames; + private final int mNumParameters; + private final Object[] mBindArgs; - /** The database this program is compiled against. - * @deprecated do not use this - */ - @Deprecated - protected SQLiteDatabase mDatabase; - - /** The SQL used to create this query */ - /* package */ final String mSql; - - /** - * Native linkage, do not modify. This comes from the database and should not be modified - * in here or in the native code. - * @deprecated do not use this - */ - @Deprecated - protected int nHandle; - - /** - * the SQLiteCompiledSql object for the given sql statement. - */ - /* package */ SQLiteCompiledSql mCompiledSql; - - /** - * SQLiteCompiledSql statement id is populated with the corresponding object from the above - * member. This member is used by the native_bind_* methods - * @deprecated do not use this - */ - @Deprecated - protected int nStatement; - - /** - * In the case of {@link SQLiteStatement}, this member stores the bindargs passed - * to the following methods, instead of actually doing the binding. - * <ul> - * <li>{@link #bindBlob(int, byte[])}</li> - * <li>{@link #bindDouble(int, double)}</li> - * <li>{@link #bindLong(int, long)}</li> - * <li>{@link #bindNull(int)}</li> - * <li>{@link #bindString(int, String)}</li> - * </ul> - * <p> - * Each entry in the array is a Pair of - * <ol> - * <li>bind arg position number</li> - * <li>the value to be bound to the bindarg</li> - * </ol> - * <p> - * It is lazily initialized in the above bind methods - * and it is cleared in {@link #clearBindings()} method. - * <p> - * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this - */ - /* package */ HashMap<Integer, Object> mBindArgs = null; - /* package */ final int mStatementType; - /* package */ static final int STATEMENT_CACHEABLE = 16; - /* package */ static final int STATEMENT_DONT_PREPARE = 32; - /* package */ static final int STATEMENT_USE_POOLED_CONN = 64; - /* package */ static final int STATEMENT_TYPE_MASK = 0x0f; - - /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { - this(db, sql, null, true); - } - - /* package */ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, - boolean compileFlag) { + SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, + CancelationSignal cancelationSignalForPrepare) { + mDatabase = db; mSql = sql.trim(); + int n = DatabaseUtils.getSqlStatementType(mSql); switch (n) { - case DatabaseUtils.STATEMENT_UPDATE: - mStatementType = n | STATEMENT_CACHEABLE; - break; - case DatabaseUtils.STATEMENT_SELECT: - mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN; - break; case DatabaseUtils.STATEMENT_BEGIN: case DatabaseUtils.STATEMENT_COMMIT: case DatabaseUtils.STATEMENT_ABORT: - mStatementType = n | STATEMENT_DONT_PREPARE; + mReadOnly = false; + mColumnNames = EMPTY_STRING_ARRAY; + mNumParameters = 0; break; + default: - mStatementType = n; - } - db.acquireReference(); - db.addSQLiteClosable(this); - mDatabase = db; - nHandle = db.mNativeHandle; - if (bindArgs != null) { - int size = bindArgs.length; - for (int i = 0; i < size; i++) { - this.addToBindArgs(i + 1, bindArgs[i]); - } - } - if (compileFlag) { - compileAndbindAllArgs(); + boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT); + SQLiteStatementInfo info = new SQLiteStatementInfo(); + db.getThreadSession().prepare(mSql, + db.getThreadDefaultConnectionFlags(assumeReadOnly), + cancelationSignalForPrepare, info); + mReadOnly = info.readOnly; + mColumnNames = info.columnNames; + mNumParameters = info.numParameters; + break; } - } - private void compileSql() { - // only cache CRUD statements - if ((mStatementType & STATEMENT_CACHEABLE) == 0) { - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - nStatement = mCompiledSql.nStatement; - // since it is not in the cache, no need to acquire() it. - return; + if (mNumParameters != 0) { + mBindArgs = new Object[mNumParameters]; + } else { + mBindArgs = null; } - mCompiledSql = mDatabase.getCompiledStatementForSql(mSql); - if (mCompiledSql == null) { - // create a new compiled-sql obj - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - - // add it to the cache of compiled-sqls - // but before adding it and thus making it available for anyone else to use it, - // make sure it is acquired by me. - mCompiledSql.acquire(); - mDatabase.addToCompiledQueries(mSql, mCompiledSql); - } else { - // it is already in compiled-sql cache. - // try to acquire the object. - if (!mCompiledSql.acquire()) { - int last = mCompiledSql.nStatement; - // the SQLiteCompiledSql in cache is in use by some other SQLiteProgram object. - // we can't have two different SQLiteProgam objects can't share the same - // CompiledSql object. create a new one. - // finalize it when I am done with it in "this" object. - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - // since it is not in the cache, no need to acquire() it. + if (bindArgs != null) { + if (bindArgs.length > mNumParameters) { + throw new IllegalArgumentException("Too many bind arguments. " + + bindArgs.length + " arguments were provided but the statement needs " + + mNumParameters + " arguments."); } + System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length); } - nStatement = mCompiledSql.nStatement; } - @Override - protected void onAllReferencesReleased() { - release(); - mDatabase.removeSQLiteClosable(this); - mDatabase.releaseReference(); + final SQLiteDatabase getDatabase() { + return mDatabase; } - @Override - protected void onAllReferencesReleasedFromContainer() { - release(); - mDatabase.releaseReference(); + final String getSql() { + return mSql; } - /* package */ void release() { - if (mCompiledSql == null) { - return; - } - mDatabase.releaseCompiledSqlObj(mSql, mCompiledSql); - mCompiledSql = null; - nStatement = 0; + final Object[] getBindArgs() { + return mBindArgs; } - /** - * Returns a unique identifier for this program. - * - * @return a unique identifier for this program - * @deprecated do not use this method. it is not guaranteed to be the same across executions of - * the SQL statement contained in this object. - */ - @Deprecated - public final int getUniqueId() { - return -1; + final String[] getColumnNames() { + return mColumnNames; } - /** - * used only for testing purposes - */ - /* package */ int getSqlStatementId() { - synchronized(this) { - return (mCompiledSql == null) ? 0 : nStatement; - } + /** @hide */ + protected final SQLiteSession getSession() { + return mDatabase.getThreadSession(); } - /* package */ String getSqlString() { - return mSql; + /** @hide */ + protected final int getConnectionFlags() { + return mDatabase.getThreadDefaultConnectionFlags(mReadOnly); + } + + /** @hide */ + protected final void onCorruption() { + mDatabase.onCorruption(); } /** + * Unimplemented. * @deprecated This method is deprecated and must not be used. - * - * @param sql the SQL string to compile - * @param forceCompilation forces the SQL to be recompiled in the event that there is an - * existing compiled SQL program already around */ @Deprecated - protected void compile(String sql, boolean forceCompilation) { - // TODO is there a need for this? - } - - private void bind(int type, int index, Object value) { - mDatabase.verifyDbIsOpen(); - addToBindArgs(index, (type == Cursor.FIELD_TYPE_NULL) ? null : value); - if (nStatement > 0) { - // bind only if the SQL statement is compiled - acquireReference(); - try { - switch (type) { - case Cursor.FIELD_TYPE_NULL: - native_bind_null(index); - break; - case Cursor.FIELD_TYPE_BLOB: - native_bind_blob(index, (byte[]) value); - break; - case Cursor.FIELD_TYPE_FLOAT: - native_bind_double(index, (Double) value); - break; - case Cursor.FIELD_TYPE_INTEGER: - native_bind_long(index, (Long) value); - break; - case Cursor.FIELD_TYPE_STRING: - default: - native_bind_string(index, (String) value); - break; - } - } finally { - releaseReference(); - } - } + public final int getUniqueId() { + return -1; } /** @@ -260,7 +127,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param index The 1-based index to the parameter to bind null to */ public void bindNull(int index) { - bind(Cursor.FIELD_TYPE_NULL, index, null); + bind(index, null); } /** @@ -271,7 +138,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindLong(int index, long value) { - bind(Cursor.FIELD_TYPE_INTEGER, index, value); + bind(index, value); } /** @@ -282,7 +149,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindDouble(int index, double value) { - bind(Cursor.FIELD_TYPE_FLOAT, index, value); + bind(index, value); } /** @@ -290,13 +157,13 @@ public abstract class SQLiteProgram extends SQLiteClosable { * {@link #clearBindings} is called. * * @param index The 1-based index to the parameter to bind - * @param value The value to bind + * @param value The value to bind, must not be null */ public void bindString(int index, String value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - bind(Cursor.FIELD_TYPE_STRING, index, value); + bind(index, value); } /** @@ -304,29 +171,21 @@ public abstract class SQLiteProgram extends SQLiteClosable { * {@link #clearBindings} is called. * * @param index The 1-based index to the parameter to bind - * @param value The value to bind + * @param value The value to bind, must not be null */ public void bindBlob(int index, byte[] value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - bind(Cursor.FIELD_TYPE_BLOB, index, value); + bind(index, value); } /** * Clears all existing bindings. Unset bindings are treated as NULL. */ public void clearBindings() { - mBindArgs = null; - if (this.nStatement == 0) { - return; - } - mDatabase.verifyDbIsOpen(); - acquireReference(); - try { - native_clear_bindings(); - } finally { - releaseReference(); + if (mBindArgs != null) { + Arrays.fill(mBindArgs, null); } } @@ -334,99 +193,33 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Release this program's resources, making it invalid. */ public void close() { - mBindArgs = null; - if (nHandle == 0 || !mDatabase.isOpen()) { - return; - } releaseReference(); } - private void addToBindArgs(int index, Object value) { - if (mBindArgs == null) { - mBindArgs = new HashMap<Integer, Object>(); - } - mBindArgs.put(index, value); - } - - /* package */ void compileAndbindAllArgs() { - if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { - if (mBindArgs != null) { - throw new IllegalArgumentException("Can't pass bindargs for this sql :" + mSql); - } - // no need to prepare this SQL statement - return; - } - if (nStatement == 0) { - // SQL statement is not compiled yet. compile it now. - compileSql(); - } - if (mBindArgs == null) { - return; - } - for (int index : mBindArgs.keySet()) { - Object value = mBindArgs.get(index); - if (value == null) { - native_bind_null(index); - } else if (value instanceof Double || value instanceof Float) { - native_bind_double(index, ((Number) value).doubleValue()); - } else if (value instanceof Number) { - native_bind_long(index, ((Number) value).longValue()); - } else if (value instanceof Boolean) { - Boolean bool = (Boolean)value; - native_bind_long(index, (bool) ? 1 : 0); - if (bool) { - native_bind_long(index, 1); - } else { - native_bind_long(index, 0); - } - } else if (value instanceof byte[]){ - native_bind_blob(index, (byte[]) value); - } else { - native_bind_string(index, value.toString()); - } - } - } - /** * Given an array of String bindArgs, this method binds all of them in one single call. * - * @param bindArgs the String array of bind args. + * @param bindArgs the String array of bind args, none of which must be null. */ public void bindAllArgsAsStrings(String[] bindArgs) { - if (bindArgs == null) { - return; - } - int size = bindArgs.length; - for (int i = 0; i < size; i++) { - bindString(i + 1, bindArgs[i]); + if (bindArgs != null) { + for (int i = bindArgs.length; i != 0; i--) { + bindString(i, bindArgs[i - 1]); + } } } - /* package */ synchronized final void setNativeHandle(int nHandle) { - this.nHandle = nHandle; + @Override + protected void onAllReferencesReleased() { + clearBindings(); } - /** - * @deprecated This method is deprecated and must not be used. - * Compiles SQL into a SQLite program. - * - * <P>The database lock must be held when calling this method. - * @param sql The SQL to compile. - */ - @Deprecated - protected final native void native_compile(String sql); - - /** - * @deprecated This method is deprecated and must not be used. - */ - @Deprecated - protected final native void native_finalize(); - - protected final native void native_bind_null(int index); - protected final native void native_bind_long(int index, long value); - protected final native void native_bind_double(int index, double value); - protected final native void native_bind_string(int index, String value); - protected final native void native_bind_blob(int index, byte[] value); - private final native void native_clear_bindings(); + private void bind(int index, Object value) { + if (index < 1 || index > mNumParameters) { + throw new IllegalArgumentException("Cannot bind argument at index " + + index + " because the index is out of range. " + + "The statement has " + mNumParameters + " parameters."); + } + mBindArgs[index - 1] = value; + } } - diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index faf6cba..df2e260 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -16,160 +16,69 @@ package android.database.sqlite; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.CursorWindow; -import android.os.SystemClock; -import android.text.TextUtils; import android.util.Log; /** - * A SQLite program that represents a query that reads the resulting rows into a CursorWindow. - * This class is used by SQLiteCursor and isn't useful itself. - * - * SQLiteQuery is not internally synchronized so code using a SQLiteQuery from multiple - * threads should perform its own synchronization when using the SQLiteQuery. + * Represents a query that reads the resulting rows into a {@link SQLiteQuery}. + * This class is used by {@link SQLiteCursor} and isn't useful itself. + * <p> + * This class is not thread-safe. + * </p> */ -public class SQLiteQuery extends SQLiteProgram { +public final class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - private static native int nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, - int startPos, int offsetParam); - - private static native int nativeColumnCount(int statementPtr); - private static native String nativeColumnName(int statementPtr, int columnIndex); - - /** The index of the unbound OFFSET parameter */ - private int mOffsetIndex = 0; + private final CancelationSignal mCancelationSignal; - private boolean mClosed = false; - - /** - * Create a persistent query object. - * - * @param db The database that this query object is associated with - * @param query The SQL string for this query. - * @param offsetIndex The 1-based index to the OFFSET parameter, - */ - /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) { - super(db, query); - mOffsetIndex = offsetIndex; - bindAllArgsAsStrings(bindArgs); - } + SQLiteQuery(SQLiteDatabase db, String query, CancelationSignal cancelationSignal) { + super(db, query, null, cancelationSignal); - /** - * Constructor used to create new instance to replace a given instance of this class. - * This constructor is used when the current Query object is now associated with a different - * {@link SQLiteDatabase} object. - * - * @param db The database that this query object is associated with - * @param query the instance of {@link SQLiteQuery} to be replaced - */ - /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) { - super(db, query.mSql); - this.mBindArgs = query.mBindArgs; - this.mOffsetIndex = query.mOffsetIndex; + mCancelationSignal = cancelationSignal; } /** - * Reads rows into a buffer. This method acquires the database lock. + * Reads rows into a buffer. * * @param window The window to fill into - * @return number of total rows in the query + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled. + * @param countAllRows True to count all rows that the query would + * return regardless of whether they fit in the window. + * @return Number of rows that were enumerated. Might not be all rows + * unless countAllRows is true. + * + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. */ - /* package */ int fillWindow(CursorWindow window) { - mDatabase.lock(mSql); - long timeStart = SystemClock.uptimeMillis(); + int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { + acquireReference(); try { - acquireReference(); + window.acquireReference(); try { - window.acquireReference(); - int startPos = window.getStartPosition(); - int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, - startPos, mOffsetIndex); - if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) { - long elapsed = SystemClock.uptimeMillis() - timeStart; - if (SQLiteDebug.shouldLogSlowQuery(elapsed)) { - Log.d(TAG, "fillWindow took " + elapsed - + " ms: window=\"" + window - + "\", startPos=" + startPos - + ", offset=" + mOffsetIndex - + ", filledRows=" + window.getNumRows() - + ", countedRows=" + numRows - + ", query=\"" + mSql + "\"" - + ", args=[" + (mBindArgs != null ? - TextUtils.join(", ", mBindArgs.values()) : "") - + "]"); - } - } - mDatabase.logTimeStat(mSql, timeStart); + int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(), + window, startPos, requiredPos, countAllRows, getConnectionFlags(), + mCancelationSignal); return numRows; - } catch (IllegalStateException e){ - // simply ignore it - return 0; - } catch (SQLiteDatabaseCorruptException e) { - mDatabase.onCorruption(); - throw e; - } catch (SQLiteException e) { - Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); - throw e; + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } catch (SQLiteException ex) { + Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); + throw ex; } finally { window.releaseReference(); } } finally { releaseReference(); - mDatabase.unlock(); - } - } - - /** - * Get the column count for the statement. Only valid on query based - * statements. The database must be locked - * when calling this method. - * - * @return The number of column in the statement's result set. - */ - /* package */ int columnCountLocked() { - acquireReference(); - try { - return nativeColumnCount(nStatement); - } finally { - releaseReference(); - } - } - - /** - * Retrieves the column name for the given column index. The database must be locked - * when calling this method. - * - * @param columnIndex the index of the column to get the name for - * @return The requested column's name - */ - /* package */ String columnNameLocked(int columnIndex) { - acquireReference(); - try { - return nativeColumnName(nStatement, columnIndex); - } finally { - releaseReference(); } } @Override public String toString() { - return "SQLiteQuery: " + mSql; - } - - @Override - public void close() { - super.close(); - mClosed = true; - } - - /** - * Called by SQLiteCursor when it is requeried. - */ - /* package */ void requery() { - if (mClosed) { - throw new IllegalStateException("requerying a closed cursor"); - } - compileAndbindAllArgs(); + return "SQLiteQuery: " + getSql(); } } diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 8f8eb6e..89469cb 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.Cursor; import android.database.DatabaseUtils; import android.provider.BaseColumns; @@ -137,8 +139,9 @@ public class SQLiteQueryBuilder /** * Sets the cursor factory to be used for the query. You can use * one factory for all queries on a database but it is normally - * easier to specify the factory when doing this query. @param - * factory the factor to use + * easier to specify the factory when doing this query. + * + * @param factory the factory to use. */ public void setCursorFactory(SQLiteDatabase.CursorFactory factory) { mFactory = factory; @@ -289,7 +292,7 @@ public class SQLiteQueryBuilder String selection, String[] selectionArgs, String groupBy, String having, String sortOrder) { return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, - null /* limit */); + null /* limit */, null /* cancelationSignal */); } /** @@ -327,6 +330,48 @@ public class SQLiteQueryBuilder public Cursor query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) { + return query(db, projectionIn, selection, selectionArgs, + groupBy, having, sortOrder, limit, null); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY + * itself). Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return a cursor over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public Cursor query(SQLiteDatabase db, String[] projectionIn, + String selection, String[] selectionArgs, String groupBy, + String having, String sortOrder, String limit, CancelationSignal cancelationSignal) { if (mTables == null) { return null; } @@ -341,7 +386,8 @@ public class SQLiteQueryBuilder // in both the wrapped and original forms. String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, having, sortOrder, limit); - validateSql(db, sqlForValidation); // will throw if query is invalid + validateQuerySql(db, sqlForValidation, + cancelationSignal); // will throw if query is invalid } String sql = buildQuery( @@ -353,20 +399,18 @@ public class SQLiteQueryBuilder } return db.rawQueryWithFactory( mFactory, sql, selectionArgs, - SQLiteDatabase.findEditTable(mTables)); // will throw if query is invalid + SQLiteDatabase.findEditTable(mTables), + cancelationSignal); // will throw if query is invalid } /** - * Verifies that a SQL statement is valid by compiling it. + * Verifies that a SQL SELECT statement is valid by compiling it. * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. */ - private void validateSql(SQLiteDatabase db, String sql) { - db.lock(sql); - try { - new SQLiteCompiledSql(db, sql).releaseSqlStatement(); - } finally { - db.unlock(); - } + private void validateQuerySql(SQLiteDatabase db, String sql, + CancelationSignal cancelationSignal) { + db.getThreadSession().prepare(sql, + db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancelationSignal, null); } /** diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java new file mode 100644 index 0000000..b5a3e31 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteSession.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2011 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 android.database.sqlite; + +import android.content.CancelationSignal; +import android.content.OperationCanceledException; +import android.database.CursorWindow; +import android.database.DatabaseUtils; +import android.os.ParcelFileDescriptor; + +/** + * Provides a single client the ability to use a database. + * + * <h2>About database sessions</h2> + * <p> + * Database access is always performed using a session. The session + * manages the lifecycle of transactions and database connections. + * </p><p> + * Sessions can be used to perform both read-only and read-write operations. + * There is some advantage to knowing when a session is being used for + * read-only purposes because the connection pool can optimize the use + * of the available connections to permit multiple read-only operations + * to execute in parallel whereas read-write operations may need to be serialized. + * </p><p> + * When <em>Write Ahead Logging (WAL)</em> is enabled, the database can + * execute simultaneous read-only and read-write transactions, provided that + * at most one read-write transaction is performed at a time. When WAL is not + * enabled, read-only transactions can execute in parallel but read-write + * transactions are mutually exclusive. + * </p> + * + * <h2>Ownership and concurrency guarantees</h2> + * <p> + * Session objects are not thread-safe. In fact, session objects are thread-bound. + * The {@link SQLiteDatabase} uses a thread-local variable to associate a session + * with each thread for the use of that thread alone. Consequently, each thread + * has its own session object and therefore its own transaction state independent + * of other threads. + * </p><p> + * A thread has at most one session per database. This constraint ensures that + * a thread can never use more than one database connection at a time for a + * given database. As the number of available database connections is limited, + * if a single thread tried to acquire multiple connections for the same database + * at the same time, it might deadlock. Therefore we allow there to be only + * one session (so, at most one connection) per thread per database. + * </p> + * + * <h2>Transactions</h2> + * <p> + * There are two kinds of transaction: implicit transactions and explicit + * transactions. + * </p><p> + * An implicit transaction is created whenever a database operation is requested + * and there is no explicit transaction currently in progress. An implicit transaction + * only lasts for the duration of the database operation in question and then it + * is ended. If the database operation was successful, then its changes are committed. + * </p><p> + * An explicit transaction is started by calling {@link #beginTransaction} and + * specifying the desired transaction mode. Once an explicit transaction has begun, + * all subsequent database operations will be performed as part of that transaction. + * To end an explicit transaction, first call {@link #setTransactionSuccessful} if the + * transaction was successful, then call {@link #end}. If the transaction was + * marked successful, its changes will be committed, otherwise they will be rolled back. + * </p><p> + * Explicit transactions can also be nested. A nested explicit transaction is + * started with {@link #beginTransaction}, marked successful with + * {@link #setTransactionSuccessful}and ended with {@link #endTransaction}. + * If any nested transaction is not marked successful, then the entire transaction + * including all of its nested transactions will be rolled back + * when the outermost transaction is ended. + * </p><p> + * To improve concurrency, an explicit transaction can be yielded by calling + * {@link #yieldTransaction}. If there is contention for use of the database, + * then yielding ends the current transaction, commits its changes, releases the + * database connection for use by another session for a little while, and starts a + * new transaction with the same properties as the original one. + * Changes committed by {@link #yieldTransaction} cannot be rolled back. + * </p><p> + * When a transaction is started, the client can provide a {@link SQLiteTransactionListener} + * to listen for notifications of transaction-related events. + * </p><p> + * Recommended usage: + * <code><pre> + * // First, begin the transaction. + * session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0); + * try { + * // Then do stuff... + * session.execute("INSERT INTO ...", null, 0); + * + * // As the very last step before ending the transaction, mark it successful. + * session.setTransactionSuccessful(); + * } finally { + * // Finally, end the transaction. + * // This statement will commit the transaction if it was marked successful or + * // roll it back otherwise. + * session.endTransaction(); + * } + * </pre></code> + * </p> + * + * <h2>Database connections</h2> + * <p> + * A {@link SQLiteDatabase} can have multiple active sessions at the same + * time. Each session acquires and releases connections to the database + * as needed to perform each requested database transaction. If all connections + * are in use, then database transactions on some sessions will block until a + * connection becomes available. + * </p><p> + * The session acquires a single database connection only for the duration + * of a single (implicit or explicit) database transaction, then releases it. + * This characteristic allows a small pool of database connections to be shared + * efficiently by multiple sessions as long as they are not all trying to perform + * database transactions at the same time. + * </p> + * + * <h2>Responsiveness</h2> + * <p> + * Because there are a limited number of database connections and the session holds + * a database connection for the entire duration of a database transaction, + * it is important to keep transactions short. This is especially important + * for read-write transactions since they may block other transactions + * from executing. Consider calling {@link #yieldTransaction} periodically + * during long-running transactions. + * </p><p> + * Another important consideration is that transactions that take too long to + * run may cause the application UI to become unresponsive. Even if the transaction + * is executed in a background thread, the user will get bored and + * frustrated if the application shows no data for several seconds while + * a transaction runs. + * </p><p> + * Guidelines: + * <ul> + * <li>Do not perform database transactions on the UI thread.</li> + * <li>Keep database transactions as short as possible.</li> + * <li>Simple queries often run faster than complex queries.</li> + * <li>Measure the performance of your database transactions.</li> + * <li>Consider what will happen when the size of the data set grows. + * A query that works well on 100 rows may struggle with 10,000.</li> + * </ul> + * + * <h2>Reentrance</h2> + * <p> + * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + * </p> + * + * @hide + */ +public final class SQLiteSession { + private final SQLiteConnectionPool mConnectionPool; + + private SQLiteConnection mConnection; + private int mConnectionFlags; + private int mConnectionUseCount; + private Transaction mTransactionPool; + private Transaction mTransactionStack; + + /** + * Transaction mode: Deferred. + * <p> + * In a deferred transaction, no locks are acquired on the database + * until the first operation is performed. If the first operation is + * read-only, then a <code>SHARED</code> lock is acquired, otherwise + * a <code>RESERVED</code> lock is acquired. + * </p><p> + * While holding a <code>SHARED</code> lock, this session is only allowed to + * read but other sessions are allowed to read or write. + * While holding a <code>RESERVED</code> lock, this session is allowed to read + * or write but other sessions are only allowed to read. + * </p><p> + * Because the lock is only acquired when needed in a deferred transaction, + * it is possible for another session to write to the database first before + * this session has a chance to do anything. + * </p><p> + * Corresponds to the SQLite <code>BEGIN DEFERRED</code> transaction mode. + * </p> + */ + public static final int TRANSACTION_MODE_DEFERRED = 0; + + /** + * Transaction mode: Immediate. + * <p> + * When an immediate transaction begins, the session acquires a + * <code>RESERVED</code> lock. + * </p><p> + * While holding a <code>RESERVED</code> lock, this session is allowed to read + * or write but other sessions are only allowed to read. + * </p><p> + * Corresponds to the SQLite <code>BEGIN IMMEDIATE</code> transaction mode. + * </p> + */ + public static final int TRANSACTION_MODE_IMMEDIATE = 1; + + /** + * Transaction mode: Exclusive. + * <p> + * When an exclusive transaction begins, the session acquires an + * <code>EXCLUSIVE</code> lock. + * </p><p> + * While holding an <code>EXCLUSIVE</code> lock, this session is allowed to read + * or write but no other sessions are allowed to access the database. + * </p><p> + * Corresponds to the SQLite <code>BEGIN EXCLUSIVE</code> transaction mode. + * </p> + */ + public static final int TRANSACTION_MODE_EXCLUSIVE = 2; + + /** + * Creates a session bound to the specified connection pool. + * + * @param connectionPool The connection pool. + */ + public SQLiteSession(SQLiteConnectionPool connectionPool) { + if (connectionPool == null) { + throw new IllegalArgumentException("connectionPool must not be null"); + } + + mConnectionPool = connectionPool; + } + + /** + * Returns true if the session has a transaction in progress. + * + * @return True if the session has a transaction in progress. + */ + public boolean hasTransaction() { + return mTransactionStack != null; + } + + /** + * Returns true if the session has a nested transaction in progress. + * + * @return True if the session has a nested transaction in progress. + */ + public boolean hasNestedTransaction() { + return mTransactionStack != null && mTransactionStack.mParent != null; + } + + /** + * Returns true if the session has an active database connection. + * + * @return True if the session has an active database connection. + */ + public boolean hasConnection() { + return mConnection != null; + } + + /** + * Begins a transaction. + * <p> + * Transactions may nest. If the transaction is not in progress, + * then a database connection is obtained and a new transaction is started. + * Otherwise, a nested transaction is started. + * </p><p> + * Each call to {@link #beginTransaction} must be matched exactly by a call + * to {@link #endTransaction}. To mark a transaction as successful, + * call {@link #setTransactionSuccessful} before calling {@link #endTransaction}. + * If the transaction is not successful, or if any of its nested + * transactions were not successful, then the entire transaction will + * be rolled back when the outermost transaction is ended. + * </p> + * + * @param transactionMode The transaction mode. One of: {@link #TRANSACTION_MODE_DEFERRED}, + * {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}. + * Ignored when creating a nested transaction. + * @param transactionListener The transaction listener, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been + * called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #setTransactionSuccessful + * @see #yieldTransaction + * @see #endTransaction + */ + public void beginTransaction(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags, + CancelationSignal cancelationSignal) { + throwIfTransactionMarkedSuccessful(); + beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags, + cancelationSignal); + } + + private void beginTransactionUnchecked(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + if (mTransactionStack == null) { + acquireConnection(null, connectionFlags, cancelationSignal); // might throw + } + try { + // Set up the transaction such that we can back out safely + // in case we fail part way. + if (mTransactionStack == null) { + // Execute SQL might throw a runtime exception. + switch (transactionMode) { + case TRANSACTION_MODE_IMMEDIATE: + mConnection.execute("BEGIN IMMEDIATE;", null, + cancelationSignal); // might throw + break; + case TRANSACTION_MODE_EXCLUSIVE: + mConnection.execute("BEGIN EXCLUSIVE;", null, + cancelationSignal); // might throw + break; + default: + mConnection.execute("BEGIN;", null, cancelationSignal); // might throw + break; + } + } + + // Listener might throw a runtime exception. + if (transactionListener != null) { + try { + transactionListener.onBegin(); // might throw + } catch (RuntimeException ex) { + if (mTransactionStack == null) { + mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw + } + throw ex; + } + } + + // Bookkeeping can't throw, except an OOM, which is just too bad... + Transaction transaction = obtainTransaction(transactionMode, transactionListener); + transaction.mParent = mTransactionStack; + mTransactionStack = transaction; + } finally { + if (mTransactionStack == null) { + releaseConnection(); // might throw + } + } + } + + /** + * Marks the current transaction as having completed successfully. + * <p> + * This method can be called at most once between {@link #beginTransaction} and + * {@link #endTransaction} to indicate that the changes made by the transaction should be + * committed. If this method is not called, the changes will be rolled back + * when the transaction is ended. + * </p> + * + * @throws IllegalStateException if there is no current transaction, or if + * {@link #setTransactionSuccessful} has already been called for the current transaction. + * + * @see #beginTransaction + * @see #endTransaction + */ + public void setTransactionSuccessful() { + throwIfNoTransaction(); + throwIfTransactionMarkedSuccessful(); + + mTransactionStack.mMarkedSuccessful = true; + } + + /** + * Ends the current transaction and commits or rolls back changes. + * <p> + * If this is the outermost transaction (not nested within any other + * transaction), then the changes are committed if {@link #setTransactionSuccessful} + * was called or rolled back otherwise. + * </p><p> + * This method must be called exactly once for each call to {@link #beginTransaction}. + * </p> + * + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws IllegalStateException if there is no current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #beginTransaction + * @see #setTransactionSuccessful + * @see #yieldTransaction + */ + public void endTransaction(CancelationSignal cancelationSignal) { + throwIfNoTransaction(); + assert mConnection != null; + + endTransactionUnchecked(cancelationSignal); + } + + private void endTransactionUnchecked(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + final Transaction top = mTransactionStack; + boolean successful = top.mMarkedSuccessful && !top.mChildFailed; + + RuntimeException listenerException = null; + final SQLiteTransactionListener listener = top.mListener; + if (listener != null) { + try { + if (successful) { + listener.onCommit(); // might throw + } else { + listener.onRollback(); // might throw + } + } catch (RuntimeException ex) { + listenerException = ex; + successful = false; + } + } + + mTransactionStack = top.mParent; + recycleTransaction(top); + + if (mTransactionStack != null) { + if (!successful) { + mTransactionStack.mChildFailed = true; + } + } else { + try { + if (successful) { + mConnection.execute("COMMIT;", null, cancelationSignal); // might throw + } else { + mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw + } + } finally { + releaseConnection(); // might throw + } + } + + if (listenerException != null) { + throw listenerException; + } + } + + /** + * Temporarily ends a transaction to let other threads have use of + * the database. Begins a new transaction after a specified delay. + * <p> + * If there are other threads waiting to acquire connections, + * then the current transaction is committed and the database + * connection is released. After a short delay, a new transaction + * is started. + * </p><p> + * The transaction is assumed to be successful so far. Do not call + * {@link #setTransactionSuccessful()} before calling this method. + * This method will fail if the transaction has already been marked + * successful. + * </p><p> + * The changes that were committed by a yield cannot be rolled back later. + * </p><p> + * Before this method was called, there must already have been + * a transaction in progress. When this method returns, there will + * still be a transaction in progress, either the same one as before + * or a new one if the transaction was actually yielded. + * </p><p> + * This method should not be called when there is a nested transaction + * in progress because it is not possible to yield a nested transaction. + * If <code>throwIfNested</code> is true, then attempting to yield + * a nested transaction will throw {@link IllegalStateException}, otherwise + * the method will return <code>false</code> in that case. + * </p><p> + * If there is no nested transaction in progress but a previous nested + * transaction failed, then the transaction is not yielded (because it + * must be rolled back) and this method returns <code>false</code>. + * </p> + * + * @param sleepAfterYieldDelayMillis A delay time to wait after yielding + * the database connection to allow other threads some time to run. + * If the value is less than or equal to zero, there will be no additional + * delay beyond the time it will take to begin a new transaction. + * @param throwIfUnsafe If true, then instead of returning false when no + * transaction is in progress, a nested transaction is in progress, or when + * the transaction has already been marked successful, throws {@link IllegalStateException}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return True if the transaction was actually yielded. + * + * @throws IllegalStateException if <code>throwIfNested</code> is true and + * there is no current transaction, there is a nested transaction in progress or + * if {@link #setTransactionSuccessful} has already been called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #beginTransaction + * @see #endTransaction + */ + public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe, + CancelationSignal cancelationSignal) { + if (throwIfUnsafe) { + throwIfNoTransaction(); + throwIfTransactionMarkedSuccessful(); + throwIfNestedTransaction(); + } else { + if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful + || mTransactionStack.mParent != null) { + return false; + } + } + assert mConnection != null; + + if (mTransactionStack.mChildFailed) { + return false; + } + + return yieldTransactionUnchecked(sleepAfterYieldDelayMillis, + cancelationSignal); // might throw + } + + private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) { + return false; + } + + final int transactionMode = mTransactionStack.mMode; + final SQLiteTransactionListener listener = mTransactionStack.mListener; + final int connectionFlags = mConnectionFlags; + endTransactionUnchecked(cancelationSignal); // might throw + + if (sleepAfterYieldDelayMillis > 0) { + try { + Thread.sleep(sleepAfterYieldDelayMillis); + } catch (InterruptedException ex) { + // we have been interrupted, that's all we need to do + } + } + + beginTransactionUnchecked(transactionMode, listener, connectionFlags, + cancelationSignal); // might throw + return true; + } + + /** + * Prepares a statement for execution but does not bind its parameters or execute it. + * <p> + * This method can be used to check for syntax errors during compilation + * prior to execution of the statement. If the {@code outStatementInfo} argument + * is not null, the provided {@link SQLiteStatementInfo} object is populated + * with information about the statement. + * </p><p> + * A prepared statement makes no reference to the arguments that may eventually + * be bound to it, consequently it it possible to cache certain prepared statements + * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable, + * then it will be stored in the cache for later and reused if possible. + * </p> + * + * @param sql The SQL statement to prepare. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate + * with information about the statement, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error. + * @throws OperationCanceledException if the operation was canceled. + */ + public void prepare(String sql, int connectionFlags, CancelationSignal cancelationSignal, + SQLiteStatementInfo outStatementInfo) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + mConnection.prepare(sql, outStatementInfo); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that does not return a result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public void execute(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + mConnection.execute(sql, bindArgs, cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single <code>long</code> result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a <code>long</code>, or zero if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLong(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForLong(sql, bindArgs, cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single {@link String} result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a <code>String</code>, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public String executeForString(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return null; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForString(sql, bindArgs, cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single BLOB result as a + * file descriptor to a shared memory region. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The file descriptor for a shared memory region that contains + * the value of the first column in the first row of the result set as a BLOB, + * or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + int connectionFlags, CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return null; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForBlobFileDescriptor(sql, bindArgs, + cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a count of the number of rows + * that were changed. Use for UPDATE or DELETE SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were changed. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForChangedRowCount(sql, bindArgs, + cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns the row id of the last row inserted + * by the statement. Use for INSERT SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The row id of the last row that was inserted, or 0 if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForLastInsertedRowId(sql, bindArgs, + cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement and populates the specified {@link CursorWindow} + * with a range of results. Returns the number of rows that were counted + * during query execution. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param window The cursor window to clear and fill. + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled + * so that it does. Must be greater than or equal to <code>startPos</code>. + * @param countAllRows True to count all rows that the query would return + * regagless of whether they fit in the window. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were counted during query execution. Might + * not be all rows in the result set unless <code>countAllRows</code> is true. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + int connectionFlags, CancelationSignal cancelationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + if (window == null) { + throw new IllegalArgumentException("window must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { + window.clear(); + return 0; + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw + try { + return mConnection.executeForCursorWindow(sql, bindArgs, + window, startPos, requiredPos, countAllRows, + cancelationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Performs special reinterpretation of certain SQL statements such as "BEGIN", + * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are + * maintained. + * + * This function is mainly used to support legacy apps that perform their + * own transactions by executing raw SQL rather than calling {@link #beginTransaction} + * and the like. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * @return True if the statement was of a special form that was handled here, + * false otherwise. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + final int type = DatabaseUtils.getSqlStatementType(sql); + switch (type) { + case DatabaseUtils.STATEMENT_BEGIN: + beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags, + cancelationSignal); + return true; + + case DatabaseUtils.STATEMENT_COMMIT: + setTransactionSuccessful(); + endTransaction(cancelationSignal); + return true; + + case DatabaseUtils.STATEMENT_ABORT: + endTransaction(cancelationSignal); + return true; + } + return false; + } + + private void acquireConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { + if (mConnection == null) { + assert mConnectionUseCount == 0; + mConnection = mConnectionPool.acquireConnection(sql, connectionFlags, + cancelationSignal); // might throw + mConnectionFlags = connectionFlags; + } + mConnectionUseCount += 1; + } + + private void releaseConnection() { + assert mConnection != null; + assert mConnectionUseCount > 0; + if (--mConnectionUseCount == 0) { + try { + mConnectionPool.releaseConnection(mConnection); // might throw + } finally { + mConnection = null; + } + } + } + + private void throwIfNoTransaction() { + if (mTransactionStack == null) { + throw new IllegalStateException("Cannot perform this operation because " + + "there is no current transaction."); + } + } + + private void throwIfTransactionMarkedSuccessful() { + if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) { + throw new IllegalStateException("Cannot perform this operation because " + + "the transaction has already been marked successful. The only " + + "thing you can do now is call endTransaction()."); + } + } + + private void throwIfNestedTransaction() { + if (mTransactionStack == null && mTransactionStack.mParent != null) { + throw new IllegalStateException("Cannot perform this operation because " + + "a nested transaction is in progress."); + } + } + + private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) { + Transaction transaction = mTransactionPool; + if (transaction != null) { + mTransactionPool = transaction.mParent; + transaction.mParent = null; + transaction.mMarkedSuccessful = false; + transaction.mChildFailed = false; + } else { + transaction = new Transaction(); + } + transaction.mMode = mode; + transaction.mListener = listener; + return transaction; + } + + private void recycleTransaction(Transaction transaction) { + transaction.mParent = mTransactionPool; + transaction.mListener = null; + mTransactionPool = transaction; + } + + private static final class Transaction { + public Transaction mParent; + public int mMode; + public SQLiteTransactionListener mListener; + public boolean mMarkedSuccessful; + public boolean mChildFailed; + } +} diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index ff973a7..b1092d7 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -16,47 +16,19 @@ package android.database.sqlite; -import android.database.DatabaseUtils; import android.os.ParcelFileDescriptor; -import android.os.SystemClock; -import android.util.Log; - -import java.io.IOException; - -import dalvik.system.BlockGuard; /** - * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused. - * The statement cannot return multiple rows, but 1x1 result sets are allowed. - * Don't use SQLiteStatement constructor directly, please use - * {@link SQLiteDatabase#compileStatement(String)} - *<p> - * SQLiteStatement is NOT internally synchronized so code using a SQLiteStatement from multiple - * threads should perform its own synchronization when using the SQLiteStatement. + * Represents a statement that can be executed against a database. The statement + * cannot return multiple rows or columns, but single value (1 x 1) result sets + * are supported. + * <p> + * This class is not thread-safe. + * </p> */ -@SuppressWarnings("deprecation") -public class SQLiteStatement extends SQLiteProgram -{ - private static final String TAG = "SQLiteStatement"; - - private static final boolean READ = true; - private static final boolean WRITE = false; - - private SQLiteDatabase mOrigDb; - private int mState; - /** possible value for {@link #mState}. indicates that a transaction is started. */ - private static final int TRANS_STARTED = 1; - /** possible value for {@link #mState}. indicates that a lock is acquired. */ - private static final int LOCK_ACQUIRED = 2; - - /** - * Don't use SQLiteStatement constructor directly, please use - * {@link SQLiteDatabase#compileStatement(String)} - * @param db - * @param sql - */ - /* package */ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { - super(db, sql, bindArgs, false /* don't compile sql statement */); +public final class SQLiteStatement extends SQLiteProgram { + SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { + super(db, sql, bindArgs, null); } /** @@ -67,7 +39,15 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public void execute() { - executeUpdateDelete(); + acquireReference(); + try { + getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } } /** @@ -79,21 +59,15 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public int executeUpdateDelete() { + acquireReference(); try { - saveSqlAsLastSqlStatement(); - acquireAndLock(WRITE); - int numChanges = 0; - if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { - // since the statement doesn't have to be prepared, - // call the following native method which will not prepare - // the query plan - native_executeSql(mSql); - } else { - numChanges = native_execute(); - } - return numChanges; + return getSession().executeForChangedRowCount( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -107,23 +81,18 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { + acquireReference(); try { - saveSqlAsLastSqlStatement(); - acquireAndLock(WRITE); - return native_executeInsert(); + return getSession().executeForLastInsertedRowId( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } - private void saveSqlAsLastSqlStatement() { - if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_UPDATE) || - (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_BEGIN) { - mDatabase.setLastSqlStatement(mSql); - } - } /** * Execute a statement that returns a 1 by 1 table with a numeric value. * For example, SELECT COUNT(*) FROM table; @@ -133,17 +102,15 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - long retValue = native_1x1_long(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForLong( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -156,17 +123,15 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - String retValue = native_1x1_string(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForString( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -179,121 +144,20 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - ParcelFileDescriptor retValue = native_1x1_blob_ashmem(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (IOException ex) { - Log.e(TAG, "simpleQueryForBlobFileDescriptor() failed", ex); - return null; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForBlobFileDescriptor( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); - } - } - - /** - * Called before every method in this class before executing a SQL statement, - * this method does the following: - * <ul> - * <li>make sure the database is open</li> - * <li>get a database connection from the connection pool,if possible</li> - * <li>notifies {@link BlockGuard} of read/write</li> - * <li>if the SQL statement is an update, start transaction if not already in one. - * otherwise, get lock on the database</li> - * <li>acquire reference on this object</li> - * <li>and then return the current time _after_ the database lock was acquired</li> - * </ul> - * <p> - * This method removes the duplicate code from the other public - * methods in this class. - */ - private long acquireAndLock(boolean rwFlag) { - mState = 0; - // use pooled database connection handles for SELECT SQL statements - mDatabase.verifyDbIsOpen(); - SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0) - ? mDatabase.getDbConnection(mSql) : mDatabase; - // use the database connection obtained above - mOrigDb = mDatabase; - mDatabase = db; - setNativeHandle(mDatabase.mNativeHandle); - if (rwFlag == WRITE) { - BlockGuard.getThreadPolicy().onWriteToDisk(); - } else { - BlockGuard.getThreadPolicy().onReadFromDisk(); - } - - /* - * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction"). - * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held. - * beginTransaction() methods in SQLiteDatabase call lockForced() before - * calling execSQL("BEGIN transaction"). - */ - if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) { - if (!mDatabase.isDbLockedByCurrentThread()) { - // transaction is NOT started by calling beginTransaction() methods in - // SQLiteDatabase - mDatabase.setTransactionUsingExecSqlFlag(); - } - } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_UPDATE) { - // got update SQL statement. if there is NO pending transaction, start one - if (!mDatabase.inTransaction()) { - mDatabase.beginTransactionNonExclusive(); - mState = TRANS_STARTED; - } + releaseReference(); } - // do I have database lock? if not, grab it. - if (!mDatabase.isDbLockedByCurrentThread()) { - mDatabase.lock(mSql); - mState = LOCK_ACQUIRED; - } - - acquireReference(); - long startTime = SystemClock.uptimeMillis(); - mDatabase.closePendingStatements(); - compileAndbindAllArgs(); - return startTime; } - /** - * this method releases locks and references acquired in {@link #acquireAndLock(boolean)} - */ - private void releaseAndUnlock() { - releaseReference(); - if (mState == TRANS_STARTED) { - try { - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - } else if (mState == LOCK_ACQUIRED) { - mDatabase.unlock(); - } - if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_COMMIT || - (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_ABORT) { - mDatabase.resetTransactionUsingExecSqlFlag(); - } - clearBindings(); - // release the compiled sql statement so that the caller's SQLiteStatement no longer - // has a hard reference to a database object that may get deallocated at any point. - release(); - // restore the database connection handle to the original value - mDatabase = mOrigDb; - setNativeHandle(mDatabase.mNativeHandle); + @Override + public String toString() { + return "SQLiteProgram: " + getSql(); } - - private final native int native_execute(); - private final native long native_executeInsert(); - private final native long native_1x1_long(); - private final native String native_1x1_string(); - private final native ParcelFileDescriptor native_1x1_blob_ashmem() throws IOException; - private final native void native_executeSql(String sql); } diff --git a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java b/core/java/android/database/sqlite/SQLiteStatementInfo.java index bcf95e2..3edfdb0 100644 --- a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java +++ b/core/java/android/database/sqlite/SQLiteStatementInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2011 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. @@ -17,15 +17,23 @@ package android.database.sqlite; /** - * Thrown if the database can't be closed because of some un-closed - * Cursor or SQLiteStatement objects. Could happen when a thread is trying to close - * the database while another thread still hasn't closed a Cursor on that database. + * Describes a SQLite statement. + * * @hide */ -public class SQLiteUnfinalizedObjectsException extends SQLiteException { - public SQLiteUnfinalizedObjectsException() {} +public final class SQLiteStatementInfo { + /** + * The number of parameters that the statement has. + */ + public int numParameters; + + /** + * The names of all columns in the result set of the statement. + */ + public String[] columnNames; - public SQLiteUnfinalizedObjectsException(String error) { - super(error); - } + /** + * True if the statement is read-only. + */ + public boolean readOnly; } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 7ca6155..cca208a 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -138,7 +138,7 @@ public class Camera { private static final int CAMERA_MSG_COMPRESSED_IMAGE = 0x100; private static final int CAMERA_MSG_RAW_IMAGE_NOTIFY = 0x200; private static final int CAMERA_MSG_PREVIEW_METADATA = 0x400; - private static final int CAMERA_MSG_ALL_MSGS = 0x4FF; + private static final int CAMERA_MSG_FOCUS_MOVE = 0x800; private int mNativeContext; // accessed by native methods private EventHandler mEventHandler; @@ -148,6 +148,7 @@ public class Camera { private PreviewCallback mPreviewCallback; private PictureCallback mPostviewCallback; private AutoFocusCallback mAutoFocusCallback; + private AutoFocusMoveCallback mAutoFocusMoveCallback; private OnZoomChangeListener mZoomListener; private FaceDetectionListener mFaceListener; private ErrorCallback mErrorCallback; @@ -302,6 +303,12 @@ public class Camera { native_setup(new WeakReference<Camera>(this), cameraId); } + /** + * An empty Camera for testing purpose. + */ + Camera() { + } + protected void finalize() { release(); } @@ -492,6 +499,7 @@ public class Camera { mPostviewCallback = null; mJpegCallback = null; mAutoFocusCallback = null; + mAutoFocusMoveCallback = null; } private native final void _stopPreview(); @@ -737,6 +745,12 @@ public class Camera { } return; + case CAMERA_MSG_FOCUS_MOVE: + if (mAutoFocusMoveCallback != null) { + mAutoFocusMoveCallback.onAutoFocusMoving(msg.arg1 == 0 ? false : true, mCamera); + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -849,6 +863,39 @@ public class Camera { private native final void native_cancelAutoFocus(); /** + * Callback interface used to notify on auto focus start and stop. + * + * <p>This is useful for continuous autofocus -- {@link Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} + * and {@link Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can + * show autofocus animation.</p> + * + * @hide + */ + public interface AutoFocusMoveCallback + { + /** + * Called when the camera auto focus starts or stops. + * + * @param start true if focus starts to move, false if focus stops to move + * @param camera the Camera service object + */ + void onAutoFocusMoving(boolean start, Camera camera); + } + + /** + * Sets camera auto-focus move callback. + * + * @param cb the callback to run + * @hide + */ + public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) { + mAutoFocusMoveCallback = cb; + enableFocusMoveCallback((mAutoFocusMoveCallback != null) ? 1 : 0); + } + + private native void enableFocusMoveCallback(int enable); + + /** * Callback interface used to signal the moment of actual image capture. * * @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback) @@ -1310,6 +1357,18 @@ public class Camera { } /** + * Returns an empty {@link Parameters} for testing purpose. + * + * @return an Parameter object. + * + * @hide + */ + public static Parameters getEmptyParameters() { + Camera camera = new Camera(); + return camera.new Parameters(); + } + + /** * Image size (width and height dimensions). */ public class Size { diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 10c1195..23ae21b 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -100,6 +100,9 @@ public class ExtractEditText extends EditText { @Override public boolean onTextContextMenuItem(int id) { if (mIME != null && mIME.onExtractTextContextMenuItem(id)) { + // Mode was started on Extracted, needs to be stopped here. + // Cut and paste will change the text, which stops selection mode. + if (id == android.R.id.copy) stopSelectionActionMode(); return true; } return super.onTextContextMenuItem(id); diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 0052dd0..a569317 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -360,6 +360,11 @@ public class ConnectivityManager { } } + /** + * Gets you info about the current data network. + * Call {@link NetworkInfo#isConnected()} on the returned {@link NetworkInfo} + * to check if the device has a data connection. + */ public NetworkInfo getActiveNetworkInfo() { try { return mService.getActiveNetworkInfo(); diff --git a/core/java/android/net/DhcpStateMachine.java b/core/java/android/net/DhcpStateMachine.java index fc6a44a..397a12a 100644 --- a/core/java/android/net/DhcpStateMachine.java +++ b/core/java/android/net/DhcpStateMachine.java @@ -347,21 +347,25 @@ public class DhcpStateMachine extends StateMachine { if (success) { if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName); - long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion - - //Sanity check for renewal - //TODO: would be good to notify the user that his network configuration is - //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS - if (leaseDuration < MIN_RENEWAL_TIME_SECS) { - leaseDuration = MIN_RENEWAL_TIME_SECS; - } - //Do it a bit earlier than half the lease duration time - //to beat the native DHCP client and avoid extra packets - //48% for one hour lease time = 29 minutes - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + - leaseDuration * 480, //in milliseconds - mDhcpRenewalIntent); + long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion + + //Sanity check for renewal + if (leaseDuration >= 0) { + //TODO: would be good to notify the user that his network configuration is + //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS + if (leaseDuration < MIN_RENEWAL_TIME_SECS) { + leaseDuration = MIN_RENEWAL_TIME_SECS; + } + //Do it a bit earlier than half the lease duration time + //to beat the native DHCP client and avoid extra packets + //48% for one hour lease time = 29 minutes + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + + leaseDuration * 480, //in milliseconds + mDhcpRenewalIntent); + } else { + //infinite lease time, no renewal needed + } mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal) .sendToTarget(); diff --git a/core/java/android/net/InterfaceConfiguration.java b/core/java/android/net/InterfaceConfiguration.java index 89b5915..8cdd153 100644 --- a/core/java/android/net/InterfaceConfiguration.java +++ b/core/java/android/net/InterfaceConfiguration.java @@ -16,34 +16,84 @@ package android.net; -import android.os.Parcelable; import android.os.Parcel; +import android.os.Parcelable; + +import com.google.android.collect.Sets; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.util.HashSet; /** - * A simple object for retrieving / setting an interfaces configuration + * Configuration details for a network interface. + * * @hide */ public class InterfaceConfiguration implements Parcelable { - public String hwAddr; - public LinkAddress addr; - public String interfaceFlags; + private String mHwAddr; + private LinkAddress mAddr; + private HashSet<String> mFlags = Sets.newHashSet(); - public InterfaceConfiguration() { - super(); - } + private static final String FLAG_UP = "up"; + private static final String FLAG_DOWN = "down"; + @Override public String toString() { - StringBuffer str = new StringBuffer(); + final StringBuilder builder = new StringBuilder(); + builder.append("mHwAddr=").append(mHwAddr); + builder.append(" mAddr=").append(String.valueOf(mAddr)); + builder.append(" mFlags=").append(getFlags()); + return builder.toString(); + } + + public Iterable<String> getFlags() { + return mFlags; + } + + public boolean hasFlag(String flag) { + validateFlag(flag); + return mFlags.contains(flag); + } + + public void clearFlag(String flag) { + validateFlag(flag); + mFlags.remove(flag); + } + + public void setFlag(String flag) { + validateFlag(flag); + mFlags.add(flag); + } + + /** + * Set flags to mark interface as up. + */ + public void setInterfaceUp() { + mFlags.remove(FLAG_DOWN); + mFlags.add(FLAG_UP); + } - str.append("ipddress "); - str.append((addr != null) ? addr.toString() : "NULL"); - str.append(" flags ").append(interfaceFlags); - str.append(" hwaddr ").append(hwAddr); + /** + * Set flags to mark interface as down. + */ + public void setInterfaceDown() { + mFlags.remove(FLAG_UP); + mFlags.add(FLAG_DOWN); + } + + public LinkAddress getLinkAddress() { + return mAddr; + } + + public void setLinkAddress(LinkAddress addr) { + mAddr = addr; + } + + public String getHardwareAddress() { + return mHwAddr; + } - return str.toString(); + public void setHardwareAddress(String hwAddr) { + mHwAddr = hwAddr; } /** @@ -55,8 +105,8 @@ public class InterfaceConfiguration implements Parcelable { */ public boolean isActive() { try { - if(interfaceFlags.contains("up")) { - for (byte b : addr.getAddress().getAddress()) { + if (hasFlag(FLAG_UP)) { + for (byte b : mAddr.getAddress().getAddress()) { if (b != 0) return true; } } @@ -66,38 +116,49 @@ public class InterfaceConfiguration implements Parcelable { return false; } - /** Implement the Parcelable interface {@hide} */ + /** {@inheritDoc} */ public int describeContents() { return 0; } - /** Implement the Parcelable interface {@hide} */ + /** {@inheritDoc} */ public void writeToParcel(Parcel dest, int flags) { - dest.writeString(hwAddr); - if (addr != null) { + dest.writeString(mHwAddr); + if (mAddr != null) { dest.writeByte((byte)1); - dest.writeParcelable(addr, flags); + dest.writeParcelable(mAddr, flags); } else { dest.writeByte((byte)0); } - dest.writeString(interfaceFlags); + dest.writeInt(mFlags.size()); + for (String flag : mFlags) { + dest.writeString(flag); + } } - /** Implement the Parcelable interface {@hide} */ - public static final Creator<InterfaceConfiguration> CREATOR = - new Creator<InterfaceConfiguration>() { - public InterfaceConfiguration createFromParcel(Parcel in) { - InterfaceConfiguration info = new InterfaceConfiguration(); - info.hwAddr = in.readString(); - if (in.readByte() == 1) { - info.addr = in.readParcelable(null); - } - info.interfaceFlags = in.readString(); - return info; + public static final Creator<InterfaceConfiguration> CREATOR = new Creator< + InterfaceConfiguration>() { + public InterfaceConfiguration createFromParcel(Parcel in) { + InterfaceConfiguration info = new InterfaceConfiguration(); + info.mHwAddr = in.readString(); + if (in.readByte() == 1) { + info.mAddr = in.readParcelable(null); } - - public InterfaceConfiguration[] newArray(int size) { - return new InterfaceConfiguration[size]; + final int size = in.readInt(); + for (int i = 0; i < size; i++) { + info.mFlags.add(in.readString()); } - }; + return info; + } + + public InterfaceConfiguration[] newArray(int size) { + return new InterfaceConfiguration[size]; + } + }; + + private static void validateFlag(String flag) { + if (flag.indexOf(' ') >= 0) { + throw new IllegalArgumentException("flag contains space: " + flag); + } + } } diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java index aa6400b..1a74abf 100644 --- a/core/java/android/net/NetworkIdentity.java +++ b/core/java/android/net/NetworkIdentity.java @@ -45,7 +45,7 @@ public class NetworkIdentity { @Override public int hashCode() { - return Objects.hashCode(mType, mSubType, mSubscriberId); + return Objects.hashCode(mType, mSubType, mSubscriberId, mRoaming); } @Override diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 537750a..7286f0d 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -22,8 +22,9 @@ import android.os.Parcel; import java.util.EnumMap; /** - * Describes the status of a network interface of a given type - * (currently either Mobile or Wifi). + * Describes the status of a network interface. + * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents + * the current network connection. */ public class NetworkInfo implements Parcelable { @@ -38,7 +39,7 @@ public class NetworkInfo implements Parcelable { * <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr> * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr> * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr> - * <tr><td><code>CONNECTED</code></td><td<code>CONNECTED</code></td></tr> + * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr> * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr> * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr> * <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr> @@ -159,9 +160,12 @@ public class NetworkInfo implements Parcelable { } /** - * Reports the type of network (currently mobile or Wi-Fi) to which the - * info in this object pertains. - * @return the network type + * Reports the type of network to which the + * info in this {@code NetworkInfo} pertains. + * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link + * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link + * ConnectivityManager#TYPE_ETHERNET}, {@link ConnectivityManager#TYPE_BLUETOOTH}, or other + * types defined by {@link ConnectivityManager} */ public int getType() { synchronized (this) { @@ -226,6 +230,7 @@ public class NetworkInfo implements Parcelable { /** * Indicates whether network connectivity exists and it is possible to establish * connections and pass data. + * <p>Always call this before attempting to perform data transactions. * @return {@code true} if network connectivity exists, {@code false} otherwise. */ public boolean isConnected() { diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java index 1b24f0c..d9ea700 100644 --- a/core/java/android/net/NetworkPolicy.java +++ b/core/java/android/net/NetworkPolicy.java @@ -39,16 +39,18 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { public long warningBytes; public long limitBytes; public long lastSnooze; + public boolean metered; private static final long DEFAULT_MTU = 1500; public NetworkPolicy(NetworkTemplate template, int cycleDay, long warningBytes, long limitBytes, - long lastSnooze) { + long lastSnooze, boolean metered) { this.template = checkNotNull(template, "missing NetworkTemplate"); this.cycleDay = cycleDay; this.warningBytes = warningBytes; this.limitBytes = limitBytes; this.lastSnooze = lastSnooze; + this.metered = metered; } public NetworkPolicy(Parcel in) { @@ -57,6 +59,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { warningBytes = in.readLong(); limitBytes = in.readLong(); lastSnooze = in.readLong(); + metered = in.readInt() != 0; } /** {@inheritDoc} */ @@ -66,6 +69,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { dest.writeLong(warningBytes); dest.writeLong(limitBytes); dest.writeLong(lastSnooze); + dest.writeInt(metered ? 1 : 0); } /** {@inheritDoc} */ @@ -99,16 +103,16 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { @Override public int hashCode() { - return Objects.hashCode(template, cycleDay, warningBytes, limitBytes, lastSnooze); + return Objects.hashCode(template, cycleDay, warningBytes, limitBytes, lastSnooze, metered); } @Override public boolean equals(Object obj) { if (obj instanceof NetworkPolicy) { final NetworkPolicy other = (NetworkPolicy) obj; - return Objects.equal(template, other.template) && cycleDay == other.cycleDay - && warningBytes == other.warningBytes && limitBytes == other.limitBytes - && lastSnooze == other.lastSnooze; + return cycleDay == other.cycleDay && warningBytes == other.warningBytes + && limitBytes == other.limitBytes && lastSnooze == other.lastSnooze + && metered == other.metered && Objects.equal(template, other.template); } return false; } @@ -116,7 +120,8 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { @Override public String toString() { return "NetworkPolicy[" + template + "]: cycleDay=" + cycleDay + ", warningBytes=" - + warningBytes + ", limitBytes=" + limitBytes + ", lastSnooze=" + lastSnooze; + + warningBytes + ", limitBytes=" + limitBytes + ", lastSnooze=" + lastSnooze + + ", metered=" + metered; } public static final Creator<NetworkPolicy> CREATOR = new Creator<NetworkPolicy>() { diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index f6e627c..7a1ef66 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -16,8 +16,6 @@ package android.net; -import static com.android.internal.util.Preconditions.checkNotNull; - import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -40,8 +38,6 @@ import java.util.HashSet; * @hide */ public class NetworkStats implements Parcelable { - private static final String TAG = "NetworkStats"; - /** {@link #iface} value when interface details unavailable. */ public static final String IFACE_ALL = null; /** {@link #uid} value when UID details unavailable. */ @@ -106,6 +102,15 @@ public class NetworkStats implements Parcelable { this.operations = operations; } + public boolean isNegative() { + return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0; + } + + public boolean isEmpty() { + return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0 + && operations == 0; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -347,6 +352,7 @@ public class NetworkStats implements Parcelable { * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface}, * since operation counts are at data layer. */ + @Deprecated public void spliceOperationsFrom(NetworkStats stats) { for (int i = 0; i < size; i++) { final int j = stats.findIndex(IFACE_ALL, uid[i], set[i], tag[i]); @@ -401,7 +407,7 @@ public class NetworkStats implements Parcelable { * Return total of all fields represented by this snapshot object. */ public Entry getTotal(Entry recycle) { - return getTotal(recycle, null, UID_ALL); + return getTotal(recycle, null, UID_ALL, false); } /** @@ -409,7 +415,7 @@ public class NetworkStats implements Parcelable { * the requested {@link #uid}. */ public Entry getTotal(Entry recycle, int limitUid) { - return getTotal(recycle, null, limitUid); + return getTotal(recycle, null, limitUid, false); } /** @@ -417,7 +423,11 @@ public class NetworkStats implements Parcelable { * the requested {@link #iface}. */ public Entry getTotal(Entry recycle, HashSet<String> limitIface) { - return getTotal(recycle, limitIface, UID_ALL); + return getTotal(recycle, limitIface, UID_ALL, false); + } + + public Entry getTotalIncludingTags(Entry recycle) { + return getTotal(recycle, null, UID_ALL, true); } /** @@ -427,7 +437,8 @@ public class NetworkStats implements Parcelable { * @param limitIface Set of {@link #iface} to include in total; or {@code * null} to include all ifaces. */ - private Entry getTotal(Entry recycle, HashSet<String> limitIface, int limitUid) { + private Entry getTotal( + Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) { final Entry entry = recycle != null ? recycle : new Entry(); entry.iface = IFACE_ALL; @@ -446,7 +457,7 @@ public class NetworkStats implements Parcelable { if (matchesUid && matchesIface) { // skip specific tags, since already counted in TAG_NONE - if (tag[i] != TAG_NONE) continue; + if (tag[i] != TAG_NONE && !includeTags) continue; entry.rxBytes += rxBytes[i]; entry.rxPackets += rxPackets[i]; @@ -463,62 +474,64 @@ public class NetworkStats implements Parcelable { * between two snapshots in time. Assumes that statistics rows collect over * time, and that none of them have disappeared. */ - public NetworkStats subtract(NetworkStats value) throws NonMonotonicException { - return subtract(value, false); + public NetworkStats subtract(NetworkStats right) { + return subtract(this, right, null, null); } /** - * Subtract the given {@link NetworkStats}, effectively leaving the delta + * Subtract the two given {@link NetworkStats} objects, returning the delta * between two snapshots in time. Assumes that statistics rows collect over * time, and that none of them have disappeared. - * - * @param clampNonMonotonic When non-monotonic stats are found, just clamp - * to 0 instead of throwing {@link NonMonotonicException}. + * <p> + * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. */ - public NetworkStats subtract(NetworkStats value, boolean clampNonMonotonic) - throws NonMonotonicException { - final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime; + public static <C> NetworkStats subtract( + NetworkStats left, NetworkStats right, NonMonotonicObserver<C> observer, C cookie) { + long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime; if (deltaRealtime < 0) { - throw new NonMonotonicException(this, value); + if (observer != null) { + observer.foundNonMonotonic(left, -1, right, -1, cookie); + } + deltaRealtime = 0; } // result will have our rows, and elapsed time between snapshots final Entry entry = new Entry(); - final NetworkStats result = new NetworkStats(deltaRealtime, size); - for (int i = 0; i < size; i++) { - entry.iface = iface[i]; - entry.uid = uid[i]; - entry.set = set[i]; - entry.tag = tag[i]; + final NetworkStats result = new NetworkStats(deltaRealtime, left.size); + for (int i = 0; i < left.size; i++) { + entry.iface = left.iface[i]; + entry.uid = left.uid[i]; + entry.set = left.set[i]; + entry.tag = left.tag[i]; // find remote row that matches, and subtract - final int j = value.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i); + final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i); if (j == -1) { // newly appearing row, return entire value - entry.rxBytes = rxBytes[i]; - entry.rxPackets = rxPackets[i]; - entry.txBytes = txBytes[i]; - entry.txPackets = txPackets[i]; - entry.operations = operations[i]; + entry.rxBytes = left.rxBytes[i]; + entry.rxPackets = left.rxPackets[i]; + entry.txBytes = left.txBytes[i]; + entry.txPackets = left.txPackets[i]; + entry.operations = left.operations[i]; } else { // existing row, subtract remote value - entry.rxBytes = rxBytes[i] - value.rxBytes[j]; - entry.rxPackets = rxPackets[i] - value.rxPackets[j]; - entry.txBytes = txBytes[i] - value.txBytes[j]; - entry.txPackets = txPackets[i] - value.txPackets[j]; - entry.operations = operations[i] - value.operations[j]; + entry.rxBytes = left.rxBytes[i] - right.rxBytes[j]; + entry.rxPackets = left.rxPackets[i] - right.rxPackets[j]; + entry.txBytes = left.txBytes[i] - right.txBytes[j]; + entry.txPackets = left.txPackets[i] - right.txPackets[j]; + entry.operations = left.operations[i] - right.operations[j]; if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0 || entry.operations < 0) { - if (clampNonMonotonic) { - entry.rxBytes = Math.max(entry.rxBytes, 0); - entry.rxPackets = Math.max(entry.rxPackets, 0); - entry.txBytes = Math.max(entry.txBytes, 0); - entry.txPackets = Math.max(entry.txPackets, 0); - entry.operations = Math.max(entry.operations, 0); - } else { - throw new NonMonotonicException(this, i, value, j); + if (observer != null) { + observer.foundNonMonotonic(left, i, right, j, cookie); } + entry.rxBytes = Math.max(entry.rxBytes, 0); + entry.rxPackets = Math.max(entry.rxPackets, 0); + entry.txBytes = Math.max(entry.txBytes, 0); + entry.txPackets = Math.max(entry.txPackets, 0); + entry.operations = Math.max(entry.operations, 0); } } @@ -665,22 +678,8 @@ public class NetworkStats implements Parcelable { } }; - public static class NonMonotonicException extends Exception { - public final NetworkStats left; - public final NetworkStats right; - public final int leftIndex; - public final int rightIndex; - - public NonMonotonicException(NetworkStats left, NetworkStats right) { - this(left, -1, right, -1); - } - - public NonMonotonicException( - NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) { - this.left = checkNotNull(left, "missing left"); - this.right = checkNotNull(right, "missing right"); - this.leftIndex = leftIndex; - this.rightIndex = rightIndex; - } + public interface NonMonotonicObserver<C> { + public void foundNonMonotonic( + NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie); } } diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java index 8c01331..faf8a3f 100644 --- a/core/java/android/net/NetworkStatsHistory.java +++ b/core/java/android/net/NetworkStatsHistory.java @@ -26,16 +26,18 @@ import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray; import static android.net.NetworkStatsHistory.Entry.UNKNOWN; import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray; import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray; +import static com.android.internal.util.ArrayUtils.total; import android.os.Parcel; import android.os.Parcelable; import android.util.MathUtils; +import com.android.internal.util.IndentingPrintWriter; + import java.io.CharArrayWriter; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.io.PrintWriter; import java.net.ProtocolException; import java.util.Arrays; import java.util.Random; @@ -74,6 +76,7 @@ public class NetworkStatsHistory implements Parcelable { private long[] txBytes; private long[] txPackets; private long[] operations; + private long totalBytes; public static class Entry { public static final long UNKNOWN = -1; @@ -106,6 +109,12 @@ public class NetworkStatsHistory implements Parcelable { if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize]; if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize]; bucketCount = 0; + totalBytes = 0; + } + + public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) { + this(bucketDuration, existing.estimateResizeBuckets(bucketDuration)); + recordEntireHistory(existing); } public NetworkStatsHistory(Parcel in) { @@ -118,6 +127,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = readLongArray(in); operations = readLongArray(in); bucketCount = bucketStart.length; + totalBytes = in.readLong(); } /** {@inheritDoc} */ @@ -130,6 +140,7 @@ public class NetworkStatsHistory implements Parcelable { writeLongArray(out, txBytes, bucketCount); writeLongArray(out, txPackets, bucketCount); writeLongArray(out, operations, bucketCount); + out.writeLong(totalBytes); } public NetworkStatsHistory(DataInputStream in) throws IOException { @@ -144,6 +155,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = new long[bucketStart.length]; operations = new long[bucketStart.length]; bucketCount = bucketStart.length; + totalBytes = total(rxBytes) + total(txBytes); break; } case VERSION_ADD_PACKETS: @@ -158,6 +170,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = readVarLongArray(in); operations = readVarLongArray(in); bucketCount = bucketStart.length; + totalBytes = total(rxBytes) + total(txBytes); break; } default: { @@ -208,6 +221,13 @@ public class NetworkStatsHistory implements Parcelable { } /** + * Return total bytes represented by this history. + */ + public long getTotalBytes() { + return totalBytes; + } + + /** * Return index of bucket that contains or is immediately before the * requested time. */ @@ -266,13 +286,16 @@ public class NetworkStatsHistory implements Parcelable { * distribute across internal buckets, creating new buckets as needed. */ public void recordData(long start, long end, NetworkStats.Entry entry) { - if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0 - || entry.operations < 0) { + long rxBytes = entry.rxBytes; + long rxPackets = entry.rxPackets; + long txBytes = entry.txBytes; + long txPackets = entry.txPackets; + long operations = entry.operations; + + if (entry.isNegative()) { throw new IllegalArgumentException("tried recording negative data"); } - if (entry.rxBytes == 0 && entry.rxPackets == 0 && entry.txBytes == 0 && entry.txPackets == 0 - && entry.operations == 0) { - // nothing to record; skip + if (entry.isEmpty()) { return; } @@ -295,21 +318,23 @@ public class NetworkStatsHistory implements Parcelable { if (overlap <= 0) continue; // integer math each time is faster than floating point - final long fracRxBytes = entry.rxBytes * overlap / duration; - final long fracRxPackets = entry.rxPackets * overlap / duration; - final long fracTxBytes = entry.txBytes * overlap / duration; - final long fracTxPackets = entry.txPackets * overlap / duration; - final long fracOperations = entry.operations * overlap / duration; + final long fracRxBytes = rxBytes * overlap / duration; + final long fracRxPackets = rxPackets * overlap / duration; + final long fracTxBytes = txBytes * overlap / duration; + final long fracTxPackets = txPackets * overlap / duration; + final long fracOperations = operations * overlap / duration; addLong(activeTime, i, overlap); - addLong(rxBytes, i, fracRxBytes); entry.rxBytes -= fracRxBytes; - addLong(rxPackets, i, fracRxPackets); entry.rxPackets -= fracRxPackets; - addLong(txBytes, i, fracTxBytes); entry.txBytes -= fracTxBytes; - addLong(txPackets, i, fracTxPackets); entry.txPackets -= fracTxPackets; - addLong(operations, i, fracOperations); entry.operations -= fracOperations; + addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes; + addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets; + addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes; + addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets; + addLong(this.operations, i, fracOperations); operations -= fracOperations; duration -= overlap; } + + totalBytes += entry.rxBytes + entry.txBytes; } /** @@ -394,6 +419,7 @@ public class NetworkStatsHistory implements Parcelable { /** * Remove buckets older than requested cutoff. */ + @Deprecated public void removeBucketsBefore(long cutoff) { int i; for (i = 0; i < bucketCount; i++) { @@ -415,6 +441,8 @@ public class NetworkStatsHistory implements Parcelable { if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length); if (operations != null) operations = Arrays.copyOfRange(operations, i, length); bucketCount -= i; + + // TODO: subtract removed values from totalBytes } } @@ -527,19 +555,17 @@ public class NetworkStatsHistory implements Parcelable { return (long) (start + (r.nextFloat() * (end - start))); } - public void dump(String prefix, PrintWriter pw, boolean fullHistory) { - pw.print(prefix); + public void dump(IndentingPrintWriter pw, boolean fullHistory) { pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration); + pw.increaseIndent(); final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32); if (start > 0) { - pw.print(prefix); - pw.print(" (omitting "); pw.print(start); pw.println(" buckets)"); + pw.print("(omitting "); pw.print(start); pw.println(" buckets)"); } for (int i = start; i < bucketCount; i++) { - pw.print(prefix); - pw.print(" bucketStart="); pw.print(bucketStart[i]); + pw.print("bucketStart="); pw.print(bucketStart[i]); if (activeTime != null) { pw.print(" activeTime="); pw.print(activeTime[i]); } if (rxBytes != null) { pw.print(" rxBytes="); pw.print(rxBytes[i]); } if (rxPackets != null) { pw.print(" rxPackets="); pw.print(rxPackets[i]); } @@ -548,12 +574,14 @@ public class NetworkStatsHistory implements Parcelable { if (operations != null) { pw.print(" operations="); pw.print(operations[i]); } pw.println(); } + + pw.decreaseIndent(); } @Override public String toString() { final CharArrayWriter writer = new CharArrayWriter(); - dump("", new PrintWriter(writer), false); + dump(new IndentingPrintWriter(writer, " "), false); return writer.toString(); } @@ -579,6 +607,10 @@ public class NetworkStatsHistory implements Parcelable { if (array != null) array[i] += value; } + public int estimateResizeBuckets(long newBucketDuration) { + return (int) (size() * getBucketDuration() / newBucketDuration); + } + /** * Utility methods for interacting with {@link DataInputStream} and * {@link DataOutputStream}, mostly dealing with writing partial arrays. diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 418b82f..8ebfd8d 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -18,6 +18,7 @@ package android.net; import static android.net.ConnectivityManager.TYPE_ETHERNET; import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; import static android.net.ConnectivityManager.TYPE_WIMAX; import static android.net.NetworkIdentity.scrubSubscriberId; import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G; @@ -231,10 +232,13 @@ public class NetworkTemplate implements Parcelable { * Check if matches Wi-Fi network template. */ private boolean matchesWifi(NetworkIdentity ident) { - if (ident.mType == TYPE_WIFI) { - return true; + switch (ident.mType) { + case TYPE_WIFI: + case TYPE_WIFI_P2P: + return true; + default: + return false; } - return false; } /** diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index cd585b2..dfdea38 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -20,7 +20,6 @@ import android.app.DownloadManager; import android.app.backup.BackupManager; import android.content.Context; import android.media.MediaPlayer; -import android.net.NetworkStats.NonMonotonicException; import android.os.RemoteException; import android.os.ServiceManager; @@ -193,15 +192,12 @@ public class TrafficStats { throw new IllegalStateException("not profiling data"); } - try { - // subtract starting values and return delta - final NetworkStats profilingStop = getDataLayerSnapshotForUid(context); - final NetworkStats profilingDelta = profilingStop.subtract(sActiveProfilingStart); - sActiveProfilingStart = null; - return profilingDelta; - } catch (NonMonotonicException e) { - throw new RuntimeException(e); - } + // subtract starting values and return delta + final NetworkStats profilingStop = getDataLayerSnapshotForUid(context); + final NetworkStats profilingDelta = NetworkStats.subtract( + profilingStop, sActiveProfilingStart, null, null); + sActiveProfilingStart = null; + return profilingDelta; } } diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 9d28eff..defe7aa 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -19,19 +19,19 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; - import java.io.File; -import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.io.ByteArrayOutputStream; import java.net.URLEncoder; +import java.nio.charset.Charsets; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.RandomAccess; import java.util.Set; +import libcore.net.UriCodec; /** * Immutable URI reference. A URI reference includes a URI and a fragment, the @@ -1305,7 +1305,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * * <p>An opaque URI follows this pattern: * {@code <scheme>:<opaque part>#<fragment>} - * + * * <p>Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI. */ public static final class Builder { @@ -1646,6 +1646,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { /** * Searches the query string for the first value with the given key. * + * <p><strong>Warning:</strong> Prior to Ice Cream Sandwich, this decoded + * the '+' character as '+' rather than ' '. + * * @param key which will be encoded * @throws UnsupportedOperationException if this isn't a hierarchical URI * @throws NullPointerException if key is null @@ -1679,9 +1682,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { if (separator - start == encodedKey.length() && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { if (separator == end) { - return ""; + return ""; } else { - return decode(query.substring(separator + 1, end)); + String encodedValue = query.substring(separator + 1, end); + return UriCodec.decode(encodedValue, true, Charsets.UTF_8, false); } } @@ -1713,6 +1717,38 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { return (!"false".equals(flag) && !"0".equals(flag)); } + /** + * Return a normalized representation of this Uri. + * + * <p>A normalized Uri has a lowercase scheme component. + * This aligns the Uri with Android best practices for + * intent filtering. + * + * <p>For example, "HTTP://www.android.com" becomes + * "http://www.android.com" + * + * <p>All URIs received from outside Android (such as user input, + * or external sources like Bluetooth, NFC, or the Internet) should + * be normalized before they are used to create an Intent. + * + * <p class="note">This method does <em>not</em> validate bad URI's, + * or 'fix' poorly formatted URI's - so do not use it for input validation. + * A Uri will always be returned, even if the Uri is badly formatted to + * begin with and a scheme component cannot be found. + * + * @return normalized Uri (never null) + * @see {@link android.content.Intent#setData} + * @see {@link #setNormalizedData} + */ + public Uri normalize() { + String scheme = getScheme(); + if (scheme == null) return this; // give up + String lowerScheme = scheme.toLowerCase(Locale.US); + if (scheme.equals(lowerScheme)) return this; // no change + + return buildUpon().scheme(lowerScheme).build(); + } + /** Identifies a null parcelled Uri. */ private static final int NULL_TYPE_ID = 0; @@ -1877,9 +1913,6 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { || (allow != null && allow.indexOf(c) != NOT_FOUND); } - /** Unicode replacement character: \\uFFFD. */ - private static final byte[] REPLACEMENT = { (byte) 0xFF, (byte) 0xFD }; - /** * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. * Replaces invalid octets with the unicode replacement character @@ -1890,104 +1923,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * s is null */ public static String decode(String s) { - /* - Compared to java.net.URLEncoderDecoder.decode(), this method decodes a - chunk at a time instead of one character at a time, and it doesn't - throw exceptions. It also only allocates memory when necessary--if - there's nothing to decode, this method won't do much. - */ - if (s == null) { return null; } - - // Lazily-initialized buffers. - StringBuilder decoded = null; - ByteArrayOutputStream out = null; - - int oldLength = s.length(); - - // This loop alternates between copying over normal characters and - // escaping in chunks. This results in fewer method calls and - // allocations than decoding one character at a time. - int current = 0; - while (current < oldLength) { - // Start in "copying" mode where we copy over normal characters. - - // Find the next escape sequence. - int nextEscape = s.indexOf('%', current); - - if (nextEscape == NOT_FOUND) { - if (decoded == null) { - // We didn't actually decode anything. - return s; - } else { - // Append the remainder and return the decoded string. - decoded.append(s, current, oldLength); - return decoded.toString(); - } - } - - // Prepare buffers. - if (decoded == null) { - // Looks like we're going to need the buffers... - // We know the new string will be shorter. Using the old length - // may overshoot a bit, but it will save us from resizing the - // buffer. - decoded = new StringBuilder(oldLength); - out = new ByteArrayOutputStream(4); - } else { - // Clear decoding buffer. - out.reset(); - } - - // Append characters leading up to the escape. - if (nextEscape > current) { - decoded.append(s, current, nextEscape); - - current = nextEscape; - } else { - // assert current == nextEscape - } - - // Switch to "decoding" mode where we decode a string of escape - // sequences. - - // Decode and append escape sequences. Escape sequences look like - // "%ab" where % is literal and a and b are hex digits. - try { - do { - if (current + 2 >= oldLength) { - // Truncated escape sequence. - out.write(REPLACEMENT); - } else { - int a = Character.digit(s.charAt(current + 1), 16); - int b = Character.digit(s.charAt(current + 2), 16); - - if (a == -1 || b == -1) { - // Non hex digits. - out.write(REPLACEMENT); - } else { - // Combine the hex digits into one byte and write. - out.write((a << 4) + b); - } - } - - // Move passed the escape sequence. - current += 3; - } while (current < oldLength && s.charAt(current) == '%'); - - // Decode UTF-8 bytes into a string and append it. - decoded.append(out.toString(DEFAULT_ENCODING)); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - // If we don't have a buffer, we didn't have to decode anything. - return decoded == null ? s : decoded.toString(); + return UriCodec.decode(s, false, Charsets.UTF_8, false); } /** @@ -2342,7 +2281,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { * * @param baseUri Uri to append path segment to * @param pathSegment encoded path segment to append - * @return a new Uri based on baseUri with the given segment appended to + * @return a new Uri based on baseUri with the given segment appended to * the path * @throws NullPointerException if baseUri is null */ diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index 92be373..f94d320 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -17,32 +17,20 @@ package android.net.http; -import com.android.internal.net.DomainNameValidator; - -import org.apache.harmony.security.provider.cert.X509CertImpl; -import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl; - import java.io.IOException; - import java.security.cert.Certificate; import java.security.cert.CertificateException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.util.Date; - +import javax.net.ssl.DefaultHostnameVerifier; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; +import org.apache.harmony.security.provider.cert.X509CertImpl; +import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl; /** * Class responsible for all server certificate validation functionality - * + * * {@hide} */ class CertificateChainValidator { @@ -53,6 +41,9 @@ class CertificateChainValidator { private static final CertificateChainValidator sInstance = new CertificateChainValidator(); + private static final DefaultHostnameVerifier sVerifier + = new DefaultHostnameVerifier(); + /** * @return The singleton instance of the certificates chain validator */ @@ -147,7 +138,10 @@ class CertificateChainValidator { throw new IllegalArgumentException("certificate for this site is null"); } - if (!DomainNameValidator.match(currCertificate, domain)) { + boolean valid = domain != null + && !domain.isEmpty() + && sVerifier.verify(domain, currCertificate); + if (!valid) { if (HttpLog.LOGV) { HttpLog.v("certificate not for this host: " + domain); } diff --git a/core/java/android/net/http/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java index 5f65dfa..21736aa 100644 --- a/core/java/android/net/http/HttpResponseCache.java +++ b/core/java/android/net/http/HttpResponseCache.java @@ -136,6 +136,18 @@ import org.apache.http.impl.client.DefaultHttpClient; * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale); * }</pre> + * + * <h3>Working With Earlier Releases</h3> + * This class was added in Android 4.0 (Ice Cream Sandwich). Use reflection to + * enable the response cache without impacting earlier releases: <pre> {@code + * try { + * File httpCacheDir = new File(context.getCacheDir(), "http"); + * long httpCacheSize = 10 * 1024 * 1024; // 10 MiB + * Class.forName("android.net.http.HttpResponseCache") + * .getMethod("install", File.class, long.class) + * .invoke(null, httpCacheDir, httpCacheSize); + * } catch (Exception httpResponseCacheNotAvailable) { + * }}</pre> */ public final class HttpResponseCache extends ResponseCache implements Closeable { diff --git a/core/java/android/nfc/FormatException.java b/core/java/android/nfc/FormatException.java index 7045a03..a57de1e 100644 --- a/core/java/android/nfc/FormatException.java +++ b/core/java/android/nfc/FormatException.java @@ -24,4 +24,8 @@ public class FormatException extends Exception { public FormatException(String message) { super(message); } + + public FormatException(String message, Throwable e) { + super(message, e); + } } diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 0b93ad0..61bc324 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -17,7 +17,6 @@ package android.nfc; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.IntentFilter; import android.nfc.NdefMessage; import android.nfc.Tag; @@ -44,4 +43,6 @@ interface INfcAdapter void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); void setForegroundNdefPush(in NdefMessage msg, in INdefPushCallback callback); + + void dispatch(in Tag tag); } diff --git a/core/java/android/nfc/LlcpPacket.java b/core/java/android/nfc/LlcpPacket.java deleted file mode 100644 index 9919dc4..0000000 --- a/core/java/android/nfc/LlcpPacket.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2010 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 android.nfc; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Represents a LLCP packet received in a LLCP Connectionless communication; - * @hide - */ -public class LlcpPacket implements Parcelable { - - private final int mRemoteSap; - - private final byte[] mDataBuffer; - - /** - * Creates a LlcpPacket to be sent to a remote Service Access Point number - * (SAP) - * - * @param sap Remote Service Access Point number - * @param data Data buffer - */ - public LlcpPacket(int sap, byte[] data) { - mRemoteSap = sap; - mDataBuffer = data; - } - - /** - * Returns the remote Service Access Point number - */ - public int getRemoteSap() { - return mRemoteSap; - } - - /** - * Returns the data buffer - */ - public byte[] getDataBuffer() { - return mDataBuffer; - } - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mRemoteSap); - dest.writeInt(mDataBuffer.length); - dest.writeByteArray(mDataBuffer); - } - - public static final Parcelable.Creator<LlcpPacket> CREATOR = new Parcelable.Creator<LlcpPacket>() { - public LlcpPacket createFromParcel(Parcel in) { - // Remote SAP - short sap = (short)in.readInt(); - - // Data Buffer - int dataLength = in.readInt(); - byte[] data = new byte[dataLength]; - in.readByteArray(data); - - return new LlcpPacket(sap, data); - } - - public LlcpPacket[] newArray(int size) { - return new LlcpPacket[size]; - } - }; -}
\ No newline at end of file diff --git a/core/java/android/nfc/NdefMessage.java b/core/java/android/nfc/NdefMessage.java index c79fabf..5df9272 100644 --- a/core/java/android/nfc/NdefMessage.java +++ b/core/java/android/nfc/NdefMessage.java @@ -16,90 +16,190 @@ package android.nfc; +import java.nio.ByteBuffer; +import java.util.Arrays; + import android.os.Parcel; import android.os.Parcelable; + /** - * Represents an NDEF (NFC Data Exchange Format) data message that contains one or more {@link - * NdefRecord}s. - * <p>An NDEF message includes "records" that can contain different sets of data, such as - * MIME-type media, a URI, or one of the supported RTD types (see {@link NdefRecord}). An NDEF - * message always contains zero or more NDEF records.</p> - * <p>This is an immutable data class. + * Represents an immutable NDEF Message. + * <p> + * NDEF (NFC Data Exchange Format) is a light-weight binary format, + * used to encapsulate typed data. It is specified by the NFC Forum, + * for transmission and storage with NFC, however it is transport agnostic. + * <p> + * NDEF defines messages and records. An NDEF Record contains + * typed data, such as MIME-type media, a URI, or a custom + * application payload. An NDEF Message is a container for + * one or more NDEF Records. + * <p> + * When an Android device receives an NDEF Message + * (for example by reading an NFC tag) it processes it through + * a dispatch mechanism to determine an activity to launch. + * The type of the <em>first</em> record in the message has + * special importance for message dispatch, so design this record + * carefully. + * <p> + * Use {@link #NdefMessage(byte[])} to construct an NDEF Message from + * binary data, or {@link #NdefMessage(NdefRecord[])} to + * construct from one or more {@link NdefRecord}s. + * <p class="note"> + * {@link NdefMessage} and {@link NdefRecord} implementations are + * always available, even on Android devices that do not have NFC hardware. + * <p class="note"> + * {@link NdefRecord}s are intended to be immutable (and thread-safe), + * however they may contain mutable fields. So take care not to modify + * mutable fields passed into constructors, or modify mutable fields + * obtained by getter methods, unless such modification is explicitly + * marked as safe. + * + * @see NfcAdapter#ACTION_NDEF_DISCOVERED + * @see NdefRecord */ public final class NdefMessage implements Parcelable { - private static final byte FLAG_MB = (byte) 0x80; - private static final byte FLAG_ME = (byte) 0x40; - private final NdefRecord[] mRecords; /** - * Create an NDEF message from raw bytes. - * <p> - * Validation is performed to make sure the Record format headers are valid, - * and the ID + TYPE + PAYLOAD fields are of the correct size. - * @throws FormatException + * Construct an NDEF Message by parsing raw bytes.<p> + * Strict validation of the NDEF binary structure is performed: + * there must be at least one record, every record flag must + * be correct, and the total length of the message must match + * the length of the input data.<p> + * This parser can handle chunked records, and converts them + * into logical {@link NdefRecord}s within the message.<p> + * Once the input data has been parsed to one or more logical + * records, basic validation of the tnf, type, id, and payload fields + * of each record is performed, as per the documentation on + * on {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])}<p> + * If either strict validation of the binary format fails, or + * basic validation during record construction fails, a + * {@link FormatException} is thrown<p> + * Deep inspection of the type, id and payload fields of + * each record is not performed, so it is possible to parse input + * that has a valid binary format and confirms to the basic + * validation requirements of + * {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])}, + * but fails more strict requirements as specified by the + * NFC Forum. + * + * <p class="note"> + * It is safe to re-use the data byte array after construction: + * this constructor will make an internal copy of all necessary fields. + * + * @param data raw bytes to parse + * @throws FormatException if the data cannot be parsed */ public NdefMessage(byte[] data) throws FormatException { - mRecords = null; // stop compiler complaints about final field - if (parseNdefMessage(data) == -1) { - throw new FormatException("Error while parsing NDEF message"); + if (data == null) throw new NullPointerException("data is null"); + ByteBuffer buffer = ByteBuffer.wrap(data); + + mRecords = NdefRecord.parse(buffer, false); + + if (buffer.remaining() > 0) { + throw new FormatException("trailing data"); + } + } + + /** + * Construct an NDEF Message from one or more NDEF Records. + * + * @param record first record (mandatory) + * @param records additional records (optional) + */ + public NdefMessage(NdefRecord record, NdefRecord ... records) { + // validate + if (record == null) throw new NullPointerException("record cannot be null"); + + for (NdefRecord r : records) { + if (r == null) { + throw new NullPointerException("record cannot be null"); + } } + + mRecords = new NdefRecord[1 + records.length]; + mRecords[0] = record; + System.arraycopy(records, 0, mRecords, 1, records.length); } /** - * Create an NDEF message from NDEF records. + * Construct an NDEF Message from one or more NDEF Records. + * + * @param records one or more records */ public NdefMessage(NdefRecord[] records) { - mRecords = new NdefRecord[records.length]; - System.arraycopy(records, 0, mRecords, 0, records.length); + // validate + if (records.length < 1) { + throw new IllegalArgumentException("must have at least one record"); + } + for (NdefRecord r : records) { + if (r == null) { + throw new NullPointerException("records cannot contain null"); + } + } + + mRecords = records; } /** - * Get the NDEF records inside this NDEF message. + * Get the NDEF Records inside this NDEF Message.<p> + * An {@link NdefMessage} always has one or more NDEF Records: so the + * following code to retrieve the first record is always safe + * (no need to check for null or array length >= 1): + * <pre> + * NdefRecord firstRecord = ndefMessage.getRecords()[0]; + * </pre> * - * @return array of zero or more NDEF records. + * @return array of one or more NDEF records. */ public NdefRecord[] getRecords() { - return mRecords.clone(); + return mRecords; } /** - * Returns a byte array representation of this entire NDEF message. + * Return the length of this NDEF Message if it is written to a byte array + * with {@link #toByteArray}.<p> + * An NDEF Message can be formatted to bytes in different ways + * depending on chunking, SR, and ID flags, so the length returned + * by this method may not be equal to the length of the original + * byte array used to construct this NDEF Message. However it will + * always be equal to the length of the byte array produced by + * {@link #toByteArray}. + * + * @return length of this NDEF Message when written to bytes with {@link #toByteArray} + * @see #toByteArray */ - public byte[] toByteArray() { - //TODO: allocate the byte array once, copy each record once - //TODO: process MB and ME flags outside loop - if ((mRecords == null) || (mRecords.length == 0)) - return new byte[0]; - - byte[] msg = {}; - - for (int i = 0; i < mRecords.length; i++) { - byte[] record = mRecords[i].toByteArray(); - byte[] tmp = new byte[msg.length + record.length]; - - /* Make sure the Message Begin flag is set only for the first record */ - if (i == 0) { - record[0] |= FLAG_MB; - } else { - record[0] &= ~FLAG_MB; - } - - /* Make sure the Message End flag is set only for the last record */ - if (i == (mRecords.length - 1)) { - record[0] |= FLAG_ME; - } else { - record[0] &= ~FLAG_ME; - } + public int getByteArrayLength() { + int length = 0; + for (NdefRecord r : mRecords) { + length += r.getByteLength(); + } + return length; + } - System.arraycopy(msg, 0, tmp, 0, msg.length); - System.arraycopy(record, 0, tmp, msg.length, record.length); + /** + * Return this NDEF Message as raw bytes.<p> + * The NDEF Message is formatted as per the NDEF 1.0 specification, + * and the byte array is suitable for network transmission or storage + * in an NFC Forum NDEF compatible tag.<p> + * This method will not chunk any records, and will always use the + * short record (SR) format and omit the identifier field when possible. + * + * @return NDEF Message in binary format + * @see getByteArrayLength + */ + public byte[] toByteArray() { + int length = getByteArrayLength(); + ByteBuffer buffer = ByteBuffer.allocate(length); - msg = tmp; + for (int i=0; i<mRecords.length; i++) { + boolean mb = (i == 0); // first record + boolean me = (i == mRecords.length - 1); // last record + mRecords[i].writeToByteBuffer(buffer, mb, me); } - return msg; + return buffer.array(); } @Override @@ -128,5 +228,26 @@ public final class NdefMessage implements Parcelable { } }; - private native int parseNdefMessage(byte[] data); + @Override + public int hashCode() { + return Arrays.hashCode(mRecords); + } + + /** + * Returns true if the specified NDEF Message contains + * identical NDEF Records. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + NdefMessage other = (NdefMessage) obj; + return Arrays.equals(mRecords, other.mRecords); + } + + @Override + public String toString() { + return "NdefMessage " + Arrays.toString(mRecords); + } }
\ No newline at end of file diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index 26571ff..0e9e8f4 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -16,83 +16,144 @@ package android.nfc; +import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; - -import java.lang.UnsupportedOperationException; -import java.nio.charset.Charset; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; import java.nio.charset.Charsets; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Locale; /** - * Represents a logical (unchunked) NDEF (NFC Data Exchange Format) record. - * <p>An NDEF record always contains: + * Represents an immutable NDEF Record. + * <p> + * NDEF (NFC Data Exchange Format) is a light-weight binary format, + * used to encapsulate typed data. It is specified by the NFC Forum, + * for transmission and storage with NFC, however it is transport agnostic. + * <p> + * NDEF defines messages and records. An NDEF Record contains + * typed data, such as MIME-type media, a URI, or a custom + * application payload. An NDEF Message is a container for + * one or more NDEF Records. + * <p> + * This class represents logical (complete) NDEF Records, and can not be + * used to represent chunked (partial) NDEF Records. However + * {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message + * containing chunked records, and will return a message with unchunked + * (complete) records. + * <p> + * A logical NDEF Record always contains a 3-bit TNF (Type Name Field) + * that provides high level typing for the rest of the record. The + * remaining fields are variable length and not always present: * <ul> - * <li>3-bit TNF (Type Name Format) field: Indicates how to interpret the type field - * <li>Variable length type: Describes the record format - * <li>Variable length ID: A unique identifier for the record - * <li>Variable length payload: The actual data payload + * <li><em>type</em>: detailed typing for the payload</li> + * <li><em>id</em>: identifier meta-data, not commonly used</li> + * <li><em>payload</em>: the actual payload</li> * </ul> - * <p>The underlying record - * representation may be chunked across several NDEF records when the payload is - * large. - * <p>This is an immutable data class. + * <p> + * Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime} + * and {@link NdefRecord#createExternal} are included to create well-formatted + * NDEF Records with correctly set tnf, type, id and payload fields, please + * use these helpers whenever possible. + * <p> + * Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])} + * if you know what you are doing and what to set the fields individually. + * Only basic validation is performed with this constructor, so it is possible + * to create records that do not confirm to the strict NFC Forum + * specifications. + * <p> + * The binary representation of an NDEF Record includes additional flags to + * indicate location with an NDEF message, provide support for chunking of + * NDEF records, and to pack optional fields. This class does not expose + * those details. To write an NDEF Record as binary you must first put it + * into an @{link NdefMessage}, then call {@link NdefMessage#toByteArray()}. + * <p class="note"> + * {@link NdefMessage} and {@link NdefRecord} implementations are + * always available, even on Android devices that do not have NFC hardware. + * <p class="note"> + * {@link NdefRecord}s are intended to be immutable (and thread-safe), + * however they may contain mutable fields. So take care not to modify + * mutable fields passed into constructors, or modify mutable fields + * obtained by getter methods, unless such modification is explicitly + * marked as safe. + * + * @see NfcAdapter#ACTION_NDEF_DISCOVERED + * @see NdefMessage */ public final class NdefRecord implements Parcelable { /** - * Indicates no type, id, or payload is associated with this NDEF Record. - * <p> - * Type, id and payload fields must all be empty to be a valid TNF_EMPTY - * record. + * Indicates the record is empty.<p> + * Type, id and payload fields are empty in a {@literal TNF_EMPTY} record. */ public static final short TNF_EMPTY = 0x00; /** - * Indicates the type field uses the RTD type name format. + * Indicates the type field contains a well-known RTD type name.<p> + * Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}. * <p> - * Use this TNF with RTD types such as RTD_TEXT, RTD_URI. + * The RTD type name format is specified in NFCForum-TS-RTD_1.0. + * + * @see #RTD_URI + * @see #RTD_TEXT + * @see #RTD_SMART_POSTER + * @see #createUri */ public static final short TNF_WELL_KNOWN = 0x01; /** - * Indicates the type field contains a value that follows the media-type BNF - * construct defined by RFC 2046. + * Indicates the type field contains a media-type BNF + * construct, defined by RFC 2046.<p> + * Use this with MIME type names such as {@literal "image/jpeg"}, or + * using the helper {@link #createMime}. + * + * @see #createMime */ public static final short TNF_MIME_MEDIA = 0x02; /** - * Indicates the type field contains a value that follows the absolute-URI - * BNF construct defined by RFC 3986. + * Indicates the type field contains an absolute-URI + * BNF construct defined by RFC 3986.<p> + * When creating new records prefer {@link #createUri}, + * since it offers more compact URI encoding + * ({@literal #RTD_URI} allows compression of common URI prefixes). + * + * @see #createUri */ public static final short TNF_ABSOLUTE_URI = 0x03; /** - * Indicates the type field contains a value that follows the RTD external - * name specification. + * Indicates the type field contains an external type name.<p> + * Used to encode custom payloads. When creating new records + * use the helper {@link #createExternal}.<p> + * The external-type RTD format is specified in NFCForum-TS-RTD_1.0.<p> * <p> * Note this TNF should not be used with RTD_TEXT or RTD_URI constants. * Those are well known RTD constants, not external RTD constants. + * + * @see #createExternal */ public static final short TNF_EXTERNAL_TYPE = 0x04; /** - * Indicates the payload type is unknown. - * <p> - * This is similar to the "application/octet-stream" MIME type. The payload - * type is not explicitly encoded within the NDEF Message. + * Indicates the payload type is unknown.<p> + * NFC Forum explains this should be treated similarly to the + * "application/octet-stream" MIME type. The payload + * type is not explicitly encoded within the record. * <p> - * The type field must be empty to be a valid TNF_UNKNOWN record. + * The type field is empty in an {@literal TNF_UNKNOWN} record. */ public static final short TNF_UNKNOWN = 0x05; /** * Indicates the payload is an intermediate or final chunk of a chunked - * NDEF Record. - * <p> - * The payload type is specified in the first chunk, and subsequent chunks - * must use TNF_UNCHANGED with an empty type field. TNF_UNCHANGED must not - * be used in any other situation. + * NDEF Record.<p> + * {@literal TNF_UNCHANGED} can not be used with this class + * since all {@link NdefRecord}s are already unchunked, however they + * may appear in the binary format. */ public static final short TNF_UNCHANGED = 0x06; @@ -106,42 +167,49 @@ public final class NdefRecord implements Parcelable { public static final short TNF_RESERVED = 0x07; /** - * RTD Text type. For use with TNF_WELL_KNOWN. + * RTD Text type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_TEXT = {0x54}; // "T" /** - * RTD URI type. For use with TNF_WELL_KNOWN. + * RTD URI type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_URI = {0x55}; // "U" /** - * RTD Smart Poster type. For use with TNF_WELL_KNOWN. + * RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_SMART_POSTER = {0x53, 0x70}; // "Sp" /** - * RTD Alternative Carrier type. For use with TNF_WELL_KNOWN. + * RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63}; // "ac" /** - * RTD Handover Carrier type. For use with TNF_WELL_KNOWN. + * RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63}; // "Hc" /** - * RTD Handover Request type. For use with TNF_WELL_KNOWN. + * RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72}; // "Hr" /** - * RTD Handover Select type. For use with TNF_WELL_KNOWN. + * RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN */ public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs" /** - * RTD Android app type. For use with TNF_EXTERNAL. + * RTD Android app type. For use with {@literal TNF_EXTERNAL}. * <p> * The payload of a record with type RTD_ANDROID_APP * should be the package name identifying an application. @@ -161,8 +229,7 @@ public final class NdefRecord implements Parcelable { private static final byte FLAG_IL = (byte) 0x08; /** - * NFC Forum "URI Record Type Definition" - * + * NFC Forum "URI Record Type Definition"<p> * This is a mapping of "URI Identifier Codes" to URI string prefixes, * per section 3.2.2 of the NFC Forum URI Record Type Definition document. */ @@ -204,84 +271,292 @@ public final class NdefRecord implements Parcelable { "urn:epc:", // 0x22 }; - private final byte mFlags; + private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private final short mTnf; private final byte[] mType; private final byte[] mId; private final byte[] mPayload; /** - * Construct an NDEF Record. + * Create a new Android Application Record (AAR). + * <p> + * This record indicates to other Android devices the package + * that should be used to handle the entire NDEF message. + * You can embed this record anywhere into your message + * to ensure that the intended package receives the message. + * <p> + * When an Android device dispatches an {@link NdefMessage} + * containing one or more Android application records, + * the applications contained in those records will be the + * preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED} + * intent, in the order in which they appear in the message. + * This dispatch behavior was first added to Android in + * Ice Cream Sandwich. * <p> - * Applications should not attempt to manually chunk NDEF Records - the - * implementation of android.nfc will automatically chunk an NDEF Record - * when necessary (and only present a single logical NDEF Record to the - * application). So applications should not use TNF_UNCHANGED. + * If none of the applications have a are installed on the device, + * a Market link will be opened to the first application. + * <p> + * Note that Android application records do not overrule + * applications that have called + * {@link NfcAdapter#enableForegroundDispatch}. * - * @param tnf a 3-bit TNF constant - * @param type byte array, containing zero to 255 bytes, must not be null - * @param id byte array, containing zero to 255 bytes, must not be null - * @param payload byte array, containing zero to (2 ** 32 - 1) bytes, - * must not be null + * @param packageName Android package name + * @return Android application NDEF record */ - public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) { - /* New NDEF records created by applications will have FLAG_MB|FLAG_ME - * set by default; when multiple records are stored in a - * {@link NdefMessage}, these flags will be corrected when the {@link NdefMessage} - * is serialized to bytes. - */ - this(tnf, type, id, payload, (byte)(FLAG_MB|FLAG_ME)); + public static NdefRecord createApplicationRecord(String packageName) { + if (packageName == null) throw new NullPointerException("packageName is null"); + if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty"); + + return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, + packageName.getBytes(Charsets.UTF_8)); } /** - * @hide + * Create a new NDEF Record containing a URI.<p> + * Use this method to encode a URI (or URL) into an NDEF Record.<p> + * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.<p> + * The uri parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uri + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uri URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uri is empty or invalid */ - /*package*/ NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload, byte flags) { - /* check arguments */ - if ((type == null) || (id == null) || (payload == null)) { - throw new IllegalArgumentException("Illegal null argument"); - } + public static NdefRecord createUri(Uri uri) { + if (uri == null) throw new NullPointerException("uri is null"); + + uri = uri.normalize(); + String uriString = uri.toString(); + if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty"); - if (tnf < 0 || tnf > 0x07) { - throw new IllegalArgumentException("TNF out of range " + tnf); + byte prefix = 0; + for (int i = 1; i < URI_PREFIX_MAP.length; i++) { + if (uriString.startsWith(URI_PREFIX_MAP[i])) { + prefix = (byte) i; + uriString = uriString.substring(URI_PREFIX_MAP[i].length()); + break; + } } + byte[] uriBytes = uriString.getBytes(Charsets.UTF_8); + byte[] recordBytes = new byte[uriBytes.length + 1]; + recordBytes[0] = prefix; + System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length); + return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes); + } - /* Determine if it is a short record */ - if(payload.length < 0xFF) { - flags |= FLAG_SR; + /** + * Create a new NDEF Record containing a URI.<p> + * Use this method to encode a URI (or URL) into an NDEF Record.<p> + * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.<p> + * The uriString parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uriString + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uriString string URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uriString is empty or invalid + */ + public static NdefRecord createUri(String uriString) { + return createUri(Uri.parse(uriString)); + } + + /** + * Create a new NDEF Record containing MIME data.<p> + * Use this method to encode MIME-typed data into an NDEF Record, + * such as "text/plain", or "image/jpeg".<p> + * The mimeType parameter will be normalized with + * {@link Intent#normalizeMimeType} to follow Android best + * practices for intent filtering, for example to force lower-case. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown + * if the mimeType parameter has serious problems, + * for example if it is empty, so always catch this + * exception if you are passing user-generated data into this method. + * <p> + * For efficiency, This method might not make an internal copy of the + * mimeData byte array, so take care not + * to modify the mimeData byte array while still using the returned + * NdefRecord. + * + * @param mimeType a valid MIME type + * @param mimeData MIME data as bytes + * @return an NDEF Record containing the MIME-typed data + * @throws IllegalArugmentException if the mimeType is empty or invalid + * + */ + public static NdefRecord createMime(String mimeType, byte[] mimeData) { + if (mimeType == null) throw new NullPointerException("mimeType is null"); + + // We only do basic MIME type validation: trying to follow the + // RFCs strictly only ends in tears, since there are lots of MIME + // types in common use that are not strictly valid as per RFC rules + mimeType = Intent.normalizeMimeType(mimeType); + if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); + int slashIndex = mimeType.indexOf('/'); + if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); + if (slashIndex == mimeType.length() - 1) { + throw new IllegalArgumentException("mimeType must have minor type"); } + // missing '/' is allowed - /* Determine if an id is present */ - if(id.length != 0) { - flags |= FLAG_IL; + // MIME RFCs suggest ASCII encoding for content-type + byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII); + return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData); + } + + /** + * Create a new NDEF Record containing external (application-specific) data.<p> + * Use this method to encode application specific data into an NDEF Record. + * The data is typed by a domain name (usually your Android package name) and + * a domain-specific type. This data is packaged into a "NFC Forum External + * Type" NDEF Record.<p> + * NFC Forum requires that the domain and type used in an external record + * are treated as case insensitive, however Android intent filtering is + * always case sensitive. So this method will force the domain and type to + * lower-case before creating the NDEF Record.<p> + * The unchecked exception {@link IllegalArgumentException} will be thrown + * if the domain and type have serious problems, for example if either field + * is empty, so always catch this + * exception if you are passing user-generated data into this method.<p> + * There are no such restrictions on the payload data.<p> + * For efficiency, This method might not make an internal copy of the + * data byte array, so take care not + * to modify the data byte array while still using the returned + * NdefRecord. + * + * Reference specification: NFCForum-TS-RTD_1.0 + * @param domain domain-name of issuing organization + * @param type domain-specific type of data + * @param data payload as bytes + * @throws IllegalArugmentException if either domain or type are empty or invalid + */ + public static NdefRecord createExternal(String domain, String type, byte[] data) { + if (domain == null) throw new NullPointerException("domain is null"); + if (type == null) throw new NullPointerException("type is null"); + + domain = domain.trim().toLowerCase(Locale.US); + type = type.trim().toLowerCase(Locale.US); + + if (domain.length() == 0) throw new IllegalArgumentException("domain is empty"); + if (type.length() == 0) throw new IllegalArgumentException("type is empty"); + + byte[] byteDomain = domain.getBytes(Charsets.UTF_8); + byte[] byteType = type.getBytes(Charsets.UTF_8); + byte[] b = new byte[byteDomain.length + 1 + byteType.length]; + System.arraycopy(byteDomain, 0, b, 0, byteDomain.length); + b[byteDomain.length] = ':'; + System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length); + + return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data); + } + + /** + * Construct an NDEF Record from its component fields.<p> + * Recommend to use helpers such as {#createUri} or + * {{@link #createExternal} where possible, since they perform + * stricter validation that the record is correctly formatted + * as per NDEF specifications. However if you know what you are + * doing then this constructor offers the most flexibility.<p> + * An {@link NdefRecord} represents a logical (complete) + * record, and cannot represent NDEF Record chunks.<p> + * Basic validation of the tnf, type, id and payload is performed + * as per the following rules: + * <ul> + * <li>The tnf paramter must be a 3-bit value.</li> + * <li>Records with a tnf of {@link #TNF_EMPTY} cannot have a type, + * id or payload.</li> + * <li>Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07} + * cannot have a type.</li> + * <li>Records with a tnf of {@link #TNF_UNCHANGED} are not allowed + * since this class only represents complete (unchunked) records.</li> + * </ul> + * This minimal validation is specified by + * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).<p> + * If any of the above validation + * steps fail then {@link IllegalArgumentException} is thrown.<p> + * Deep inspection of the type, id and payload fields is not + * performed, so it is possible to create NDEF Records + * that conform to section 3.2.6 + * but fail other more strict NDEF specification requirements. For + * example, the payload may be invalid given the tnf and type. + * <p> + * To omit a type, id or payload field, set the parameter to an + * empty byte array or null. + * + * @param tnf a 3-bit TNF constant + * @param type byte array, containing zero to 255 bytes, or null + * @param id byte array, containing zero to 255 bytes, or null + * @param payload byte array, containing zero to (2 ** 32 - 1) bytes, + * or null + * @throws IllegalArugmentException if a valid record cannot be created + */ + public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) { + /* convert nulls */ + if (type == null) type = EMPTY_BYTE_ARRAY; + if (id == null) id = EMPTY_BYTE_ARRAY; + if (payload == null) payload = EMPTY_BYTE_ARRAY; + + String message = validateTnf(tnf, type, id, payload); + if (message != null) { + throw new IllegalArgumentException(message); } - mFlags = flags; mTnf = tnf; - mType = type.clone(); - mId = id.clone(); - mPayload = payload.clone(); + mType = type; + mId = id; + mPayload = payload; } /** - * Construct an NDEF Record from raw bytes. - * <p> - * Validation is performed to make sure the header is valid, and that - * the id, type and payload sizes appear to be valid. + * Construct an NDEF Record from raw bytes.<p> + * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])} + * instead. This is because it does not make sense to parse a record: + * the NDEF binary format is only defined for a message, and the + * record flags MB and ME do not make sense outside of the context of + * an entire message.<p> + * This implementation will attempt to parse a single record by ignoring + * the MB and ME flags, and otherwise following the rules of + * {@link NdefMessage#NdefMessage(byte[])}.<p> * - * @throws FormatException if the data is not a valid NDEF record + * @param data raw bytes to parse + * @throws FormatException if the data cannot be parsed into a valid record + * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead. */ + @Deprecated public NdefRecord(byte[] data) throws FormatException { - /* Prevent compiler to complain about unassigned final fields */ - mFlags = 0; - mTnf = 0; - mType = null; - mId = null; - mPayload = null; - /* Perform actual parsing */ - if (parseNdefRecord(data) == -1) { - throw new FormatException("Error while parsing NDEF record"); + ByteBuffer buffer = ByteBuffer.wrap(data); + NdefRecord[] rs = parse(buffer, true); + + if (buffer.remaining() > 0) { + throw new FormatException("data too long"); } + + mTnf = rs[0].mTnf; + mType = rs[0].mType; + mId = rs[0].mId; + mPayload = rs[0].mPayload; } /** @@ -298,6 +573,9 @@ public final class NdefRecord implements Parcelable { * <p> * This should be used in conjunction with the TNF field to determine the * payload format. + * <p> + * Returns an empty byte array if this record + * does not have a type field. */ public byte[] getType() { return mType.clone(); @@ -305,6 +583,9 @@ public final class NdefRecord implements Parcelable { /** * Returns the variable length ID. + * <p> + * Returns an empty byte array if this record + * does not have an id field. */ public byte[] getId() { return mId.clone(); @@ -312,125 +593,349 @@ public final class NdefRecord implements Parcelable { /** * Returns the variable length payload. + * <p> + * Returns an empty byte array if this record + * does not have a payload field. */ public byte[] getPayload() { return mPayload.clone(); } /** - * Helper to return the NdefRecord as a URI. - * TODO: Consider making a member method instead of static - * TODO: Consider more validation that this is a URI record - * TODO: Make a public API - * @hide + * Return this NDEF Record as a byte array.<p> + * This method is deprecated, use {@link NdefMessage#toByteArray} + * instead. This is because the NDEF binary format is not defined for + * a record outside of the context of a message: the MB and ME flags + * cannot be set without knowing the location inside a message.<p> + * This implementation will attempt to serialize a single record by + * always setting the MB and ME flags (in other words, assume this + * is a single-record NDEF Message).<p> + * + * @deprecated use {@link NdefMessage#toByteArray()} instead */ - public static Uri parseWellKnownUriRecord(NdefRecord record) throws FormatException { - byte[] payload = record.getPayload(); - if (payload.length < 2) { - throw new FormatException("Payload is not a valid URI (missing prefix)"); - } + @Deprecated + public byte[] toByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(getByteLength()); + writeToByteBuffer(buffer, true, true); + return buffer.array(); + } - /* - * payload[0] contains the URI Identifier Code, per the - * NFC Forum "URI Record Type Definition" section 3.2.2. - * - * payload[1]...payload[payload.length - 1] contains the rest of - * the URI. - */ - int prefixIndex = (payload[0] & 0xff); - if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { - throw new FormatException("Payload is not a valid URI (invalid prefix)"); + /** + * Map this record to a MIME type, or return null if it cannot be mapped.<p> + * Currently this method considers all {@link #TNF_MIME_MEDIA} records to + * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as + * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string + * is returned, otherwise null is returned.<p> + * This method does not perform validation that the MIME type is + * actually valid. It always attempts to + * return a string containing the type if this is a MIME record.<p> + * The returned MIME type will by normalized to lower-case using + * {@link Intent#normalizeMimeType}.<p> + * The MIME payload can be obtained using {@link #getPayload}. + * + * @return MIME type as a string, or null if this is not a MIME record + */ + public String toMimeType() { + switch (mTnf) { + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) { + return "text/plain"; + } + break; + case NdefRecord.TNF_MIME_MEDIA: + String mimeType = new String(mType, Charsets.US_ASCII); + return Intent.normalizeMimeType(mimeType); } - String prefix = URI_PREFIX_MAP[prefixIndex]; - byte[] fullUri = concat(prefix.getBytes(Charsets.UTF_8), - Arrays.copyOfRange(payload, 1, payload.length)); - return Uri.parse(new String(fullUri, Charsets.UTF_8)); + return null; } /** - * Creates an Android application NDEF record. - * <p> - * This record indicates to other Android devices the package - * that should be used to handle the rest of the NDEF message. - * You can embed this record anywhere into your NDEF message - * to ensure that the intended package receives the message. - * <p> - * When an Android device dispatches an {@link NdefMessage} - * containing one or more Android application records, - * the applications contained in those records will be the - * preferred target for the NDEF_DISCOVERED intent, in - * the order in which they appear in the {@link NdefMessage}. - * This dispatch behavior was first added to Android in - * Ice Cream Sandwich. - * <p> - * If none of the applications are installed on the device, - * a Market link will be opened to the first application. - * <p> - * Note that Android application records do not overrule - * applications that have called - * {@link NfcAdapter#enableForegroundDispatch}. + * Map this record to a URI, or return null if it cannot be mapped.<p> + * Currently this method considers the following to be URI records: + * <ul> + * <li>{@link #TNF_ABSOLUTE_URI} records.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER} + * and containing a URI record in the NDEF message nested in the payload. + * </li> + * <li>{@link #TNF_EXTERNAL_TYPE} records.</li> + * </ul> + * If this is not a URI record by the above rules, then null is returned.<p> + * This method does not perform validation that the URI is + * actually valid: it always attempts to create and return a URI if + * this record appears to be a URI record by the above rules.<p> + * The returned URI will be normalized to have a lower case scheme + * using {@link Uri#normalize}.<p> * - * @param packageName Android package name - * @return Android application NDEF record + * @return URI, or null if this is not a URI record */ - public static NdefRecord createApplicationRecord(String packageName) { - return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, new byte[] {}, - packageName.getBytes(Charsets.US_ASCII)); + public Uri toUri() { + return toUri(false); + } + + private Uri toUri(boolean inSmartPoster) { + switch (mTnf) { + case TNF_WELL_KNOWN: + if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) { + try { + // check payload for a nested NDEF Message containing a URI + NdefMessage nestedMessage = new NdefMessage(mPayload); + for (NdefRecord nestedRecord : nestedMessage.getRecords()) { + Uri uri = nestedRecord.toUri(true); + if (uri != null) { + return uri; + } + } + } catch (FormatException e) { } + } else if (Arrays.equals(mType, RTD_URI)) { + return parseWktUri().normalize(); + } + break; + + case TNF_ABSOLUTE_URI: + Uri uri = Uri.parse(new String(mType, Charsets.UTF_8)); + return uri.normalize(); + + case TNF_EXTERNAL_TYPE: + if (inSmartPoster) { + break; + } + return Uri.parse("vnd.android.nfc://ext/" + new String(mType, Charsets.US_ASCII)); + } + return null; } /** - * Creates an NDEF record of well known type URI. + * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records. + * @return complete URI, or null if invalid */ - public static NdefRecord createUri(Uri uri) { - return createUri(uri.toString()); + private Uri parseWktUri() { + if (mPayload.length < 2) { + return null; + } + + // payload[0] contains the URI Identifier Code, as per + // NFC Forum "URI Record Type Definition" section 3.2.2. + int prefixIndex = (mPayload[0] & (byte)0xFF); + if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { + return null; + } + String prefix = URI_PREFIX_MAP[prefixIndex]; + String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length), + Charsets.UTF_8); + return Uri.parse(prefix + suffix); } /** - * Creates an NDEF record of well known type URI. + * Main record parsing method.<p> + * Expects NdefMessage to begin immediately, allows trailing data.<p> + * Currently has strict validation of all fields as per NDEF 1.0 + * specification section 2.5. We will attempt to keep this as strict as + * possible to encourage well-formatted NDEF.<p> + * Always returns 1 or more NdefRecord's, or throws FormatException. + * + * @param buffer ByteBuffer to read from + * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record + * @return one or more records + * @throws FormatException on any parsing error */ - public static NdefRecord createUri(String uriString) { - byte prefix = 0x0; - for (int i = 1; i < URI_PREFIX_MAP.length; i++) { - if (uriString.startsWith(URI_PREFIX_MAP[i])) { - prefix = (byte) i; - uriString = uriString.substring(URI_PREFIX_MAP[i].length()); - break; + static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException { + List<NdefRecord> records = new ArrayList<NdefRecord>(); + + try { + byte[] type = null; + byte[] id = null; + byte[] payload = null; + ArrayList<byte[]> chunks = new ArrayList<byte[]>(); + boolean inChunk = false; + short chunkTnf = -1; + boolean me = false; + + while (!me) { + byte flag = buffer.get(); + + boolean mb = (flag & NdefRecord.FLAG_MB) != 0; + me = (flag & NdefRecord.FLAG_ME) != 0; + boolean cf = (flag & NdefRecord.FLAG_CF) != 0; + boolean sr = (flag & NdefRecord.FLAG_SR) != 0; + boolean il = (flag & NdefRecord.FLAG_IL) != 0; + short tnf = (short)(flag & 0x07); + + if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) { + throw new FormatException("expected MB flag"); + } else if (mb && records.size() != 0 && !ignoreMbMe) { + throw new FormatException("unexpected MB flag"); + } else if (inChunk && il) { + throw new FormatException("unexpected IL flag in non-leading chunk"); + } else if (cf && me) { + throw new FormatException("unexpected ME flag in non-trailing chunk"); + } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) { + throw new FormatException("expected TNF_UNCHANGED in non-leading chunk"); + } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) { + throw new FormatException("" + + "unexpected TNF_UNCHANGED in first chunk or unchunked record"); + } + + int typeLength = buffer.get() & 0xFF; + long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL); + int idLength = il ? (buffer.get() & 0xFF) : 0; + + if (inChunk && typeLength != 0) { + throw new FormatException("expected zero-length type in non-leading chunk"); + } + + if (!inChunk) { + type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY); + id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY); + buffer.get(type); + buffer.get(id); + } + + ensureSanePayloadSize(payloadLength); + payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY); + buffer.get(payload); + + if (cf && !inChunk) { + // first chunk + chunks.clear(); + chunkTnf = tnf; + } + if (cf || inChunk) { + // any chunk + chunks.add(payload); + } + if (!cf && inChunk) { + // last chunk, flatten the payload + payloadLength = 0; + for (byte[] p : chunks) { + payloadLength += p.length; + } + ensureSanePayloadSize(payloadLength); + payload = new byte[(int)payloadLength]; + int i = 0; + for (byte[] p : chunks) { + System.arraycopy(p, 0, payload, i, p.length); + i += p.length; + } + tnf = chunkTnf; + } + if (cf) { + // more chunks to come + inChunk = true; + continue; + } else { + inChunk = false; + } + + String error = validateTnf(tnf, type, id, payload); + if (error != null) { + throw new FormatException(error); + } + records.add(new NdefRecord(tnf, type, id, payload)); + if (ignoreMbMe) { // for parsing a single NdefRecord + break; + } } + } catch (BufferUnderflowException e) { + throw new FormatException("expected more data", e); } - byte[] uriBytes = uriString.getBytes(Charsets.UTF_8); - byte[] recordBytes = new byte[uriBytes.length + 1]; - recordBytes[0] = prefix; - System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length); - return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, new byte[0], recordBytes); + return records.toArray(new NdefRecord[records.size()]); } - private static byte[] concat(byte[]... arrays) { - int length = 0; - for (byte[] array : arrays) { - length += array.length; + private static void ensureSanePayloadSize(long size) throws FormatException { + if (size > MAX_PAYLOAD_SIZE) { + throw new FormatException( + "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE); } - byte[] result = new byte[length]; - int pos = 0; - for (byte[] array : arrays) { - System.arraycopy(array, 0, result, pos, array.length); - pos += array.length; + } + + /** + * Perform simple validation that the tnf is valid.<p> + * Validates the requirements of NFCForum-TS-NDEF_1.0 section + * 3.2.6 (Type Name Format). This just validates that the tnf + * is valid, and that the relevant type, id and payload + * fields are present (or empty) for this tnf. It does not + * perform any deep inspection of the type, id and payload fields.<p> + * Also does not allow TNF_UNCHANGED since this class is only used + * to present logical (unchunked) records. + * + * @return null if valid, or a string error if invalid. + */ + static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) { + switch (tnf) { + case TNF_EMPTY: + if (type.length != 0 || id.length != 0 || payload.length != 0) { + return "unexpected data in TNF_EMPTY record"; + } + return null; + case TNF_WELL_KNOWN: + case TNF_MIME_MEDIA: + case TNF_ABSOLUTE_URI: + case TNF_EXTERNAL_TYPE: + return null; + case TNF_UNKNOWN: + case TNF_RESERVED: + if (type.length != 0) { + return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record"; + } + return null; + case TNF_UNCHANGED: + return "unexpected TNF_UNCHANGED in first chunk or logical record"; + default: + return String.format("unexpected tnf value: 0x%02x", tnf); } - return result; } /** - * Returns this entire NDEF Record as a byte array. + * Serialize record for network transmission.<p> + * Uses specified MB and ME flags.<p> + * Does not chunk records. */ - public byte[] toByteArray() { - return generate(mFlags, mTnf, mType, mId, mPayload); + void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) { + boolean sr = mPayload.length < 256; + boolean il = mId.length > 0; + + byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) | + (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf); + buffer.put(flags); + + buffer.put((byte)mType.length); + if (sr) { + buffer.put((byte)mPayload.length); + } else { + buffer.putInt(mPayload.length); + } + if (il) { + buffer.put((byte)mId.length); + } + + buffer.put(mType); + buffer.put(mId); + buffer.put(mPayload); + } + + /** + * Get byte length of serialized record. + */ + int getByteLength() { + int length = 3 + mType.length + mId.length + mPayload.length; + + boolean sr = mPayload.length < 256; + boolean il = mId.length > 0; + + if (!sr) length += 3; + if (il) length += 1; + + return length; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mFlags); dest.writeInt(mTnf); dest.writeInt(mType.length); dest.writeByteArray(mType); @@ -442,8 +947,8 @@ public final class NdefRecord implements Parcelable { public static final Parcelable.Creator<NdefRecord> CREATOR = new Parcelable.Creator<NdefRecord>() { + @Override public NdefRecord createFromParcel(Parcel in) { - byte flags = (byte)in.readInt(); short tnf = (short)in.readInt(); int typeLength = in.readInt(); byte[] type = new byte[typeLength]; @@ -455,13 +960,55 @@ public final class NdefRecord implements Parcelable { byte[] payload = new byte[payloadLength]; in.readByteArray(payload); - return new NdefRecord(tnf, type, id, payload, flags); + return new NdefRecord(tnf, type, id, payload); } + @Override public NdefRecord[] newArray(int size) { return new NdefRecord[size]; } }; - private native int parseNdefRecord(byte[] data); - private native byte[] generate(short flags, short tnf, byte[] type, byte[] id, byte[] data); + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(mId); + result = prime * result + Arrays.hashCode(mPayload); + result = prime * result + mTnf; + result = prime * result + Arrays.hashCode(mType); + return result; + } + + /** + * Returns true if the specified NDEF Record contains + * identical tnf, type, id and payload fields. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + NdefRecord other = (NdefRecord) obj; + if (!Arrays.equals(mId, other.mId)) return false; + if (!Arrays.equals(mPayload, other.mPayload)) return false; + if (mTnf != other.mTnf) return false; + return Arrays.equals(mType, other.mType); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf)); + if (mType.length > 0) b.append(" type=").append(bytesToString(mType)); + if (mId.length > 0) b.append(" id=").append(bytesToString(mId)); + if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload)); + return b.toString(); + } + + private static StringBuilder bytesToString(byte[] bs) { + StringBuilder s = new StringBuilder(); + for (byte b : bs) { + s.append(String.format("%02X", b)); + } + return s; + } } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index a903a5f..5176857 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -66,6 +66,9 @@ public final class NfcAdapter { * <p>If the tag has an NDEF payload this intent is started before * {@link #ACTION_TECH_DISCOVERED}. If any activities respond to this intent neither * {@link #ACTION_TECH_DISCOVERED} or {@link #ACTION_TAG_DISCOVERED} will be started. + * + * <p>The MIME type or data URI of this intent are normalized before dispatch - + * so that MIME, URI scheme and URI host are always lower-case. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; @@ -151,9 +154,13 @@ public final class NfcAdapter { public static final String EXTRA_TAG = "android.nfc.extra.TAG"; /** - * Optional extra containing an array of {@link NdefMessage} present on the discovered tag for - * the {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and - * {@link #ACTION_TAG_DISCOVERED} intents. + * Extra containing an array of {@link NdefMessage} present on the discovered tag.<p> + * This extra is mandatory for {@link #ACTION_NDEF_DISCOVERED} intents, + * and optional for {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents.<p> + * When this extra is present there will always be at least one + * {@link NdefMessage} element. Most NDEF tags have only one NDEF message, + * but we use an array for future compatibility. */ public static final String EXTRA_NDEF_MESSAGES = "android.nfc.extra.NDEF_MESSAGES"; @@ -363,8 +370,11 @@ public final class NfcAdapter { throw new IllegalArgumentException("context cannot be null"); } context = context.getApplicationContext(); - /* use getSystemService() instead of just instantiating to take - * advantage of the context's cached NfcManager & NfcAdapter */ + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } + /* use getSystemService() for consistency */ NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); if (manager == null) { // NFC not available @@ -379,9 +389,14 @@ public final class NfcAdapter { * for many NFC API methods. Those methods will fail when called on an NfcAdapter * object created from this method.<p> * @deprecated use {@link #getDefaultAdapter(Context)} + * @hide */ @Deprecated public static NfcAdapter getDefaultAdapter() { + // introduced in API version 9 (GB 2.3) + // deprecated in API version 10 (GB 2.3.3) + // removed from public API in version 16 (ICS MR2) + // should maintain as a hidden API for binary compatibility for a little longer Log.w(TAG, "WARNING: NfcAdapter.getDefaultAdapter() is deprecated, use " + "NfcAdapter.getDefaultAdapter(Context) instead", new Exception()); @@ -448,11 +463,13 @@ public final class NfcAdapter { /** * Return true if this NFC Adapter has any features enabled. * - * <p>Application may use this as a helper to suggest that the user - * should turn on NFC in Settings. * <p>If this method returns false, the NFC hardware is guaranteed not to - * generate or respond to any NFC transactions. + * generate or respond to any NFC communication over its NFC radio. + * <p>Applications can use this to check if NFC is enabled. Applications + * can request Settings UI allowing the user to toggle NFC using: + * <p><pre>startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))</pre> * + * @see android.provider.Settings#ACTION_NFC_SETTINGS * @return true if this NFC Adapter has any features enabled */ public boolean isEnabled() { @@ -793,6 +810,7 @@ public final class NfcAdapter { * @throws IllegalStateException if the Activity has already been paused * @deprecated use {@link #setNdefPushMessage} instead */ + @Deprecated public void disableForegroundNdefPush(Activity activity) { if (activity == null) { throw new NullPointerException(); @@ -804,61 +822,6 @@ public final class NfcAdapter { } /** - * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated - * @deprecated use {@link CreateNdefMessageCallback} or {@link OnNdefPushCompleteCallback} - * @hide - */ - @Deprecated - public interface NdefPushCallback { - /** - * @deprecated use {@link CreateNdefMessageCallback} instead - */ - @Deprecated - NdefMessage createMessage(); - /** - * @deprecated use{@link OnNdefPushCompleteCallback} instead - */ - @Deprecated - void onMessagePushed(); - } - - /** - * TODO: Remove this - * Converts new callbacks to old callbacks. - */ - static final class LegacyCallbackWrapper implements CreateNdefMessageCallback, - OnNdefPushCompleteCallback { - final NdefPushCallback mLegacyCallback; - LegacyCallbackWrapper(NdefPushCallback legacyCallback) { - mLegacyCallback = legacyCallback; - } - @Override - public void onNdefPushComplete(NfcEvent event) { - mLegacyCallback.onMessagePushed(); - } - @Override - public NdefMessage createNdefMessage(NfcEvent event) { - return mLegacyCallback.createMessage(); - } - } - - /** - * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated - * @deprecated use {@link #setNdefPushMessageCallback} instead - * @hide - */ - @Deprecated - public void enableForegroundNdefPush(Activity activity, final NdefPushCallback callback) { - if (activity == null || callback == null) { - throw new NullPointerException(); - } - enforceResumed(activity); - LegacyCallbackWrapper callbackWrapper = new LegacyCallbackWrapper(callback); - mNfcActivityManager.setNdefPushMessageCallback(activity, callbackWrapper); - mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callbackWrapper); - } - - /** * Enable NDEF Push feature. * <p>This API is for the Settings application. * @hide @@ -887,16 +850,28 @@ public final class NfcAdapter { } /** - * Return true if NDEF Push feature is enabled. - * <p>This function can return true even if NFC is currently turned-off. - * This indicates that NDEF Push is not currently active, but it has - * been requested by the user and will be active as soon as NFC is turned - * on. - * <p>If you want to check if NDEF PUsh sharing is currently active, use - * <code>{@link #isEnabled()} && {@link #isNdefPushEnabled()}</code> + * Return true if the NDEF Push (Android Beam) feature is enabled. + * <p>This function will return true only if both NFC is enabled, and the + * NDEF Push feature is enabled. + * <p>Note that if NFC is enabled but NDEF Push is disabled then this + * device can still <i>receive</i> NDEF messages, it just cannot send them. + * <p>Applications cannot directly toggle the NDEF Push feature, but they + * can request Settings UI allowing the user to toggle NDEF Push using + * <code>startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS))</code> + * <p>Example usage in an Activity that requires NDEF Push: + * <p><pre> + * protected void onResume() { + * super.onResume(); + * if (!nfcAdapter.isEnabled()) { + * startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + * } else if (!nfcAdapter.isNdefPushEnabled()) { + * startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS)); + * } + * } + * </pre> * + * @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS * @return true if NDEF Push feature is enabled - * @hide */ public boolean isNdefPushEnabled() { try { @@ -908,6 +883,24 @@ public final class NfcAdapter { } /** + * Inject a mock NFC tag.<p> + * Used for testing purposes. + * <p class="note">Requires the + * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission. + * @hide + */ + public void dispatch(Tag tag) { + if (tag == null) { + throw new NullPointerException("tag cannot be null"); + } + try { + sService.dispatch(tag); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** * @hide */ public INfcAdapterExtras getNfcAdapterExtrasInterface() { diff --git a/core/java/android/nfc/NfcManager.java b/core/java/android/nfc/NfcManager.java index ef5c7ba..ea08014 100644 --- a/core/java/android/nfc/NfcManager.java +++ b/core/java/android/nfc/NfcManager.java @@ -46,6 +46,10 @@ public final class NfcManager { public NfcManager(Context context) { NfcAdapter adapter; context = context.getApplicationContext(); + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } try { adapter = NfcAdapter.getNfcAdapter(context); } catch (UnsupportedOperationException e) { diff --git a/core/java/android/nfc/Tag.java b/core/java/android/nfc/Tag.java index a73067a..f9b765c 100644 --- a/core/java/android/nfc/Tag.java +++ b/core/java/android/nfc/Tag.java @@ -108,14 +108,14 @@ import java.util.Arrays; * <p> */ public final class Tag implements Parcelable { - /*package*/ final byte[] mId; - /*package*/ final int[] mTechList; - /*package*/ final String[] mTechStringList; - /*package*/ final Bundle[] mTechExtras; - /*package*/ final int mServiceHandle; // for use by NFC service, 0 indicates a mock - /*package*/ final INfcTag mTagService; + final byte[] mId; + final int[] mTechList; + final String[] mTechStringList; + final Bundle[] mTechExtras; + final int mServiceHandle; // for use by NFC service, 0 indicates a mock + final INfcTag mTagService; // interface to NFC service, will be null if mock tag - /*package*/ int mConnectedTechnology; + int mConnectedTechnology; /** * Hidden constructor to be used by NFC service and internal classes. @@ -148,7 +148,7 @@ public final class Tag implements Parcelable { * @hide */ public static Tag createMockTag(byte[] id, int[] techList, Bundle[] techListExtras) { - // set serviceHandle to 0 to indicate mock tag + // set serviceHandle to 0 and tagService to null to indicate mock tag return new Tag(id, techList, techListExtras, 0, null); } @@ -266,6 +266,9 @@ public final class Tag implements Parcelable { throw new IllegalStateException("Close connection to the technology first!"); } + if (mTagService == null) { + throw new IOException("Mock tags don't support this operation."); + } try { Tag newTag = mTagService.rediscover(getServiceHandle()); if (newTag != null) { diff --git a/core/java/android/nfc/tech/BasicTagTechnology.java b/core/java/android/nfc/tech/BasicTagTechnology.java index 913ae0e..b6b347c 100644 --- a/core/java/android/nfc/tech/BasicTagTechnology.java +++ b/core/java/android/nfc/tech/BasicTagTechnology.java @@ -18,7 +18,6 @@ package android.nfc.tech; import android.nfc.ErrorCodes; import android.nfc.Tag; -import android.nfc.TagLostException; import android.nfc.TransceiveResult; import android.os.RemoteException; import android.util.Log; @@ -28,12 +27,13 @@ import java.io.IOException; /** * A base class for tag technologies that are built on top of transceive(). */ -/* package */ abstract class BasicTagTechnology implements TagTechnology { +abstract class BasicTagTechnology implements TagTechnology { private static final String TAG = "NFC"; - /*package*/ final Tag mTag; - /*package*/ boolean mIsConnected; - /*package*/ int mSelectedTechnology; + final Tag mTag; + + boolean mIsConnected; + int mSelectedTechnology; BasicTagTechnology(Tag tag, int tech) throws RemoteException { mTag = tag; @@ -139,7 +139,7 @@ import java.io.IOException; } } /** Internal transceive */ - /*package*/ byte[] transceive(byte[] data, boolean raw) throws IOException { + byte[] transceive(byte[] data, boolean raw) throws IOException { checkConnected(); try { diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java index b266bb6..226e079 100644 --- a/core/java/android/nfc/tech/Ndef.java +++ b/core/java/android/nfc/tech/Ndef.java @@ -259,6 +259,9 @@ public final class Ndef extends BasicTagTechnology { try { INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + throw new IOException("Mock tags don't support this operation."); + } int serviceHandle = mTag.getServiceHandle(); if (tagService.isNdef(serviceHandle)) { NdefMessage msg = tagService.ndefRead(serviceHandle); @@ -303,6 +306,9 @@ public final class Ndef extends BasicTagTechnology { try { INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + throw new IOException("Mock tags don't support this operation."); + } int serviceHandle = mTag.getServiceHandle(); if (tagService.isNdef(serviceHandle)) { int errorCode = tagService.ndefWrite(serviceHandle, msg); @@ -335,6 +341,9 @@ public final class Ndef extends BasicTagTechnology { */ public boolean canMakeReadOnly() { INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + return false; + } try { return tagService.canMakeReadOnly(mNdefType); } catch (RemoteException e) { @@ -366,6 +375,9 @@ public final class Ndef extends BasicTagTechnology { try { INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + return false; + } if (tagService.isNdef(mTag.getServiceHandle())) { int errorCode = tagService.ndefMakeReadOnly(mTag.getServiceHandle()); switch (errorCode) { diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 9dea4c4..fd6bed7 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -135,6 +135,8 @@ import java.util.concurrent.atomic.AtomicInteger; * <p>There are a few threading rules that must be followed for this class to * work properly:</p> * <ul> + * <li>The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li> * <li>The task instance must be created on the UI thread.</li> * <li>{@link #execute} must be invoked on the UI thread.</li> * <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute}, @@ -195,6 +197,7 @@ public abstract class AsyncTask<Params, Progress, Result> { private volatile Status mStatus = Status.PENDING; + private final AtomicBoolean mCancelled = new AtomicBoolean(); private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); private static class SerialExecutor implements Executor { @@ -261,6 +264,7 @@ public abstract class AsyncTask<Params, Progress, Result> { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked return postResult(doInBackground(mParams)); } }; @@ -269,9 +273,7 @@ public abstract class AsyncTask<Params, Progress, Result> { @Override protected void done() { try { - final Result result = get(); - - postResultIfNotInvoked(result); + postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { @@ -295,6 +297,7 @@ public abstract class AsyncTask<Params, Progress, Result> { } private Result postResult(Result result) { + @SuppressWarnings("unchecked") Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result)); message.sendToTarget(); @@ -411,7 +414,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * @see #cancel(boolean) */ public final boolean isCancelled() { - return mFuture.isCancelled(); + return mCancelled.get(); } /** @@ -444,6 +447,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * @see #onCancelled(Object) */ public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); return mFuture.cancel(mayInterruptIfRunning); } diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index c25ebb7..24569fa 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -337,13 +337,16 @@ public class Binder implements IBinder { try { res = onTransact(code, data, reply, flags); } catch (RemoteException e) { + reply.setDataPosition(0); reply.writeException(e); res = true; } catch (RuntimeException e) { + reply.setDataPosition(0); reply.writeException(e); res = true; } catch (OutOfMemoryError e) { RuntimeException re = new RuntimeException("Out of memory", e); + reply.setDataPosition(0); reply.writeException(re); res = true; } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 88fea91..c106092 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -167,6 +167,8 @@ public class Build { * medium density normal size screens unless otherwise indicated). * They can still explicitly specify screen support either way with the * supports-screens manifest tag. + * <li> {@link android.widget.TabHost} will use the new dark tab + * background design. * </ul> */ public static final int DONUT = 4; @@ -208,6 +210,13 @@ public class Build { /** * November 2010: Android 2.3 + * + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li> The application's notification icons will be shown on the new + * dark status bar background, so must be visible in this situation. + * </ul> */ public static final int GINGERBREAD = 9; @@ -224,14 +233,34 @@ public class Build { * <ul> * <li> The default theme for applications is now dark holographic: * {@link android.R.style#Theme_Holo}. + * <li> On large screen devices that do not have a physical menu + * button, the soft (compatibility) menu is disabled. * <li> The activity lifecycle has changed slightly as per * {@link android.app.Activity}. + * <li> An application will crash if it does not call through + * to the super implementation of its + * {@link android.app.Activity#onPause Activity.onPause()} method. * <li> When an application requires a permission to access one of * its components (activity, receiver, service, provider), this * permission is no longer enforced when the application wants to * access its own component. This means it can require a permission * on a component that it does not itself hold and still access that * component. + * <li> {@link android.content.Context#getSharedPreferences + * Context.getSharedPreferences()} will not automatically reload + * the preferences if they have changed on storage, unless + * {@link android.content.Context#MODE_MULTI_PROCESS} is used. + * <li> {@link android.view.ViewGroup#setMotionEventSplittingEnabled} + * will default to true. + * <li> {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} + * is enabled by default on windows. + * <li> {@link android.widget.PopupWindow#isSplitTouchEnabled() + * PopupWindow.isSplitTouchEnabled()} will return true by default. + * <li> {@link android.widget.GridView} and {@link android.widget.ListView} + * will use {@link android.view.View#setActivated View.setActivated} + * for selected items if they do not implement {@link android.widget.Checkable}. + * <li> {@link android.widget.Scroller} will be constructed with + * "flywheel" behavior enabled by default. * </ul> */ public static final int HONEYCOMB = 11; @@ -266,13 +295,26 @@ public class Build { * preferred over the older screen size buckets and for older devices * the appropriate buckets will be inferred from them.</p> * - * <p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT} + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li><p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT} * and {@link android.content.pm.PackageManager#FEATURE_SCREEN_LANDSCAPE} - * features are introduced in this release. Applications that target + * features were introduced in this release. Applications that target * previous platform versions are assumed to require both portrait and * landscape support in the device; when targeting Honeycomb MR1 or * greater the application is responsible for specifying any specific * orientation it requires.</p> + * <li><p>{@link android.os.AsyncTask} will use the serial executor + * by default when calling {@link android.os.AsyncTask#execute}.</p> + * <li><p>{@link android.content.pm.ActivityInfo#configChanges + * ActivityInfo.configChanges} will have the + * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE} and + * {@link android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE} + * bits set; these need to be cleared for older applications because + * some developers have done absolute comparisons against this value + * instead of correctly masking the bits they are interested in. + * </ul> */ public static final int HONEYCOMB_MR2 = 13; @@ -306,14 +348,31 @@ public class Build { * <li> The fadingEdge attribute on views will be ignored (fading edges is no * longer a standard part of the UI). A new requiresFadingEdge attribute allows * applications to still force fading edges on for special cases. + * <li> {@link android.content.Context#bindService Context.bindService()} + * will not automatically add in {@link android.content.Context#BIND_WAIVE_PRIORITY}. + * <li> App Widgets will have standard padding automatically added around + * them, rather than relying on the padding being baked into the widget itself. + * <li> An exception will be thrown if you try to change the type of a + * window after it has been added to the window manager. Previously this + * would result in random incorrect behavior. + * <li> {@link android.view.animation.AnimationSet} will parse out + * the duration, fillBefore, fillAfter, repeatMode, and startOffset + * XML attributes that are defined. + * <li> {@link android.app.ActionBar#setHomeButtonEnabled + * ActionBar.setHomeButtonEnabled()} is false by default. * </ul> */ public static final int ICE_CREAM_SANDWICH = 14; /** - * Android 4.0.3. + * December 2011: Android 4.0.3. */ public static final int ICE_CREAM_SANDWICH_MR1 = 15; + + /** + * Next up on Android! + */ + public static final int JELLY_BEAN = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 9a53d76..270e9be 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -45,4 +45,5 @@ interface IPowerManager // sets the brightness of the backlights (screen, keyboard, button) 0-255 void setBacklightBrightness(int brightness); void setAttentionLight(boolean on, int color); + void setAutoBrightnessAdjustment(float adj); } diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index e1bc275..cdf235d 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -219,6 +219,36 @@ public class Process { public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1; /** + * Default scheduling policy + * @hide + */ + public static final int SCHED_OTHER = 0; + + /** + * First-In First-Out scheduling policy + * @hide + */ + public static final int SCHED_FIFO = 1; + + /** + * Round-Robin scheduling policy + * @hide + */ + public static final int SCHED_RR = 2; + + /** + * Batch scheduling policy + * @hide + */ + public static final int SCHED_BATCH = 3; + + /** + * Idle scheduling policy + * @hide + */ + public static final int SCHED_IDLE = 5; + + /** * Default thread group - gets a 'normal' share of the CPU * @hide */ @@ -675,6 +705,24 @@ public class Process { throws IllegalArgumentException; /** + * Set the scheduling policy and priority of a thread, based on Linux. + * + * @param tid The identifier of the thread/process to change. + * @param policy A Linux scheduling policy such as SCHED_OTHER etc. + * @param priority A Linux priority level in a range appropriate for the given policy. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * <var>tid</var> does not exist, or if <var>priority</var> is out of range for the policy. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * scheduling policy or priority. + * + * {@hide} + */ + public static final native void setThreadScheduler(int tid, int policy, int priority) + throws IllegalArgumentException; + + /** * Determine whether the current environment supports multiple processes. * * @return Returns true if the system can run in multiple processes, else diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 73e8d98..43cf74e 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.security.GeneralSecurityException; import java.security.PublicKey; @@ -103,7 +104,12 @@ public class RecoverySystem { Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); - trusted.add(cf.generateCertificate(zip.getInputStream(entry))); + InputStream is = zip.getInputStream(entry); + try { + trusted.add(cf.generateCertificate(is)); + } finally { + is.close(); + } } } finally { zip.close(); @@ -162,8 +168,6 @@ public class RecoverySystem { int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); - Log.v(TAG, String.format("comment size %d; signature start %d", - commentSize, signatureStart)); byte[] eocd = new byte[commentSize + 22]; raf.seek(fileLen - (commentSize + 22)); diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 83acef8..d724d56 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -6747,6 +6747,39 @@ public final class ContactsContract { */ public static final String NAMESPACE = DataColumns.DATA2; } + + /** + * <p> + * Convenient functionalities for "callable" data. Note that, this is NOT a separate data + * kind. + * </p> + * <p> + * This URI allows the ContactsProvider to return a unified result for "callable" data + * that users can use for calling purposes. {@link Phone} and {@link SipAddress} are the + * current examples for "callable", but may be expanded to the other types. + * </p> + * <p> + * Each returned row may have a different MIMETYPE and thus different interpretation for + * each column. For example the meaning for {@link Phone}'s type is different than + * {@link SipAddress}'s. + * </p> + * + * @hide + */ + public static final class Callable implements DataColumnsWithJoins, CommonColumns { + /** + * Similar to {@link Phone#CONTENT_URI}, but returns callable data instead of only + * phone numbers. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI, + "callables"); + /** + * Similar to {@link Phone#CONTENT_FILTER_URI}, but allows users to filter callable + * data. + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(CONTENT_URI, + "filter"); + } } /** diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 4e01672..d11219b 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -62,6 +62,14 @@ public final class MediaStore { public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END"; /** + * The method name used by the media scanner and mtp to tell the media provider to + * rescan and reclassify that have become unhidden because of renaming folders or + * removing nomedia files + * @hide + */ + public static final String UNHIDE_CALL = "unhide"; + + /** * Activity Action: Launch a music player. * The activity should be able to play, browse, or manipulate music files stored on the device. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 84b0c8b..f14d27e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -569,7 +569,26 @@ public final class Settings { "android.settings.DEVICE_INFO_SETTINGS"; /** - * Activity Action: Show NFC sharing settings. + * Activity Action: Show NFC settings. + * <p> + * This shows UI that allows NFC to be turned on or off. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing + * @see android.nfc.NfcAdapter#isEnabled() + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NFC_SETTINGS = "android.settings.NFC_SETTINGS"; + + /** + * Activity Action: Show NFC Sharing settings. + * <p> + * This shows UI that allows NDEF Push (Android Beam) to be turned on or + * off. * <p> * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. @@ -577,6 +596,7 @@ public final class Settings { * Input: Nothing. * <p> * Output: Nothing + * @see android.nfc.NfcAdapter#isNdefPushEnabled() */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NFCSHARING_SETTINGS = @@ -727,7 +747,7 @@ public final class Settings { Cursor c = null; try { c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, - new String[]{name}, null); + new String[]{name}, null, null); if (c == null) { Log.w(TAG, "Can't get key " + name + " from " + mUri); return null; @@ -1381,6 +1401,12 @@ public final class Settings { public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode"; /** + * Adjustment to auto-brightness to make it generally more (>0.0 <1.0) + * or less (<0.0 >-1.0) bright. + */ + public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj"; + + /** * SCREEN_BRIGHTNESS_MODE value for manual mode. */ public static final int SCREEN_BRIGHTNESS_MODE_MANUAL = 0; @@ -1907,6 +1933,7 @@ public final class Settings { SCREEN_OFF_TIMEOUT, SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_MODE, + SCREEN_AUTO_BRIGHTNESS_ADJ, VIBRATE_ON, MODE_RINGER, MODE_RINGER_STREAMS_AFFECTED, @@ -2759,10 +2786,10 @@ public final class Settings { public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password"; /** - * If injection of accessibility enhancing JavaScript scripts + * If injection of accessibility enhancing JavaScript screen-reader * is enabled. * <p> - * Note: Accessibility injecting scripts are served by the + * Note: The JavaScript based screen-reader is served by the * Google infrastructure and enable users with disabilities to * efficiantly navigate in and explore web content. * </p> @@ -2775,6 +2802,22 @@ public final class Settings { "accessibility_script_injection"; /** + * The URL for the injected JavaScript based screen-reader used + * for providing accessiblity of content in WebView. + * <p> + * Note: The JavaScript based screen-reader is served by the + * Google infrastructure and enable users with disabilities to + * efficiently navigate in and explore web content. + * </p> + * <p> + * This property represents a string value. + * </p> + * @hide + */ + public static final String ACCESSIBILITY_SCREEN_READER_URL = + "accessibility_script_injection_url"; + + /** * Key bindings for navigation in built-in accessibility support for web content. * <p> * Note: These key bindings are for the built-in accessibility navigation for @@ -4041,22 +4084,65 @@ public final class Settings { public static final String SETUP_PREPAID_DETECTION_REDIR_HOST = "setup_prepaid_detection_redir_host"; + /** + * Whether the screensaver is enabled. + * @hide + */ + public static final String SCREENSAVER_ENABLED = "screensaver_enabled"; + + /** + * The user's chosen screensaver component. + * + * This component will be launched by the PhoneWindowManager after a timeout when not on + * battery, or upon dock insertion (if SCREENSAVER_ACTIVATE_ON_DOCK is set to 1). + * @hide + */ + public static final String SCREENSAVER_COMPONENT = "screensaver_component"; + + /** + * Whether the screensaver should be automatically launched when the device is inserted + * into a (desk) dock. + * @hide + */ + public static final String SCREENSAVER_ACTIVATE_ON_DOCK = "screensaver_activate_on_dock"; + /** {@hide} */ public static final String NETSTATS_ENABLED = "netstats_enabled"; /** {@hide} */ public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval"; /** {@hide} */ - public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold"; + public static final String NETSTATS_TIME_CACHE_MAX_AGE = "netstats_time_cache_max_age"; /** {@hide} */ - public static final String NETSTATS_NETWORK_BUCKET_DURATION = "netstats_network_bucket_duration"; + public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes"; /** {@hide} */ - public static final String NETSTATS_NETWORK_MAX_HISTORY = "netstats_network_max_history"; + public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled"; + + /** {@hide} */ + public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration"; + /** {@hide} */ + public static final String NETSTATS_DEV_PERSIST_BYTES = "netstats_dev_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_DEV_ROTATE_AGE = "netstats_dev_rotate_age"; + /** {@hide} */ + public static final String NETSTATS_DEV_DELETE_AGE = "netstats_dev_delete_age"; + /** {@hide} */ public static final String NETSTATS_UID_BUCKET_DURATION = "netstats_uid_bucket_duration"; /** {@hide} */ - public static final String NETSTATS_UID_MAX_HISTORY = "netstats_uid_max_history"; + public static final String NETSTATS_UID_PERSIST_BYTES = "netstats_uid_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_UID_ROTATE_AGE = "netstats_uid_rotate_age"; + /** {@hide} */ + public static final String NETSTATS_UID_DELETE_AGE = "netstats_uid_delete_age"; + + /** {@hide} */ + public static final String NETSTATS_UID_TAG_BUCKET_DURATION = "netstats_uid_tag_bucket_duration"; + /** {@hide} */ + public static final String NETSTATS_UID_TAG_PERSIST_BYTES = "netstats_uid_tag_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_UID_TAG_ROTATE_AGE = "netstats_uid_tag_rotate_age"; /** {@hide} */ - public static final String NETSTATS_TAG_MAX_HISTORY = "netstats_tag_max_history"; + public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age"; /** Preferred NTP server. {@hide} */ public static final String NTP_SERVER = "ntp_server"; diff --git a/core/java/android/provider/UserDictionary.java b/core/java/android/provider/UserDictionary.java index 5a7ef85..a9b106a 100644 --- a/core/java/android/provider/UserDictionary.java +++ b/core/java/android/provider/UserDictionary.java @@ -40,6 +40,9 @@ public class UserDictionary { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); + private static final int FREQUENCY_MIN = 0; + private static final int FREQUENCY_MAX = 255; + /** * Contains the user defined words. */ @@ -87,12 +90,24 @@ public class UserDictionary { */ public static final String APP_ID = "appid"; - /** The locale type to specify that the word is common to all locales. */ + /** + * An optional shortcut for this word. When the shortcut is typed, supporting IMEs should + * suggest the word in this row as an alternate spelling too. + */ + public static final String SHORTCUT = "shortcut"; + + /** + * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}. + */ + @Deprecated public static final int LOCALE_TYPE_ALL = 0; - - /** The locale type to specify that the word is for the current locale. */ + + /** + * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}. + */ + @Deprecated public static final int LOCALE_TYPE_CURRENT = 1; - + /** * Sort by descending order of frequency. */ @@ -100,35 +115,65 @@ public class UserDictionary { /** Adds a word to the dictionary, with the given frequency and the specified * specified locale type. + * + * @deprecated Please use + * {@link #addWord(Context, String, int, String, Locale)} instead. + * * @param context the current application context * @param word the word to add to the dictionary. This should not be null or * empty. * @param localeType the locale type for this word. It should be one of * {@link #LOCALE_TYPE_ALL} or {@link #LOCALE_TYPE_CURRENT}. */ - public static void addWord(Context context, String word, + @Deprecated + public static void addWord(Context context, String word, int frequency, int localeType) { - final ContentResolver resolver = context.getContentResolver(); - if (TextUtils.isEmpty(word) || localeType < 0 || localeType > 1) { + if (localeType != LOCALE_TYPE_ALL && localeType != LOCALE_TYPE_CURRENT) { return; } - - if (frequency < 0) frequency = 0; - if (frequency > 255) frequency = 255; - String locale = null; + final Locale locale; - // TODO: Verify if this is the best way to get the current locale if (localeType == LOCALE_TYPE_CURRENT) { - locale = Locale.getDefault().toString(); + locale = Locale.getDefault(); + } else { + locale = null; } - ContentValues values = new ContentValues(4); + + addWord(context, word, frequency, null, locale); + } + + /** Adds a word to the dictionary, with the given frequency and the specified + * locale type. + * + * @param context the current application context + * @param word the word to add to the dictionary. This should not be null or + * empty. + * @param shortcut optional shortcut spelling for this word. When the shortcut + * is typed, the word may be suggested by applications that support it. May be null. + * @param locale the locale to insert the word for, or null to insert the word + * for all locales. + */ + public static void addWord(Context context, String word, + int frequency, String shortcut, Locale locale) { + final ContentResolver resolver = context.getContentResolver(); + + if (TextUtils.isEmpty(word)) { + return; + } + + if (frequency < FREQUENCY_MIN) frequency = FREQUENCY_MIN; + if (frequency > FREQUENCY_MAX) frequency = FREQUENCY_MAX; + + final int COLUMN_COUNT = 5; + ContentValues values = new ContentValues(COLUMN_COUNT); values.put(WORD, word); values.put(FREQUENCY, frequency); - values.put(LOCALE, locale); + values.put(LOCALE, null == locale ? null : locale.toString()); values.put(APP_ID, 0); // TODO: Get App UID + values.put(SHORTCUT, shortcut); Uri result = resolver.insert(CONTENT_URI, values); // It's ok if the insert doesn't succeed because the word diff --git a/core/java/android/server/BluetoothAdapterStateMachine.java b/core/java/android/server/BluetoothAdapterStateMachine.java index f617d95..7711caa 100644 --- a/core/java/android/server/BluetoothAdapterStateMachine.java +++ b/core/java/android/server/BluetoothAdapterStateMachine.java @@ -62,6 +62,17 @@ import java.io.PrintWriter; * m1 = TURN_HOT * m2 = Transition to HotOff when number of process wanting BT on is 0. * POWER_STATE_CHANGED will make the transition. + * Note: + * The diagram above shows all the states and messages that trigger normal state changes. + * The diagram above does not capture everything: + * The diagram does not capture following messages. + * - messages that do not trigger state changes + * For example, PER_PROCESS_TURN_ON received in BluetoothOn state + * - unhandled messages + * For example, USER_TURN_ON received in BluetoothOn state + * - timeout messages + * The diagram does not capture error conditions and state recoveries. + * - For example POWER_STATE_CHANGED received in BluetoothOn state */ final class BluetoothAdapterStateMachine extends StateMachine { private static final String TAG = "BluetoothAdapterStateMachine"; diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java index bfad747..41bb87f 100644 --- a/core/java/android/server/BluetoothPanProfileHandler.java +++ b/core/java/android/server/BluetoothPanProfileHandler.java @@ -377,16 +377,16 @@ final class BluetoothPanProfileHandler { try { ifcg = service.getInterfaceConfig(iface); if (ifcg != null) { + final LinkAddress linkAddr = ifcg.getLinkAddress(); InetAddress addr = null; - if (ifcg.addr == null || (addr = ifcg.addr.getAddress()) == null || + if (linkAddr == null || (addr = linkAddr.getAddress()) == null || addr.equals(NetworkUtils.numericToInetAddress("0.0.0.0")) || addr.equals(NetworkUtils.numericToInetAddress("::0"))) { addr = NetworkUtils.numericToInetAddress(address); } - ifcg.interfaceFlags = ifcg.interfaceFlags.replace("down", "up"); - ifcg.addr = new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH); - ifcg.interfaceFlags = ifcg.interfaceFlags.replace("running", ""); - ifcg.interfaceFlags = ifcg.interfaceFlags.replace(" "," "); + ifcg.setInterfaceUp(); + ifcg.clearFlag("running"); + ifcg.setLinkAddress(new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH)); service.setInterfaceConfig(iface, ifcg); if (cm.tether(iface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { Log.e(TAG, "Error tethering "+iface); diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java index 83ea874..2b8a458 100644 --- a/core/java/android/service/textservice/SpellCheckerService.java +++ b/core/java/android/service/textservice/SpellCheckerService.java @@ -139,6 +139,25 @@ public abstract class SpellCheckerService extends Service { } /** + * @hide + * The default implementation returns an array of SuggestionsInfo by simply calling + * onGetSuggestions(). + * When you override this method, make sure that suggestionsLimit is applied to suggestions + * that share the same start position and length. + */ + public SuggestionsInfo[] onGetSuggestionsMultipleForSentence(TextInfo[] textInfos, + int suggestionsLimit) { + final int length = textInfos.length; + final SuggestionsInfo[] retval = new SuggestionsInfo[length]; + for (int i = 0; i < length; ++i) { + retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit); + retval[i].setCookieAndSequence( + textInfos[i].getCookie(), textInfos[i].getSequence()); + } + return retval; + } + + /** * Request to abort all tasks executed in SpellChecker. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, @@ -201,6 +220,16 @@ public abstract class SpellCheckerService extends Service { } @Override + public void onGetSuggestionsMultipleForSentence( + TextInfo[] textInfos, int suggestionsLimit) { + try { + mListener.onGetSuggestionsForSentence( + mSession.onGetSuggestionsMultipleForSentence(textInfos, suggestionsLimit)); + } catch (RemoteException e) { + } + } + + @Override public void onCancel() { int pri = Process.getThreadPriority(Process.myTid()); try { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 18167b6..7ce96c0 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -18,7 +18,6 @@ package android.service.wallpaper; import com.android.internal.os.HandlerCaller; import com.android.internal.view.BaseIWindow; -import com.android.internal.view.BaseInputHandler; import com.android.internal.view.BaseSurfaceHolder; import android.annotation.SdkConstant; @@ -45,8 +44,8 @@ import android.view.Gravity; import android.view.IWindowSession; import android.view.InputChannel; import android.view.InputDevice; -import android.view.InputHandler; -import android.view.InputQueue; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; @@ -228,24 +227,29 @@ public abstract class WallpaperService extends Service { } }; - - final InputHandler mInputHandler = new BaseInputHandler() { + + final class WallpaperInputEventReceiver extends InputEventReceiver { + public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + @Override - public void handleMotion(MotionEvent event, - InputQueue.FinishedCallback finishedCallback) { + public void onInputEvent(InputEvent event) { boolean handled = false; try { - int source = event.getSource(); - if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - dispatchPointer(event); + if (event instanceof MotionEvent + && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event); + dispatchPointer(dup); handled = true; } } finally { - finishedCallback.finished(handled); + finishInputEvent(event, handled); } } - }; - + } + WallpaperInputEventReceiver mInputEventReceiver; + final BaseIWindow mWindow = new BaseIWindow() { @Override public void resized(int w, int h, Rect coveredInsets, @@ -534,6 +538,8 @@ public abstract class WallpaperService extends Service { } Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event); mCaller.sendMessage(msg); + } else { + event.recycle(); } } @@ -599,8 +605,8 @@ public abstract class WallpaperService extends Service { } mCreated = true; - InputQueue.registerInputChannel(mInputChannel, mInputHandler, - Looper.myQueue()); + mInputEventReceiver = new WallpaperInputEventReceiver( + mInputChannel, Looper.myLooper()); } mSurfaceHolder.mSurfaceLock.lock(); @@ -902,8 +908,9 @@ public abstract class WallpaperService extends Service { if (DEBUG) Log.v(TAG, "Removing window and destroying surface " + mSurfaceHolder.getSurface() + " of: " + this); - if (mInputChannel != null) { - InputQueue.unregisterInputChannel(mInputChannel); + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; } mSession.remove(mWindow); @@ -970,6 +977,8 @@ public abstract class WallpaperService extends Service { public void dispatchPointer(MotionEvent event) { if (mEngine != null) { mEngine.dispatchPointer(event); + } else { + event.recycle(); } } diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java deleted file mode 100644 index 29b4367..0000000 --- a/core/java/android/speech/tts/AudioMessageParams.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2011 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 android.speech.tts; - -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -class AudioMessageParams extends MessageParams { - private final BlockingMediaPlayer mPlayer; - - AudioMessageParams(UtteranceProgressDispatcher dispatcher, - String callingApp, BlockingMediaPlayer player) { - super(dispatcher, callingApp); - mPlayer = player; - } - - BlockingMediaPlayer getPlayer() { - return mPlayer; - } - - @Override - int getType() { - return TYPE_AUDIO; - } - -} diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index 46a78dc..d63f605 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -15,44 +15,20 @@ */ package android.speech.tts; -import android.media.AudioFormat; -import android.media.AudioTrack; -import android.text.TextUtils; import android.util.Log; import java.util.Iterator; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.LinkedBlockingQueue; class AudioPlaybackHandler { private static final String TAG = "TTS.AudioPlaybackHandler"; - private static final boolean DBG_THREADING = false; private static final boolean DBG = false; - private static final int MIN_AUDIO_BUFFER_SIZE = 8192; - - private static final int SYNTHESIS_START = 1; - private static final int SYNTHESIS_DATA_AVAILABLE = 2; - private static final int SYNTHESIS_DONE = 3; - - private static final int PLAY_AUDIO = 5; - private static final int PLAY_SILENCE = 6; - - private static final int SHUTDOWN = -1; - - private static final int DEFAULT_PRIORITY = 1; - private static final int HIGH_PRIORITY = 0; - - private final PriorityBlockingQueue<ListEntry> mQueue = - new PriorityBlockingQueue<ListEntry>(); + private final LinkedBlockingQueue<PlaybackQueueItem> mQueue = + new LinkedBlockingQueue<PlaybackQueueItem>(); private final Thread mHandlerThread; - private volatile MessageParams mCurrentParams = null; - // Used only for book keeping and error detection. - private volatile SynthesisMessageParams mLastSynthesisRequest = null; - // Used to order incoming messages in our priority queue. - private final AtomicLong mSequenceIdCtr = new AtomicLong(0); - + private volatile PlaybackQueueItem mCurrentWorkItem = null; AudioPlaybackHandler() { mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread"); @@ -62,82 +38,38 @@ class AudioPlaybackHandler { mHandlerThread.start(); } - /** - * Stops all synthesis for a given {@code token}. If the current token - * is currently being processed, an effort will be made to stop it but - * that is not guaranteed. - * - * NOTE: This assumes that all other messages in the queue with {@code token} - * have been removed already. - * - * NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}. - */ - private void stop(MessageParams token) { - if (token == null) { + private void stop(PlaybackQueueItem item) { + if (item == null) { return; } - if (DBG) Log.d(TAG, "Stopping token : " + token); + item.stop(false); + } - if (token.getType() == MessageParams.TYPE_SYNTHESIS) { - AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack(); - if (current != null) { - // Stop the current audio track if it's still playing. - // The audio track is thread safe in this regard. The current - // handleSynthesisDataAvailable call will return soon after this - // call. - current.stop(); - } - // This is safe because PlaybackSynthesisCallback#stop would have - // been called before this method, and will no longer enqueue any - // audio for this token. - // - // (Even if it did, all it would result in is a warning message). - mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY)); - } else if (token.getType() == MessageParams.TYPE_AUDIO) { - ((AudioMessageParams) token).getPlayer().stop(); - // No cleanup required for audio messages. - } else if (token.getType() == MessageParams.TYPE_SILENCE) { - ((SilenceMessageParams) token).getConditionVariable().open(); - // No cleanup required for silence messages. + public void enqueue(PlaybackQueueItem item) { + try { + mQueue.put(item); + } catch (InterruptedException ie) { + // This exception will never be thrown, since we allow our queue + // to be have an unbounded size. put() will therefore never block. } } - // ----------------------------------------------------- - // Methods that add and remove elements from the queue. These do not - // need to be synchronized strictly speaking, but they make the behaviour - // a lot more predictable. (though it would still be correct without - // synchronization). - // ----------------------------------------------------- - - synchronized public void removePlaybackItems(String callingApp) { - if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callingApp); - removeMessages(callingApp); + public void stopForApp(Object callerIdentity) { + if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity); + removeWorkItemsFor(callerIdentity); - final MessageParams current = getCurrentParams(); - if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { + final PlaybackQueueItem current = mCurrentWorkItem; + if (current != null && (current.getCallerIdentity() == callerIdentity)) { stop(current); } - - final MessageParams lastSynthesis = mLastSynthesisRequest; - - if (lastSynthesis != null && lastSynthesis != current && - TextUtils.equals(callingApp, lastSynthesis.getCallingApp())) { - stop(lastSynthesis); - } } - synchronized public void removeAllItems() { - if (DBG_THREADING) Log.d(TAG, "Removing all items"); + public void stop() { + if (DBG) Log.d(TAG, "Stopping all items"); removeAllMessages(); - final MessageParams current = getCurrentParams(); - final MessageParams lastSynthesis = mLastSynthesisRequest; - stop(current); - - if (lastSynthesis != null && lastSynthesis != current) { - stop(lastSynthesis); - } + stop(mCurrentWorkItem); } /** @@ -145,489 +77,64 @@ class AudioPlaybackHandler { * being handled, true otherwise. */ public boolean isSpeaking() { - return (mQueue.peek() != null) || (mCurrentParams != null); + return (mQueue.peek() != null) || (mCurrentWorkItem != null); } /** * Shut down the audio playback thread. */ - synchronized public void quit() { + public void quit() { removeAllMessages(); - stop(getCurrentParams()); - mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY)); - } - - synchronized void enqueueSynthesisStart(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token); - mQueue.add(new ListEntry(SYNTHESIS_START, token)); - } - - synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token); - mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); - } - - synchronized void enqueueSynthesisDone(SynthesisMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token); - mQueue.add(new ListEntry(SYNTHESIS_DONE, token)); - } - - synchronized void enqueueAudio(AudioMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token); - mQueue.add(new ListEntry(PLAY_AUDIO, token)); - } - - synchronized void enqueueSilence(SilenceMessageParams token) { - if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token); - mQueue.add(new ListEntry(PLAY_SILENCE, token)); - } - - // ----------------------------------------- - // End of public API methods. - // ----------------------------------------- - - // ----------------------------------------- - // Methods for managing the message queue. - // ----------------------------------------- - - /* - * The MessageLoop is a handler like implementation that - * processes messages from a priority queue. - */ - private final class MessageLoop implements Runnable { - @Override - public void run() { - while (true) { - ListEntry entry = null; - try { - entry = mQueue.take(); - } catch (InterruptedException ie) { - return; - } - - if (entry.mWhat == SHUTDOWN) { - if (DBG) Log.d(TAG, "MessageLoop : Shutting down"); - return; - } - - if (DBG) { - Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat - + " ,seqId : " + entry.mSequenceId); - } - - setCurrentParams(entry.mMessage); - handleMessage(entry); - setCurrentParams(null); - } - } + stop(mCurrentWorkItem); + mHandlerThread.interrupt(); } /* * Atomically clear the queue of all messages. */ - synchronized private void removeAllMessages() { + private void removeAllMessages() { mQueue.clear(); } /* * Remove all messages that originate from a given calling app. */ - synchronized private void removeMessages(String callingApp) { - Iterator<ListEntry> it = mQueue.iterator(); + private void removeWorkItemsFor(Object callerIdentity) { + Iterator<PlaybackQueueItem> it = mQueue.iterator(); while (it.hasNext()) { - final ListEntry current = it.next(); - // The null check is to prevent us from removing control messages, - // such as a shutdown message. - if (current.mMessage != null && - callingApp.equals(current.mMessage.getCallingApp())) { + final PlaybackQueueItem item = it.next(); + if (item.getCallerIdentity() == callerIdentity) { it.remove(); } } } /* - * An element of our priority queue of messages. Each message has a priority, - * and a sequence id (defined by the order of enqueue calls). Among messages - * with the same priority, messages that were received earlier win out. + * The MessageLoop is a handler like implementation that + * processes messages from a priority queue. */ - private final class ListEntry implements Comparable<ListEntry> { - final int mWhat; - final MessageParams mMessage; - final int mPriority; - final long mSequenceId; - - private ListEntry(int what, MessageParams message) { - this(what, message, DEFAULT_PRIORITY); - } - - private ListEntry(int what, MessageParams message, int priority) { - mWhat = what; - mMessage = message; - mPriority = priority; - mSequenceId = mSequenceIdCtr.incrementAndGet(); - } - + private final class MessageLoop implements Runnable { @Override - public int compareTo(ListEntry that) { - if (that == this) { - return 0; - } - - // Note that this is always 0, 1 or -1. - int priorityDiff = mPriority - that.mPriority; - if (priorityDiff == 0) { - // The == case cannot occur. - return (mSequenceId < that.mSequenceId) ? -1 : 1; - } - - return priorityDiff; - } - } - - private void setCurrentParams(MessageParams p) { - if (DBG_THREADING) { - if (p != null) { - Log.d(TAG, "Started handling :" + p); - } else { - Log.d(TAG, "End handling : " + mCurrentParams); - } - } - mCurrentParams = p; - } - - private MessageParams getCurrentParams() { - return mCurrentParams; - } - - // ----------------------------------------- - // Methods for dealing with individual messages, the methods - // below do the actual work. - // ----------------------------------------- - - private void handleMessage(ListEntry entry) { - final MessageParams msg = entry.mMessage; - if (entry.mWhat == SYNTHESIS_START) { - handleSynthesisStart(msg); - } else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) { - handleSynthesisDataAvailable(msg); - } else if (entry.mWhat == SYNTHESIS_DONE) { - handleSynthesisDone(msg); - } else if (entry.mWhat == PLAY_AUDIO) { - handleAudio(msg); - } else if (entry.mWhat == PLAY_SILENCE) { - handleSilence(msg); - } - } - - // Currently implemented as blocking the audio playback thread for the - // specified duration. If a call to stop() is made, the thread - // unblocks. - private void handleSilence(MessageParams msg) { - if (DBG) Log.d(TAG, "handleSilence()"); - SilenceMessageParams params = (SilenceMessageParams) msg; - params.getDispatcher().dispatchOnStart(); - if (params.getSilenceDurationMs() > 0) { - params.getConditionVariable().block(params.getSilenceDurationMs()); - } - params.getDispatcher().dispatchOnDone(); - if (DBG) Log.d(TAG, "handleSilence() done."); - } - - // Plays back audio from a given URI. No TTS engine involvement here. - private void handleAudio(MessageParams msg) { - if (DBG) Log.d(TAG, "handleAudio()"); - AudioMessageParams params = (AudioMessageParams) msg; - params.getDispatcher().dispatchOnStart(); - // Note that the BlockingMediaPlayer spawns a separate thread. - // - // TODO: This can be avoided. - params.getPlayer().startAndWait(); - params.getDispatcher().dispatchOnDone(); - if (DBG) Log.d(TAG, "handleAudio() done."); - } - - // Denotes the start of a new synthesis request. We create a new - // audio track, and prepare it for incoming data. - // - // Note that since all TTS synthesis happens on a single thread, we - // should ALWAYS see the following order : - // - // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone - // OR - // handleSynthesisCompleteDataAvailable. - private void handleSynthesisStart(MessageParams msg) { - if (DBG) Log.d(TAG, "handleSynthesisStart()"); - final SynthesisMessageParams param = (SynthesisMessageParams) msg; - - // Oops, looks like the engine forgot to call done(). We go through - // extra trouble to clean the data to prevent the AudioTrack resources - // from being leaked. - if (mLastSynthesisRequest != null) { - Log.e(TAG, "Error : Missing call to done() for request : " + - mLastSynthesisRequest); - handleSynthesisDone(mLastSynthesisRequest); - } - - mLastSynthesisRequest = param; - - // Create the audio track. - final AudioTrack audioTrack = createStreamingAudioTrack(param); - - if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]"); - - param.setAudioTrack(audioTrack); - msg.getDispatcher().dispatchOnStart(); - } - - // More data available to be flushed to the audio track. - private void handleSynthesisDataAvailable(MessageParams msg) { - final SynthesisMessageParams param = (SynthesisMessageParams) msg; - if (param.getAudioTrack() == null) { - Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param); - return; - } - - if (param != mLastSynthesisRequest) { - Log.e(TAG, "Call to dataAvailable without done() / start()"); - return; - } - - final AudioTrack audioTrack = param.getAudioTrack(); - final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer(); - - if (bufferCopy == null) { - Log.e(TAG, "No buffers available to play."); - return; - } - - int playState = audioTrack.getPlayState(); - if (playState == AudioTrack.PLAYSTATE_STOPPED) { - if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode()); - audioTrack.play(); - } - int count = 0; - while (count < bufferCopy.mBytes.length) { - // Note that we don't take bufferCopy.mOffset into account because - // it is guaranteed to be 0. - int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mBytes.length); - if (written <= 0) { - break; - } - count += written; - } - param.mBytesWritten += count; - param.mLogger.onPlaybackStart(); - } - - // Wait for the audio track to stop playing, and then release its resources. - private void handleSynthesisDone(MessageParams msg) { - final SynthesisMessageParams params = (SynthesisMessageParams) msg; - - if (DBG) Log.d(TAG, "handleSynthesisDone()"); - final AudioTrack audioTrack = params.getAudioTrack(); - - if (audioTrack == null) { - // There was already a call to handleSynthesisDone for - // this token. - return; - } - - if (params.mBytesWritten < params.mAudioBufferSize) { - if (DBG) Log.d(TAG, "Stopping audio track to flush audio, state was : " + - audioTrack.getPlayState()); - params.mIsShortUtterance = true; - audioTrack.stop(); - } - - if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + - audioTrack.hashCode()); - blockUntilDone(params); - if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]"); - - // The last call to AudioTrack.write( ) will return only after - // all data from the audioTrack has been sent to the mixer, so - // it's safe to release at this point. Make sure release() and the call - // that set the audio track to null are performed atomically. - synchronized (this) { - // Never allow the audioTrack to be observed in a state where - // it is released but non null. The only case this might happen - // is in the various stopFoo methods that call AudioTrack#stop from - // different threads, but they are synchronized on AudioPlayBackHandler#this - // too. - audioTrack.release(); - params.setAudioTrack(null); - } - if (params.isError()) { - params.getDispatcher().dispatchOnError(); - } else { - params.getDispatcher().dispatchOnDone(); - } - mLastSynthesisRequest = null; - params.mLogger.onWriteData(); - } - - /** - * The minimum increment of time to wait for an audiotrack to finish - * playing. - */ - private static final long MIN_SLEEP_TIME_MS = 20; - - /** - * The maximum increment of time to sleep while waiting for an audiotrack - * to finish playing. - */ - private static final long MAX_SLEEP_TIME_MS = 2500; - - /** - * The maximum amount of time to wait for an audio track to make progress while - * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but - * could happen in exceptional circumstances like a media_server crash. - */ - private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS; - - private static void blockUntilDone(SynthesisMessageParams params) { - if (params.mAudioTrack == null || params.mBytesWritten <= 0) { - return; - } - - if (params.mIsShortUtterance) { - // In this case we would have called AudioTrack#stop() to flush - // buffers to the mixer. This makes the playback head position - // unobservable and notification markers do not work reliably. We - // have no option but to wait until we think the track would finish - // playing and release it after. - // - // This isn't as bad as it looks because (a) We won't end up waiting - // for much longer than we should because even at 4khz mono, a short - // utterance weighs in at about 2 seconds, and (b) such short utterances - // are expected to be relatively infrequent and in a stream of utterances - // this shows up as a slightly longer pause. - blockUntilEstimatedCompletion(params); - } else { - blockUntilCompletion(params); - } - } - - private static void blockUntilEstimatedCompletion(SynthesisMessageParams params) { - final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame; - final long estimatedTimeMs = (lengthInFrames * 1000 / params.mSampleRateInHz); - - if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance"); - - try { - Thread.sleep(estimatedTimeMs); - } catch (InterruptedException ie) { - // Do nothing. - } - } - - private static void blockUntilCompletion(SynthesisMessageParams params) { - final AudioTrack audioTrack = params.mAudioTrack; - final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame; - - int previousPosition = -1; - int currentPosition = 0; - long blockedTimeMs = 0; - - while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames && - audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { - - final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / - audioTrack.getSampleRate(); - final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS); - - // Check if the audio track has made progress since the last loop - // iteration. We should then add in the amount of time that was - // spent sleeping in the last iteration. - if (currentPosition == previousPosition) { - // This works only because the sleep time that would have been calculated - // would be the same in the previous iteration too. - blockedTimeMs += sleepTimeMs; - // If we've taken too long to make progress, bail. - if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) { - Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " + - "for AudioTrack to make progress, Aborting"); - break; + public void run() { + while (true) { + PlaybackQueueItem item = null; + try { + item = mQueue.take(); + } catch (InterruptedException ie) { + if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)"); + return; } - } else { - blockedTimeMs = 0; - } - previousPosition = currentPosition; - if (DBG) Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," + - " Playback position : " + currentPosition + ", Length in frames : " - + lengthInFrames); - try { - Thread.sleep(sleepTimeMs); - } catch (InterruptedException ie) { - break; - } - } - } - - private static final long clip(long value, long min, long max) { - if (value < min) { - return min; - } - - if (value > max) { - return max; - } - - return value; - } - - private static AudioTrack createStreamingAudioTrack(SynthesisMessageParams params) { - final int channelConfig = getChannelConfig(params.mChannelCount); - final int sampleRateInHz = params.mSampleRateInHz; - final int audioFormat = params.mAudioFormat; - - int minBufferSizeInBytes - = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); - int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); - - AudioTrack audioTrack = new AudioTrack(params.mStreamType, sampleRateInHz, channelConfig, - audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); - if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - Log.w(TAG, "Unable to create audio track."); - audioTrack.release(); - return null; - } - params.mAudioBufferSize = bufferSizeInBytes; + // If stop() or stopForApp() are called between mQueue.take() + // returning and mCurrentWorkItem being set, the current work item + // will be run anyway. - setupVolume(audioTrack, params.mVolume, params.mPan); - return audioTrack; - } - - static int getChannelConfig(int channelCount) { - if (channelCount == 1) { - return AudioFormat.CHANNEL_OUT_MONO; - } else if (channelCount == 2){ - return AudioFormat.CHANNEL_OUT_STEREO; - } - - return 0; - } - - private static void setupVolume(AudioTrack audioTrack, float volume, float pan) { - float vol = clip(volume, 0.0f, 1.0f); - float panning = clip(pan, -1.0f, 1.0f); - float volLeft = vol; - float volRight = vol; - if (panning > 0.0f) { - volLeft *= (1.0f - panning); - } else if (panning < 0.0f) { - volRight *= (1.0f + panning); - } - if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight); - if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) { - Log.e(TAG, "Failed to set volume"); + mCurrentWorkItem = item; + item.run(); + mCurrentWorkItem = null; + } } } - private static float clip(float value, float min, float max) { - return value > max ? max : (value < min ? min : value); - } - } diff --git a/core/java/android/speech/tts/BlockingMediaPlayer.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index 3cf60dd..1a1fda8 100644 --- a/core/java/android/speech/tts/BlockingMediaPlayer.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -19,93 +19,44 @@ import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; import android.util.Log; -/** - * A media player that allows blocking to wait for it to finish. - */ -class BlockingMediaPlayer { - - private static final String TAG = "BlockMediaPlayer"; - - private static final String MEDIA_PLAYER_THREAD_NAME = "TTS-MediaPlayer"; +class AudioPlaybackQueueItem extends PlaybackQueueItem { + private static final String TAG = "TTS.AudioQueueItem"; private final Context mContext; private final Uri mUri; private final int mStreamType; + private final ConditionVariable mDone; - // Only accessed on the Handler thread private MediaPlayer mPlayer; private volatile boolean mFinished; - /** - * Creates a new blocking media player. - * Creating a blocking media player is a cheap operation. - * - * @param context - * @param uri - * @param streamType - */ - public BlockingMediaPlayer(Context context, Uri uri, int streamType) { + AudioPlaybackQueueItem(UtteranceProgressDispatcher dispatcher, + Object callerIdentity, + Context context, Uri uri, int streamType) { + super(dispatcher, callerIdentity); + mContext = context; mUri = uri; mStreamType = streamType; - mDone = new ConditionVariable(); - - } - /** - * Starts playback and waits for it to finish. - * Can be called from any thread. - * - * @return {@code true} if the playback finished normally, {@code false} if the playback - * failed or {@link #stop} was called before the playback finished. - */ - public boolean startAndWait() { - HandlerThread thread = new HandlerThread(MEDIA_PLAYER_THREAD_NAME); - thread.start(); - Handler handler = new Handler(thread.getLooper()); + mDone = new ConditionVariable(); + mPlayer = null; mFinished = false; - handler.post(new Runnable() { - @Override - public void run() { - startPlaying(); - } - }); - mDone.block(); - handler.post(new Runnable() { - @Override - public void run() { - finish(); - // No new messages should get posted to the handler thread after this - Looper.myLooper().quit(); - } - }); - return mFinished; } + @Override + public void run() { + final UtteranceProgressDispatcher dispatcher = getDispatcher(); - /** - * Stops playback. Can be called multiple times. - * Can be called from any thread. - */ - public void stop() { - mDone.open(); - } - - /** - * Starts playback. - * Called on the handler thread. - */ - private void startPlaying() { + dispatcher.dispatchOnStart(); mPlayer = MediaPlayer.create(mContext, mUri); if (mPlayer == null) { - Log.w(TAG, "Failed to play " + mUri); - mDone.open(); + dispatcher.dispatchOnError(); return; } + try { mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override @@ -124,16 +75,20 @@ class BlockingMediaPlayer { }); mPlayer.setAudioStreamType(mStreamType); mPlayer.start(); + mDone.block(); + finish(); } catch (IllegalArgumentException ex) { Log.w(TAG, "MediaPlayer failed", ex); mDone.open(); } + + if (mFinished) { + dispatcher.dispatchOnDone(); + } else { + dispatcher.dispatchOnError(); + } } - /** - * Stops playback and release the media player. - * Called on the handler thread. - */ private void finish() { try { mPlayer.stop(); @@ -143,4 +98,8 @@ class BlockingMediaPlayer { mPlayer.release(); } -}
\ No newline at end of file + @Override + void stop(boolean isError) { + mDone.open(); + } +} diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java new file mode 100644 index 0000000..fcadad7 --- /dev/null +++ b/core/java/android/speech/tts/BlockingAudioTrack.java @@ -0,0 +1,338 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.speech.tts; + +import android.media.AudioFormat; +import android.media.AudioTrack; +import android.util.Log; + +/** + * Exposes parts of the {@link AudioTrack} API by delegating calls to an + * underlying {@link AudioTrack}. Additionally, provides methods like + * {@link #waitAndRelease()} that will block until all audiotrack + * data has been flushed to the mixer, and is estimated to have completed + * playback. + */ +class BlockingAudioTrack { + private static final String TAG = "TTS.BlockingAudioTrack"; + private static final boolean DBG = false; + + + /** + * The minimum increment of time to wait for an AudioTrack to finish + * playing. + */ + private static final long MIN_SLEEP_TIME_MS = 20; + + /** + * The maximum increment of time to sleep while waiting for an AudioTrack + * to finish playing. + */ + private static final long MAX_SLEEP_TIME_MS = 2500; + + /** + * The maximum amount of time to wait for an audio track to make progress while + * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but + * could happen in exceptional circumstances like a media_server crash. + */ + private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS; + + /** + * Minimum size of the buffer of the underlying {@link android.media.AudioTrack} + * we create. + */ + private static final int MIN_AUDIO_BUFFER_SIZE = 8192; + + + private final int mStreamType; + private final int mSampleRateInHz; + private final int mAudioFormat; + private final int mChannelCount; + private final float mVolume; + private final float mPan; + + private final int mBytesPerFrame; + /** + * A "short utterance" is one that uses less bytes than the audio + * track buffer size (mAudioBufferSize). In this case, we need to call + * {@link AudioTrack#stop()} to send pending buffers to the mixer, and slightly + * different logic is required to wait for the track to finish. + * + * Not volatile, accessed only from the audio playback thread. + */ + private boolean mIsShortUtterance; + /** + * Will be valid after a call to {@link #init()}. + */ + private int mAudioBufferSize; + private int mBytesWritten = 0; + + private AudioTrack mAudioTrack; + private volatile boolean mStopped; + // Locks the initialization / uninitialization of the audio track. + // This is required because stop() will throw an illegal state exception + // if called before init() or after mAudioTrack.release(). + private final Object mAudioTrackLock = new Object(); + + BlockingAudioTrack(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan) { + mStreamType = streamType; + mSampleRateInHz = sampleRate; + mAudioFormat = audioFormat; + mChannelCount = channelCount; + mVolume = volume; + mPan = pan; + + mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount; + mIsShortUtterance = false; + mAudioBufferSize = 0; + mBytesWritten = 0; + + mAudioTrack = null; + mStopped = false; + } + + public void init() { + AudioTrack track = createStreamingAudioTrack(); + + synchronized (mAudioTrackLock) { + mAudioTrack = track; + } + } + + public void stop() { + synchronized (mAudioTrackLock) { + if (mAudioTrack != null) { + mAudioTrack.stop(); + } + } + mStopped = true; + } + + public int write(byte[] data) { + if (mAudioTrack == null || mStopped) { + return -1; + } + final int bytesWritten = writeToAudioTrack(mAudioTrack, data); + mBytesWritten += bytesWritten; + return bytesWritten; + } + + public void waitAndRelease() { + // For "small" audio tracks, we have to stop() them to make them mixable, + // else the audio subsystem will wait indefinitely for us to fill the buffer + // before rendering the track mixable. + // + // If mStopped is true, the track would already have been stopped, so not + // much point not doing that again. + if (mBytesWritten < mAudioBufferSize && !mStopped) { + if (DBG) { + Log.d(TAG, "Stopping audio track to flush audio, state was : " + + mAudioTrack.getPlayState() + ",stopped= " + mStopped); + } + + mIsShortUtterance = true; + mAudioTrack.stop(); + } + + // Block until the audio track is done only if we haven't stopped yet. + if (!mStopped) { + if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + mAudioTrack.hashCode()); + blockUntilDone(mAudioTrack); + } + + // The last call to AudioTrack.write( ) will return only after + // all data from the audioTrack has been sent to the mixer, so + // it's safe to release at this point. + if (DBG) Log.d(TAG, "Releasing audio track [" + mAudioTrack.hashCode() + "]"); + synchronized (mAudioTrackLock) { + mAudioTrack.release(); + mAudioTrack = null; + } + } + + + static int getChannelConfig(int channelCount) { + if (channelCount == 1) { + return AudioFormat.CHANNEL_OUT_MONO; + } else if (channelCount == 2){ + return AudioFormat.CHANNEL_OUT_STEREO; + } + + return 0; + } + + long getAudioLengthMs(int numBytes) { + final int unconsumedFrames = numBytes / mBytesPerFrame; + final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz; + + return estimatedTimeMs; + } + + private static int writeToAudioTrack(AudioTrack audioTrack, byte[] bytes) { + if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { + if (DBG) Log.d(TAG, "AudioTrack not playing, restarting : " + audioTrack.hashCode()); + audioTrack.play(); + } + + int count = 0; + while (count < bytes.length) { + // Note that we don't take bufferCopy.mOffset into account because + // it is guaranteed to be 0. + int written = audioTrack.write(bytes, count, bytes.length); + if (written <= 0) { + break; + } + count += written; + } + return count; + } + + private AudioTrack createStreamingAudioTrack() { + final int channelConfig = getChannelConfig(mChannelCount); + + int minBufferSizeInBytes + = AudioTrack.getMinBufferSize(mSampleRateInHz, channelConfig, mAudioFormat); + int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); + + AudioTrack audioTrack = new AudioTrack(mStreamType, mSampleRateInHz, channelConfig, + mAudioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); + if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + Log.w(TAG, "Unable to create audio track."); + audioTrack.release(); + return null; + } + + mAudioBufferSize = bufferSizeInBytes; + + setupVolume(audioTrack, mVolume, mPan); + return audioTrack; + } + + private static int getBytesPerFrame(int audioFormat) { + if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { + return 1; + } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { + return 2; + } + + return -1; + } + + + private void blockUntilDone(AudioTrack audioTrack) { + if (mBytesWritten <= 0) { + return; + } + + if (mIsShortUtterance) { + // In this case we would have called AudioTrack#stop() to flush + // buffers to the mixer. This makes the playback head position + // unobservable and notification markers do not work reliably. We + // have no option but to wait until we think the track would finish + // playing and release it after. + // + // This isn't as bad as it looks because (a) We won't end up waiting + // for much longer than we should because even at 4khz mono, a short + // utterance weighs in at about 2 seconds, and (b) such short utterances + // are expected to be relatively infrequent and in a stream of utterances + // this shows up as a slightly longer pause. + blockUntilEstimatedCompletion(); + } else { + blockUntilCompletion(audioTrack); + } + } + + private void blockUntilEstimatedCompletion() { + final int lengthInFrames = mBytesWritten / mBytesPerFrame; + final long estimatedTimeMs = (lengthInFrames * 1000 / mSampleRateInHz); + + if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance"); + + try { + Thread.sleep(estimatedTimeMs); + } catch (InterruptedException ie) { + // Do nothing. + } + } + + private void blockUntilCompletion(AudioTrack audioTrack) { + final int lengthInFrames = mBytesWritten / mBytesPerFrame; + + int previousPosition = -1; + int currentPosition = 0; + long blockedTimeMs = 0; + + while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames && + audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && !mStopped) { + + final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / + audioTrack.getSampleRate(); + final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS); + + // Check if the audio track has made progress since the last loop + // iteration. We should then add in the amount of time that was + // spent sleeping in the last iteration. + if (currentPosition == previousPosition) { + // This works only because the sleep time that would have been calculated + // would be the same in the previous iteration too. + blockedTimeMs += sleepTimeMs; + // If we've taken too long to make progress, bail. + if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) { + Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " + + "for AudioTrack to make progress, Aborting"); + break; + } + } else { + blockedTimeMs = 0; + } + previousPosition = currentPosition; + + if (DBG) { + Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," + + " Playback position : " + currentPosition + ", Length in frames : " + + lengthInFrames); + } + try { + Thread.sleep(sleepTimeMs); + } catch (InterruptedException ie) { + break; + } + } + } + + private static void setupVolume(AudioTrack audioTrack, float volume, float pan) { + final float vol = clip(volume, 0.0f, 1.0f); + final float panning = clip(pan, -1.0f, 1.0f); + + float volLeft = vol; + float volRight = vol; + if (panning > 0.0f) { + volLeft *= (1.0f - panning); + } else if (panning < 0.0f) { + volRight *= (1.0f + panning); + } + if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight); + if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) { + Log.e(TAG, "Failed to set volume"); + } + } + + private static final long clip(long value, long min, long max) { + if (value < min) { + return min; + } + + if (value > max) { + return max; + } + + return value; + } + + private static float clip(float value, float min, float max) { + return value > max ? max : (value < min ? min : value); + } + +} diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags index 1a9f5fe..f8654ad 100644 --- a/core/java/android/speech/tts/EventLogTags.logtags +++ b/core/java/android/speech/tts/EventLogTags.logtags @@ -2,5 +2,5 @@ option java_package android.speech.tts; -76001 tts_speak_success (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) -76002 tts_speak_failure (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1) +76001 tts_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) +76002 tts_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1) diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java index 63b954b..82ed4dd 100644 --- a/core/java/android/speech/tts/EventLogger.java +++ b/core/java/android/speech/tts/EventLogger.java @@ -17,6 +17,7 @@ package android.speech.tts; import android.os.SystemClock; import android.text.TextUtils; +import android.util.Log; /** * Writes data about a given speech synthesis request to the event logs. @@ -24,14 +25,15 @@ import android.text.TextUtils; * speech rate / pitch and the latency and overall time taken. * * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} - * might be called from any thread, but on {@link EventLogger#onPlaybackStart()} and + * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and * {@link EventLogger#onComplete()} must be called from a single thread * (usually the audio playback thread} */ class EventLogger { private final SynthesisRequest mRequest; - private final String mCallingApp; private final String mServiceApp; + private final int mCallerUid; + private final int mCallerPid; private final long mReceivedTime; private long mPlaybackStartTime = -1; private volatile long mRequestProcessingStartTime = -1; @@ -42,10 +44,10 @@ class EventLogger { private volatile boolean mStopped = false; private boolean mLogWritten = false; - EventLogger(SynthesisRequest request, String callingApp, - String serviceApp) { + EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { mRequest = request; - mCallingApp = callingApp; + mCallerUid = callerUid; + mCallerPid = callerPid; mServiceApp = serviceApp; mReceivedTime = SystemClock.elapsedRealtime(); } @@ -80,10 +82,10 @@ class EventLogger { /** * Notifies the logger that audio playback has started for some section * of the synthesis. This is normally some amount of time after the engine - * has synthesized data and varides depending on utterances and + * has synthesized data and varies depending on utterances and * other audio currently in the queue. */ - public void onPlaybackStart() { + public void onAudioDataWritten() { // For now, keep track of only the first chunk of audio // that was played. if (mPlaybackStartTime == -1) { @@ -119,10 +121,10 @@ class EventLogger { } long completionTime = SystemClock.elapsedRealtime(); - // onPlaybackStart() should normally always be called if an + // onAudioDataWritten() should normally always be called if an // error does not occur. if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { - EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallingApp, + EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, getUtteranceLength(), getLocaleString(), mRequest.getSpeechRate(), mRequest.getPitch()); return; @@ -138,7 +140,8 @@ class EventLogger { final long audioLatency = mPlaybackStartTime - mReceivedTime; final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; - EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallingApp, + + EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, getUtteranceLength(), getLocaleString(), mRequest.getSpeechRate(), mRequest.getPitch(), engineLatency, engineTotal, audioLatency); diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl index 1a8c1fb..ab63187 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -30,47 +30,47 @@ interface ITextToSpeechService { /** * Tells the engine to synthesize some speech and play it back. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param text The text to synthesize. * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int speak(in String callingApp, in String text, in int queueMode, in Bundle params); + int speak(in IBinder callingInstance, in String text, in int queueMode, in Bundle params); /** * Tells the engine to synthesize some speech and write it to a file. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param text The text to synthesize. * @param filename The file to write the synthesized audio to. * @param param Request parameters. */ - int synthesizeToFile(in String callingApp, in String text, + int synthesizeToFile(in IBinder callingInstance, in String text, in String filename, in Bundle params); /** * Plays an existing audio resource. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param audioUri URI for the audio resource (a file or android.resource URI) * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int playAudio(in String callingApp, in Uri audioUri, in int queueMode, in Bundle params); + int playAudio(in IBinder callingInstance, in Uri audioUri, in int queueMode, in Bundle params); /** * Plays silence. * - * @param callingApp The package name of the calling app. Used to connect requests - * callbacks and to clear requests when the calling app is stopping. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. * @param duration Number of milliseconds of silence to play. * @param queueMode Determines what to do to requests already in the queue. * @param param Request parameters. */ - int playSilence(in String callingApp, in long duration, in int queueMode, in Bundle params); + int playSilence(in IBinder callingInstance, in long duration, in int queueMode, in Bundle params); /** * Checks whether the service is currently playing some audio. @@ -81,10 +81,10 @@ interface ITextToSpeechService { * Interrupts the current utterance (if from the given app) and removes any utterances * in the queue that are from the given app. * - * @param callingApp Package name of the app whose utterances - * should be interrupted and cleared. + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. */ - int stop(in String callingApp); + int stop(in IBinder callingInstance); /** * Returns the language, country and variant currently being used by the TTS engine. @@ -150,6 +150,6 @@ interface ITextToSpeechService { * @param callingApp Package name for the app whose utterance the callback will handle. * @param cb The callback. */ - void setCallback(in String callingApp, ITextToSpeechCallback cb); + void setCallback(in IBinder caller, ITextToSpeechCallback cb); } diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java deleted file mode 100644 index de9cc07..0000000 --- a/core/java/android/speech/tts/MessageParams.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 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 android.speech.tts; - -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -abstract class MessageParams { - static final int TYPE_SYNTHESIS = 1; - static final int TYPE_AUDIO = 2; - static final int TYPE_SILENCE = 3; - - private final UtteranceProgressDispatcher mDispatcher; - private final String mCallingApp; - - MessageParams(UtteranceProgressDispatcher dispatcher, String callingApp) { - mDispatcher = dispatcher; - mCallingApp = callingApp; - } - - UtteranceProgressDispatcher getDispatcher() { - return mDispatcher; - } - - String getCallingApp() { - return mCallingApp; - } - - @Override - public String toString() { - return "MessageParams[" + hashCode() + "]"; - } - - abstract int getType(); -} diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java new file mode 100644 index 0000000..d0957ff --- /dev/null +++ b/core/java/android/speech/tts/PlaybackQueueItem.java @@ -0,0 +1,27 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; + +abstract class PlaybackQueueItem implements Runnable { + private final UtteranceProgressDispatcher mDispatcher; + private final Object mCallerIdentity; + + PlaybackQueueItem(TextToSpeechService.UtteranceProgressDispatcher dispatcher, + Object callerIdentity) { + mDispatcher = dispatcher; + mCallerIdentity = callerIdentity; + } + + Object getCallerIdentity() { + return mCallerIdentity; + } + + protected UtteranceProgressDispatcher getDispatcher() { + return mDispatcher; + } + + public abstract void run(); + abstract void stop(boolean isError); +} diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index 91a3452..c99f201 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -47,34 +47,34 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private final float mPan; /** - * Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}. + * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}. */ private final Object mStateLock = new Object(); // Handler associated with a thread that plays back audio requests. private final AudioPlaybackHandler mAudioTrackHandler; // A request "token", which will be non null after start() has been called. - private SynthesisMessageParams mToken = null; + private SynthesisPlaybackQueueItem mItem = null; // Whether this request has been stopped. This is useful for keeping // track whether stop() has been called before start(). In all other cases, - // a non-null value of mToken will provide the same information. + // a non-null value of mItem will provide the same information. private boolean mStopped = false; private volatile boolean mDone = false; private final UtteranceProgressDispatcher mDispatcher; - private final String mCallingApp; + private final Object mCallerIdentity; private final EventLogger mLogger; PlaybackSynthesisCallback(int streamType, float volume, float pan, AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher, - String callingApp, EventLogger logger) { + Object callerIdentity, EventLogger logger) { mStreamType = streamType; mVolume = volume; mPan = pan; mAudioTrackHandler = audioTrackHandler; mDispatcher = dispatcher; - mCallingApp = callingApp; + mCallerIdentity = callerIdentity; mLogger = logger; } @@ -89,28 +89,23 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { // Note that mLogger.mError might be true too at this point. mLogger.onStopped(); - SynthesisMessageParams token; + SynthesisPlaybackQueueItem item; synchronized (mStateLock) { if (mStopped) { Log.w(TAG, "stop() called twice"); return; } - token = mToken; + item = mItem; mStopped = true; } - if (token != null) { + if (item != null) { // This might result in the synthesis thread being woken up, at which - // point it will write an additional buffer to the token - but we + // point it will write an additional buffer to the item - but we // won't worry about that because the audio playback queue will be cleared // soon after (see SynthHandler#stop(String). - token.setIsError(wasError); - token.clearBuffers(); - if (wasError) { - // Also clean up the audio track if an error occurs. - mAudioTrackHandler.enqueueSynthesisDone(token); - } + item.stop(wasError); } else { // This happens when stop() or error() were called before start() was. @@ -145,7 +140,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { + "," + channelCount + ")"); } - int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount); if (channelConfig == 0) { Log.e(TAG, "Unsupported number of channels :" + channelCount); return TextToSpeech.ERROR; @@ -156,12 +151,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { if (DBG) Log.d(TAG, "stop() called before start(), returning."); return TextToSpeech.ERROR; } - SynthesisMessageParams params = new SynthesisMessageParams( + SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem( mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher, mCallingApp, mLogger); - mAudioTrackHandler.enqueueSynthesisStart(params); - - mToken = params; + mDispatcher, mCallerIdentity, mLogger); + mAudioTrackHandler.enqueue(item); + mItem = item; } return TextToSpeech.SUCCESS; @@ -179,21 +173,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { + length + " bytes)"); } - SynthesisMessageParams token = null; + SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { - if (mToken == null || mStopped) { + if (mItem == null || mStopped) { return TextToSpeech.ERROR; } - token = mToken; + item = mItem; } // Sigh, another copy. final byte[] bufferCopy = new byte[length]; System.arraycopy(buffer, offset, bufferCopy, 0, length); - // Might block on mToken.this, if there are too many buffers waiting to + + // Might block on mItem.this, if there are too many buffers waiting to // be consumed. - token.addBuffer(bufferCopy); - mAudioTrackHandler.enqueueSynthesisDataAvailable(token); + try { + item.put(bufferCopy); + } catch (InterruptedException ie) { + return TextToSpeech.ERROR; + } mLogger.onEngineDataReceived(); @@ -204,7 +202,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { public int done() { if (DBG) Log.d(TAG, "done()"); - SynthesisMessageParams token = null; + SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { if (mDone) { Log.w(TAG, "Duplicate call to done()"); @@ -213,14 +211,14 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { mDone = true; - if (mToken == null) { + if (mItem == null) { return TextToSpeech.ERROR; } - token = mToken; + item = mItem; } - mAudioTrackHandler.enqueueSynthesisDone(token); + item.done(); mLogger.onEngineComplete(); return TextToSpeech.SUCCESS; diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java index 9909126..a5e47ae 100644 --- a/core/java/android/speech/tts/SilenceMessageParams.java +++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java @@ -17,28 +17,29 @@ package android.speech.tts; import android.os.ConditionVariable; import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; +import android.util.Log; -class SilenceMessageParams extends MessageParams { +class SilencePlaybackQueueItem extends PlaybackQueueItem { private final ConditionVariable mCondVar = new ConditionVariable(); private final long mSilenceDurationMs; - SilenceMessageParams(UtteranceProgressDispatcher dispatcher, - String callingApp, long silenceDurationMs) { - super(dispatcher, callingApp); + SilencePlaybackQueueItem(UtteranceProgressDispatcher dispatcher, + Object callerIdentity, long silenceDurationMs) { + super(dispatcher, callerIdentity); mSilenceDurationMs = silenceDurationMs; } - long getSilenceDurationMs() { - return mSilenceDurationMs; - } - @Override - int getType() { - return TYPE_SILENCE; + public void run() { + getDispatcher().dispatchOnStart(); + if (mSilenceDurationMs > 0) { + mCondVar.block(mSilenceDurationMs); + } + getDispatcher().dispatchOnDone(); } - ConditionVariable getConditionVariable() { - return mCondVar; + @Override + void stop(boolean isError) { + mCondVar.open(); } - } diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java deleted file mode 100644 index ed66420..0000000 --- a/core/java/android/speech/tts/SynthesisMessageParams.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2011 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 android.speech.tts; - -import android.media.AudioFormat; -import android.media.AudioTrack; -import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; - -import java.util.LinkedList; - -/** - * Params required to play back a synthesis request. - */ -final class SynthesisMessageParams extends MessageParams { - private static final long MAX_UNCONSUMED_AUDIO_MS = 500; - - final int mStreamType; - final int mSampleRateInHz; - final int mAudioFormat; - final int mChannelCount; - final float mVolume; - final float mPan; - final EventLogger mLogger; - - final int mBytesPerFrame; - - volatile AudioTrack mAudioTrack; - // Written by the synthesis thread, but read on the audio playback - // thread. - volatile int mBytesWritten; - // A "short utterance" is one that uses less bytes than the audio - // track buffer size (mAudioBufferSize). In this case, we need to call - // AudioTrack#stop() to send pending buffers to the mixer, and slightly - // different logic is required to wait for the track to finish. - // - // Not volatile, accessed only from the audio playback thread. - boolean mIsShortUtterance; - int mAudioBufferSize; - // Always synchronized on "this". - int mUnconsumedBytes; - volatile boolean mIsError; - - private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); - - SynthesisMessageParams(int streamType, int sampleRate, - int audioFormat, int channelCount, - float volume, float pan, UtteranceProgressDispatcher dispatcher, - String callingApp, EventLogger logger) { - super(dispatcher, callingApp); - - mStreamType = streamType; - mSampleRateInHz = sampleRate; - mAudioFormat = audioFormat; - mChannelCount = channelCount; - mVolume = volume; - mPan = pan; - mLogger = logger; - - mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount; - - // initially null. - mAudioTrack = null; - mBytesWritten = 0; - mAudioBufferSize = 0; - mIsError = false; - } - - @Override - int getType() { - return TYPE_SYNTHESIS; - } - - synchronized void addBuffer(byte[] buffer) { - long unconsumedAudioMs = 0; - - while ((unconsumedAudioMs = getUnconsumedAudioLengthMs()) > MAX_UNCONSUMED_AUDIO_MS) { - try { - wait(); - } catch (InterruptedException ie) { - return; - } - } - - mDataBufferList.add(new ListEntry(buffer)); - mUnconsumedBytes += buffer.length; - } - - synchronized void clearBuffers() { - mDataBufferList.clear(); - mUnconsumedBytes = 0; - notifyAll(); - } - - synchronized ListEntry getNextBuffer() { - ListEntry entry = mDataBufferList.poll(); - if (entry != null) { - mUnconsumedBytes -= entry.mBytes.length; - notifyAll(); - } - - return entry; - } - - void setAudioTrack(AudioTrack audioTrack) { - mAudioTrack = audioTrack; - } - - AudioTrack getAudioTrack() { - return mAudioTrack; - } - - void setIsError(boolean isError) { - mIsError = isError; - } - - boolean isError() { - return mIsError; - } - - // Must be called synchronized on this. - private long getUnconsumedAudioLengthMs() { - final int unconsumedFrames = mUnconsumedBytes / mBytesPerFrame; - final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz; - - return estimatedTimeMs; - } - - private static int getBytesPerFrame(int audioFormat) { - if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { - return 1; - } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { - return 2; - } - - return -1; - } - - static final class ListEntry { - final byte[] mBytes; - - ListEntry(byte[] bytes) { - mBytes = bytes; - } - } -} - diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java new file mode 100644 index 0000000..d299d70 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2011 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 android.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; +import android.util.Log; + +import java.util.LinkedList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages the playback of a list of byte arrays representing audio data + * that are queued by the engine to an audio track. + */ +final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { + private static final String TAG = "TTS.SynthQueueItem"; + private static final boolean DBG = false; + + /** + * Maximum length of audio we leave unconsumed by the audio track. + * Calls to {@link #put(byte[])} will block until we have less than + * this amount of audio left to play back. + */ + private static final long MAX_UNCONSUMED_AUDIO_MS = 500; + + /** + * Guards accesses to mDataBufferList and mUnconsumedBytes. + */ + private final Lock mListLock = new ReentrantLock(); + private final Condition mReadReady = mListLock.newCondition(); + private final Condition mNotFull = mListLock.newCondition(); + + // Guarded by mListLock. + private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); + // Guarded by mListLock. + private int mUnconsumedBytes; + + /* + * While mStopped and mIsError can be written from any thread, mDone is written + * only from the synthesis thread. All three variables are read from the + * audio playback thread. + */ + private volatile boolean mStopped; + private volatile boolean mDone; + private volatile boolean mIsError; + + private final BlockingAudioTrack mAudioTrack; + private final EventLogger mLogger; + + + SynthesisPlaybackQueueItem(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan, UtteranceProgressDispatcher dispatcher, + Object callerIdentity, EventLogger logger) { + super(dispatcher, callerIdentity); + + mUnconsumedBytes = 0; + + mStopped = false; + mDone = false; + mIsError = false; + + mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat, + channelCount, volume, pan); + mLogger = logger; + } + + + @Override + public void run() { + final UtteranceProgressDispatcher dispatcher = getDispatcher(); + dispatcher.dispatchOnStart(); + + + mAudioTrack.init(); + + try { + byte[] buffer = null; + + // take() will block until: + // + // (a) there is a buffer available to tread. In which case + // a non null value is returned. + // OR (b) stop() is called in which case it will return null. + // OR (c) done() is called in which case it will return null. + while ((buffer = take()) != null) { + mAudioTrack.write(buffer); + mLogger.onAudioDataWritten(); + } + + } catch (InterruptedException ie) { + if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up."); + } + + mAudioTrack.waitAndRelease(); + + if (mIsError) { + dispatcher.dispatchOnError(); + } else { + dispatcher.dispatchOnDone(); + } + + mLogger.onWriteData(); + } + + @Override + void stop(boolean isError) { + try { + mListLock.lock(); + + // Update our internal state. + mStopped = true; + mIsError = isError; + + // Wake up the audio playback thread if it was waiting on take(). + // take() will return null since mStopped was true, and will then + // break out of the data write loop. + mReadReady.signal(); + + // Wake up the synthesis thread if it was waiting on put(). Its + // buffers will no longer be copied since mStopped is true. The + // PlaybackSynthesisCallback that this synthesis corresponds to + // would also have been stopped, and so all calls to + // Callback.onDataAvailable( ) will return errors too. + mNotFull.signal(); + } finally { + mListLock.unlock(); + } + + // Stop the underlying audio track. This will stop sending + // data to the mixer and discard any pending buffers that the + // track holds. + mAudioTrack.stop(); + } + + void done() { + try { + mListLock.lock(); + + // Update state. + mDone = true; + + // Unblocks the audio playback thread if it was waiting on take() + // after having consumed all available buffers. It will then return + // null and leave the write loop. + mReadReady.signal(); + + // Just so that engines that try to queue buffers after + // calling done() don't block the synthesis thread forever. Ideally + // this should be called from the same thread as put() is, and hence + // this call should be pointless. + mNotFull.signal(); + } finally { + mListLock.unlock(); + } + } + + + void put(byte[] buffer) throws InterruptedException { + try { + mListLock.lock(); + long unconsumedAudioMs = 0; + + while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) > + MAX_UNCONSUMED_AUDIO_MS && !mStopped) { + mNotFull.await(); + } + + // Don't bother queueing the buffer if we've stopped. The playback thread + // would have woken up when stop() is called (if it was blocked) and will + // proceed to leave the write loop since take() will return null when + // stopped. + if (mStopped) { + return; + } + + mDataBufferList.add(new ListEntry(buffer)); + mUnconsumedBytes += buffer.length; + mReadReady.signal(); + } finally { + mListLock.unlock(); + } + } + + private byte[] take() throws InterruptedException { + try { + mListLock.lock(); + + // Block if there are no available buffers, and stop() has not + // been called and done() has not been called. + while (mDataBufferList.size() == 0 && !mStopped && !mDone) { + mReadReady.await(); + } + + // If stopped, return null so that we can exit the playback loop + // as soon as possible. + if (mStopped) { + return null; + } + + // Remove the first entry from the queue. + ListEntry entry = mDataBufferList.poll(); + + // This is the normal playback loop exit case, when done() was + // called. (mDone will be true at this point). + if (entry == null) { + return null; + } + + mUnconsumedBytes -= entry.mBytes.length; + // Unblock the waiting writer. We use signal() and not signalAll() + // because there will only be one thread waiting on this (the + // Synthesis thread). + mNotFull.signal(); + + return entry.mBytes; + } finally { + mListLock.unlock(); + } + } + + static final class ListEntry { + final byte[] mBytes; + + ListEntry(byte[] bytes) { + mBytes = bytes; + } + } +} + diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index a220615..7a174af 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -486,6 +486,11 @@ public class TextToSpeech { private final Object mStartLock = new Object(); private String mRequestedEngine; + // Whether to initialize this TTS object with the default engine, + // if the requested engine is not available. Valid only if mRequestedEngine + // is not null. Used only for testing, though potentially useful API wise + // too. + private final boolean mUseFallback; private final Map<String, Uri> mEarcons; private final Map<String, Uri> mUtterances; private final Bundle mParams = new Bundle(); @@ -519,7 +524,7 @@ public class TextToSpeech { * @param engine Package name of the TTS engine to use. */ public TextToSpeech(Context context, OnInitListener listener, String engine) { - this(context, listener, engine, null); + this(context, listener, engine, null, true); } /** @@ -529,10 +534,11 @@ public class TextToSpeech { * @hide */ public TextToSpeech(Context context, OnInitListener listener, String engine, - String packageName) { + String packageName, boolean useFallback) { mContext = context; mInitListener = listener; mRequestedEngine = engine; + mUseFallback = useFallback; mEarcons = new HashMap<String, Uri>(); mUtterances = new HashMap<String, Uri>(); @@ -547,10 +553,6 @@ public class TextToSpeech { initTts(); } - private String getPackageName() { - return mPackageName; - } - private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method) { return runAction(action, errorResult, method, false); } @@ -571,10 +573,21 @@ public class TextToSpeech { private int initTts() { // Step 1: Try connecting to the engine that was requested. - if (mRequestedEngine != null && mEnginesHelper.isEngineInstalled(mRequestedEngine)) { - if (connectToEngine(mRequestedEngine)) { - mCurrentEngine = mRequestedEngine; - return SUCCESS; + if (mRequestedEngine != null) { + if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) { + if (connectToEngine(mRequestedEngine)) { + mCurrentEngine = mRequestedEngine; + return SUCCESS; + } else if (!mUseFallback) { + mCurrentEngine = null; + dispatchOnInit(ERROR); + return ERROR; + } + } else if (!mUseFallback) { + Log.i(TAG, "Requested engine not installed: " + mRequestedEngine); + mCurrentEngine = null; + dispatchOnInit(ERROR); + return ERROR; } } @@ -630,6 +643,10 @@ public class TextToSpeech { } } + private IBinder getCallerIdentity() { + return mServiceConnection.getCallerIdentity(); + } + /** * Releases the resources used by the TextToSpeech engine. * It is good practice for instance to call this method in the onDestroy() method of an Activity @@ -639,8 +656,8 @@ public class TextToSpeech { runActionNoReconnect(new Action<Void>() { @Override public Void run(ITextToSpeechService service) throws RemoteException { - service.setCallback(getPackageName(), null); - service.stop(getPackageName()); + service.setCallback(getCallerIdentity(), null); + service.stop(getCallerIdentity()); mServiceConnection.disconnect(); // Context#unbindService does not result in a call to // ServiceConnection#onServiceDisconnected. As a result, the @@ -800,10 +817,10 @@ public class TextToSpeech { public Integer run(ITextToSpeechService service) throws RemoteException { Uri utteranceUri = mUtterances.get(text); if (utteranceUri != null) { - return service.playAudio(getPackageName(), utteranceUri, queueMode, + return service.playAudio(getCallerIdentity(), utteranceUri, queueMode, getParams(params)); } else { - return service.speak(getPackageName(), text, queueMode, getParams(params)); + return service.speak(getCallerIdentity(), text, queueMode, getParams(params)); } } }, ERROR, "speak"); @@ -836,7 +853,7 @@ public class TextToSpeech { if (earconUri == null) { return ERROR; } - return service.playAudio(getPackageName(), earconUri, queueMode, + return service.playAudio(getCallerIdentity(), earconUri, queueMode, getParams(params)); } }, ERROR, "playEarcon"); @@ -863,7 +880,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.playSilence(getPackageName(), durationInMs, queueMode, + return service.playSilence(getCallerIdentity(), durationInMs, queueMode, getParams(params)); } }, ERROR, "playSilence"); @@ -926,7 +943,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.stop(getPackageName()); + return service.stop(getCallerIdentity()); } }, ERROR, "stop"); } @@ -1091,7 +1108,7 @@ public class TextToSpeech { return runAction(new Action<Integer>() { @Override public Integer run(ITextToSpeechService service) throws RemoteException { - return service.synthesizeToFile(getPackageName(), text, filename, + return service.synthesizeToFile(getCallerIdentity(), text, filename, getParams(params)); } }, ERROR, "synthesizeToFile"); @@ -1275,7 +1292,7 @@ public class TextToSpeech { mServiceConnection = this; mService = ITextToSpeechService.Stub.asInterface(service); try { - mService.setCallback(getPackageName(), mCallback); + mService.setCallback(getCallerIdentity(), mCallback); dispatchOnInit(SUCCESS); } catch (RemoteException re) { Log.e(TAG, "Error connecting to service, setCallback() failed"); @@ -1284,6 +1301,10 @@ public class TextToSpeech { } } + public IBinder getCallerIdentity() { + return mCallback; + } + public void onServiceDisconnected(ComponentName name) { synchronized(mStartLock) { mService = null; diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index aee678a..4c1a0af 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -18,6 +18,7 @@ package android.speech.tts; import android.app.Service; import android.content.Intent; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -272,9 +273,9 @@ public abstract class TextToSpeechService extends Service { return old; } - private synchronized SpeechItem maybeRemoveCurrentSpeechItem(String callingApp) { + private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { if (mCurrentSpeechItem != null && - TextUtils.equals(mCurrentSpeechItem.getCallingApp(), callingApp)) { + mCurrentSpeechItem.getCallerIdentity() == callerIdentity) { SpeechItem current = mCurrentSpeechItem; mCurrentSpeechItem = null; return current; @@ -311,7 +312,7 @@ public abstract class TextToSpeechService extends Service { } if (queueMode == TextToSpeech.QUEUE_FLUSH) { - stopForApp(speechItem.getCallingApp()); + stopForApp(speechItem.getCallerIdentity()); } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { stopAll(); } @@ -328,7 +329,7 @@ public abstract class TextToSpeechService extends Service { // stopForApp(String). // // Note that this string is interned, so the == comparison works. - msg.obj = speechItem.getCallingApp(); + msg.obj = speechItem.getCallerIdentity(); if (sendMessage(msg)) { return TextToSpeech.SUCCESS; } else { @@ -344,12 +345,12 @@ public abstract class TextToSpeechService extends Service { * * Called on a service binder thread. */ - public int stopForApp(String callingApp) { - if (TextUtils.isEmpty(callingApp)) { + public int stopForApp(Object callerIdentity) { + if (callerIdentity == null) { return TextToSpeech.ERROR; } - removeCallbacksAndMessages(callingApp); + removeCallbacksAndMessages(callerIdentity); // This stops writing data to the file / or publishing // items to the audio playback handler. // @@ -357,13 +358,13 @@ public abstract class TextToSpeechService extends Service { // belongs to the callingApp, else the item will be "orphaned" and // not stopped correctly if a stop request comes along for the item // from the app it belongs to. - SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp); + SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity); if (current != null) { current.stop(); } // Remove any enqueued audio too. - mAudioPlaybackHandler.removePlaybackItems(callingApp); + mAudioPlaybackHandler.stopForApp(callerIdentity); return TextToSpeech.SUCCESS; } @@ -377,7 +378,7 @@ public abstract class TextToSpeechService extends Service { // Remove all other items from the queue. removeCallbacksAndMessages(null); // Remove all pending playback as well. - mAudioPlaybackHandler.removeAllItems(); + mAudioPlaybackHandler.stop(); return TextToSpeech.SUCCESS; } @@ -393,18 +394,22 @@ public abstract class TextToSpeechService extends Service { * An item in the synth thread queue. */ private abstract class SpeechItem implements UtteranceProgressDispatcher { - private final String mCallingApp; + private final Object mCallerIdentity; protected final Bundle mParams; + private final int mCallerUid; + private final int mCallerPid; private boolean mStarted = false; private boolean mStopped = false; - public SpeechItem(String callingApp, Bundle params) { - mCallingApp = callingApp; + public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { + mCallerIdentity = caller; mParams = params; + mCallerUid = callerUid; + mCallerPid = callerPid; } - public String getCallingApp() { - return mCallingApp; + public Object getCallerIdentity() { + return mCallerIdentity; } /** @@ -451,7 +456,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnDone() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnDone(getCallingApp(), utteranceId); + mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId); } } @@ -459,7 +464,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnStart() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnStart(getCallingApp(), utteranceId); + mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId); } } @@ -467,10 +472,18 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnError() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnError(getCallingApp(), utteranceId); + mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId); } } + public int getCallerUid() { + return mCallerUid; + } + + public int getCallerPid() { + return mCallerPid; + } + protected synchronized boolean isStopped() { return mStopped; } @@ -518,13 +531,15 @@ public abstract class TextToSpeechService extends Service { private AbstractSynthesisCallback mSynthesisCallback; private final EventLogger mEventLogger; - public SynthesisSpeechItem(String callingApp, Bundle params, String text) { - super(callingApp, params); + public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, String text) { + super(callerIdentity, callerUid, callerPid, params); mText = text; mSynthesisRequest = new SynthesisRequest(mText, mParams); mDefaultLocale = getSettingsLocale(); setRequestParams(mSynthesisRequest); - mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName); + mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, + mPackageName); } public String getText() { @@ -563,7 +578,7 @@ public abstract class TextToSpeechService extends Service { protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this, getCallingApp(), mEventLogger); + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); } private void setRequestParams(SynthesisRequest request) { @@ -618,9 +633,10 @@ public abstract class TextToSpeechService extends Service { private class SynthesisToFileSpeechItem extends SynthesisSpeechItem { private final File mFile; - public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text, + public SynthesisToFileSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, String text, File file) { - super(callingApp, params, text); + super(callerIdentity, callerUid, callerPid, params, text); mFile = file; } @@ -678,13 +694,12 @@ public abstract class TextToSpeechService extends Service { } private class AudioSpeechItem extends SpeechItem { - - private final BlockingMediaPlayer mPlayer; - private AudioMessageParams mToken; - - public AudioSpeechItem(String callingApp, Bundle params, Uri uri) { - super(callingApp, params); - mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType()); + private final AudioPlaybackQueueItem mItem; + public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, Uri uri) { + super(callerIdentity, callerUid, callerPid, params); + mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), + TextToSpeechService.this, uri, getStreamType()); } @Override @@ -694,8 +709,7 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new AudioMessageParams(this, getCallingApp(), mPlayer); - mAudioPlaybackHandler.enqueueAudio(mToken); + mAudioPlaybackHandler.enqueue(mItem); return TextToSpeech.SUCCESS; } @@ -707,10 +721,10 @@ public abstract class TextToSpeechService extends Service { private class SilenceSpeechItem extends SpeechItem { private final long mDuration; - private SilenceMessageParams mToken; - public SilenceSpeechItem(String callingApp, Bundle params, long duration) { - super(callingApp, params); + public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, + Bundle params, long duration) { + super(callerIdentity, callerUid, callerPid, params); mDuration = duration; } @@ -721,14 +735,14 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { - mToken = new SilenceMessageParams(this, getCallingApp(), mDuration); - mAudioPlaybackHandler.enqueueSilence(mToken); + mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( + this, getCallerIdentity(), mDuration)); return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - // Do nothing. + // Do nothing, handled by AudioPlaybackHandler#stopForApp } } @@ -747,58 +761,67 @@ public abstract class TextToSpeechService extends Service { // NOTE: All calls that are passed in a calling app are interned so that // they can be used as message objects (which are tested for equality using ==). private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { - - public int speak(String callingApp, String text, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, text, params)) { + @Override + public int speak(IBinder caller, String text, int queueMode, Bundle params) { + if (!checkNonNull(caller, text, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text); + SpeechItem item = new SynthesisSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); } - public int synthesizeToFile(String callingApp, String text, String filename, + @Override + public int synthesizeToFile(IBinder caller, String text, String filename, Bundle params) { - if (!checkNonNull(callingApp, text, filename, params)) { + if (!checkNonNull(caller, text, filename, params)) { return TextToSpeech.ERROR; } File file = new File(filename); - SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp), - params, text, file); + SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(), + Binder.getCallingPid(), params, text, file); return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); } - public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, audioUri, params)) { + @Override + public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) { + if (!checkNonNull(caller, audioUri, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri); + SpeechItem item = new AudioSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); return mSynthHandler.enqueueSpeechItem(queueMode, item); } - public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { - if (!checkNonNull(callingApp, params)) { + @Override + public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { + if (!checkNonNull(caller, params)) { return TextToSpeech.ERROR; } - SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration); + SpeechItem item = new SilenceSpeechItem(caller, + Binder.getCallingUid(), Binder.getCallingPid(), params, duration); return mSynthHandler.enqueueSpeechItem(queueMode, item); } + @Override public boolean isSpeaking() { return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); } - public int stop(String callingApp) { - if (!checkNonNull(callingApp)) { + @Override + public int stop(IBinder caller) { + if (!checkNonNull(caller)) { return TextToSpeech.ERROR; } - return mSynthHandler.stopForApp(intern(callingApp)); + return mSynthHandler.stopForApp(caller); } + @Override public String[] getLanguage() { return onGetLanguage(); } @@ -807,6 +830,7 @@ public abstract class TextToSpeechService extends Service { * If defaults are enforced, then no language is "available" except * perhaps the default language selected by the user. */ + @Override public int isLanguageAvailable(String lang, String country, String variant) { if (!checkNonNull(lang)) { return TextToSpeech.ERROR; @@ -815,6 +839,7 @@ public abstract class TextToSpeechService extends Service { return onIsLanguageAvailable(lang, country, variant); } + @Override public String[] getFeaturesForLanguage(String lang, String country, String variant) { Set<String> features = onGetFeaturesForLanguage(lang, country, variant); String[] featuresArray = null; @@ -831,6 +856,7 @@ public abstract class TextToSpeechService extends Service { * There is no point loading a non default language if defaults * are enforced. */ + @Override public int loadLanguage(String lang, String country, String variant) { if (!checkNonNull(lang)) { return TextToSpeech.ERROR; @@ -839,13 +865,14 @@ public abstract class TextToSpeechService extends Service { return onLoadLanguage(lang, country, variant); } - public void setCallback(String packageName, ITextToSpeechCallback cb) { + @Override + public void setCallback(IBinder caller, ITextToSpeechCallback cb) { // Note that passing in a null callback is a valid use case. - if (!checkNonNull(packageName)) { + if (!checkNonNull(caller)) { return; } - mCallbacks.setCallback(packageName, cb); + mCallbacks.setCallback(caller, cb); } private String intern(String in) { @@ -862,18 +889,17 @@ public abstract class TextToSpeechService extends Service { }; private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { + private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback + = new HashMap<IBinder, ITextToSpeechCallback>(); - private final HashMap<String, ITextToSpeechCallback> mAppToCallback - = new HashMap<String, ITextToSpeechCallback>(); - - public void setCallback(String packageName, ITextToSpeechCallback cb) { - synchronized (mAppToCallback) { + public void setCallback(IBinder caller, ITextToSpeechCallback cb) { + synchronized (mCallerToCallback) { ITextToSpeechCallback old; if (cb != null) { - register(cb, packageName); - old = mAppToCallback.put(packageName, cb); + register(cb, caller); + old = mCallerToCallback.put(caller, cb); } else { - old = mAppToCallback.remove(packageName); + old = mCallerToCallback.remove(caller); } if (old != null && old != cb) { unregister(old); @@ -881,8 +907,8 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnDone(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnDone(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onDone(utteranceId); @@ -891,8 +917,8 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnStart(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnStart(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onStart(utteranceId); @@ -902,8 +928,8 @@ public abstract class TextToSpeechService extends Service { } - public void dispatchOnError(String packageName, String utteranceId) { - ITextToSpeechCallback cb = getCallbackFor(packageName); + public void dispatchOnError(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { cb.onError(utteranceId); @@ -914,25 +940,26 @@ public abstract class TextToSpeechService extends Service { @Override public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { - String packageName = (String) cookie; - synchronized (mAppToCallback) { - mAppToCallback.remove(packageName); + IBinder caller = (IBinder) cookie; + synchronized (mCallerToCallback) { + mCallerToCallback.remove(caller); } - mSynthHandler.stopForApp(packageName); + mSynthHandler.stopForApp(caller); } @Override public void kill() { - synchronized (mAppToCallback) { - mAppToCallback.clear(); + synchronized (mCallerToCallback) { + mCallerToCallback.clear(); super.kill(); } } - private ITextToSpeechCallback getCallbackFor(String packageName) { + private ITextToSpeechCallback getCallbackFor(Object caller) { ITextToSpeechCallback cb; - synchronized (mAppToCallback) { - cb = mAppToCallback.get(packageName); + IBinder asBinder = (IBinder) caller; + synchronized (mCallerToCallback) { + cb = mCallerToCallback.get(asBinder); } return cb; diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index c184c11..a52e2ba 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -109,6 +109,9 @@ class MeasuredText { for (int i = 0; i < spans.length; i++) { int startInPara = spanned.getSpanStart(spans[i]) - start; int endInPara = spanned.getSpanEnd(spans[i]) - start; + // The span interval may be larger and must be restricted to [start, end[ + if (startInPara < 0) startInPara = 0; + if (endInPara > len) endInPara = len; for (int j = startInPara; j < endInPara; j++) { mChars[j] = '\uFFFC'; } diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 7f8af7a..da10311 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -136,12 +136,12 @@ public class DateUtils private static java.text.DateFormat sStatusTimeFormat; private static String sElapsedFormatMMSS; private static String sElapsedFormatHMMSS; - + private static final String FAST_FORMAT_HMMSS = "%1$d:%2$02d:%3$02d"; private static final String FAST_FORMAT_MMSS = "%1$02d:%2$02d"; private static final char TIME_PADDING = '0'; private static final char TIME_SEPARATOR = ':'; - + public static final long SECOND_IN_MILLIS = 1000; public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; @@ -201,7 +201,7 @@ public class DateUtils public static final String YEAR_FORMAT_TWO_DIGITS = "%g"; public static final String WEEKDAY_FORMAT = "%A"; public static final String ABBREV_WEEKDAY_FORMAT = "%a"; - + // This table is used to lookup the resource string id of a format string // used for formatting a start and end date that fall in the same year. // The index is constructed from a bit-wise OR of the boolean values: @@ -227,7 +227,7 @@ public class DateUtils com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2, com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2, }; - + // This table is used to lookup the resource string id of a format string // used for formatting a start and end date that fall in the same month. // The index is constructed from a bit-wise OR of the boolean values: @@ -256,7 +256,7 @@ public class DateUtils /** * Request the full spelled-out name. For use with the 'abbrev' parameter of * {@link #getDayOfWeekString} and {@link #getMonthString}. - * + * * @more <p> * e.g. "Sunday" or "January" */ @@ -265,7 +265,7 @@ public class DateUtils /** * Request an abbreviated version of the name. For use with the 'abbrev' * parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. - * + * * @more <p> * e.g. "Sun" or "Jan" */ @@ -347,7 +347,7 @@ public class DateUtils * @return Localized month of the year. */ public static String getMonthString(int month, int abbrev) { - // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. + // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. // This is a shortcut to not spam the translators with too many variations // of the same string. If we find that in a language the distinction // is necessary, we can can add more without changing this API. @@ -380,7 +380,7 @@ public class DateUtils * @hide Pending API council approval */ public static String getStandaloneMonthString(int month, int abbrev) { - // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. + // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. // This is a shortcut to not spam the translators with too many variations // of the same string. If we find that in a language the distinction // is necessary, we can can add more without changing this API. @@ -434,7 +434,7 @@ public class DateUtils * <p> * Can use {@link #FORMAT_ABBREV_RELATIVE} flag to use abbreviated relative * times, like "42 mins ago". - * + * * @param time the time to describe, in milliseconds * @param now the current time in milliseconds * @param minResolution the minimum timespan to report. For example, a time @@ -450,7 +450,7 @@ public class DateUtils int flags) { Resources r = Resources.getSystem(); boolean abbrevRelative = (flags & (FORMAT_ABBREV_RELATIVE | FORMAT_ABBREV_ALL)) != 0; - + boolean past = (now >= time); long duration = Math.abs(now - time); @@ -525,7 +525,7 @@ public class DateUtils String format = r.getQuantityString(resId, (int) count); return String.format(format, count); } - + /** * Returns the number of days passed between two dates. * @@ -555,7 +555,7 @@ public class DateUtils * <li>Dec 12, 4:12 AM</li> * <li>11/14/2007, 8:20 AM</li> * </ul> - * + * * @param time some time in the past. * @param minResolution the minimum elapsed time (in milliseconds) to report * when showing relative times. For example, a time 3 seconds in @@ -584,7 +584,7 @@ public class DateUtils } CharSequence timeClause = formatDateRange(c, time, time, FORMAT_SHOW_TIME); - + String result; if (duration < transitionResolution) { CharSequence relativeClause = getRelativeTimeSpanString(time, now, minResolution, flags); @@ -601,7 +601,7 @@ public class DateUtils * Returns a string describing a day relative to the current day. For example if the day is * today this function returns "Today", if the day was a week ago it returns "7 days ago", and * if the day is in 2 weeks it returns "in 14 days". - * + * * @param r the resources to get the strings from * @param day the relative day to describe in UTC milliseconds * @param today the current time in UTC milliseconds @@ -618,7 +618,7 @@ public class DateUtils int days = Math.abs(currentDay - startDay); boolean past = (today > day); - + if (days == 1) { if (past) { return r.getString(com.android.internal.R.string.yesterday); @@ -635,7 +635,7 @@ public class DateUtils } else { resId = com.android.internal.R.plurals.in_num_days; } - + String format = r.getQuantityString(resId, days); return String.format(format, days); } @@ -677,11 +677,11 @@ public class DateUtils public static String formatElapsedTime(long elapsedSeconds) { return formatElapsedTime(null, elapsedSeconds); } - + /** * Formats an elapsed time in the form "MM:SS" or "H:MM:SS" * for display on the call-in-progress screen. - * + * * @param recycle {@link StringBuilder} to recycle, if possible * @param elapsedSeconds the elapsed time in seconds. */ @@ -724,7 +724,7 @@ public class DateUtils } sb.append(hours); sb.append(TIME_SEPARATOR); - if (minutes < 10) { + if (minutes < 10) { sb.append(TIME_PADDING); } else { sb.append(toDigitChar(minutes / 10)); @@ -755,7 +755,7 @@ public class DateUtils } else { sb.setLength(0); } - if (minutes < 10) { + if (minutes < 10) { sb.append(TIME_PADDING); } else { sb.append(toDigitChar(minutes / 10)); @@ -777,11 +777,11 @@ public class DateUtils private static char toDigitChar(long digit) { return (char) (digit + '0'); } - + /** * Format a date / time such that if the then is on the same day as now, it shows * just the time and if it's a different day, it shows just the date. - * + * * <p>The parameters dateFormat and timeFormat should each be one of * {@link java.text.DateFormat#DEFAULT}, * {@link java.text.DateFormat#FULL}, @@ -833,14 +833,14 @@ public class DateUtils public static boolean isToday(long when) { Time time = new Time(); time.set(when); - + int thenYear = time.year; int thenMonth = time.month; int thenMonthDay = time.monthDay; time.set(System.currentTimeMillis()); return (thenYear == time.year) - && (thenMonth == time.month) + && (thenMonth == time.month) && (thenMonthDay == time.monthDay); } @@ -914,7 +914,7 @@ public class DateUtils public static String writeDateTime(Calendar cal, StringBuilder sb) { int n; - + n = cal.get(Calendar.YEAR); sb.setCharAt(3, (char)('0'+n%10)); n /= 10; @@ -1015,7 +1015,7 @@ public class DateUtils /** * Formats a date or a time range according to the local conventions. - * + * * <p> * Example output strings (date formats in these examples are shown using * the US date format convention but that may change depending on the @@ -1036,10 +1036,10 @@ public class DateUtils * <li>Oct 9, 8:00am - Oct 10, 5:00pm</li> * <li>12/31/2007 - 01/01/2008</li> * </ul> - * + * * <p> * The flags argument is a bitmask of options from the following list: - * + * * <ul> * <li>FORMAT_SHOW_TIME</li> * <li>FORMAT_SHOW_WEEKDAY</li> @@ -1061,15 +1061,15 @@ public class DateUtils * <li>FORMAT_ABBREV_ALL</li> * <li>FORMAT_NUMERIC_DATE</li> * </ul> - * + * * <p> * If FORMAT_SHOW_TIME is set, the time is shown as part of the date range. * If the start and end time are the same, then just the start time is * shown. - * + * * <p> * If FORMAT_SHOW_WEEKDAY is set, then the weekday is shown. - * + * * <p> * If FORMAT_SHOW_YEAR is set, then the year is always shown. * If FORMAT_NO_YEAR is set, then the year is not shown. @@ -1082,80 +1082,91 @@ public class DateUtils * Normally the date is shown unless the start and end day are the same. * If FORMAT_SHOW_DATE is set, then the date is always shown, even for * same day ranges. - * + * * <p> * If FORMAT_NO_MONTH_DAY is set, then if the date is shown, just the * month name will be shown, not the day of the month. For example, * "January, 2008" instead of "January 6 - 12, 2008". - * + * * <p> * If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM" * and "PM" are capitalized. You should not use this flag * because in some locales these terms cannot be capitalized, and in * many others it doesn't make sense to do so even though it is possible. - * + * * <p> * If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is * shown instead of "noon". - * + * * <p> * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is * shown instead of "noon". You should probably not use this flag * because in many locales it will not make sense to capitalize * the term. - * + * * <p> * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is * shown instead of "midnight". - * + * * <p> * If FORMAT_CAP_MIDNIGHT is set and 12-hour time is used, then "Midnight" * is shown instead of "midnight". You should probably not use this * flag because in many locales it will not make sense to capitalize * the term. - * + * * <p> * If FORMAT_12HOUR is set and the time is shown, then the time is * shown in the 12-hour time format. You should not normally set this. * Instead, let the time format be chosen automatically according to the * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then * FORMAT_24HOUR takes precedence. - * + * * <p> * If FORMAT_24HOUR is set and the time is shown, then the time is * shown in the 24-hour time format. You should not normally set this. * Instead, let the time format be chosen automatically according to the * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then * FORMAT_24HOUR takes precedence. - * + * * <p> * If FORMAT_UTC is set, then the UTC time zone is used for the start * and end milliseconds unless a time zone is specified. If a time zone * is specified it will be used regardless of the FORMAT_UTC flag. - * + * * <p> * If FORMAT_ABBREV_TIME is set and 12-hour time format is used, then the * start and end times (if shown) are abbreviated by not showing the minutes * if they are zero. For example, instead of "3:00pm" the time would be * abbreviated to "3pm". - * + * * <p> * If FORMAT_ABBREV_WEEKDAY is set, then the weekday (if shown) is * abbreviated to a 3-letter string. - * + * * <p> * If FORMAT_ABBREV_MONTH is set, then the month (if shown) is abbreviated * to a 3-letter string. - * + * * <p> * If FORMAT_ABBREV_ALL is set, then the weekday and the month (if shown) * are abbreviated to 3-letter strings. - * + * * <p> * If FORMAT_NUMERIC_DATE is set, then the date is shown in numeric format * instead of using the name of the month. For example, "12/31/2008" * instead of "December 31, 2008". - * + * + * <p> + * If the end date ends at 12:00am at the beginning of a day, it is + * formatted as the end of the previous day in two scenarios: + * <ul> + * <li>For single day events. This results in "8pm - midnight" instead of + * "Nov 10, 8pm - Nov 11, 12am".</li> + * <li>When the time is not displayed. This results in "Nov 10 - 11" for + * an event with a start date of Nov 10 and an end date of Nov 12 at + * 00:00.</li> + * </ul> + * * @param context the context is required only if the time is shown * @param formatter the Formatter used for formatting the date range. * Note: be sure to call setLength(0) on StringBuilder passed to @@ -1165,7 +1176,7 @@ public class DateUtils * @param flags a bit mask of options * @param timeZone the time zone to compute the string in. Use null for local * or if the FORMAT_UTC flag is being used. - * + * * @return the formatter with the formatted date/time range appended to the string buffer. */ public static Formatter formatDateRange(Context context, Formatter formatter, long startMillis, @@ -1215,20 +1226,6 @@ public class DateUtils dayDistance = endJulianDay - startJulianDay; } - // If the end date ends at 12am at the beginning of a day, - // then modify it to make it look like it ends at midnight on - // the previous day. This will allow us to display "8pm - midnight", - // for example, instead of "Nov 10, 8pm - Nov 11, 12am". But we only do - // this if it is midnight of the same day as the start date because - // for multiple-day events, an end time of "midnight on Nov 11" is - // ambiguous and confusing (is that midnight the start of Nov 11, or - // the end of Nov 11?). - // If we are not showing the time then also adjust the end date - // for multiple-day events. This is to allow us to display, for - // example, "Nov 10 -11" for an event with a start date of Nov 10 - // and an end date of Nov 12 at 00:00. - // If the start and end time are the same, then skip this and don't - // adjust the date. if (!isInstant && (endDate.hour | endDate.minute | endDate.second) == 0 && (!showTime || dayDistance <= 1)) { @@ -1592,7 +1589,7 @@ public class DateUtils * <li>Wed, October 31</li> * <li>10/31/2007</li> * </ul> - * + * * @param context the context is required only if the time is shown * @param millis a point in time in UTC milliseconds * @param flags a bit mask of formatting options @@ -1607,13 +1604,13 @@ public class DateUtils * are counted starting at midnight, which means that assuming that the current * time is March 31st, 0:30: * <ul> - * <li>"millis=0:10 today" will be displayed as "0:10"</li> + * <li>"millis=0:10 today" will be displayed as "0:10"</li> * <li>"millis=11:30pm the day before" will be displayed as "Mar 30"</li> * </ul> * If the given millis is in a different year, then the full date is * returned in numeric format (e.g., "10/12/2008"). - * - * @param withPreposition If true, the string returned will include the correct + * + * @param withPreposition If true, the string returned will include the correct * preposition ("at 9:20am", "on 10/12/2008" or "on May 29"). */ public static CharSequence getRelativeTimeSpanString(Context c, long millis, @@ -1661,9 +1658,9 @@ public class DateUtils } return result; } - + /** - * Convenience function to return relative time string without preposition. + * Convenience function to return relative time string without preposition. * @param c context for resources * @param millis time in milliseconds * @return {@link CharSequence} containing relative time. @@ -1672,7 +1669,7 @@ public class DateUtils public static CharSequence getRelativeTimeSpanString(Context c, long millis) { return getRelativeTimeSpanString(c, millis, false /* no preposition */); } - + private static Time sNowTime; private static Time sThenTime; } diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index b4445ca..e9b0d32 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -481,6 +481,9 @@ public class Time { * @throws android.util.TimeFormatException if s cannot be parsed. */ public boolean parse3339(String s) { + if (s == null) { + throw new NullPointerException("time string is null"); + } if (nativeParse3339(s)) { timezone = TIMEZONE_UTC; return true; diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 4ec4bc4..30bb447 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -280,8 +280,6 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme if (isSelecting(buffer)) { buffer.removeSpan(LAST_TAP_DOWN); Selection.extendSelection(buffer, offset); - } else if (!widget.shouldIgnoreActionUpEvent()) { - Selection.setSelection(buffer, offset); } MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index 0f26a34..5dc206f 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -25,6 +25,7 @@ import android.os.SystemClock; import android.text.ParcelableSpan; import android.text.TextPaint; import android.text.TextUtils; +import android.util.Log; import android.widget.TextView; import java.util.Arrays; @@ -114,7 +115,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { * @param context Context for the application * @param locale locale Locale of the suggestions * @param suggestions Suggestions for the string under the span. Only the first up to - * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. + * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted. * @param flags Additional flags indicating how this span is handled in TextView * @param notificationTargetClass if not null, this class will get notified when the user * selects one of the suggestions. @@ -124,10 +125,13 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); mSuggestions = Arrays.copyOf(suggestions, N); mFlags = flags; - if (context != null && locale == null) { + if (locale != null) { + mLocaleString = locale.toString(); + } else if (context != null) { mLocaleString = context.getResources().getConfiguration().locale.toString(); } else { - mLocaleString = locale.toString(); + Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor"); + mLocaleString = ""; } if (notificationTargetClass != null) { diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index 519b980..a43d36c 100644 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -57,6 +57,13 @@ public class DisplayMetrics { public static final int DENSITY_XHIGH = 320; /** + * Standard quantized DPI for extra-extra-high-density screens. Applications + * should not generally worry about this density; relying on XHIGH graphics + * being scaled up to it should be sufficient for almost all cases. + */ + public static final int DENSITY_XXHIGH = 480; + + /** * The reference density used throughout the system. */ public static final int DENSITY_DEFAULT = DENSITY_MEDIUM; diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java new file mode 100644 index 0000000..641d1b4 --- /dev/null +++ b/core/java/android/util/LocalLog.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2006 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 android.util; + +import android.text.format.Time; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * @hide + */ +public final class LocalLog { + + private LinkedList<String> mLog; + private int mMaxLines; + private Time mNow; + + public LocalLog(int maxLines) { + mLog = new LinkedList<String>(); + mMaxLines = maxLines; + mNow = new Time(); + } + + public synchronized void log(String msg) { + if (mMaxLines > 0) { + mNow.setToNow(); + mLog.add(mNow.format("%H:%M:%S") + " - " + msg); + while (mLog.size() > mMaxLines) mLog.remove(); + } + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + Iterator<String> itr = mLog.listIterator(0); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } +} diff --git a/core/java/android/util/LocaleUtil.java b/core/java/android/util/LocaleUtil.java index 763af73..4d773f6 100644 --- a/core/java/android/util/LocaleUtil.java +++ b/core/java/android/util/LocaleUtil.java @@ -39,8 +39,6 @@ public class LocaleUtil { */ public static final int TEXT_LAYOUT_DIRECTION_RTL_DO_NOT_USE = 1; - private static final char UNDERSCORE_CHAR = '_'; - private static String ARAB_SCRIPT_SUBTAG = "Arab"; private static String HEBR_SCRIPT_SUBTAG = "Hebr"; diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java index 5540000..51e373c 100644 --- a/core/java/android/util/LruCache.java +++ b/core/java/android/util/LruCache.java @@ -54,6 +54,10 @@ import java.util.Map; * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. + * + * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's + * Support Package</a> for earlier releases. */ public class LruCache<K, V> { private final LinkedHashMap<K, V> map; @@ -82,6 +86,23 @@ public class LruCache<K, V> { } /** + * Sets the size of the cache. + * @param maxSize The new maximum size. + * + * @hide + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java new file mode 100644 index 0000000..a08d5cb --- /dev/null +++ b/core/java/android/util/SparseLongArray.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2011 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 android.util; + +import com.android.internal.util.ArrayUtils; + +/** + * SparseLongArrays map integers to longs. Unlike a normal array of longs, + * there can be gaps in the indices. It is intended to be more efficient + * than using a HashMap to map Integers to Longs. + * + * @hide + */ +public class SparseLongArray implements Cloneable { + + private int[] mKeys; + private long[] mValues; + private int mSize; + + /** + * Creates a new SparseLongArray containing no mappings. + */ + public SparseLongArray() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. + */ + public SparseLongArray(int initialCapacity) { + initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); + + mKeys = new int[initialCapacity]; + mValues = new long[initialCapacity]; + mSize = 0; + } + + @Override + public SparseLongArray clone() { + SparseLongArray clone = null; + try { + clone = (SparseLongArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the long mapped from the specified key, or <code>0</code> + * if no such mapping has been made. + */ + public long get(int key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public long get(int key, long valueIfKeyNotFound) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, long value) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (mSize >= mKeys.length) { + growKeyAndValueArrays(mSize + 1); + } + + if (mSize - i != 0) { + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + */ + public int keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseLongArray stores. + */ + public long valueAt(int index) { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return binarySearch(mKeys, 0, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, long value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + int pos = mSize; + if (pos >= mKeys.length) { + growKeyAndValueArrays(pos + 1); + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + private void growKeyAndValueArrays(int minNeededSize) { + int n = ArrayUtils.idealLongArraySize(minNeededSize); + + int[] nkeys = new int[n]; + long[] nvalues = new long[n]; + + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + private static int binarySearch(int[] a, int start, int len, long key) { + int high = start + len, low = start - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (a[guess] < key) + low = guess; + else + high = guess; + } + + if (high == start + len) + return ~(start + len); + else if (a[high] == key) + return high; + else + return ~high; + } +} diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java new file mode 100644 index 0000000..63de128 --- /dev/null +++ b/core/java/android/view/Choreographer.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2011 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 android.view; + +import com.android.internal.util.ArrayUtils; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Log; + +/** + * Coodinates animations and drawing for UI on a particular thread. + * @hide + */ +public final class Choreographer extends Handler { + private static final String TAG = "Choreographer"; + private static final boolean DEBUG = false; + + // The default amount of time in ms between animation frames. + // When vsync is not enabled, we want to have some idea of how long we should + // wait before posting the next animation message. It is important that the + // default value be less than the true inter-frame delay on all devices to avoid + // situations where we might skip frames by waiting too long (we must compensate + // for jitter and hardware variations). Regardless of this value, the animation + // and display loop is ultimately rate-limited by how fast new graphics buffers can + // be dequeued. + private static final long DEFAULT_FRAME_DELAY = 10; + + // The number of milliseconds between animation frames. + private static long sFrameDelay = DEFAULT_FRAME_DELAY; + + // Thread local storage for the choreographer. + private static final ThreadLocal<Choreographer> sThreadInstance = + new ThreadLocal<Choreographer>() { + @Override + protected Choreographer initialValue() { + Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalStateException("The current thread must have a looper!"); + } + return new Choreographer(looper); + } + }; + + // System property to enable/disable vsync for animations and drawing. + // Enabled by default. + private static final boolean USE_VSYNC = SystemProperties.getBoolean( + "debug.choreographer.vsync", true); + + // System property to enable/disable the use of the vsync / animation timer + // for drawing rather than drawing immediately. + // Temporarily disabled by default because postponing performTraversals() violates + // assumptions about traversals happening in-order relative to other posted messages. + // Bug: 5721047 + private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean( + "debug.choreographer.animdraw", false); + + private static final int MSG_DO_ANIMATION = 0; + private static final int MSG_DO_DRAW = 1; + + private final Looper mLooper; + + private OnAnimateListener[] mOnAnimateListeners; + private OnDrawListener[] mOnDrawListeners; + + private boolean mAnimationScheduled; + private boolean mDrawScheduled; + private FrameDisplayEventReceiver mFrameDisplayEventReceiver; + private long mLastAnimationTime; + private long mLastDrawTime; + + private Choreographer(Looper looper) { + super(looper); + mLooper = looper; + mLastAnimationTime = Long.MIN_VALUE; + mLastDrawTime = Long.MIN_VALUE; + } + + /** + * Gets the choreographer for this thread. + * Must be called on the UI thread. + * + * @return The choreographer for this thread. + * @throws IllegalStateException if the thread does not have a looper. + */ + public static Choreographer getInstance() { + return sThreadInstance.get(); + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * + * @return the requested time between frames, in milliseconds + */ + public static long getFrameDelay() { + return sFrameDelay; + } + + /** + * The amount of time, in milliseconds, between each frame of the animation. This is a + * requested time that the animation will attempt to honor, but the actual delay between + * frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + * + * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + * + * @param frameDelay the requested time between frames, in milliseconds + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + + /** + * Schedules animation (and drawing) to occur on the next frame synchronization boundary. + * Must be called on the UI thread. + */ + public void scheduleAnimation() { + if (!mAnimationScheduled) { + mAnimationScheduled = true; + if (USE_VSYNC) { + if (DEBUG) { + Log.d(TAG, "Scheduling vsync for animation."); + } + if (mFrameDisplayEventReceiver == null) { + mFrameDisplayEventReceiver = new FrameDisplayEventReceiver(mLooper); + } + mFrameDisplayEventReceiver.scheduleVsync(); + } else { + final long now = SystemClock.uptimeMillis(); + final long nextAnimationTime = Math.max(mLastAnimationTime + sFrameDelay, now); + if (DEBUG) { + Log.d(TAG, "Scheduling animation in " + (nextAnimationTime - now) + " ms."); + } + sendEmptyMessageAtTime(MSG_DO_ANIMATION, nextAnimationTime); + } + } + } + + /** + * Schedules drawing to occur on the next frame synchronization boundary. + * Must be called on the UI thread. + */ + public void scheduleDraw() { + if (!mDrawScheduled) { + mDrawScheduled = true; + if (USE_ANIMATION_TIMER_FOR_DRAW) { + scheduleAnimation(); + } else { + if (DEBUG) { + Log.d(TAG, "Scheduling draw immediately."); + } + sendEmptyMessage(MSG_DO_DRAW); + } + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DO_ANIMATION: + doAnimation(); + break; + case MSG_DO_DRAW: + doDraw(); + break; + } + } + + private void doAnimation() { + if (mAnimationScheduled) { + mAnimationScheduled = false; + + final long start = SystemClock.uptimeMillis(); + if (DEBUG) { + Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime) + + " ms have elapsed since previous animation."); + } + mLastAnimationTime = start; + + final OnAnimateListener[] listeners = mOnAnimateListeners; + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].onAnimate(); + } + } + + if (DEBUG) { + Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms."); + } + } + + if (USE_ANIMATION_TIMER_FOR_DRAW) { + doDraw(); + } + } + + private void doDraw() { + if (mDrawScheduled) { + mDrawScheduled = false; + + final long start = SystemClock.uptimeMillis(); + if (DEBUG) { + Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime) + + " ms have elapsed since previous draw."); + } + mLastDrawTime = start; + + final OnDrawListener[] listeners = mOnDrawListeners; + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].onDraw(); + } + } + + if (DEBUG) { + Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms."); + } + } + } + + /** + * Adds an animation listener. + * Must be called on the UI thread. + * + * @param listener The listener to add. + */ + public void addOnAnimateListener(OnAnimateListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Adding onAnimate listener: " + listener); + } + + mOnAnimateListeners = ArrayUtils.appendElement(OnAnimateListener.class, + mOnAnimateListeners, listener); + } + + /** + * Removes an animation listener. + * Must be called on the UI thread. + * + * @param listener The listener to remove. + */ + public void removeOnAnimateListener(OnAnimateListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Removing onAnimate listener: " + listener); + } + + mOnAnimateListeners = ArrayUtils.removeElement(OnAnimateListener.class, + mOnAnimateListeners, listener); + stopTimingLoopIfNoListeners(); + } + + /** + * Adds a draw listener. + * Must be called on the UI thread. + * + * @param listener The listener to add. + */ + public void addOnDrawListener(OnDrawListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Adding onDraw listener: " + listener); + } + + mOnDrawListeners = ArrayUtils.appendElement(OnDrawListener.class, + mOnDrawListeners, listener); + } + + /** + * Removes a draw listener. + * Must be called on the UI thread. + * + * @param listener The listener to remove. + */ + public void removeOnDrawListener(OnDrawListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (DEBUG) { + Log.d(TAG, "Removing onDraw listener: " + listener); + } + + mOnDrawListeners = ArrayUtils.removeElement(OnDrawListener.class, + mOnDrawListeners, listener); + stopTimingLoopIfNoListeners(); + } + + private void stopTimingLoopIfNoListeners() { + if (mOnDrawListeners == null && mOnAnimateListeners == null) { + if (DEBUG) { + Log.d(TAG, "Stopping timing loop."); + } + + if (mAnimationScheduled) { + mAnimationScheduled = false; + if (!USE_VSYNC) { + removeMessages(MSG_DO_ANIMATION); + } + } + + if (mDrawScheduled) { + mDrawScheduled = false; + if (!USE_ANIMATION_TIMER_FOR_DRAW) { + removeMessages(MSG_DO_DRAW); + } + } + + if (mFrameDisplayEventReceiver != null) { + mFrameDisplayEventReceiver.dispose(); + mFrameDisplayEventReceiver = null; + } + } + } + + /** + * Listens for animation frame timing events. + */ + public static interface OnAnimateListener { + /** + * Called to animate properties before drawing the frame. + */ + public void onAnimate(); + } + + /** + * Listens for draw frame timing events. + */ + public static interface OnDrawListener { + /** + * Called to draw the frame. + */ + public void onDraw(); + } + + private final class FrameDisplayEventReceiver extends DisplayEventReceiver { + public FrameDisplayEventReceiver(Looper looper) { + super(looper); + } + + @Override + public void onVsync(long timestampNanos, int frame) { + doAnimation(); + } + } +} diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java new file mode 100644 index 0000000..d6711ee --- /dev/null +++ b/core/java/android/view/DisplayEventReceiver.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 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 android.view; + +import dalvik.system.CloseGuard; + +import android.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +/** + * Provides a low-level mechanism for an application to receive display events + * such as vertical sync. + * @hide + */ +public abstract class DisplayEventReceiver { + private static final String TAG = "DisplayEventReceiver"; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private int mReceiverPtr; + + // We keep a reference message queue object here so that it is not + // GC'd while the native peer of the receiver is using them. + private MessageQueue mMessageQueue; + + private static native int nativeInit(DisplayEventReceiver receiver, + MessageQueue messageQueue); + private static native void nativeDispose(int receiverPtr); + private static native void nativeScheduleVsync(int receiverPtr); + + /** + * Creates a display event receiver. + * + * @param looper The looper to use when invoking callbacks. + */ + public DisplayEventReceiver(Looper looper) { + if (looper == null) { + throw new IllegalArgumentException("looper must not be null"); + } + + mMessageQueue = looper.getQueue(); + mReceiverPtr = nativeInit(this, mMessageQueue); + + mCloseGuard.open("dispose"); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + /** + * Disposes the receiver. + */ + public void dispose() { + if (mCloseGuard != null) { + mCloseGuard.close(); + } + if (mReceiverPtr != 0) { + nativeDispose(mReceiverPtr); + mReceiverPtr = 0; + } + mMessageQueue = null; + } + + /** + * Called when a vertical sync pulse is received. + * The recipient should render a frame and then call {@link #scheduleVsync} + * to schedule the next vertical sync pulse. + * + * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()} + * timebase. + * @param frame The frame number. Increases by one for each vertical sync interval. + */ + public void onVsync(long timestampNanos, int frame) { + } + + /** + * Schedules a single vertical sync pulse to be delivered when the next + * display frame begins. + */ + public void scheduleVsync() { + if (mReceiverPtr == 0) { + Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + + "receiver has already been disposed."); + } else { + nativeScheduleVsync(mReceiverPtr); + } + } + + // Called from native code. + @SuppressWarnings("unused") + private void dispatchVsync(long timestampNanos, int frame) { + onVsync(timestampNanos, frame); + } +} diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java index 8f4ece0..fec0d4b 100644 --- a/core/java/android/view/DisplayList.java +++ b/core/java/android/view/DisplayList.java @@ -32,20 +32,20 @@ public abstract class DisplayList { * * @return A canvas to record drawing operations. */ - abstract HardwareCanvas start(); + public abstract HardwareCanvas start(); /** * Ends the recording for this display list. A display list cannot be * replayed if recording is not finished. */ - abstract void end(); + public abstract void end(); /** * Invalidates the display list, indicating that it should be repopulated * with new drawing commands prior to being used again. Calling this method * causes calls to {@link #isValid()} to return <code>false</code>. */ - abstract void invalidate(); + public abstract void invalidate(); /** * Returns whether the display list is currently usable. If this returns false, @@ -53,12 +53,12 @@ public abstract class DisplayList { * * @return boolean true if the display list is able to be replayed, false otherwise. */ - abstract boolean isValid(); + public abstract boolean isValid(); /** * Return the amount of memory used by this display list. * * @return The size of this display list in bytes */ - abstract int getSize(); + public abstract int getSize(); } diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 4ca299f..748ec0c 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -22,6 +22,7 @@ import android.graphics.ColorFilter; import android.graphics.DrawFilter; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; import android.graphics.Path; import android.graphics.Picture; import android.graphics.PorterDuff; @@ -61,6 +62,7 @@ class GLES20Canvas extends HardwareCanvas { private final float[] mLine = new float[4]; private final Rect mClipBounds = new Rect(); + private final RectF mPathBounds = new RectF(); private DrawFilter mFilter; @@ -154,6 +156,7 @@ class GLES20Canvas extends HardwareCanvas { static native void nSetTextureLayerTransform(int layerId, int matrix); static native void nDestroyLayer(int layerId); static native void nDestroyLayerDeferred(int layerId); + static native void nFlushLayer(int layerId); static native boolean nCopyLayer(int layerId, int bitmap); /////////////////////////////////////////////////////////////////////////// @@ -186,7 +189,7 @@ class GLES20Canvas extends HardwareCanvas { } private static native int nGetMaximumTextureWidth(); - private static native int nGetMaximumTextureHeight(); + private static native int nGetMaximumTextureHeight(); /////////////////////////////////////////////////////////////////////////// // Setup @@ -246,7 +249,7 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDisableVsync(); @Override - void onPreDraw(Rect dirty) { + public void onPreDraw(Rect dirty) { if (dirty != null) { nPrepareDirty(mRenderer, dirty.left, dirty.top, dirty.right, dirty.bottom, mOpaque); } else { @@ -259,12 +262,30 @@ class GLES20Canvas extends HardwareCanvas { boolean opaque); @Override - void onPostDraw() { + public void onPostDraw() { nFinish(mRenderer); } private static native void nFinish(int renderer); + /** + * Returns the size of the stencil buffer required by the underlying + * implementation. + * + * @return The minimum number of bits the stencil buffer must. Always >= 0. + * + * @hide + */ + public static int getStencilSize() { + return nGetStencilSize(); + } + + private static native int nGetStencilSize(); + + /////////////////////////////////////////////////////////////////////////// + // Functor + /////////////////////////////////////////////////////////////////////////// + @Override public boolean callDrawGLFunction(int drawGLFunction) { return nCallDrawGLFunction(mRenderer, drawGLFunction); @@ -272,7 +293,6 @@ class GLES20Canvas extends HardwareCanvas { private static native boolean nCallDrawGLFunction(int renderer, int drawGLFunction); - /////////////////////////////////////////////////////////////////////////// // Memory /////////////////////////////////////////////////////////////////////////// @@ -405,12 +425,18 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean clipPath(Path path) { - throw new UnsupportedOperationException(); + // TODO: Implement + path.computeBounds(mPathBounds, true); + return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top, + mPathBounds.right, mPathBounds.bottom, Region.Op.INTERSECT.nativeInt); } @Override public boolean clipPath(Path path, Region.Op op) { - throw new UnsupportedOperationException(); + // TODO: Implement + path.computeBounds(mPathBounds, true); + return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top, + mPathBounds.right, mPathBounds.bottom, op.nativeInt); } @Override @@ -458,12 +484,18 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean clipRegion(Region region) { - throw new UnsupportedOperationException(); + // TODO: Implement + region.getBounds(mClipBounds); + return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top, + mClipBounds.right, mClipBounds.bottom, Region.Op.INTERSECT.nativeInt); } @Override public boolean clipRegion(Region region, Region.Op op) { - throw new UnsupportedOperationException(); + // TODO: Implement + region.getBounds(mClipBounds); + return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top, + mClipBounds.right, mClipBounds.bottom, op.nativeInt); } @Override @@ -483,12 +515,14 @@ class GLES20Canvas extends HardwareCanvas { @Override public boolean quickReject(Path path, EdgeType type) { - throw new UnsupportedOperationException(); + path.computeBounds(mPathBounds, true); + return nQuickReject(mRenderer, mPathBounds.left, mPathBounds.top, + mPathBounds.right, mPathBounds.bottom, type.nativeInt); } @Override public boolean quickReject(RectF rect, EdgeType type) { - return quickReject(rect.left, rect.top, rect.right, rect.bottom, type); + return nQuickReject(mRenderer, rect.left, rect.top, rect.right, rect.bottom, type.nativeInt); } /////////////////////////////////////////////////////////////////////////// @@ -525,11 +559,12 @@ class GLES20Canvas extends HardwareCanvas { @Override public void setMatrix(Matrix matrix) { - nSetMatrix(mRenderer, matrix.native_instance); + nSetMatrix(mRenderer, matrix == null ? 0 : matrix.native_instance); } private static native void nSetMatrix(int renderer, int matrix); + @SuppressWarnings("deprecation") @Override public void getMatrix(Matrix matrix) { nGetMatrix(mRenderer, matrix.native_instance); @@ -642,8 +677,17 @@ class GLES20Canvas extends HardwareCanvas { @Override public void setDrawFilter(DrawFilter filter) { mFilter = filter; + if (filter == null) { + nResetPaintFilter(mRenderer); + } else if (filter instanceof PaintFlagsDrawFilter) { + PaintFlagsDrawFilter flagsFilter = (PaintFlagsDrawFilter) filter; + nSetupPaintFilter(mRenderer, flagsFilter.clearBits, flagsFilter.setBits); + } } + private static native void nResetPaintFilter(int renderer); + private static native void nSetupPaintFilter(int renderer, int clearBits, int setBits); + @Override public DrawFilter getDrawFilter() { return mFilter; @@ -892,17 +936,42 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPicture(Picture picture) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + picture.endRecording(); + // TODO: Implement rendering } @Override public void drawPicture(Picture picture, Rect dst) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + save(); + translate(dst.left, dst.top); + if (picture.getWidth() > 0 && picture.getHeight() > 0) { + scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight()); + } + drawPicture(picture); + restore(); } @Override public void drawPicture(Picture picture, RectF dst) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + save(); + translate(dst.left, dst.top); + if (picture.getWidth() > 0 && picture.getHeight() > 0) { + scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight()); + } + drawPicture(picture); + restore(); } @Override @@ -927,16 +996,42 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDrawPoints(int renderer, float[] points, int offset, int count, int paint); + @SuppressWarnings("deprecation") @Override public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { - // TODO: Implement + if (index < 0 || index + count > text.length || count * 2 > pos.length) { + throw new IndexOutOfBoundsException(); + } + + int modifiers = setupModifiers(paint); + try { + nDrawPosText(mRenderer, text, index, count, pos, paint.mNativePaint); + } finally { + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + } } + private static native void nDrawPosText(int renderer, char[] text, int index, int count, + float[] pos, int paint); + + @SuppressWarnings("deprecation") @Override public void drawPosText(String text, float[] pos, Paint paint) { - // TODO: Implement + if (text.length() * 2 > pos.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + int modifiers = setupModifiers(paint); + try { + nDrawPosText(mRenderer, text, 0, text.length(), pos, paint.mNativePaint); + } finally { + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + } } + private static native void nDrawPosText(int renderer, String text, int start, int end, + float[] pos, int paint); + @Override public void drawRect(float left, float top, float right, float bottom, Paint paint) { int modifiers = setupModifiers(paint); diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java index 4ca5e98..0cb9449 100644 --- a/core/java/android/view/GLES20DisplayList.java +++ b/core/java/android/view/GLES20DisplayList.java @@ -43,7 +43,7 @@ class GLES20DisplayList extends DisplayList { } @Override - HardwareCanvas start() { + public HardwareCanvas start() { if (mCanvas != null) { throw new IllegalStateException("Recording has already started"); } @@ -55,7 +55,7 @@ class GLES20DisplayList extends DisplayList { } @Override - void invalidate() { + public void invalidate() { if (mCanvas != null) { mCanvas.recycle(); mCanvas = null; @@ -64,12 +64,12 @@ class GLES20DisplayList extends DisplayList { } @Override - boolean isValid() { + public boolean isValid() { return mValid; } @Override - void end() { + public void end() { if (mCanvas != null) { if (mFinalizer != null) { mCanvas.end(mFinalizer.mNativeDisplayList); @@ -83,7 +83,7 @@ class GLES20DisplayList extends DisplayList { } @Override - int getSize() { + public int getSize() { if (mFinalizer == null) return 0; return GLES20Canvas.getDisplayListSize(mFinalizer.mNativeDisplayList); } diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index fd3b9e5..4f25792 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -60,6 +60,13 @@ abstract class GLES20Layer extends HardwareLayer { } mLayer = 0; } + + @Override + void flush() { + if (mLayer != 0) { + GLES20Canvas.nFlushLayer(mLayer); + } + } static class Finalizer { private int mLayerId; diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index a496a9e..25d08ac 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -193,10 +193,8 @@ public class GestureDetector { } } - // TODO: ViewConfiguration - private int mBiggerTouchSlopSquare = 20 * 20; - private int mTouchSlopSquare; + private int mDoubleTapTouchSlopSquare; private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; private int mMaximumFlingVelocity; @@ -391,10 +389,11 @@ public class GestureDetector { mIgnoreMultitouch = ignoreMultitouch; // Fallback to support pre-donuts releases - int touchSlop, doubleTapSlop; + int touchSlop, doubleTapSlop, doubleTapTouchSlop; if (context == null) { //noinspection deprecation touchSlop = ViewConfiguration.getTouchSlop(); + doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); //noinspection deprecation mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); @@ -402,11 +401,13 @@ public class GestureDetector { } else { final ViewConfiguration configuration = ViewConfiguration.get(context); touchSlop = configuration.getScaledTouchSlop(); + doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop(); doubleTapSlop = configuration.getScaledDoubleTapSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); } mTouchSlopSquare = touchSlop * touchSlop; + mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop; mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; } @@ -545,7 +546,7 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } - if (distance > mBiggerTouchSlopSquare) { + if (distance > mDoubleTapTouchSlopSquare) { mAlwaysInBiggerTapRegion = false; } } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index 23b3abc..cbdbbde 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -42,12 +42,12 @@ public abstract class HardwareCanvas extends Canvas { * * @param dirty The dirty rectangle to update, can be null. */ - abstract void onPreDraw(Rect dirty); + public abstract void onPreDraw(Rect dirty); /** * Invoked after all drawing operation have been performed. */ - abstract void onPostDraw(); + public abstract void onPostDraw(); /** * Draws the specified display list onto this canvas. @@ -61,7 +61,7 @@ public abstract class HardwareCanvas extends Canvas { * @return True if the content of the display list requires another * drawing pass (invalidate()), false otherwise */ - abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty); + public abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty); /** * Outputs the specified display list to the log. This method exists for use by diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index 28389ab..d5666f3 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -116,6 +116,11 @@ abstract class HardwareLayer { abstract void destroy(); /** + * Flush the render queue associated with this layer. + */ + abstract void flush(); + + /** * This must be invoked before drawing onto this layer. * @param currentCanvas */ diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index ccb6489..e0749de 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -238,6 +238,15 @@ public abstract class HardwareRenderer { private static native void nSetupShadersDiskCache(String cacheFile); /** + * Notifies EGL that the frame is about to be rendered. + */ + private static void beginFrame() { + nBeginFrame(); + } + + private static native void nBeginFrame(); + + /** * Interface used to receive callbacks whenever a view is drawn by * a hardware renderer instance. */ @@ -276,7 +285,7 @@ public abstract class HardwareRenderer { * * @return A new display list. */ - abstract DisplayList createDisplayList(); + public abstract DisplayList createDisplayList(); /** * Creates a new hardware layer. A hardware layer built by calling this @@ -316,14 +325,13 @@ public abstract class HardwareRenderer { * potentially lost the hardware renderer. The hardware renderer should be * reinitialized and setup when the render {@link #isRequested()} and * {@link #isEnabled()}. - * + * * @param width The width of the drawing surface. * @param height The height of the drawing surface. - * @param attachInfo The - * @param holder + * @param holder The target surface */ - void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo, - SurfaceHolder holder) throws Surface.OutOfResourcesException { + void initializeIfNeeded(int width, int height, SurfaceHolder holder) + throws Surface.OutOfResourcesException { if (isRequested()) { // We lost the gl context, so recreate it. if (!isEnabled()) { @@ -441,6 +449,8 @@ public abstract class HardwareRenderer { } boolean mDirtyRegionsEnabled; + boolean mUpdateDirtyRegions; + final boolean mVsyncDisabled; final int mGlVersion; @@ -675,6 +685,12 @@ public abstract class HardwareRenderer { initCaches(); + enableDirtyRegions(); + + return mEglContext.getGL(); + } + + private void enableDirtyRegions() { // If mDirtyRegions is set, this means we have an EGL configuration // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set if (sDirtyRegions) { @@ -690,8 +706,6 @@ public abstract class HardwareRenderer { // configuration (see RENDER_DIRTY_REGIONS) mDirtyRegionsEnabled = GLES20Canvas.isBackBufferPreserved(); } - - return mEglContext.getGL(); } abstract void initCaches(); @@ -745,6 +759,9 @@ public abstract class HardwareRenderer { if (!createSurface(holder)) { return; } + + mUpdateDirtyRegions = true; + if (mCanvas != null) { setEnabled(true); } @@ -800,6 +817,7 @@ public abstract class HardwareRenderer { } void onPreDraw(Rect dirty) { + } void onPostDraw() { @@ -824,6 +842,8 @@ public abstract class HardwareRenderer { dirty = null; } + beginFrame(); + onPreDraw(dirty); HardwareCanvas canvas = mCanvas; @@ -837,10 +857,37 @@ public abstract class HardwareRenderer { (view.mPrivateFlags & View.INVALIDATED) == View.INVALIDATED; view.mPrivateFlags &= ~View.INVALIDATED; + final long getDisplayListStartTime; + if (ViewDebug.DEBUG_LATENCY) { + getDisplayListStartTime = System.nanoTime(); + } + DisplayList displayList = view.getDisplayList(); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- getDisplayList() took " + + ((now - getDisplayListStartTime) * 0.000001f) + "ms"); + } + if (displayList != null) { - if (canvas.drawDisplayList(displayList, view.getWidth(), - view.getHeight(), mRedrawClip)) { + final long drawDisplayListStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawDisplayListStartTime = System.nanoTime(); + } + + boolean invalidateNeeded = canvas.drawDisplayList( + displayList, view.getWidth(), + view.getHeight(), mRedrawClip); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- drawDisplayList() took " + + ((now - drawDisplayListStartTime) * 0.000001f) + + "ms, invalidateNeeded=" + invalidateNeeded + "."); + } + + if (invalidateNeeded) { if (mRedrawClip.isEmpty() || view.getParent() == null) { view.invalidate(); } else { @@ -872,7 +919,19 @@ public abstract class HardwareRenderer { attachInfo.mIgnoreDirtyState = false; + final long eglSwapBuffersStartTime; + if (ViewDebug.DEBUG_LATENCY) { + eglSwapBuffersStartTime = System.nanoTime(); + } + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- eglSwapBuffers() took " + + ((now - eglSwapBuffersStartTime) * 0.000001f) + "ms"); + } + checkEglErrors(); return dirty == null; @@ -904,6 +963,10 @@ public abstract class HardwareRenderer { fallback(true); return SURFACE_STATE_ERROR; } else { + if (mUpdateDirtyRegions) { + enableDirtyRegions(); + mUpdateDirtyRegions = false; + } return SURFACE_STATE_UPDATED; } } @@ -984,7 +1047,7 @@ public abstract class HardwareRenderer { EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 0, - EGL_STENCIL_SIZE, 0, + EGL_STENCIL_SIZE, GLES20Canvas.getStencilSize(), EGL_SURFACE_TYPE, EGL_WINDOW_BIT | (dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), EGL_NONE @@ -1031,7 +1094,7 @@ public abstract class HardwareRenderer { } @Override - DisplayList createDisplayList() { + public DisplayList createDisplayList() { return new GLES20DisplayList(); } diff --git a/core/java/android/view/InputEvent.java b/core/java/android/view/InputEvent.java index 01ddcc9..5602436 100755 --- a/core/java/android/view/InputEvent.java +++ b/core/java/android/view/InputEvent.java @@ -19,6 +19,8 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; +import java.util.concurrent.atomic.AtomicInteger; + /** * Common base class for input events. */ @@ -27,8 +29,21 @@ public abstract class InputEvent implements Parcelable { protected static final int PARCEL_TOKEN_MOTION_EVENT = 1; /** @hide */ protected static final int PARCEL_TOKEN_KEY_EVENT = 2; - + + // Next sequence number. + private static final AtomicInteger mNextSeq = new AtomicInteger(); + + /** @hide */ + protected int mSeq; + + /** @hide */ + protected boolean mRecycled; + + private static final boolean TRACK_RECYCLED_LOCATION = false; + private RuntimeException mRecycledLocation; + /*package*/ InputEvent() { + mSeq = mNextSeq.getAndIncrement(); } /** @@ -82,7 +97,44 @@ public abstract class InputEvent implements Parcelable { * objects are fine. See {@link KeyEvent#recycle()} for details. * @hide */ - public abstract void recycle(); + public void recycle() { + if (TRACK_RECYCLED_LOCATION) { + if (mRecycledLocation != null) { + throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation); + } + mRecycledLocation = new RuntimeException("Last recycled here"); + } else { + if (mRecycled) { + throw new RuntimeException(toString() + " recycled twice!"); + } + mRecycled = true; + } + } + + /** + * Conditionally recycled the event if it is appropriate to do so after + * dispatching the event to an application. + * + * If the event is a {@link MotionEvent} then it is recycled. + * + * If the event is a {@link KeyEvent} then it is NOT recycled, because applications + * expect key events to be immutable so once the event has been dispatched to + * the application we can no longer recycle it. + * @hide + */ + public void recycleIfNeededAfterDispatch() { + recycle(); + } + + /** + * Reinitializes the event on reuse (after recycling). + * @hide + */ + protected void prepareForReuse() { + mRecycled = false; + mRecycledLocation = null; + mSeq = mNextSeq.getAndIncrement(); + } /** * Gets a private flag that indicates when the system has detected that this input event @@ -113,6 +165,22 @@ public abstract class InputEvent implements Parcelable { */ public abstract long getEventTimeNano(); + /** + * Gets the unique sequence number of this event. + * Every input event that is created or received by a process has a + * unique sequence number. Moreover, a new sequence number is obtained + * each time an event object is recycled. + * + * Sequence numbers are only guaranteed to be locally unique within a process. + * Sequence numbers are not preserved when events are parceled. + * + * @return The unique sequence number of this event. + * @hide + */ + public int getSequenceNumber() { + return mSeq; + } + public int describeContents() { return 0; } diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 9b081b2..fafe416 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -58,7 +58,7 @@ public final class InputEventConsistencyVerifier { // so that the verifier can detect when it has been asked to verify the same event twice. // It does not make sense to examine the contents of the last event since it may have // been recycled. - private InputEvent mLastEvent; + private int mLastEventSeq; private String mLastEventType; private int mLastNestingLevel; @@ -140,7 +140,7 @@ public final class InputEventConsistencyVerifier { * Resets the state of the input event consistency verifier. */ public void reset() { - mLastEvent = null; + mLastEventSeq = -1; mLastNestingLevel = 0; mTrackballDown = false; mTrackballUnhandled = false; @@ -573,17 +573,18 @@ public final class InputEventConsistencyVerifier { private boolean startEvent(InputEvent event, int nestingLevel, String eventType) { // Ignore the event if we already checked it at a higher nesting level. - if (event == mLastEvent && nestingLevel < mLastNestingLevel + final int seq = event.getSequenceNumber(); + if (seq == mLastEventSeq && nestingLevel < mLastNestingLevel && eventType == mLastEventType) { return false; } if (nestingLevel > 0) { - mLastEvent = event; + mLastEventSeq = seq; mLastEventType = eventType; mLastNestingLevel = nestingLevel; } else { - mLastEvent = null; + mLastEventSeq = -1; mLastEventType = null; mLastNestingLevel = 0; } diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java new file mode 100644 index 0000000..764d8dc --- /dev/null +++ b/core/java/android/view/InputEventReceiver.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2011 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 android.view; + +import dalvik.system.CloseGuard; + +import android.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +/** + * Provides a low-level mechanism for an application to receive input events. + * @hide + */ +public abstract class InputEventReceiver { + private static final String TAG = "InputEventReceiver"; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private int mReceiverPtr; + + // We keep references to the input channel and message queue objects here so that + // they are not GC'd while the native peer of the receiver is using them. + private InputChannel mInputChannel; + private MessageQueue mMessageQueue; + + // The sequence number of the event that is in progress. + private int mEventSequenceNumberInProgress = -1; + + private static native int nativeInit(InputEventReceiver receiver, + InputChannel inputChannel, MessageQueue messageQueue); + private static native void nativeDispose(int receiverPtr); + private static native void nativeFinishInputEvent(int receiverPtr, boolean handled); + + /** + * Creates an input event receiver bound to the specified input channel. + * + * @param inputChannel The input channel. + * @param looper The looper to use when invoking callbacks. + */ + public InputEventReceiver(InputChannel inputChannel, Looper looper) { + if (inputChannel == null) { + throw new IllegalArgumentException("inputChannel must not be null"); + } + if (looper == null) { + throw new IllegalArgumentException("looper must not be null"); + } + + mInputChannel = inputChannel; + mMessageQueue = looper.getQueue(); + mReceiverPtr = nativeInit(this, inputChannel, mMessageQueue); + + mCloseGuard.open("dispose"); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + /** + * Disposes the receiver. + */ + public void dispose() { + if (mCloseGuard != null) { + mCloseGuard.close(); + } + if (mReceiverPtr != 0) { + nativeDispose(mReceiverPtr); + mReceiverPtr = 0; + } + mInputChannel = null; + mMessageQueue = null; + } + + /** + * Called when an input event is received. + * The recipient should process the input event and then call {@link #finishInputEvent} + * to indicate whether the event was handled. No new input events will be received + * until {@link #finishInputEvent} is called. + * + * @param event The input event that was received. + */ + public void onInputEvent(InputEvent event) { + finishInputEvent(event, false); + } + + /** + * Finishes an input event and indicates whether it was handled. + * + * @param event The input event that was finished. + * @param handled True if the event was handled. + */ + public void finishInputEvent(InputEvent event, boolean handled) { + if (event == null) { + throw new IllegalArgumentException("event must not be null"); + } + if (mReceiverPtr == 0) { + Log.w(TAG, "Attempted to finish an input event but the input event " + + "receiver has already been disposed."); + } else { + if (event.getSequenceNumber() != mEventSequenceNumberInProgress) { + Log.w(TAG, "Attempted to finish an input event that is not in progress."); + } else { + mEventSequenceNumberInProgress = -1; + nativeFinishInputEvent(mReceiverPtr, handled); + } + } + event.recycleIfNeededAfterDispatch(); + } + + // Called from native code. + @SuppressWarnings("unused") + private void dispatchInputEvent(InputEvent event) { + mEventSequenceNumberInProgress = event.getSequenceNumber(); + onInputEvent(event); + } + + public static interface Factory { + public InputEventReceiver createInputEventReceiver( + InputChannel inputChannel, Looper looper); + } +} diff --git a/core/java/android/view/InputHandler.java b/core/java/android/view/InputHandler.java deleted file mode 100644 index 14ce14c..0000000 --- a/core/java/android/view/InputHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2010 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 android.view; - -/** - * Handles input messages that arrive on an input channel. - * @hide - */ -public interface InputHandler { - /** - * Handle a key event. - * It is the responsibility of the callee to ensure that the finished callback is - * eventually invoked when the event processing is finished and the input system - * can send the next event. - * @param event The key event data. - * @param finishedCallback The callback to invoke when event processing is finished. - */ - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback); - - /** - * Handle a motion event. - * It is the responsibility of the callee to ensure that the finished callback is - * eventually invoked when the event processing is finished and the input system - * can send the next event. - * @param event The motion event data. - * @param finishedCallback The callback to invoke when event processing is finished. - */ - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback); -} diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java index 5735b63..909a3b2 100644 --- a/core/java/android/view/InputQueue.java +++ b/core/java/android/view/InputQueue.java @@ -16,18 +16,11 @@ package android.view; -import android.os.MessageQueue; -import android.util.Slog; - /** * An input queue provides a mechanism for an application to receive incoming * input events. Currently only usable from native code. */ public final class InputQueue { - private static final String TAG = "InputQueue"; - - private static final boolean DEBUG = false; - /** * Interface to receive notification of when an InputQueue is associated * and dissociated with a thread. @@ -48,13 +41,6 @@ public final class InputQueue { final InputChannel mChannel; - private static final Object sLock = new Object(); - - private static native void nativeRegisterInputChannel(InputChannel inputChannel, - InputHandler inputHandler, MessageQueue messageQueue); - private static native void nativeUnregisterInputChannel(InputChannel inputChannel); - private static native void nativeFinished(long finishedToken, boolean handled); - /** @hide */ public InputQueue(InputChannel channel) { mChannel = channel; @@ -64,121 +50,4 @@ public final class InputQueue { public InputChannel getInputChannel() { return mChannel; } - - /** - * Registers an input channel and handler. - * @param inputChannel The input channel to register. - * @param inputHandler The input handler to input events send to the target. - * @param messageQueue The message queue on whose thread the handler should be invoked. - * @hide - */ - public static void registerInputChannel(InputChannel inputChannel, InputHandler inputHandler, - MessageQueue messageQueue) { - if (inputChannel == null) { - throw new IllegalArgumentException("inputChannel must not be null"); - } - if (inputHandler == null) { - throw new IllegalArgumentException("inputHandler must not be null"); - } - if (messageQueue == null) { - throw new IllegalArgumentException("messageQueue must not be null"); - } - - synchronized (sLock) { - if (DEBUG) { - Slog.d(TAG, "Registering input channel '" + inputChannel + "'"); - } - - nativeRegisterInputChannel(inputChannel, inputHandler, messageQueue); - } - } - - /** - * Unregisters an input channel. - * Does nothing if the channel is not currently registered. - * @param inputChannel The input channel to unregister. - * @hide - */ - public static void unregisterInputChannel(InputChannel inputChannel) { - if (inputChannel == null) { - throw new IllegalArgumentException("inputChannel must not be null"); - } - - synchronized (sLock) { - if (DEBUG) { - Slog.d(TAG, "Unregistering input channel '" + inputChannel + "'"); - } - - nativeUnregisterInputChannel(inputChannel); - } - } - - @SuppressWarnings("unused") - private static void dispatchKeyEvent(InputHandler inputHandler, - KeyEvent event, long finishedToken) { - FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken); - inputHandler.handleKey(event, finishedCallback); - } - - @SuppressWarnings("unused") - private static void dispatchMotionEvent(InputHandler inputHandler, - MotionEvent event, long finishedToken) { - FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken); - inputHandler.handleMotion(event, finishedCallback); - } - - /** - * A callback that must be invoked to when finished processing an event. - * @hide - */ - public static final class FinishedCallback { - private static final boolean DEBUG_RECYCLING = false; - - private static final int RECYCLE_MAX_COUNT = 4; - - private static FinishedCallback sRecycleHead; - private static int sRecycleCount; - - private FinishedCallback mRecycleNext; - private long mFinishedToken; - - private FinishedCallback() { - } - - public static FinishedCallback obtain(long finishedToken) { - synchronized (sLock) { - FinishedCallback callback = sRecycleHead; - if (callback != null) { - sRecycleHead = callback.mRecycleNext; - sRecycleCount -= 1; - callback.mRecycleNext = null; - } else { - callback = new FinishedCallback(); - } - callback.mFinishedToken = finishedToken; - return callback; - } - } - - public void finished(boolean handled) { - synchronized (sLock) { - if (mFinishedToken == -1) { - throw new IllegalStateException("Event finished callback already invoked."); - } - - nativeFinished(mFinishedToken, handled); - mFinishedToken = -1; - - if (sRecycleCount < RECYCLE_MAX_COUNT) { - mRecycleNext = sRecycleHead; - sRecycleHead = this; - sRecycleCount += 1; - - if (DEBUG_RECYCLING) { - Slog.d(TAG, "Recycled finished callbacks: " + sRecycleCount); - } - } - } - } - } } diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index f53e42c..104ed6a 100755 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -1225,7 +1225,6 @@ public class KeyEvent extends InputEvent implements Parcelable { private static KeyEvent gRecyclerTop; private KeyEvent mNext; - private boolean mRecycled; private int mDeviceId; private int mSource; @@ -1535,8 +1534,8 @@ public class KeyEvent extends InputEvent implements Parcelable { gRecyclerTop = ev.mNext; gRecyclerUsed -= 1; } - ev.mRecycled = false; ev.mNext = null; + ev.prepareForReuse(); return ev; } @@ -1597,11 +1596,9 @@ public class KeyEvent extends InputEvent implements Parcelable { * * @hide */ + @Override public final void recycle() { - if (mRecycled) { - throw new RuntimeException(toString() + " recycled twice!"); - } - mRecycled = true; + super.recycle(); mCharacters = null; synchronized (gRecyclerLock) { @@ -1613,6 +1610,12 @@ public class KeyEvent extends InputEvent implements Parcelable { } } + /** @hide */ + @Override + public final void recycleIfNeededAfterDispatch() { + // Do nothing. + } + /** * Create a new key event that is the same as the given one, but whose * event time and repeat count are replaced with the given value. diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 8e0ab1a..92e8f4e 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -167,7 +167,6 @@ import android.util.SparseArray; */ public final class MotionEvent extends InputEvent implements Parcelable { private static final long NS_PER_MS = 1000000; - private static final boolean TRACK_RECYCLED_LOCATION = false; /** * An invalid pointer id. @@ -1315,8 +1314,6 @@ public final class MotionEvent extends InputEvent implements Parcelable { private int mNativePtr; private MotionEvent mNext; - private RuntimeException mRecycledLocation; - private boolean mRecycled; private static native int nativeInitialize(int nativePtr, int deviceId, int source, int action, int flags, int edgeFlags, @@ -1397,9 +1394,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { gRecyclerTop = ev.mNext; gRecyclerUsed -= 1; } - ev.mRecycledLocation = null; - ev.mRecycled = false; ev.mNext = null; + ev.prepareForReuse(); return ev; } @@ -1646,20 +1642,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * Recycle the MotionEvent, to be re-used by a later caller. After calling * this function you must not ever touch the event again. */ + @Override public final void recycle() { - // Ensure recycle is only called once! - if (TRACK_RECYCLED_LOCATION) { - if (mRecycledLocation != null) { - throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation); - } - mRecycledLocation = new RuntimeException("Last recycled here"); - //Log.w("MotionEvent", "Recycling event " + this, mRecycledLocation); - } else { - if (mRecycled) { - throw new RuntimeException(toString() + " recycled twice!"); - } - mRecycled = true; - } + super.recycle(); synchronized (gRecyclerLock) { if (gRecyclerUsed < MAX_RECYCLED) { diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index dfb2c32..1c35e31 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -29,7 +29,7 @@ import android.util.PoolableManager; * to begin tracking. Put the motion events you receive into it with * {@link #addMovement(MotionEvent)}. When you want to determine the velocity call * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)} - * and {@link #getXVelocity(int)} to retrieve the velocity for each pointer id. + * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id. */ public final class VelocityTracker implements Poolable<VelocityTracker> { private static final Pool<VelocityTracker> sPool = Pools.synchronizedPool( @@ -39,6 +39,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { } public void onAcquired(VelocityTracker element) { + // Intentionally empty } public void onReleased(VelocityTracker element) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 54bb056..39f603d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -62,6 +62,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; @@ -1970,6 +1971,21 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 0x00000002; /** + * Find views that contain {@link AccessibilityNodeProvider}. Such + * a View is a root of virtual view hierarchy and may contain the searched + * text. If this flag is set Views with providers are automatically + * added and it is a responsibility of the client to call the APIs of + * the provider to determine whether the virtual tree rooted at this View + * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s + * represeting the virtual views with this text. + * + * @see #findViewsWithText(ArrayList, CharSequence, int) + * + * @hide + */ + public static final int FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS = 0x00000004; + + /** * Controls the over-scroll mode for this view. * See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)}, * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}, @@ -2610,7 +2626,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal /** * Text direction is using "first strong algorithm". The first strong directional character * determines the paragraph direction. If there is no strong directional character, the - * paragraph direction is the view's resolved ayout direction. + * paragraph direction is the view's resolved layout direction. * * @hide */ @@ -2640,6 +2656,13 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int TEXT_DIRECTION_RTL = 4; /** + * Text direction is coming from the system Locale. + * + * @hide + */ + public static final int TEXT_DIRECTION_LOCALE = 5; + + /** * Default text direction is inherited * * @hide @@ -2656,13 +2679,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"), @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"), @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"), - @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL") + @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE") }) private int mTextDirection = DEFAULT_TEXT_DIRECTION; /** * The resolved text direction. This needs resolution if the value is - * TEXT_DIRECTION_INHERIT. The resolution matches mTextDirection if that is + * TEXT_DIRECTION_INHERIT. The resolution matches mTextDirection if it is * not TEXT_DIRECTION_INHERIT, otherwise resolution proceeds up the parent * chain of the view. * @@ -2673,7 +2697,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"), @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"), @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"), - @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL") + @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE") }) private int mResolvedTextDirection = TEXT_DIRECTION_INHERIT; @@ -4082,7 +4107,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { event.setSource(this); - event.setClassName(getClass().getName()); + event.setClassName(View.class.getName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); event.setContentDescription(mContentDescription); @@ -4108,14 +4133,20 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * Note: The client is responsible for recycling the obtained instance by calling * {@link AccessibilityNodeInfo#recycle()} to minimize object creation. * </p> + * * @return A populated {@link AccessibilityNodeInfo}. * * @see AccessibilityNodeInfo */ public AccessibilityNodeInfo createAccessibilityNodeInfo() { - AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); - onInitializeAccessibilityNodeInfo(info); - return info; + AccessibilityNodeProvider provider = getAccessibilityNodeProvider(); + if (provider != null) { + return provider.createAccessibilityNodeInfo(View.NO_ID); + } else { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); + onInitializeAccessibilityNodeInfo(info); + return info; + } } /** @@ -4181,7 +4212,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } info.setPackageName(mContext.getPackageName()); - info.setClassName(getClass().getName()); + info.setClassName(View.class.getName()); info.setContentDescription(getContentDescription()); info.setEnabled(isEnabled()); @@ -4220,6 +4251,36 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + * <p> + * If this method returns an instance, this instance is responsible for managing + * {@link AccessibilityNodeInfo}s describing the virtual sub-tree rooted at this + * View including the one representing the View itself. Similarly the returned + * instance is responsible for performing accessibility actions on any virtual + * view or the root view itself. + * </p> + * <p> + * If an {@link AccessibilityDelegate} has been specified via calling + * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its + * {@link AccessibilityDelegate#getAccessibilityNodeProvider(View)} + * is responsible for handling this call. + * </p> + * + * @return The provider. + * + * @see AccessibilityNodeProvider + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (mAccessibilityDelegate != null) { + return mAccessibilityDelegate.getAccessibilityNodeProvider(this); + } else { + return null; + } + } + + /** * Gets the unique identifier of this view on the screen for accessibility purposes. * If this {@link View} is not attached to any window, {@value #NO_ID} is returned. * @@ -5238,14 +5299,18 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * * @param outViews The output list of matching Views. * @param searched The text to match against. - * + * * @see #FIND_VIEWS_WITH_TEXT * @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION * @see #setContentDescription(CharSequence) */ public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) { - if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 && !TextUtils.isEmpty(searched) - && !TextUtils.isEmpty(mContentDescription)) { + if (getAccessibilityNodeProvider() != null) { + if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) { + outViews.add(this); + } + } else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 + && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mContentDescription)) { String searchedLowerCase = searched.toString().toLowerCase(); String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase(); if (contentDescriptionLowerCase.contains(searchedLowerCase)) { @@ -6757,7 +6822,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if ((changed & VISIBILITY_MASK) != 0) { if (mParent instanceof ViewGroup) { - ((ViewGroup) mParent).onChildVisibilityChanged(this, (flags & VISIBILITY_MASK)); + ((ViewGroup) mParent).onChildVisibilityChanged(this, (changed & VISIBILITY_MASK), + (flags & VISIBILITY_MASK)); ((View) mParent).invalidate(true); } else if (mParent != null) { mParent.invalidateChild(this, null); @@ -7532,15 +7598,17 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ public void setAlpha(float alpha) { ensureTransformationInfo(); - mTransformationInfo.mAlpha = alpha; - invalidateParentCaches(); - if (onSetAlpha((int) (alpha * 255))) { - mPrivateFlags |= ALPHA_SET; - // subclass is handling alpha - don't optimize rendering cache invalidation - invalidate(true); - } else { - mPrivateFlags &= ~ALPHA_SET; - invalidate(false); + if (mTransformationInfo.mAlpha != alpha) { + mTransformationInfo.mAlpha = alpha; + invalidateParentCaches(); + if (onSetAlpha((int) (alpha * 255))) { + mPrivateFlags |= ALPHA_SET; + // subclass is handling alpha - don't optimize rendering cache invalidation + invalidate(true); + } else { + mPrivateFlags &= ~ALPHA_SET; + invalidate(false); + } } } @@ -7551,18 +7619,22 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * alpha (the return value for onSetAlpha()). * * @param alpha The new value for the alpha property - * @return true if the View subclass handles alpha (the return value for onSetAlpha()) + * @return true if the View subclass handles alpha (the return value for onSetAlpha()) and + * the new value for the alpha property is different from the old value */ boolean setAlphaNoInvalidation(float alpha) { ensureTransformationInfo(); - mTransformationInfo.mAlpha = alpha; - boolean subclassHandlesAlpha = onSetAlpha((int) (alpha * 255)); - if (subclassHandlesAlpha) { - mPrivateFlags |= ALPHA_SET; - } else { - mPrivateFlags &= ~ALPHA_SET; + if (mTransformationInfo.mAlpha != alpha) { + mTransformationInfo.mAlpha = alpha; + boolean subclassHandlesAlpha = onSetAlpha((int) (alpha * 255)); + if (subclassHandlesAlpha) { + mPrivateFlags |= ALPHA_SET; + return true; + } else { + mPrivateFlags &= ~ALPHA_SET; + } } - return subclassHandlesAlpha; + return false; } /** @@ -7925,84 +7997,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** - * @hide - */ - public void setFastTranslationX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationX = x; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastTranslationY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationY = y; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationX = x - mLeft; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationY = y - mTop; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastScaleX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mScaleX = x; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastScaleY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mScaleY = y; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastAlpha(float alpha) { - ensureTransformationInfo(); - mTransformationInfo.mAlpha = alpha; - } - - /** - * @hide - */ - public void setFastRotationY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mRotationY = y; - info.mMatrixDirty = true; - } - - /** * Hit rectangle in parent's coordinates * * @param outRect The hit rectangle of the view. @@ -8579,37 +8573,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** - * @hide - */ - public void fastInvalidate() { - if (skipInvalidate()) { - return; - } - if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) || - (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID || - (mPrivateFlags & INVALIDATED) != INVALIDATED) { - if (mParent instanceof View) { - ((View) mParent).mPrivateFlags |= INVALIDATED; - } - mPrivateFlags &= ~DRAWN; - mPrivateFlags |= DIRTY; - mPrivateFlags |= INVALIDATED; - mPrivateFlags &= ~DRAWING_CACHE_VALID; - if (mParent != null && mAttachInfo != null) { - if (mAttachInfo.mHardwareAccelerated) { - mParent.invalidateChild(this, null); - } else { - final Rect r = mAttachInfo.mTmpInvalRect; - r.set(0, 0, mRight - mLeft, mBottom - mTop); - // Don't call invalidate -- we don't want to internally scroll - // our own bounds - mParent.invalidateChild(this, r); - } - } - } - } - - /** * Used to indicate that the parent of this view should clear its caches. This functionality * is used to force the parent to rebuild its display list (when hardware-accelerated), * which is necessary when various parent-managed properties of the view change, such as @@ -10151,6 +10114,13 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal break; } } + + // Make sure the HardwareRenderer.validate() was invoked before calling this method + void flushLayer() { + if (mLayerType == LAYER_TYPE_HARDWARE && mHardwareLayer != null) { + mHardwareLayer.flush(); + } + } /** * <p>Returns a hardware layer that can be used to draw this view again @@ -10163,6 +10133,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal !mAttachInfo.mHardwareRenderer.isEnabled()) { return null; } + + if (!mAttachInfo.mHardwareRenderer.validate()) return null; final int width = mRight - mLeft; final int height = mBottom - mTop; @@ -10237,12 +10209,15 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ boolean destroyLayer() { if (mHardwareLayer != null) { - mHardwareLayer.destroy(); - mHardwareLayer = null; - - invalidate(true); - invalidateParentCaches(); + AttachInfo info = mAttachInfo; + if (info != null && info.mHardwareRenderer != null && + info.mHardwareRenderer.isEnabled() && info.mHardwareRenderer.validate()) { + mHardwareLayer.destroy(); + mHardwareLayer = null; + invalidate(true); + invalidateParentCaches(); + } return true; } return false; @@ -10356,6 +10331,19 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * @return The HardwareRenderer associated with that view or null if hardware rendering + * is not supported or this this has not been attached to a window. + * + * @hide + */ + public HardwareRenderer getHardwareRenderer() { + if (mAttachInfo != null) { + return mAttachInfo.mHardwareRenderer; + } + return null; + } + + /** * <p>Returns a display list that can be used to draw this view again * without executing its draw method.</p> * @@ -12152,13 +12140,16 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @param location an array of two integers in which to hold the coordinates */ public void getLocationInWindow(int[] location) { - // When the view is not attached to a window, this method does not make sense - if (mAttachInfo == null) return; - if (location == null || location.length < 2) { throw new IllegalArgumentException("location must be an array of two integers"); } + if (mAttachInfo == null) { + // When the view is not attached to a window, this method does not make sense + location[0] = location[1] = 0; + return; + } + float[] position = mAttachInfo.mTmpTransformLocation; position[0] = position[1] = 0.0f; @@ -13720,6 +13711,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * {@link #TEXT_DIRECTION_ANY_RTL}, * {@link #TEXT_DIRECTION_LTR}, * {@link #TEXT_DIRECTION_RTL}, + * {@link #TEXT_DIRECTION_LOCALE}, * * @hide */ @@ -13737,6 +13729,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * {@link #TEXT_DIRECTION_ANY_RTL}, * {@link #TEXT_DIRECTION_LTR}, * {@link #TEXT_DIRECTION_RTL}, + * {@link #TEXT_DIRECTION_LOCALE}, * * @hide */ @@ -13757,6 +13750,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * {@link #TEXT_DIRECTION_ANY_RTL}, * {@link #TEXT_DIRECTION_LTR}, * {@link #TEXT_DIRECTION_RTL}, + * {@link #TEXT_DIRECTION_LOCALE}, * * @hide */ @@ -13800,7 +13794,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>alpha</code> functionality handled by the * {@link View#setAlpha(float)} and {@link View#getAlpha()} methods. */ - public static Property<View, Float> ALPHA = new FloatProperty<View>("alpha") { + public static final Property<View, Float> ALPHA = new FloatProperty<View>("alpha") { @Override public void setValue(View object, float value) { object.setAlpha(value); @@ -13816,7 +13810,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>translationX</code> functionality handled by the * {@link View#setTranslationX(float)} and {@link View#getTranslationX()} methods. */ - public static Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") { + public static final Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") { @Override public void setValue(View object, float value) { object.setTranslationX(value); @@ -13832,7 +13826,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>translationY</code> functionality handled by the * {@link View#setTranslationY(float)} and {@link View#getTranslationY()} methods. */ - public static Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") { + public static final Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") { @Override public void setValue(View object, float value) { object.setTranslationY(value); @@ -13848,7 +13842,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>x</code> functionality handled by the * {@link View#setX(float)} and {@link View#getX()} methods. */ - public static Property<View, Float> X = new FloatProperty<View>("x") { + public static final Property<View, Float> X = new FloatProperty<View>("x") { @Override public void setValue(View object, float value) { object.setX(value); @@ -13864,7 +13858,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>y</code> functionality handled by the * {@link View#setY(float)} and {@link View#getY()} methods. */ - public static Property<View, Float> Y = new FloatProperty<View>("y") { + public static final Property<View, Float> Y = new FloatProperty<View>("y") { @Override public void setValue(View object, float value) { object.setY(value); @@ -13880,7 +13874,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>rotation</code> functionality handled by the * {@link View#setRotation(float)} and {@link View#getRotation()} methods. */ - public static Property<View, Float> ROTATION = new FloatProperty<View>("rotation") { + public static final Property<View, Float> ROTATION = new FloatProperty<View>("rotation") { @Override public void setValue(View object, float value) { object.setRotation(value); @@ -13896,7 +13890,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>rotationX</code> functionality handled by the * {@link View#setRotationX(float)} and {@link View#getRotationX()} methods. */ - public static Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") { + public static final Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") { @Override public void setValue(View object, float value) { object.setRotationX(value); @@ -13912,7 +13906,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>rotationY</code> functionality handled by the * {@link View#setRotationY(float)} and {@link View#getRotationY()} methods. */ - public static Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") { + public static final Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") { @Override public void setValue(View object, float value) { object.setRotationY(value); @@ -13928,7 +13922,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>scaleX</code> functionality handled by the * {@link View#setScaleX(float)} and {@link View#getScaleX()} methods. */ - public static Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") { + public static final Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") { @Override public void setValue(View object, float value) { object.setScaleX(value); @@ -13944,7 +13938,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * A Property wrapper around the <code>scaleY</code> functionality handled by the * {@link View#setScaleY(float)} and {@link View#getScaleY()} methods. */ - public static Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") { + public static final Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") { @Override public void setValue(View object, float value) { object.setScaleY(value); @@ -14995,5 +14989,23 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal AccessibilityEvent event) { return host.onRequestSendAccessibilityEventInternal(child, event); } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + * <p> + * The default implementation behaves as + * {@link View#getAccessibilityNodeProvider() View#getAccessibilityNodeProvider()} for + * the case of no accessibility delegate been set. + * </p> + * + * @return The provider. + * + * @see AccessibilityNodeProvider + */ + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + return null; + } } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 9bd42ef..b455ad5 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -49,7 +49,7 @@ public class ViewConfiguration { /** * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in - * pixels + * dips */ private static final int SCROLL_BAR_SIZE = 10; @@ -64,7 +64,7 @@ public class ViewConfiguration { private static final int SCROLL_BAR_DEFAULT_DELAY = 300; /** - * Defines the length of the fading edges in pixels + * Defines the length of the fading edges in dips */ private static final int FADING_EDGE_LENGTH = 12; @@ -134,7 +134,7 @@ public class ViewConfiguration { private static final int ZOOM_CONTROLS_TIMEOUT = 3000; /** - * Inset in pixels to look for touchable content when the user touches the edge of the screen + * Inset in dips to look for touchable content when the user touches the edge of the screen */ private static final int EDGE_SLOP = 12; @@ -152,6 +152,12 @@ public class ViewConfiguration { private static final int TOUCH_SLOP = 8; /** + * Distance the first touch can wander before we stop considering this event a double tap + * (in dips) + */ + private static final int DOUBLE_TAP_TOUCH_SLOP = TOUCH_SLOP; + + /** * Distance a touch can wander before we think the user is attempting a paged scroll * (in dips) * @@ -166,28 +172,28 @@ public class ViewConfiguration { private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2; /** - * Distance between the first touch and second touch to still be considered a double tap + * Distance in dips between the first touch and second touch to still be considered a double tap */ private static final int DOUBLE_TAP_SLOP = 100; /** - * Distance a touch needs to be outside of a window's bounds for it to + * Distance in dips a touch needs to be outside of a window's bounds for it to * count as outside for purposes of dismissing the window. */ private static final int WINDOW_TOUCH_SLOP = 16; /** - * Minimum velocity to initiate a fling, as measured in pixels per second + * Minimum velocity to initiate a fling, as measured in dips per second */ private static final int MINIMUM_FLING_VELOCITY = 50; /** - * Maximum velocity to initiate a fling, as measured in pixels per second + * Maximum velocity to initiate a fling, as measured in dips per second */ private static final int MAXIMUM_FLING_VELOCITY = 8000; /** - * Distance between a touch up event denoting the end of a touch exploration + * Distance in dips between a touch up event denoting the end of a touch exploration * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. */ @@ -214,12 +220,12 @@ public class ViewConfiguration { private static final float SCROLL_FRICTION = 0.015f; /** - * Max distance to overscroll for edge effects + * Max distance in dips to overscroll for edge effects */ private static final int OVERSCROLL_DISTANCE = 0; /** - * Max distance to overfling for edge effects + * Max distance in dips to overfling for edge effects */ private static final int OVERFLING_DISTANCE = 6; @@ -229,6 +235,7 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; + private final int mDoubleTapTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; private final int mScaledTouchExplorationTapSlop; @@ -255,6 +262,7 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; + mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; @@ -318,6 +326,8 @@ public class ViewConfiguration { mTouchSlop = res.getDimensionPixelSize( com.android.internal.R.dimen.config_viewConfigurationTouchSlop); mPagingTouchSlop = mTouchSlop * 2; + + mDoubleTapTouchSlop = mTouchSlop; } /** @@ -342,7 +352,7 @@ public class ViewConfiguration { /** * @return The width of the horizontal scrollbar and the height of the vertical - * scrollbar in pixels + * scrollbar in dips * * @deprecated Use {@link #getScaledScrollBarSize()} instead. */ @@ -374,7 +384,7 @@ public class ViewConfiguration { } /** - * @return the length of the fading edges in pixels + * @return the length of the fading edges in dips * * @deprecated Use {@link #getScaledFadingEdgeLength()} instead. */ @@ -469,7 +479,7 @@ public class ViewConfiguration { } /** - * @return Inset in pixels to look for touchable content when the user touches the edge of the + * @return Inset in dips to look for touchable content when the user touches the edge of the * screen * * @deprecated Use {@link #getScaledEdgeSlop()} instead. @@ -488,7 +498,7 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is scrolling in pixels + * @return Distance in dips a touch can wander before we think the user is scrolling * * @deprecated Use {@link #getScaledTouchSlop()} instead. */ @@ -498,22 +508,31 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is scrolling in pixels + * @return Distance in pixels a touch can wander before we think the user is scrolling */ public int getScaledTouchSlop() { return mTouchSlop; } /** - * @return Distance a touch can wander before we think the user is scrolling a full page - * in dips + * @return Distance in pixels the first touch can wander before we do not consider this a + * potential double tap event + * @hide + */ + public int getScaledDoubleTapTouchSlop() { + return mDoubleTapTouchSlop; + } + + /** + * @return Distance in pixels a touch can wander before we think the user is scrolling a full + * page */ public int getScaledPagingTouchSlop() { return mPagingTouchSlop; } /** - * @return Distance between the first touch and second touch to still be + * @return Distance in dips between the first touch and second touch to still be * considered a double tap * @deprecated Use {@link #getScaledDoubleTapSlop()} instead. * @hide The only client of this should be GestureDetector, which needs this @@ -525,7 +544,7 @@ public class ViewConfiguration { } /** - * @return Distance between the first touch and second touch to still be + * @return Distance in pixels between the first touch and second touch to still be * considered a double tap */ public int getScaledDoubleTapSlop() { @@ -533,7 +552,7 @@ public class ViewConfiguration { } /** - * @return Distance between a touch up event denoting the end of a touch exploration + * @return Distance in pixels between a touch up event denoting the end of a touch exploration * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. * @@ -557,7 +576,7 @@ public class ViewConfiguration { } /** - * @return Distance a touch must be outside the bounds of a window for it + * @return Distance in dips a touch must be outside the bounds of a window for it * to be counted as outside the window for purposes of dismissing that * window. * @@ -569,16 +588,15 @@ public class ViewConfiguration { } /** - * @return Distance a touch must be outside the bounds of a window for it - * to be counted as outside the window for purposes of dismissing that - * window. + * @return Distance in pixels a touch must be outside the bounds of a window for it + * to be counted as outside the window for purposes of dismissing that window. */ public int getScaledWindowTouchSlop() { return mWindowTouchSlop; } /** - * @return Minimum velocity to initiate a fling, as measured in pixels per second. + * @return Minimum velocity to initiate a fling, as measured in dips per second. * * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead. */ @@ -595,7 +613,7 @@ public class ViewConfiguration { } /** - * @return Maximum velocity to initiate a fling, as measured in pixels per second. + * @return Maximum velocity to initiate a fling, as measured in dips per second. * * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead. */ @@ -634,14 +652,16 @@ public class ViewConfiguration { } /** - * @return The maximum distance a View should overscroll by when showing edge effects. + * @return The maximum distance a View should overscroll by when showing edge effects (in + * pixels). */ public int getScaledOverscrollDistance() { return mOverscrollDistance; } /** - * @return The maximum distance a View should overfling by when showing edge effects. + * @return The maximum distance a View should overfling by when showing edge effects (in + * pixels). */ public int getScaledOverflingDistance() { return mOverflingDistance; diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 65e72c9..c1db572 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -127,16 +127,19 @@ public class ViewDebug { * Logs the relative difference between the time an event was created and the time it * was delivered. * - * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers(). - * This is time that the event loop spends blocked and unresponsive. Ideally, drawing - * and animations should be perfectly synchronized with VSYNC so that swap buffers - * is instantaneous. + * Logs the time spent waiting for Surface.lockCanvas(), Surface.unlockCanvasAndPost() + * or eglSwapBuffers(). This is time that the event loop spends blocked and unresponsive. + * Ideally, drawing and animations should be perfectly synchronized with VSYNC so that + * dequeuing and queueing buffers is instantaneous. * - * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw(). + * Logs the time spent in ViewRoot.performTraversals() and ViewRoot.performDraw(). * @hide */ public static final boolean DEBUG_LATENCY = false; + /** @hide */ + public static final String DEBUG_LATENCY_TAG = "ViewLatency"; + /** * <p>Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link false} is set diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index ee66c4d..d906a16 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -888,18 +888,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Called when a view's visibility has changed. Notify the parent to take any appropriate + * action. + * + * @param child The view whose visibility has changed + * @param oldVisibility The previous visibility value (GONE, INVISIBLE, or VISIBLE). + * @param newVisibility The new visibility value (GONE, INVISIBLE, or VISIBLE). * @hide - * @param child - * @param visibility */ - protected void onChildVisibilityChanged(View child, int visibility) { + protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) { if (mTransition != null) { - if (visibility == VISIBLE) { - mTransition.showChild(this, child); + if (newVisibility == VISIBLE) { + mTransition.showChild(this, child, oldVisibility); } else { - mTransition.hideChild(this, child); - } - if (visibility != VISIBLE) { + mTransition.hideChild(this, child, newVisibility); // Only track this on disappearing views - appearing views are already visible // and don't need special handling during drawChild() if (mVisibilityChangingChildren == null) { @@ -914,7 +916,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // in all cases, for drags if (mCurrentDrag != null) { - if (visibility == VISIBLE) { + if (newVisibility == VISIBLE) { notifyChildOfDrag(child); } } @@ -2229,6 +2231,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); + info.setClassName(ViewGroup.class.getName()); for (int i = 0, count = mChildrenCount; i < count; i++) { View child = mChildren[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE @@ -2238,6 +2241,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + @Override + void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); + event.setClassName(ViewGroup.class.getName()); + } + /** * {@inheritDoc} */ @@ -2654,8 +2663,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager child.onAnimationStart(); } - more = a.getTransformation(drawingTime, mChildTransformation, - scalingRequired ? mAttachInfo.mApplicationScale : 1f); + more = a.getTransformation(drawingTime, mChildTransformation, 1f); if (scalingRequired && mAttachInfo.mApplicationScale != 1f) { if (mInvalidationTransformation == null) { mInvalidationTransformation = new Transformation(); @@ -2952,6 +2960,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mDrawLayers = enabled; invalidate(true); + boolean flushLayers = !enabled; + AttachInfo info = mAttachInfo; + if (info != null && info.mHardwareRenderer != null && + info.mHardwareRenderer.isEnabled()) { + if (!info.mHardwareRenderer.validate()) { + flushLayers = false; + } + } else { + flushLayers = false; + } + // We need to invalidate any child with a layer. For instance, // if a child is backed by a hardware layer and we disable layers // the child is marked as not dirty (flags cleared the last time @@ -2962,6 +2981,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < mChildrenCount; i++) { View child = mChildren[i]; if (child.mLayerType != LAYER_TYPE_NONE) { + if (flushLayers) child.flushLayer(); child.invalidate(true); } } @@ -3044,8 +3064,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * {@inheritDoc} + * Sets <code>t</code> to be the static transformation of the child, if set, returning a + * boolean to indicate whether a static transform was set. The default implementation + * simply returns <code>false</code>; subclasses may override this method for different + * behavior. * + * @param child The child view whose static transform is being requested + * @param t The Transformation which will hold the result + * @return true if the transformation was set, false otherwise * @see #setStaticTransformationsEnabled(boolean) */ protected boolean getChildStaticTransformation(View child, Transformation t) { @@ -3943,8 +3969,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } while (parent != null); } else { // Check whether the child that requests the invalidate is fully opaque + // Views being animated or transformed are not considered opaque because we may + // be invalidating their old position and need the parent to paint behind them. + Matrix childMatrix = child.getMatrix(); final boolean isOpaque = child.isOpaque() && !drawAnimation && - child.getAnimation() == null; + child.getAnimation() == null && childMatrix.isIdentity(); // Mark the child as dirty, using the appropriate flag // Make sure we do not set both flags at the same time int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY; @@ -3958,7 +3987,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int[] location = attachInfo.mInvalidateChildLocation; location[CHILD_LEFT_INDEX] = child.mLeft; location[CHILD_TOP_INDEX] = child.mTop; - Matrix childMatrix = child.getMatrix(); if (!childMatrix.isIdentity()) { RectF boundingRect = attachInfo.mTmpTransformRect; boundingRect.set(dirty); diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 84dc7d8..89a1ef2 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -837,6 +837,11 @@ public class ViewPropertyAnimator { */ @Override public void onAnimationUpdate(ValueAnimator animation) { + PropertyBundle propertyBundle = mAnimatorMap.get(animation); + if (propertyBundle == null) { + // Shouldn't happen, but just to play it safe + return; + } // alpha requires slightly different treatment than the other (transform) properties. // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation // logic is dependent on how the view handles an internal call to onSetAlpha(). @@ -845,7 +850,6 @@ public class ViewPropertyAnimator { boolean alphaHandled = false; mView.invalidateParentCaches(); float fraction = animation.getAnimatedFraction(); - PropertyBundle propertyBundle = mAnimatorMap.get(animation); int propertyMask = propertyBundle.mPropertyMask; if ((propertyMask & TRANSFORM_MASK) != 0) { mView.invalidate(false); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 6c982eb..1a4bdf4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -57,7 +57,6 @@ import android.util.Poolable; import android.util.PoolableManager; import android.util.Pools; import android.util.Slog; -import android.util.SparseArray; import android.util.TypedValue; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; @@ -65,6 +64,7 @@ import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; @@ -95,7 +95,8 @@ import java.util.List; */ @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) public final class ViewRootImpl extends Handler implements ViewParent, - View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { + View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks, + Choreographer.OnDrawListener { private static final String TAG = "ViewRootImpl"; private static final boolean DBG = false; private static final boolean LOCAL_LOGV = false; @@ -109,7 +110,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; private static final boolean DEBUG_FPS = false; - private static final boolean WATCH_POINTER = false; /** * Set this system property to true to force the view hierarchy to render @@ -153,9 +153,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, final TypedValue mTmpValue = new TypedValue(); final InputMethodCallback mInputMethodCallback; - final SparseArray<Object> mPendingEvents = new SparseArray<Object>(); - int mPendingEventSeq = 0; - final Thread mThread; final WindowLeaked mLocation; @@ -203,13 +200,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, InputQueue.Callback mInputQueueCallback; InputQueue mInputQueue; FallbackEventHandler mFallbackEventHandler; + Choreographer mChoreographer; final Rect mTempRect; // used in the transaction to not thrash the heap. final Rect mVisRect; // used to retrieve visible rect of focused view. boolean mTraversalScheduled; long mLastTraversalFinishedTimeNanos; - long mLastDrawDurationNanos; + long mLastDrawFinishedTimeNanos; boolean mWillDrawSoon; boolean mLayoutRequested; boolean mFirst; @@ -218,7 +216,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, boolean mNewSurfaceNeeded; boolean mHasHadWindowFocus; boolean mLastWasImTarget; - InputEventMessage mPendingInputEvents = null; + + // Pool of queued input events. + private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10; + private QueuedInputEvent mQueuedInputEventPool; + private int mQueuedInputEventPoolSize; + + // Input event queue. + QueuedInputEvent mFirstPendingInputEvent; + QueuedInputEvent mCurrentInputEvent; + boolean mProcessInputEventsScheduled; boolean mWindowAttributesChanged = false; int mWindowAttributesChangesFlag = 0; @@ -367,6 +374,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context); mProfileRendering = Boolean.parseBoolean( SystemProperties.get(PROPERTY_PROFILE_RENDERING, "false")); + mChoreographer = Choreographer.getInstance(); } public static void addFirstDrawHandler(Runnable callback) { @@ -418,6 +426,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { + mChoreographer.addOnDrawListener(this); + mView = view; mFallbackEventHandler.setView(view); mWindowAttributes.copyFrom(attrs); @@ -551,8 +561,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputQueue = new InputQueue(mInputChannel); mInputQueueCallback.onInputQueueCreated(mInputQueue); } else { - InputQueue.registerInputChannel(mInputChannel, mInputHandler, - Looper.myQueue()); + mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, + Looper.myLooper()); } } @@ -787,23 +797,19 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; - - //noinspection ConstantConditions - if (ViewDebug.DEBUG_LATENCY && mLastTraversalFinishedTimeNanos != 0) { - final long now = System.nanoTime(); - Log.d(TAG, "Latency: Scheduled traversal, it has been " - + ((now - mLastTraversalFinishedTimeNanos) * 0.000001f) - + "ms since the last traversal finished."); - } - - sendEmptyMessage(DO_TRAVERSAL); + mChoreographer.scheduleDraw(); } } public void unscheduleTraversals() { + mTraversalScheduled = false; + } + + @Override + public void onDraw() { if (mTraversalScheduled) { mTraversalScheduled = false; - removeMessages(DO_TRAVERSAL); + doTraversal(); } } @@ -840,24 +846,45 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - private void processInputEvents(boolean outOfOrder) { - while (mPendingInputEvents != null) { - handleMessage(mPendingInputEvents.mMessage); - InputEventMessage tmpMessage = mPendingInputEvents; - mPendingInputEvents = mPendingInputEvents.mNext; - tmpMessage.recycle(); - if (outOfOrder) { - removeMessages(PROCESS_INPUT_EVENTS); + private void doTraversal() { + doProcessInputEvents(); + + if (mProfile) { + Debug.startMethodTracing("ViewAncestor"); + } + + final long traversalStartTime; + if (ViewDebug.DEBUG_LATENCY) { + traversalStartTime = System.nanoTime(); + if (mLastTraversalFinishedTimeNanos != 0) { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals(); it has been " + + ((traversalStartTime - mLastTraversalFinishedTimeNanos) * 0.000001f) + + "ms since the last traversals finished."); + } else { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals()."); } } + + performTraversals(); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took " + + ((now - traversalStartTime) * 0.000001f) + + "ms."); + mLastTraversalFinishedTimeNanos = now; + } + + if (mProfile) { + Debug.stopMethodTracing(); + mProfile = false; + } } private void performTraversals() { // cache mView since it is used so much below... final View host = mView; - processInputEvents(true); - if (DBG) { System.out.println("======================================"); System.out.println("performTraversals"); @@ -867,10 +894,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (host == null || !mAdded) return; - mTraversalScheduled = false; mWillDrawSoon = true; boolean windowSizeMayChange = false; - boolean fullRedrawNeeded = mFullRedrawNeeded; boolean newSurface = false; boolean surfaceChanged = false; WindowManager.LayoutParams lp = mWindowAttributes; @@ -895,7 +920,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, CompatibilityInfo compatibilityInfo = mCompatibilityInfo.get(); if (compatibilityInfo.supportsScreen() == mLastInCompatMode) { params = lp; - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; if (mLastInCompatMode) { params.flags &= ~WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW; @@ -910,7 +935,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, Rect frame = mWinFrame; if (mFirst) { - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { @@ -954,7 +979,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { if (DEBUG_ORIENTATION) Log.v(TAG, "View " + host + " resized to: " + frame); - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mLayoutRequested = true; windowSizeMayChange = true; } @@ -1292,7 +1317,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // before actually drawing them, so it can display then // all at once. newSurface = true; - fullRedrawNeeded = true; + mFullRedrawNeeded = true; mPreviousTransparentRegion.setEmpty(); if (mAttachInfo.mHardwareRenderer != null) { @@ -1328,7 +1353,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } else if (surfaceGenerationId != mSurface.getGenerationId() && mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) { - fullRedrawNeeded = true; + mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.updateSurface(mHolder); } catch (Surface.OutOfResourcesException e) { @@ -1614,6 +1639,11 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + // Remember if we must report the next draw. + if ((relayoutResult & WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) { + mReportNextDraw = true; + } + boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw() || viewVisibility != View.VISIBLE; @@ -1624,42 +1654,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } mPendingTransitions.clear(); } - mFullRedrawNeeded = false; - - final long drawStartTime; - if (ViewDebug.DEBUG_LATENCY) { - drawStartTime = System.nanoTime(); - } - - draw(fullRedrawNeeded); - - if (ViewDebug.DEBUG_LATENCY) { - mLastDrawDurationNanos = System.nanoTime() - drawStartTime; - } - if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0 - || mReportNextDraw) { - if (LOCAL_LOGV) { - Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); - } - mReportNextDraw = false; - if (mSurfaceHolder != null && mSurface.isValid()) { - mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); - SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); - if (callbacks != null) { - for (SurfaceHolder.Callback c : callbacks) { - if (c instanceof SurfaceHolder.Callback2) { - ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( - mSurfaceHolder); - } - } - } - } - try { - sWindowSession.finishDrawing(mWindow); - } catch (RemoteException e) { - } - } + performDraw(); } else { // End any pending transitions on this non-visible window if (mPendingTransitions != null && mPendingTransitions.size() > 0) { @@ -1668,14 +1664,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, } mPendingTransitions.clear(); } - // We were supposed to report when we are done drawing. Since we canceled the - // draw, remember it here. - if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) { - mReportNextDraw = true; - } - if (fullRedrawNeeded) { - mFullRedrawNeeded = true; - } if (viewVisibility == View.VISIBLE) { // Try again @@ -1819,6 +1807,56 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + private void performDraw() { + final long drawStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawStartTime = System.nanoTime(); + if (mLastDrawFinishedTimeNanos != 0) { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw(); it has been " + + ((drawStartTime - mLastDrawFinishedTimeNanos) * 0.000001f) + + "ms since the last draw finished."); + } else { + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw()."); + } + } + + final boolean fullRedrawNeeded = mFullRedrawNeeded; + mFullRedrawNeeded = false; + draw(fullRedrawNeeded); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performDraw() took " + + ((now - drawStartTime) * 0.000001f) + + "ms."); + mLastDrawFinishedTimeNanos = now; + } + + if (mReportNextDraw) { + mReportNextDraw = false; + + if (LOCAL_LOGV) { + Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); + } + if (mSurfaceHolder != null && mSurface.isValid()) { + mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); + SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + if (c instanceof SurfaceHolder.Callback2) { + ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( + mSurfaceHolder); + } + } + } + } + try { + sWindowSession.finishDrawing(mWindow); + } catch (RemoteException e) { + } + } + } + private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; if (surface == null || !surface.isValid()) { @@ -1857,8 +1895,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, mCurScrollY = yoff; fullRedrawNeeded = true; } - float appScale = mAttachInfo.mApplicationScale; - boolean scalingRequired = mAttachInfo.mScalingRequired; + + final float appScale = mAttachInfo.mApplicationScale; + final boolean scalingRequired = mAttachInfo.mScalingRequired; int resizeAlpha = 0; if (mResizeBuffer != null) { @@ -1873,7 +1912,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - Rect dirty = mDirty; + final Rect dirty = mDirty; if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); @@ -1891,35 +1930,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } - if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - if (!dirty.isEmpty() || mIsAnimating) { - mIsAnimating = false; - mHardwareYOffset = yoff; - mResizeAlpha = resizeAlpha; - - mCurrentDirty.set(dirty); - mCurrentDirty.union(mPreviousDirty); - mPreviousDirty.set(dirty); - dirty.setEmpty(); - - Rect currentDirty = mCurrentDirty; - if (animating) { - currentDirty = null; - } - - if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this, currentDirty)) { - mPreviousDirty.set(0, 0, mWidth, mHeight); - } - } - - if (animating) { - mFullRedrawNeeded = true; - scheduleTraversals(); - } - - return; - } - if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v(TAG, "Draw " + mView + "/" + mWindowAttributes.getTitle() @@ -1930,64 +1940,79 @@ public final class ViewRootImpl extends Handler implements ViewParent, } if (!dirty.isEmpty() || mIsAnimating) { - Canvas canvas; - try { - int left = dirty.left; - int top = dirty.top; - int right = dirty.right; - int bottom = dirty.bottom; - - final long lockCanvasStartTime; - if (ViewDebug.DEBUG_LATENCY) { - lockCanvasStartTime = System.nanoTime(); - } + if (mAttachInfo.mHardwareRenderer != null + && mAttachInfo.mHardwareRenderer.isEnabled()) { + // Draw with hardware renderer. + mIsAnimating = false; + mHardwareYOffset = yoff; + mResizeAlpha = resizeAlpha; - canvas = surface.lockCanvas(dirty); + mCurrentDirty.set(dirty); + mCurrentDirty.union(mPreviousDirty); + mPreviousDirty.set(dirty); + dirty.setEmpty(); - if (ViewDebug.DEBUG_LATENCY) { - long now = System.nanoTime(); - Log.d(TAG, "Latency: Spent " - + ((now - lockCanvasStartTime) * 0.000001f) - + "ms waiting for surface.lockCanvas()"); + if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this, + animating ? null : mCurrentDirty)) { + mPreviousDirty.set(0, 0, mWidth, mHeight); } + } else { + // Draw with software renderer. + Canvas canvas; + try { + int left = dirty.left; + int top = dirty.top; + int right = dirty.right; + int bottom = dirty.bottom; + + final long lockCanvasStartTime; + if (ViewDebug.DEBUG_LATENCY) { + lockCanvasStartTime = System.nanoTime(); + } - if (left != dirty.left || top != dirty.top || right != dirty.right || - bottom != dirty.bottom) { - mAttachInfo.mIgnoreDirtyState = true; - } + canvas = mSurface.lockCanvas(dirty); - // TODO: Do this in native - canvas.setDensity(mDensity); - } catch (Surface.OutOfResourcesException e) { - Log.e(TAG, "OutOfResourcesException locking surface", e); - try { - if (!sWindowSession.outOfMemory(mWindow)) { - Slog.w(TAG, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- lockCanvas() took " + + ((now - lockCanvasStartTime) * 0.000001f) + "ms"); } - } catch (RemoteException ex) { - } - mLayoutRequested = true; // ask wm for a new surface next time. - return; - } catch (IllegalArgumentException e) { - Log.e(TAG, "IllegalArgumentException locking surface", e); - // Don't assume this is due to out of memory, it could be - // something else, and if it is something else then we could - // kill stuff (or ourself) for no reason. - mLayoutRequested = true; // ask wm for a new surface next time. - return; - } - try { - if (!dirty.isEmpty() || mIsAnimating) { - long startTime = 0L; + if (left != dirty.left || top != dirty.top || right != dirty.right || + bottom != dirty.bottom) { + mAttachInfo.mIgnoreDirtyState = true; + } + + // TODO: Do this in native + canvas.setDensity(mDensity); + } catch (Surface.OutOfResourcesException e) { + Log.e(TAG, "OutOfResourcesException locking surface", e); + try { + if (!sWindowSession.outOfMemory(mWindow)) { + Slog.w(TAG, "No processes killed for memory; killing self"); + Process.killProcess(Process.myPid()); + } + } catch (RemoteException ex) { + } + mLayoutRequested = true; // ask wm for a new surface next time. + return; + } catch (IllegalArgumentException e) { + Log.e(TAG, "IllegalArgumentException locking surface", e); + // Don't assume this is due to out of memory, it could be + // something else, and if it is something else then we could + // kill stuff (or ourself) for no reason. + mLayoutRequested = true; // ask wm for a new surface next time. + return; + } + try { if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" + canvas.getWidth() + ", h=" + canvas.getHeight()); //canvas.drawARGB(255, 255, 0, 0); } + long startTime = 0L; if (ViewDebug.DEBUG_PROFILE_DRAWING) { startTime = SystemClock.elapsedRealtime(); } @@ -2023,7 +2048,19 @@ public final class ViewRootImpl extends Handler implements ViewParent, canvas.setScreenDensity(scalingRequired ? DisplayMetrics.DENSITY_DEVICE : 0); mAttachInfo.mSetIgnoreDirtyState = false; + + final long drawStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawStartTime = System.nanoTime(); + } + mView.draw(canvas); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- draw() took " + + ((now - drawStartTime) * 0.000001f) + "ms"); + } } finally { if (!mAttachInfo.mSetIgnoreDirtyState) { // Only clear the flag if it was not set during the mView.draw() call @@ -2038,15 +2075,25 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (ViewDebug.DEBUG_PROFILE_DRAWING) { EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); } - } + } finally { + final long unlockCanvasAndPostStartTime; + if (ViewDebug.DEBUG_LATENCY) { + unlockCanvasAndPostStartTime = System.nanoTime(); + } - } finally { - surface.unlockCanvasAndPost(canvas); - } - } + surface.unlockCanvasAndPost(canvas); - if (LOCAL_LOGV) { - Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- unlockCanvasAndPost() took " + + ((now - unlockCanvasAndPostStartTime) * 0.000001f) + "ms"); + } + + if (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } + } + } } if (animating) { @@ -2265,8 +2312,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputQueueCallback.onInputQueueDestroyed(mInputQueue); mInputQueueCallback = null; mInputQueue = null; - } else if (mInputChannel != null) { - InputQueue.unregisterInputChannel(mInputChannel); + } else if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; } try { sWindowSession.remove(mWindow); @@ -2279,6 +2327,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, mInputChannel.dispose(); mInputChannel = null; } + + mChoreographer.removeOnDrawListener(this); } void updateConfiguration(Configuration config, boolean force) { @@ -2333,17 +2383,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public final static int DO_TRAVERSAL = 1000; public final static int DIE = 1001; public final static int RESIZED = 1002; public final static int RESIZED_REPORT = 1003; public final static int WINDOW_FOCUS_CHANGED = 1004; public final static int DISPATCH_KEY = 1005; - public final static int DISPATCH_POINTER = 1006; - public final static int DISPATCH_TRACKBALL = 1007; public final static int DISPATCH_APP_VISIBILITY = 1008; public final static int DISPATCH_GET_NEW_SURFACE = 1009; - public final static int FINISHED_EVENT = 1010; + public final static int IME_FINISHED_EVENT = 1010; public final static int DISPATCH_KEY_FROM_IME = 1011; public final static int FINISH_INPUT_CONNECTION = 1012; public final static int CHECK_FOCUS = 1013; @@ -2356,14 +2403,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, public final static int DO_PERFORM_ACCESSIBILITY_ACTION = 1020; public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 1021; public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 1022; - public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1023; - public final static int PROCESS_INPUT_EVENTS = 1024; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 1023; + public final static int DO_PROCESS_INPUT_EVENTS = 1024; @Override public String getMessageName(Message message) { switch (message.what) { - case DO_TRAVERSAL: - return "DO_TRAVERSAL"; case DIE: return "DIE"; case RESIZED: @@ -2374,16 +2419,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, return "WINDOW_FOCUS_CHANGED"; case DISPATCH_KEY: return "DISPATCH_KEY"; - case DISPATCH_POINTER: - return "DISPATCH_POINTER"; - case DISPATCH_TRACKBALL: - return "DISPATCH_TRACKBALL"; case DISPATCH_APP_VISIBILITY: return "DISPATCH_APP_VISIBILITY"; case DISPATCH_GET_NEW_SURFACE: return "DISPATCH_GET_NEW_SURFACE"; - case FINISHED_EVENT: - return "FINISHED_EVENT"; + case IME_FINISHED_EVENT: + return "IME_FINISHED_EVENT"; case DISPATCH_KEY_FROM_IME: return "DISPATCH_KEY_FROM_IME"; case FINISH_INPUT_CONNECTION: @@ -2408,11 +2449,10 @@ public final class ViewRootImpl extends Handler implements ViewParent, return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; - case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: - return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT"; - case PROCESS_INPUT_EVENTS: - return "PROCESS_INPUT_EVENTS"; - + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: + return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; + case DO_PROCESS_INPUT_EVENTS: + return "DO_PROCESS_INPUT_EVENTS"; } return super.getMessageName(message); } @@ -2428,51 +2468,12 @@ public final class ViewRootImpl extends Handler implements ViewParent, info.target.invalidate(info.left, info.top, info.right, info.bottom); info.release(); break; - case DO_TRAVERSAL: - if (mProfile) { - Debug.startMethodTracing("ViewAncestor"); - } - - final long traversalStartTime; - if (ViewDebug.DEBUG_LATENCY) { - traversalStartTime = System.nanoTime(); - mLastDrawDurationNanos = 0; - } - - performTraversals(); - - if (ViewDebug.DEBUG_LATENCY) { - long now = System.nanoTime(); - Log.d(TAG, "Latency: Spent " - + ((now - traversalStartTime) * 0.000001f) - + "ms in performTraversals(), with " - + (mLastDrawDurationNanos * 0.000001f) - + "ms of that time in draw()"); - mLastTraversalFinishedTimeNanos = now; - } - - if (mProfile) { - Debug.stopMethodTracing(); - mProfile = false; - } - break; - case FINISHED_EVENT: - handleFinishedEvent(msg.arg1, msg.arg2 != 0); - break; - case DISPATCH_KEY: - deliverKeyEvent((KeyEvent)msg.obj, msg.arg1 != 0); - break; - case DISPATCH_POINTER: - deliverPointerEvent((MotionEvent) msg.obj, msg.arg1 != 0); + case IME_FINISHED_EVENT: + handleImeFinishedEvent(msg.arg1, msg.arg2 != 0); break; - case DISPATCH_TRACKBALL: - deliverTrackballEvent((MotionEvent) msg.obj, msg.arg1 != 0); - break; - case DISPATCH_GENERIC_MOTION: - deliverGenericMotionEvent((MotionEvent) msg.obj, msg.arg1 != 0); - break; - case PROCESS_INPUT_EVENTS: - processInputEvents(false); + case DO_PROCESS_INPUT_EVENTS: + mProcessInputEventsScheduled = false; + doProcessInputEvents(); break; case DISPATCH_APP_VISIBILITY: handleAppVisibility(msg.arg1 != 0); @@ -2528,7 +2529,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, - mAttachInfo, mHolder); + mHolder); } catch (Surface.OutOfResourcesException e) { Log.e(TAG, "OutOfResourcesException locking surface", e); try { @@ -2583,6 +2584,10 @@ public final class ViewRootImpl extends Handler implements ViewParent, case DIE: doDie(); break; + case DISPATCH_KEY: { + KeyEvent event = (KeyEvent)msg.obj; + enqueueInputEvent(event, null, 0); + } break; case DISPATCH_KEY_FROM_IME: { if (LOCAL_LOGV) Log.v( TAG, "Dispatching key " @@ -2594,7 +2599,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, //noinspection UnusedAssignment event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); } - deliverKeyEventPostIme((KeyEvent)msg.obj, false); + enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME); } break; case FINISH_INPUT_CONNECTION: { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -2647,79 +2652,15 @@ public final class ViewRootImpl extends Handler implements ViewParent, .findAccessibilityNodeInfoByViewIdUiThread(msg); } } break; - case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: { + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { if (mView != null) { getAccessibilityInteractionController() - .findAccessibilityNodeInfosByViewTextUiThread(msg); + .findAccessibilityNodeInfosByTextUiThread(msg); } } break; } } - private void startInputEvent(InputQueue.FinishedCallback finishedCallback) { - if (mFinishedCallback != null) { - Slog.w(TAG, "Received a new input event from the input queue but there is " - + "already an unfinished input event in progress."); - } - - if (ViewDebug.DEBUG_LATENCY) { - mInputEventReceiveTimeNanos = System.nanoTime(); - mInputEventDeliverTimeNanos = 0; - mInputEventDeliverPostImeTimeNanos = 0; - } - - mFinishedCallback = finishedCallback; - } - - private void finishInputEvent(InputEvent event, boolean handled) { - if (LOCAL_LOGV) Log.v(TAG, "Telling window manager input event is finished"); - - if (mFinishedCallback == null) { - Slog.w(TAG, "Attempted to tell the input queue that the current input event " - + "is finished but there is no input event actually in progress."); - return; - } - - if (ViewDebug.DEBUG_LATENCY) { - final long now = System.nanoTime(); - final long eventTime = event.getEventTimeNano(); - final StringBuilder msg = new StringBuilder(); - msg.append("Latency: Spent "); - msg.append((now - mInputEventReceiveTimeNanos) * 0.000001f); - msg.append("ms processing "); - if (event instanceof KeyEvent) { - final KeyEvent keyEvent = (KeyEvent)event; - msg.append("key event, action="); - msg.append(KeyEvent.actionToString(keyEvent.getAction())); - } else { - final MotionEvent motionEvent = (MotionEvent)event; - msg.append("motion event, action="); - msg.append(MotionEvent.actionToString(motionEvent.getAction())); - msg.append(", historySize="); - msg.append(motionEvent.getHistorySize()); - } - msg.append(", handled="); - msg.append(handled); - msg.append(", received at +"); - msg.append((mInputEventReceiveTimeNanos - eventTime) * 0.000001f); - if (mInputEventDeliverTimeNanos != 0) { - msg.append("ms, delivered at +"); - msg.append((mInputEventDeliverTimeNanos - eventTime) * 0.000001f); - } - if (mInputEventDeliverPostImeTimeNanos != 0) { - msg.append("ms, delivered post IME at +"); - msg.append((mInputEventDeliverPostImeTimeNanos - eventTime) * 0.000001f); - } - msg.append("ms, finished at +"); - msg.append((now - eventTime) * 0.000001f); - msg.append("ms."); - Log.d(TAG, msg.toString()); - } - - mFinishedCallback.finished(handled); - mFinishedCallback = null; - } - /** * Something in the current window tells us we need to change the touch mode. For * example, we are not in touch mode, and the user touches the screen. @@ -2841,11 +2782,27 @@ public final class ViewRootImpl extends Handler implements ViewParent, return false; } - private void deliverPointerEvent(MotionEvent event, boolean sendDone) { + private void deliverInputEvent(QueuedInputEvent q) { if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); + q.mDeliverTimeNanos = System.nanoTime(); } + if (q.mEvent instanceof KeyEvent) { + deliverKeyEvent(q); + } else { + final int source = q.mEvent.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + deliverPointerEvent(q); + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + deliverTrackballEvent(q); + } else { + deliverGenericMotionEvent(q); + } + } + } + + private void deliverPointerEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; final boolean isTouchEvent = event.isTouchEvent(); if (mInputEventConsistencyVerifier != null) { if (isTouchEvent) { @@ -2857,7 +2814,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -2892,41 +2849,23 @@ public final class ViewRootImpl extends Handler implements ViewParent, lt.sample("B Dispatched PointerEvents ", System.nanoTime() - event.getEventTimeNano()); } if (handled) { - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); return; } // Pointer event was unhandled. - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); } - private void finishMotionEvent(MotionEvent event, boolean sendDone, boolean handled) { - event.recycle(); - if (sendDone) { - finishInputEvent(event, handled); - } - //noinspection ConstantConditions - if (LOCAL_LOGV || WATCH_POINTER) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - Log.i(TAG, "Done dispatching!"); - } - } - } - - private void deliverTrackballEvent(MotionEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - - if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); - + private void deliverTrackballEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTrackballEvent(event, 0); } // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -2938,7 +2877,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // touch mode here. ensureTouchMode(false); - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); mLastTrackballTime = Integer.MIN_VALUE; return; } @@ -2962,18 +2901,18 @@ public final class ViewRootImpl extends Handler implements ViewParent, case MotionEvent.ACTION_DOWN: x.reset(2); y.reset(2); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); break; case MotionEvent.ACTION_UP: x.reset(2); y.reset(2); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); break; } @@ -3024,38 +2963,35 @@ public final class ViewRootImpl extends Handler implements ViewParent, + keycode); movement--; int repeatCount = accelMovement - movement; - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); + InputDevice.SOURCE_KEYBOARD)); } while (movement > 0) { if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: " + keycode); movement--; curTime = SystemClock.uptimeMillis(); - deliverKeyEvent(new KeyEvent(curTime, curTime, + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_DOWN, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); - deliverKeyEvent(new KeyEvent(curTime, curTime, + InputDevice.SOURCE_KEYBOARD)); + dispatchKey(new KeyEvent(curTime, curTime, KeyEvent.ACTION_UP, keycode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK, - InputDevice.SOURCE_KEYBOARD), false); - } + InputDevice.SOURCE_KEYBOARD)); + } mLastTrackballTime = curTime; } // Unfortunately we can't tell whether the application consumed the keys, so // we always consider the trackball event handled. - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); } - private void deliverGenericMotionEvent(MotionEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - + private void deliverGenericMotionEvent(QueuedInputEvent q) { + final MotionEvent event = (MotionEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); } @@ -3068,7 +3004,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (isJoystick) { updateJoystickDirection(event, false); } - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); return; } @@ -3077,16 +3013,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (isJoystick) { updateJoystickDirection(event, false); } - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); return; } if (isJoystick) { // Translate the joystick event into DPAD keys and try to deliver those. updateJoystickDirection(event, true); - finishMotionEvent(event, sendDone, true); + finishInputEvent(q, true); } else { - finishMotionEvent(event, sendDone, false); + finishInputEvent(q, false); } } @@ -3108,9 +3044,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (xDirection != mLastJoystickXDirection) { if (mLastJoystickXKeyCode != 0) { - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastJoystickXKeyCode = 0; } @@ -3119,17 +3055,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (xDirection != 0 && synthesizeNewKeys) { mLastJoystickXKeyCode = xDirection > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); } } if (yDirection != mLastJoystickYDirection) { if (mLastJoystickYKeyCode != 0) { - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_UP, mLastJoystickYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); mLastJoystickYKeyCode = 0; } @@ -3138,9 +3074,9 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (yDirection != 0 && synthesizeNewKeys) { mLastJoystickYKeyCode = yDirection > 0 ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; - deliverKeyEvent(new KeyEvent(time, time, + dispatchKey(new KeyEvent(time, time, KeyEvent.ACTION_DOWN, mLastJoystickYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false); + deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); } } } @@ -3231,91 +3167,81 @@ public final class ViewRootImpl extends Handler implements ViewParent, return false; } - int enqueuePendingEvent(Object event, boolean sendDone) { - int seq = mPendingEventSeq+1; - if (seq < 0) seq = 0; - mPendingEventSeq = seq; - mPendingEvents.put(seq, event); - return sendDone ? seq : -seq; - } - - Object retrievePendingEvent(int seq) { - if (seq < 0) seq = -seq; - Object event = mPendingEvents.get(seq); - if (event != null) { - mPendingEvents.remove(seq); - } - return event; - } - - private void deliverKeyEvent(KeyEvent event, boolean sendDone) { - if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverTimeNanos = System.nanoTime(); - } - + private void deliverKeyEvent(QueuedInputEvent q) { + final KeyEvent event = (KeyEvent)q.mEvent; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 0); } - // If there is no view, then the event will not be handled. - if (mView == null || !mAdded) { - finishKeyEvent(event, sendDone, false); - return; - } - - if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView); + if ((q.mFlags & QueuedInputEvent.FLAG_DELIVER_POST_IME) == 0) { + // If there is no view, then the event will not be handled. + if (mView == null || !mAdded) { + finishInputEvent(q, false); + return; + } - // Perform predispatching before the IME. - if (mView.dispatchKeyEventPreIme(event)) { - finishKeyEvent(event, sendDone, true); - return; - } + if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView); - // Dispatch to the IME before propagating down the view hierarchy. - // The IME will eventually call back into handleFinishedEvent. - if (mLastWasImTarget) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - int seq = enqueuePendingEvent(event, sendDone); - if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" - + seq + " event=" + event); - imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback); + // Perform predispatching before the IME. + if (mView.dispatchKeyEventPreIme(event)) { + finishInputEvent(q, true); return; } + + // Dispatch to the IME before propagating down the view hierarchy. + // The IME will eventually call back into handleImeFinishedEvent. + if (mLastWasImTarget) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + final int seq = event.getSequenceNumber(); + if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" + + seq + " event=" + event); + imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback); + return; + } + } } // Not dispatching to IME, continue with post IME actions. - deliverKeyEventPostIme(event, sendDone); + deliverKeyEventPostIme(q); } - private void handleFinishedEvent(int seq, boolean handled) { - final KeyEvent event = (KeyEvent)retrievePendingEvent(seq); - if (DEBUG_IMF) Log.v(TAG, "IME finished event: seq=" + seq - + " handled=" + handled + " event=" + event); - if (event != null) { - final boolean sendDone = seq >= 0; + void handleImeFinishedEvent(int seq, boolean handled) { + final QueuedInputEvent q = mCurrentInputEvent; + if (q != null && q.mEvent.getSequenceNumber() == seq) { + final KeyEvent event = (KeyEvent)q.mEvent; + if (DEBUG_IMF) { + Log.v(TAG, "IME finished event: seq=" + seq + + " handled=" + handled + " event=" + event); + } if (handled) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); } else { - deliverKeyEventPostIme(event, sendDone); + deliverKeyEventPostIme(q); + } + } else { + if (DEBUG_IMF) { + Log.v(TAG, "IME finished event: seq=" + seq + + " handled=" + handled + ", event not found!"); } } } - private void deliverKeyEventPostIme(KeyEvent event, boolean sendDone) { + private void deliverKeyEventPostIme(QueuedInputEvent q) { + final KeyEvent event = (KeyEvent)q.mEvent; if (ViewDebug.DEBUG_LATENCY) { - mInputEventDeliverPostImeTimeNanos = System.nanoTime(); + q.mDeliverPostImeTimeNanos = System.nanoTime(); } // If the view went away, then the event will not be handled. if (mView == null || !mAdded) { - finishKeyEvent(event, sendDone, false); + finishInputEvent(q, false); return; } // If the key's purpose is to exit touch mode then we consume it and consider it handled. if (checkForLeavingTouchModeAndConsume(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3325,7 +3251,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3335,14 +3261,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, && event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(event.getKeyCode())) { if (mView.dispatchKeyShortcutEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } // Apply the fallback event policy. if (mFallbackEventHandler.dispatchKeyEvent(event)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } @@ -3397,14 +3323,14 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (v.requestFocus(direction, mTempRect)) { playSoundEffect( SoundEffectConstants.getContantForFocusDirection(direction)); - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } // Give the focused view a last chance to handle the dpad key. if (mView.dispatchUnhandledMove(focused, direction)) { - finishKeyEvent(event, sendDone, true); + finishInputEvent(q, true); return; } } @@ -3412,13 +3338,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, } // Key was unhandled. - finishKeyEvent(event, sendDone, false); - } - - private void finishKeyEvent(KeyEvent event, boolean sendDone, boolean handled) { - if (sendDone) { - finishInputEvent(event, handled); - } + finishInputEvent(q, false); } /* drag/drop */ @@ -3743,8 +3663,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void dispatchFinishedEvent(int seq, boolean handled) { - Message msg = obtainMessage(FINISHED_EVENT); + void dispatchImeFinishedEvent(int seq, boolean handled) { + Message msg = obtainMessage(IME_FINISHED_EVENT); msg.arg1 = seq; msg.arg2 = handled ? 1 : 0; sendMessage(msg); @@ -3773,152 +3693,182 @@ public final class ViewRootImpl extends Handler implements ViewParent, sendMessage(msg); } - private long mInputEventReceiveTimeNanos; - private long mInputEventDeliverTimeNanos; - private long mInputEventDeliverPostImeTimeNanos; - private InputQueue.FinishedCallback mFinishedCallback; - - private final InputHandler mInputHandler = new InputHandler() { - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); - dispatchKey(event, true); - } - - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); - dispatchMotion(event, true); - } - }; - /** - * Utility class used to queue up input events which are then handled during - * performTraversals(). Doing it this way allows us to ensure that we are up to date with - * all input events just prior to drawing, instead of placing those events on the regular - * handler queue, potentially behind a drawing event. + * Represents a pending input event that is waiting in a queue. + * + * Input events are processed in serial order by the timestamp specified by + * {@link InputEvent#getEventTime()}. In general, the input dispatcher delivers + * one input event to the application at a time and waits for the application + * to finish handling it before delivering the next one. + * + * However, because the application or IME can synthesize and inject multiple + * key events at a time without going through the input dispatcher, we end up + * needing a queue on the application's side. */ - static class InputEventMessage { - Message mMessage; - InputEventMessage mNext; + private static final class QueuedInputEvent { + public static final int FLAG_DELIVER_POST_IME = 1 << 0; - private static final Object sPoolSync = new Object(); - private static InputEventMessage sPool; - private static int sPoolSize = 0; + public QueuedInputEvent mNext; - private static final int MAX_POOL_SIZE = 10; + public InputEvent mEvent; + public InputEventReceiver mReceiver; + public int mFlags; - private InputEventMessage(Message m) { - mMessage = m; - mNext = null; - } + // Used for latency calculations. + public long mReceiveTimeNanos; + public long mDeliverTimeNanos; + public long mDeliverPostImeTimeNanos; + } - /** - * Return a new Message instance from the global pool. Allows us to - * avoid allocating new objects in many cases. - */ - public static InputEventMessage obtain(Message msg) { - synchronized (sPoolSync) { - if (sPool != null) { - InputEventMessage m = sPool; - sPool = m.mNext; - m.mNext = null; - sPoolSize--; - m.mMessage = msg; - return m; - } - } - return new InputEventMessage(msg); + private QueuedInputEvent obtainQueuedInputEvent(InputEvent event, + InputEventReceiver receiver, int flags) { + QueuedInputEvent q = mQueuedInputEventPool; + if (q != null) { + mQueuedInputEventPoolSize -= 1; + mQueuedInputEventPool = q.mNext; + q.mNext = null; + } else { + q = new QueuedInputEvent(); } - /** - * Return the message to the pool. - */ - public void recycle() { - mMessage.recycle(); - synchronized (sPoolSync) { - if (sPoolSize < MAX_POOL_SIZE) { - mNext = sPool; - sPool = this; - sPoolSize++; - } - } + q.mEvent = event; + q.mReceiver = receiver; + q.mFlags = flags; + return q; + } + private void recycleQueuedInputEvent(QueuedInputEvent q) { + q.mEvent = null; + q.mReceiver = null; + + if (mQueuedInputEventPoolSize < MAX_QUEUED_INPUT_EVENT_POOL_SIZE) { + mQueuedInputEventPoolSize += 1; + q.mNext = mQueuedInputEventPool; + mQueuedInputEventPool = q; } } - /** - * Place the input event message at the end of the current pending list - */ - private void enqueueInputEvent(Message msg, long when) { - InputEventMessage inputMessage = InputEventMessage.obtain(msg); - if (mPendingInputEvents == null) { - mPendingInputEvents = inputMessage; + void enqueueInputEvent(InputEvent event, + InputEventReceiver receiver, int flags) { + QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); + + if (ViewDebug.DEBUG_LATENCY) { + q.mReceiveTimeNanos = System.nanoTime(); + q.mDeliverTimeNanos = 0; + q.mDeliverPostImeTimeNanos = 0; + } + + // Always enqueue the input event in order, regardless of its time stamp. + // We do this because the application or the IME may inject key events + // in response to touch events and we want to ensure that the injected keys + // are processed in the order they were received and we cannot trust that + // the time stamp of injected events are monotonic. + QueuedInputEvent last = mFirstPendingInputEvent; + if (last == null) { + mFirstPendingInputEvent = q; } else { - InputEventMessage currMessage = mPendingInputEvents; - while (currMessage.mNext != null) { - currMessage = currMessage.mNext; + while (last.mNext != null) { + last = last.mNext; } - currMessage.mNext = inputMessage; + last.mNext = q; } - sendEmptyMessageAtTime(PROCESS_INPUT_EVENTS, when); + + scheduleProcessInputEvents(); } - public void dispatchKey(KeyEvent event) { - dispatchKey(event, false); + private void scheduleProcessInputEvents() { + if (!mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = true; + sendEmptyMessage(DO_PROCESS_INPUT_EVENTS); + } } - private void dispatchKey(KeyEvent event, boolean sendDone) { - //noinspection ConstantConditions - if (false && event.getAction() == KeyEvent.ACTION_DOWN) { - if (event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) { - if (DBG) Log.d("keydisp", "==================================================="); - if (DBG) Log.d("keydisp", "Focused view Hierarchy is:"); + private void doProcessInputEvents() { + while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) { + QueuedInputEvent q = mFirstPendingInputEvent; + mFirstPendingInputEvent = q.mNext; + q.mNext = null; + mCurrentInputEvent = q; + deliverInputEvent(q); + } + + // We are done processing all input events that we can process right now + // so we can clear the pending flag immediately. + if (mProcessInputEventsScheduled) { + mProcessInputEventsScheduled = false; + removeMessages(DO_PROCESS_INPUT_EVENTS); + } + } - debug(); + private void finishInputEvent(QueuedInputEvent q, boolean handled) { + if (q != mCurrentInputEvent) { + throw new IllegalStateException("finished input event out of order"); + } - if (DBG) Log.d("keydisp", "==================================================="); + if (ViewDebug.DEBUG_LATENCY) { + final long now = System.nanoTime(); + final long eventTime = q.mEvent.getEventTimeNano(); + final StringBuilder msg = new StringBuilder(); + msg.append("Spent "); + msg.append((now - q.mReceiveTimeNanos) * 0.000001f); + msg.append("ms processing "); + if (q.mEvent instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)q.mEvent; + msg.append("key event, action="); + msg.append(KeyEvent.actionToString(keyEvent.getAction())); + } else { + final MotionEvent motionEvent = (MotionEvent)q.mEvent; + msg.append("motion event, action="); + msg.append(MotionEvent.actionToString(motionEvent.getAction())); + msg.append(", historySize="); + msg.append(motionEvent.getHistorySize()); } + msg.append(", handled="); + msg.append(handled); + msg.append(", received at +"); + msg.append((q.mReceiveTimeNanos - eventTime) * 0.000001f); + if (q.mDeliverTimeNanos != 0) { + msg.append("ms, delivered at +"); + msg.append((q.mDeliverTimeNanos - eventTime) * 0.000001f); + } + if (q.mDeliverPostImeTimeNanos != 0) { + msg.append("ms, delivered post IME at +"); + msg.append((q.mDeliverPostImeTimeNanos - eventTime) * 0.000001f); + } + msg.append("ms, finished at +"); + msg.append((now - eventTime) * 0.000001f); + msg.append("ms."); + Log.d(ViewDebug.DEBUG_LATENCY_TAG, msg.toString()); } - Message msg = obtainMessage(DISPATCH_KEY); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; + if (q.mReceiver != null) { + q.mReceiver.finishInputEvent(q.mEvent, handled); + } else { + q.mEvent.recycleIfNeededAfterDispatch(); + } - if (LOCAL_LOGV) Log.v( - TAG, "sending key " + event + " to " + mView); + recycleQueuedInputEvent(q); - enqueueInputEvent(msg, event.getEventTime()); - } - - private void dispatchMotion(MotionEvent event, boolean sendDone) { - int source = event.getSource(); - if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - dispatchPointer(event, sendDone); - } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { - dispatchTrackball(event, sendDone); - } else { - dispatchGenericMotion(event, sendDone); + mCurrentInputEvent = null; + if (mFirstPendingInputEvent != null) { + scheduleProcessInputEvents(); } } - private void dispatchPointer(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_POINTER); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); - } + final class WindowInputEventReceiver extends InputEventReceiver { + public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } - private void dispatchTrackball(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_TRACKBALL); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); + @Override + public void onInputEvent(InputEvent event) { + enqueueInputEvent(event, this, 0); + } } + WindowInputEventReceiver mInputEventReceiver; - private void dispatchGenericMotion(MotionEvent event, boolean sendDone) { - Message msg = obtainMessage(DISPATCH_GENERIC_MOTION); - msg.obj = event; - msg.arg1 = sendDone ? 1 : 0; - enqueueInputEvent(msg, event.getEventTime()); + public void dispatchKey(KeyEvent event) { + enqueueInputEvent(event, null, 0); } public void dispatchAppVisibility(boolean visible) { @@ -4100,7 +4050,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void finishedEvent(int seq, boolean handled) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { - viewAncestor.dispatchFinishedEvent(seq, handled); + viewAncestor.dispatchImeFinishedEvent(seq, handled); } } @@ -4575,52 +4525,52 @@ public final class ViewRootImpl extends Handler implements ViewParent, */ static final class AccessibilityInteractionConnection extends IAccessibilityInteractionConnection.Stub { - private final WeakReference<ViewRootImpl> mRootImpl; + private final WeakReference<ViewRootImpl> mViewRootImpl; - AccessibilityInteractionConnection(ViewRootImpl viewAncestor) { - mRootImpl = new WeakReference<ViewRootImpl>(viewAncestor); + AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) { + mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl); } - public void findAccessibilityNodeInfoByAccessibilityId(int accessibilityId, + public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityId, + .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, interactionId, callback, interrogatingPid, interrogatingTid); } } - public void performAccessibilityAction(int accessibilityId, int action, + public void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .performAccessibilityActionClientThread(accessibilityId, action, interactionId, - callback, interogatingPid, interrogatingTid); + .performAccessibilityActionClientThread(accessibilityNodeId, action, + interactionId, callback, interogatingPid, interrogatingTid); } } public void findAccessibilityNodeInfoByViewId(int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback, interrogatingPid, interrogatingTid); } } - public void findAccessibilityNodeInfosByViewText(String text, int accessibilityId, + public void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { - ViewRootImpl viewRootImpl = mRootImpl.get(); - if (viewRootImpl != null) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfosByViewTextClientThread(text, accessibilityId, + .findAccessibilityNodeInfosByTextClientThread(text, accessibilityNodeId, interactionId, callback, interrogatingPid, interrogatingTid); } } @@ -4693,14 +4643,18 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void findAccessibilityNodeInfoByAccessibilityIdClientThread(int accessibilityId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + public void findAccessibilityNodeInfoByAccessibilityIdClientThread( + long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + long interrogatingTid) { Message message = Message.obtain(); message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; - message.arg1 = accessibilityId; - message.arg2 = interactionId; - message.obj = callback; + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; // If the interrogation is performed by the same thread as the main UI // thread in this process, set the message as a static reference so // after this call completes the same thread but in the interrogating @@ -4708,23 +4662,31 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { - final int accessibilityId = message.arg1; - final int interactionId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) message.obj; - + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); AccessibilityNodeInfo info = null; try { - View target = findViewByAccessibilityId(accessibilityId); - if (target != null) { - info = target.createAccessibilityNodeInfo(); + View target = findViewByAccessibilityId(accessibilityViewId); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + info = provider.createAccessibilityNodeInfo(virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + info = target.createAccessibilityNodeInfo(); + } } } finally { try { @@ -4750,7 +4712,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } @@ -4778,16 +4741,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void findAccessibilityNodeInfosByViewTextClientThread(String text, - int accessibilityViewId, int interactionId, + public void findAccessibilityNodeInfosByTextClientThread(String text, + long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { Message message = Message.obtain(); - message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT; + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; SomeArgs args = mPool.acquire(); args.arg1 = text; - args.argi1 = accessibilityViewId; - args.argi2 = interactionId; + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; args.arg2 = callback; message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -4797,53 +4761,64 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interrogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } - public void findAccessibilityNodeInfosByViewTextUiThread(Message message) { + public void findAccessibilityNodeInfosByTextUiThread(Message message) { SomeArgs args = (SomeArgs) message.obj; final String text = (String) args.arg1; final int accessibilityViewId = args.argi1; - final int interactionId = args.argi2; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg2; mPool.release(args); - List<AccessibilityNodeInfo> infos = null; try { - ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; - foundViews.clear(); - - View root = null; + View target = null; if (accessibilityViewId != View.NO_ID) { - root = findViewByAccessibilityId(accessibilityViewId); + target = findViewByAccessibilityId(accessibilityViewId); } else { - root = ViewRootImpl.this.mView; - } - - if (root == null || root.getVisibility() != View.VISIBLE) { - return; - } - - root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT - | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION); - if (foundViews.isEmpty()) { - return; + target = ViewRootImpl.this.mView; } - - infos = mTempAccessibilityNodeInfoList; - infos.clear(); - - final int viewCount = foundViews.size(); - for (int i = 0; i < viewCount; i++) { - View foundView = foundViews.get(i); - if (foundView.getVisibility() == View.VISIBLE) { - infos.add(foundView.createAccessibilityNodeInfo()); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + infos = provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; + foundViews.clear(); + target.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT + | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION + | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); + if (!foundViews.isEmpty()) { + infos = mTempAccessibilityNodeInfoList; + infos.clear(); + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + if (foundView.getVisibility() == View.VISIBLE) { + provider = foundView.getAccessibilityNodeProvider(); + if (provider != null) { + List<AccessibilityNodeInfo> infosFromProvider = + provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + if (infosFromProvider != null) { + infos.addAll(infosFromProvider); + } + } else { + infos.add(foundView.createAccessibilityNodeInfo()); + } + } + } + } } - } + } } finally { try { callback.setFindAccessibilityNodeInfosResult(infos, interactionId); @@ -4853,15 +4828,16 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void performAccessibilityActionClientThread(int accessibilityId, int action, + public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interogatingPid, long interrogatingTid) { Message message = Message.obtain(); message.what = DO_PERFORM_ACCESSIBILITY_ACTION; + message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + message.arg2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); SomeArgs args = mPool.acquire(); - args.argi1 = accessibilityId; - args.argi2 = action; - args.argi3 = interactionId; + args.argi1 = action; + args.argi2 = interactionId; args.arg1 = callback; message.obj = args; // If the interrogation is performed by the same thread as the main UI @@ -4871,36 +4847,60 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (interogatingPid == Process.myPid() && interrogatingTid == Looper.getMainLooper().getThread().getId()) { message.setTarget(ViewRootImpl.this); - AccessibilityInteractionClient.getInstance().setSameThreadMessage(message); + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); } else { sendMessage(message); } } public void perfromAccessibilityActionUiThread(Message message) { + final int accessibilityViewId = message.arg1; + final int virtualDescendantId = message.arg2; SomeArgs args = (SomeArgs) message.obj; - final int accessibilityId = args.argi1; - final int action = args.argi2; - final int interactionId = args.argi3; + final int action = args.argi1; + final int interactionId = args.argi2; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; mPool.release(args); - boolean succeeded = false; try { - switch (action) { - case AccessibilityNodeInfo.ACTION_FOCUS: { - succeeded = performActionFocus(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { - succeeded = performActionClearFocus(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_SELECT: { - succeeded = performActionSelect(accessibilityId); - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { - succeeded = performActionClearSelection(accessibilityId); - } break; + View target = findViewByAccessibilityId(accessibilityViewId); + if (target != null && target.getVisibility() == View.VISIBLE) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + succeeded = provider.performAccessibilityAction(action, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (!target.hasFocus()) { + // Get out of touch mode since accessibility + // wants to move focus around. + ensureTouchMode(false); + succeeded = target.requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (target.hasFocus()) { + target.clearFocus(); + succeeded = !target.isFocused(); + } + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + if (!target.isSelected()) { + target.setSelected(true); + succeeded = target.isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + if (target.isSelected()) { + target.setSelected(false); + succeeded = !target.isSelected(); + } + } break; + } + } } } finally { try { @@ -4911,52 +4911,6 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - private boolean performActionFocus(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - // Get out of touch mode since accessibility wants to move focus around. - ensureTouchMode(false); - return target.requestFocus(); - } - - private boolean performActionClearFocus(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (!target.isFocused()) { - return false; - } - target.clearFocus(); - return !target.isFocused(); - } - - private boolean performActionSelect(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (target.isSelected()) { - return false; - } - target.setSelected(true); - return target.isSelected(); - } - - private boolean performActionClearSelection(int accessibilityId) { - View target = findViewByAccessibilityId(accessibilityId); - if (target == null || target.getVisibility() != View.VISIBLE) { - return false; - } - if (!target.isSelected()) { - return false; - } - target.setSelected(false); - return !target.isSelected(); - } - private View findViewByAccessibilityId(int accessibilityId) { View root = ViewRootImpl.this.mView; if (root == null) { diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index db87175..7fd3389 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -185,7 +185,8 @@ public final class ViewTreeObserver { mTouchableInsets = TOUCHABLE_INSETS_FRAME; } - @Override public boolean equals(Object o) { + @Override + public boolean equals(Object o) { try { if (o == null) { return false; @@ -288,6 +289,14 @@ public final class ViewTreeObserver { } } + if (observer.mOnScrollChangedListeners != null) { + if (mOnScrollChangedListeners != null) { + mOnScrollChangedListeners.addAll(observer.mOnScrollChangedListeners); + } else { + mOnScrollChangedListeners = observer.mOnScrollChangedListeners; + } + } + observer.kill(); } @@ -349,10 +358,26 @@ public final class ViewTreeObserver { * @param victim The callback to remove * * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @deprecated Use #removeOnGlobalLayoutListener instead * * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener) */ + @Deprecated public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) { + removeOnGlobalLayoutListener(victim); + } + + /** + * Remove a previously installed global layout callback + * + * @param victim The callback to remove + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener) + */ + public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) { checkIsAlive(); if (mOnGlobalLayoutListeners == null) { return; diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 48fe0df..24a3066 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -400,7 +400,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { - if (mActiveStreamType == -1) { + if (mActiveStreamType != streamType) { reorderSliders(streamType); } onShowVolumeChanged(streamType, flags); diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index cbaa3df..6ec2e8d 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -353,7 +353,8 @@ public interface WindowManagerPolicy { * Add a fake window to the window manager. This window sits * at the top of the other windows and consumes events. */ - public FakeWindow addFakeWindow(Looper looper, InputHandler inputHandler, + public FakeWindow addFakeWindow(Looper looper, + InputEventReceiver.Factory inputEventReceiverFactory, String name, int windowType, int layoutParamsFlags, boolean canReceiveKeys, boolean hasFocus, boolean touchFullscreen); } diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java index b46028e..c28b220 100755 --- a/core/java/android/view/WindowOrientationListener.java +++ b/core/java/android/view/WindowOrientationListener.java @@ -21,6 +21,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.util.FloatMath; import android.util.Log; import android.util.Slog; @@ -48,6 +49,8 @@ public abstract class WindowOrientationListener { private static final boolean DEBUG = false; private static final boolean localLOGV = DEBUG || false; + private static final boolean USE_GRAVITY_SENSOR = false; + private SensorManager mSensorManager; private boolean mEnabled; private int mRate; @@ -79,7 +82,8 @@ public abstract class WindowOrientationListener { private WindowOrientationListener(Context context, int rate) { mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); mRate = rate; - mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mSensor = mSensorManager.getDefaultSensor(USE_GRAVITY_SENSOR + ? Sensor.TYPE_GRAVITY : Sensor.TYPE_ACCELEROMETER); if (mSensor != null) { // Create listener only if sensors do exist mSensorEventListener = new SensorEventListenerImpl(this); @@ -179,7 +183,7 @@ public abstract class WindowOrientationListener { * cartesian space because the orientation calculations are sensitive to the * absolute magnitude of the acceleration. In particular, there are singularities * in the calculation as the magnitude approaches 0. By performing the low-pass - * filtering early, we can eliminate high-frequency impulses systematically. + * filtering early, we can eliminate most spurious high-frequency impulses due to noise. * * - Convert the acceleromter vector from cartesian to spherical coordinates. * Since we're dealing with rotation of the device, this is the sensible coordinate @@ -204,11 +208,17 @@ public abstract class WindowOrientationListener { * new orientation proposal. * * Details are explained inline. + * + * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for + * signal processing background. */ static final class SensorEventListenerImpl implements SensorEventListener { // We work with all angles in degrees in this class. private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI); + // Number of nanoseconds per millisecond. + private static final long NANOS_PER_MS = 1000000; + // Indices into SensorEvent.values for the accelerometer sensor. private static final int ACCELEROMETER_DATA_X = 0; private static final int ACCELEROMETER_DATA_Y = 1; @@ -216,40 +226,41 @@ public abstract class WindowOrientationListener { private final WindowOrientationListener mOrientationListener; - /* State for first order low-pass filtering of accelerometer data. - * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for - * signal processing background. - */ - - private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds - private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; - - // The current proposal. We wait for the proposal to be stable for a - // certain amount of time before accepting it. - // - // The basic idea is to ignore intermediate poses of the device while the - // user is picking up, putting down or turning the device. - private long mProposalTime; - private int mProposalRotation; - private long mProposalAgeMS; - private boolean mProposalSettled; - - // A historical trace of tilt and orientation angles. Used to determine whether - // the device posture has settled down. - private static final int HISTORY_SIZE = 20; - private int mHistoryIndex; // index of most recent sample - private int mHistoryLength; // length of historical trace - private final long[] mHistoryTimestampMS = new long[HISTORY_SIZE]; - private final float[] mHistoryMagnitudes = new float[HISTORY_SIZE]; - private final int[] mHistoryTiltAngles = new int[HISTORY_SIZE]; - private final int[] mHistoryOrientationAngles = new int[HISTORY_SIZE]; + // The minimum amount of time that a predicted rotation must be stable before it + // is accepted as a valid rotation proposal. This value can be quite small because + // the low-pass filter already suppresses most of the noise so we're really just + // looking for quick confirmation that the last few samples are in agreement as to + // the desired orientation. + private static final long PROPOSAL_SETTLE_TIME_NANOS = 40 * NANOS_PER_MS; + + // The minimum amount of time that must have elapsed since the device last exited + // the flat state (time since it was picked up) before the proposed rotation + // can change. + private static final long PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS = 500 * NANOS_PER_MS; + + // The mininum amount of time that must have elapsed since the device stopped + // swinging (time since device appeared to be in the process of being put down + // or put away into a pocket) before the proposed rotation can change. + private static final long PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS = 300 * NANOS_PER_MS; + + // If the tilt angle remains greater than the specified angle for a minimum of + // the specified time, then the device is deemed to be lying flat + // (just chillin' on a table). + private static final float FLAT_ANGLE = 75; + private static final long FLAT_TIME_NANOS = 1000 * NANOS_PER_MS; + + // If the tilt angle has increased by at least delta degrees within the specified amount + // of time, then the device is deemed to be swinging away from the user + // down towards flat (tilt = 90). + private static final float SWING_AWAY_ANGLE_DELTA = 20; + private static final long SWING_TIME_NANOS = 300 * NANOS_PER_MS; // The maximum sample inter-arrival time in milliseconds. // If the acceleration samples are further apart than this amount in time, we reset the // state of the low-pass filter and orientation properties. This helps to handle // boundary conditions when the device is turned on, wakes from suspend or there is // a significant gap in samples. - private static final float MAX_FILTER_DELTA_TIME_MS = 1000; + private static final long MAX_FILTER_DELTA_TIME_NANOS = 1000 * NANOS_PER_MS; // The acceleration filter time constant. // @@ -269,8 +280,10 @@ public abstract class WindowOrientationListener { // // Filtering adds latency proportional the time constant (inversely proportional // to the cutoff frequency) so we don't want to make the time constant too - // large or we can lose responsiveness. - private static final float FILTER_TIME_CONSTANT_MS = 100.0f; + // large or we can lose responsiveness. Likewise we don't want to make it too + // small or we do a poor job suppressing acceleration spikes. + // Empirically, 100ms seems to be too small and 500ms is too large. + private static final float FILTER_TIME_CONSTANT_MS = 200.0f; /* State for orientation detection. */ @@ -288,9 +301,9 @@ public abstract class WindowOrientationListener { // // In both cases, we postpone choosing an orientation. private static final float MIN_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY * 0.5f; + SensorManager.STANDARD_GRAVITY * 0.3f; private static final float MAX_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY * 1.5f; + SensorManager.STANDARD_GRAVITY * 1.25f; // Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e. // when screen is facing the sky or ground), we completely ignore orientation data. @@ -321,33 +334,38 @@ public abstract class WindowOrientationListener { // orientation. private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45; - // The number of milliseconds for which the device posture must be stable - // before we perform an orientation change. If the device appears to be rotating - // (being picked up, put down) then we keep waiting until it settles. - private static final int SETTLE_TIME_MIN_MS = 200; + // Timestamp and value of the last accelerometer sample. + private long mLastFilteredTimestampNanos; + private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; + + // The last proposed rotation, -1 if unknown. + private int mProposedRotation; + + // Value of the current predicted rotation, -1 if unknown. + private int mPredictedRotation; - // The maximum number of milliseconds to wait for the posture to settle before - // accepting the current proposal regardless. - private static final int SETTLE_TIME_MAX_MS = 500; + // Timestamp of when the predicted rotation most recently changed. + private long mPredictedRotationTimestampNanos; - // The maximum change in magnitude that can occur during the settle time. - // Tuning this constant particularly helps to filter out situations where the - // device is being picked up or put down by the user. - private static final float SETTLE_MAGNITUDE_MAX_DELTA = - SensorManager.STANDARD_GRAVITY * 0.2f; + // Timestamp when the device last appeared to be flat for sure (the flat delay elapsed). + private long mFlatTimestampNanos; - // The maximum change in tilt angle that can occur during the settle time. - private static final int SETTLE_TILT_ANGLE_MAX_DELTA = 8; + // Timestamp when the device last appeared to be swinging. + private long mSwingTimestampNanos; - // The maximum change in orientation angle that can occur during the settle time. - private static final int SETTLE_ORIENTATION_ANGLE_MAX_DELTA = 8; + // History of observed tilt angles. + private static final int TILT_HISTORY_SIZE = 40; + private float[] mTiltHistory = new float[TILT_HISTORY_SIZE]; + private long[] mTiltHistoryTimestampNanos = new long[TILT_HISTORY_SIZE]; + private int mTiltHistoryIndex; public SensorEventListenerImpl(WindowOrientationListener orientationListener) { mOrientationListener = orientationListener; + reset(); } public int getProposedRotation() { - return mProposalSettled ? mProposalRotation : -1; + return mProposedRotation; } @Override @@ -365,8 +383,9 @@ public abstract class WindowOrientationListener { float z = event.values[ACCELEROMETER_DATA_Z]; if (log) { - Slog.v(TAG, "Raw acceleration vector: " + - "x=" + x + ", y=" + y + ", z=" + z); + Slog.v(TAG, "Raw acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z + + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); } // Apply a low-pass filter to the acceleration up vector in cartesian space. @@ -374,14 +393,16 @@ public abstract class WindowOrientationListener { // or when we see values of (0, 0, 0) which indicates that we polled the // accelerometer too soon after turning it on and we don't have any data yet. final long now = event.timestamp; - final float timeDeltaMS = (now - mLastTimestamp) * 0.000001f; - boolean skipSample; - if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS + final long then = mLastFilteredTimestampNanos; + final float timeDeltaMS = (now - then) * 0.000001f; + final boolean skipSample; + if (now < then + || now > then + MAX_FILTER_DELTA_TIME_NANOS || (x == 0 && y == 0 && z == 0)) { if (log) { Slog.v(TAG, "Resetting orientation listener."); } - clearProposal(); + reset(); skipSample = true; } else { final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); @@ -389,27 +410,28 @@ public abstract class WindowOrientationListener { y = alpha * (y - mLastFilteredY) + mLastFilteredY; z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; if (log) { - Slog.v(TAG, "Filtered acceleration vector: " + - "x=" + x + ", y=" + y + ", z=" + z); + Slog.v(TAG, "Filtered acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z + + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); } skipSample = false; } - mLastTimestamp = now; + mLastFilteredTimestampNanos = now; mLastFilteredX = x; mLastFilteredY = y; mLastFilteredZ = z; - final int oldProposedRotation = getProposedRotation(); + boolean isFlat = false; + boolean isSwinging = false; if (!skipSample) { // Calculate the magnitude of the acceleration vector. - final float magnitude = (float) Math.sqrt(x * x + y * y + z * z); + final float magnitude = FloatMath.sqrt(x * x + y * y + z * z); if (magnitude < MIN_ACCELERATION_MAGNITUDE || magnitude > MAX_ACCELERATION_MAGNITUDE) { if (log) { - Slog.v(TAG, "Ignoring sensor data, magnitude out of range: " - + "magnitude=" + magnitude); + Slog.v(TAG, "Ignoring sensor data, magnitude out of range."); } - clearProposal(); + clearPredictedRotation(); } else { // Calculate the tilt angle. // This is the angle between the up vector and the x-y plane (the plane of @@ -420,14 +442,25 @@ public abstract class WindowOrientationListener { final int tiltAngle = (int) Math.round( Math.asin(z / magnitude) * RADIANS_TO_DEGREES); + // Determine whether the device appears to be flat or swinging. + if (isFlat(now)) { + isFlat = true; + mFlatTimestampNanos = now; + } + if (isSwinging(now, tiltAngle)) { + isSwinging = true; + mSwingTimestampNanos = now; + } + addTiltHistoryEntry(now, tiltAngle); + // If the tilt angle is too close to horizontal then we cannot determine // the orientation angle of the screen. if (Math.abs(tiltAngle) > MAX_TILT) { if (log) { Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " - + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle); + + "tiltAngle=" + tiltAngle); } - clearProposal(); + clearPredictedRotation(); } else { // Calculate the orientation angle. // This is the angle between the x-y projection of the up vector onto @@ -445,86 +478,93 @@ public abstract class WindowOrientationListener { nearestRotation = 0; } - // Determine the proposed orientation. - if (!isTiltAngleAcceptable(nearestRotation, tiltAngle) - || !isOrientationAngleAcceptable(nearestRotation, + // Determine the predicted orientation. + if (isTiltAngleAcceptable(nearestRotation, tiltAngle) + && isOrientationAngleAcceptable(nearestRotation, orientationAngle)) { + updatePredictedRotation(now, nearestRotation); if (log) { - Slog.v(TAG, "Ignoring sensor data, no proposal: " - + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle); + Slog.v(TAG, "Predicted: " + + "tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle + + ", predictedRotation=" + mPredictedRotation + + ", predictedRotationAgeMS=" + + ((now - mPredictedRotationTimestampNanos) + * 0.000001f)); } - clearProposal(); } else { if (log) { - Slog.v(TAG, "Proposal: " - + "magnitude=" + magnitude - + ", tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle - + ", proposalRotation=" + mProposalRotation); + Slog.v(TAG, "Ignoring sensor data, no predicted rotation: " + + "tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle); } - updateProposal(nearestRotation, now / 1000000L, - magnitude, tiltAngle, orientationAngle); + clearPredictedRotation(); } } } } + // Determine new proposed rotation. + final int oldProposedRotation = mProposedRotation; + if (mPredictedRotation < 0 || isPredictedRotationAcceptable(now)) { + mProposedRotation = mPredictedRotation; + } + // Write final statistics about where we are in the orientation detection process. - final int proposedRotation = getProposedRotation(); if (log) { - final float proposalConfidence = Math.min( - mProposalAgeMS * 1.0f / SETTLE_TIME_MIN_MS, 1.0f); Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation - + ", proposedRotation=" + proposedRotation + + ", proposedRotation=" + mProposedRotation + + ", predictedRotation=" + mPredictedRotation + ", timeDeltaMS=" + timeDeltaMS - + ", proposalRotation=" + mProposalRotation - + ", proposalAgeMS=" + mProposalAgeMS - + ", proposalConfidence=" + proposalConfidence); + + ", isFlat=" + isFlat + + ", isSwinging=" + isSwinging + + ", timeUntilSettledMS=" + remainingMS(now, + mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) + + ", timeUntilFlatDelayExpiredMS=" + remainingMS(now, + mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) + + ", timeUntilSwingDelayExpiredMS=" + remainingMS(now, + mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS)); } // Tell the listener. - if (proposedRotation != oldProposedRotation && proposedRotation >= 0) { + if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) { if (log) { - Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + proposedRotation + Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + mProposedRotation + ", oldProposedRotation=" + oldProposedRotation); } - mOrientationListener.onProposedRotationChanged(proposedRotation); + mOrientationListener.onProposedRotationChanged(mProposedRotation); } } /** - * Returns true if the tilt angle is acceptable for a proposed - * orientation transition. + * Returns true if the tilt angle is acceptable for a given predicted rotation. */ - private boolean isTiltAngleAcceptable(int proposedRotation, - int tiltAngle) { - return tiltAngle >= TILT_TOLERANCE[proposedRotation][0] - && tiltAngle <= TILT_TOLERANCE[proposedRotation][1]; + private boolean isTiltAngleAcceptable(int rotation, int tiltAngle) { + return tiltAngle >= TILT_TOLERANCE[rotation][0] + && tiltAngle <= TILT_TOLERANCE[rotation][1]; } /** - * Returns true if the orientation angle is acceptable for a proposed - * orientation transition. + * Returns true if the orientation angle is acceptable for a given predicted rotation. * * This function takes into account the gap between adjacent orientations * for hysteresis. */ - private boolean isOrientationAngleAcceptable(int proposedRotation, int orientationAngle) { + private boolean isOrientationAngleAcceptable(int rotation, int orientationAngle) { // If there is no current rotation, then there is no gap. // The gap is used only to introduce hysteresis among advertised orientation // changes to avoid flapping. final int currentRotation = mOrientationListener.mCurrentRotation; if (currentRotation >= 0) { - // If the proposed rotation is the same or is counter-clockwise adjacent, - // then we set a lower bound on the orientation angle. + // If the specified rotation is the same or is counter-clockwise adjacent + // to the current rotation, then we set a lower bound on the orientation angle. // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90, // then we want to check orientationAngle > 45 + GAP / 2. - if (proposedRotation == currentRotation - || proposedRotation == (currentRotation + 1) % 4) { - int lowerBound = proposedRotation * 90 - 45 + if (rotation == currentRotation + || rotation == (currentRotation + 1) % 4) { + int lowerBound = rotation * 90 - 45 + ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (proposedRotation == 0) { + if (rotation == 0) { if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { return false; } @@ -535,15 +575,15 @@ public abstract class WindowOrientationListener { } } - // If the proposed rotation is the same or is clockwise adjacent, + // If the specified rotation is the same or is clockwise adjacent, // then we set an upper bound on the orientation angle. - // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_270, + // For example, if currentRotation is ROTATION_0 and rotation is ROTATION_270, // then we want to check orientationAngle < 315 - GAP / 2. - if (proposedRotation == currentRotation - || proposedRotation == (currentRotation + 3) % 4) { - int upperBound = proposedRotation * 90 + 45 + if (rotation == currentRotation + || rotation == (currentRotation + 3) % 4) { + int upperBound = rotation * 90 + 45 - ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (proposedRotation == 0) { + if (rotation == 0) { if (orientationAngle <= 45 && orientationAngle > upperBound) { return false; } @@ -557,66 +597,97 @@ public abstract class WindowOrientationListener { return true; } - private void clearProposal() { - mProposalRotation = -1; - mProposalAgeMS = 0; - mProposalSettled = false; - } + /** + * Returns true if the predicted rotation is ready to be advertised as a + * proposed rotation. + */ + private boolean isPredictedRotationAcceptable(long now) { + // The predicted rotation must have settled long enough. + if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) { + return false; + } - private void updateProposal(int rotation, long timestampMS, - float magnitude, int tiltAngle, int orientationAngle) { - if (mProposalRotation != rotation) { - mProposalTime = timestampMS; - mProposalRotation = rotation; - mHistoryIndex = 0; - mHistoryLength = 0; + // The last flat state (time since picked up) must have been sufficiently long ago. + if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) { + return false; } - final int index = mHistoryIndex; - mHistoryTimestampMS[index] = timestampMS; - mHistoryMagnitudes[index] = magnitude; - mHistoryTiltAngles[index] = tiltAngle; - mHistoryOrientationAngles[index] = orientationAngle; - mHistoryIndex = (index + 1) % HISTORY_SIZE; - if (mHistoryLength < HISTORY_SIZE) { - mHistoryLength += 1; + // The last swing state (time since last movement to put down) must have been + // sufficiently long ago. + if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) { + return false; } - long age = 0; - for (int i = 1; i < mHistoryLength; i++) { - final int olderIndex = (index + HISTORY_SIZE - i) % HISTORY_SIZE; - if (Math.abs(mHistoryMagnitudes[olderIndex] - magnitude) - > SETTLE_MAGNITUDE_MAX_DELTA) { + // Looks good! + return true; + } + + private void reset() { + mLastFilteredTimestampNanos = Long.MIN_VALUE; + mProposedRotation = -1; + mFlatTimestampNanos = Long.MIN_VALUE; + mSwingTimestampNanos = Long.MIN_VALUE; + clearPredictedRotation(); + clearTiltHistory(); + } + + private void clearPredictedRotation() { + mPredictedRotation = -1; + mPredictedRotationTimestampNanos = Long.MIN_VALUE; + } + + private void updatePredictedRotation(long now, int rotation) { + if (mPredictedRotation != rotation) { + mPredictedRotation = rotation; + mPredictedRotationTimestampNanos = now; + } + } + + private void clearTiltHistory() { + mTiltHistoryTimestampNanos[0] = Long.MIN_VALUE; + mTiltHistoryIndex = 1; + } + + private void addTiltHistoryEntry(long now, float tilt) { + mTiltHistory[mTiltHistoryIndex] = tilt; + mTiltHistoryTimestampNanos[mTiltHistoryIndex] = now; + mTiltHistoryIndex = (mTiltHistoryIndex + 1) % TILT_HISTORY_SIZE; + mTiltHistoryTimestampNanos[mTiltHistoryIndex] = Long.MIN_VALUE; + } + + private boolean isFlat(long now) { + for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { + if (mTiltHistory[i] < FLAT_ANGLE) { break; } - if (angleAbsoluteDelta(mHistoryTiltAngles[olderIndex], - tiltAngle) > SETTLE_TILT_ANGLE_MAX_DELTA) { - break; + if (mTiltHistoryTimestampNanos[i] + FLAT_TIME_NANOS <= now) { + // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. + return true; } - if (angleAbsoluteDelta(mHistoryOrientationAngles[olderIndex], - orientationAngle) > SETTLE_ORIENTATION_ANGLE_MAX_DELTA) { + } + return false; + } + + private boolean isSwinging(long now, float tilt) { + for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { + if (mTiltHistoryTimestampNanos[i] + SWING_TIME_NANOS < now) { break; } - age = timestampMS - mHistoryTimestampMS[olderIndex]; - if (age >= SETTLE_TIME_MIN_MS) { - break; + if (mTiltHistory[i] + SWING_AWAY_ANGLE_DELTA <= tilt) { + // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. + return true; } } - mProposalAgeMS = age; - if (age >= SETTLE_TIME_MIN_MS - || timestampMS - mProposalTime >= SETTLE_TIME_MAX_MS) { - mProposalSettled = true; - } else { - mProposalSettled = false; - } + return false; } - private static int angleAbsoluteDelta(int a, int b) { - int delta = Math.abs(a - b); - if (delta > 180) { - delta = 360 - delta; - } - return delta; + private int nextTiltHistoryIndex(int index) { + index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; + return mTiltHistoryTimestampNanos[index] != Long.MIN_VALUE ? index : -1; + } + + private static float remainingMS(long now, long until) { + return now >= until ? 0 : (until - now) * 0.000001f; } } } diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 91dcac8..75b875a 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -844,7 +844,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.mParcelableData = parcel.readParcelable(null); parcel.readList(record.mText, null); record.mSourceWindowId = parcel.readInt(); - record.mSourceViewId = parcel.readInt(); + record.mSourceNodeId = parcel.readLong(); record.mSealed = (parcel.readInt() == 1); } @@ -893,7 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par parcel.writeParcelable(record.mParcelableData, flags); parcel.writeList(record.mText); parcel.writeInt(record.mSourceWindowId); - parcel.writeInt(record.mSourceViewId); + parcel.writeLong(record.mSourceNodeId); parcel.writeInt(record.mSealed ? 1 : 0); } @@ -914,7 +914,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par if (DEBUG) { builder.append("\n"); builder.append("; sourceWindowId: ").append(mSourceWindowId); - builder.append("; sourceViewId: ").append(mSourceViewId); + builder.append("; mSourceNodeId: ").append(mSourceNodeId); for (int i = 0; i < mRecords.size(); i++) { AccessibilityRecord record = mRecords.get(i); builder.append(" Record "); diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 96653e5..95c070c 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -22,6 +22,7 @@ import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; +import android.util.LongSparseArray; import android.util.SparseArray; import java.util.Collections; @@ -73,7 +74,8 @@ public final class AccessibilityInteractionClient private static final Object sStaticLock = new Object(); - private static AccessibilityInteractionClient sInstance; + private static final LongSparseArray<AccessibilityInteractionClient> sClients = + new LongSparseArray<AccessibilityInteractionClient>(); private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); @@ -96,17 +98,36 @@ public final class AccessibilityInteractionClient new SparseArray<IAccessibilityServiceConnection>(); /** - * @return The singleton of this class. + * @return The client for the current thread. */ public static AccessibilityInteractionClient getInstance() { + final long threadId = Thread.currentThread().getId(); + return getInstanceForThread(threadId); + } + + /** + * <strong>Note:</strong> We keep one instance per interrogating thread since + * the instance contains state which can lead to undesired thread interleavings. + * We do not have a thread local variable since other threads should be able to + * look up the correct client knowing a thread id. See ViewRootImpl for details. + * + * @return The client for a given <code>threadId</code>. + */ + public static AccessibilityInteractionClient getInstanceForThread(long threadId) { synchronized (sStaticLock) { - if (sInstance == null) { - sInstance = new AccessibilityInteractionClient(); + AccessibilityInteractionClient client = sClients.get(threadId); + if (client == null) { + client = new AccessibilityInteractionClient(); + sClients.put(threadId, client); } - return sInstance; + return client; } } + private AccessibilityInteractionClient() { + /* reducing constructor visibility */ + } + /** * Sets the message to be processed if the interacted view hierarchy * and the interacting client are running in the same thread. @@ -125,17 +146,18 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique node accessibility id + * (accessibility view and virtual descendant id). * @return An {@link AccessibilityNodeInfo} if found, null otherwise. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, - int accessibilityWindowId, int accessibilityViewId) { + int accessibilityWindowId, long accessibilityNodeId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( - accessibilityWindowId, accessibilityViewId, interactionId, this, + accessibilityWindowId, accessibilityNodeId, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -205,14 +227,14 @@ public final class AccessibilityInteractionClient * @param text The searched text. * @return A list of found {@link AccessibilityNodeInfo}s. */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow( + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow( int connectionId, String text) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = - connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text, + connection.findAccessibilityNodeInfosByTextInActiveWindow(text, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -244,18 +266,18 @@ public final class AccessibilityInteractionClient * @param connectionId The id of a connection for interacting with the system. * @param text The searched text. * @param accessibilityWindowId A unique window id. - * @param accessibilityViewId A unique View accessibility id from where to start the search. - * Use {@link android.view.View#NO_ID} to start from the root. + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from + * where to start the search. Use {@link android.view.View#NO_ID} to start from the root. * @return A list of found {@link AccessibilityNodeInfo}s. */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId, - String text, int accessibilityWindowId, int accessibilityViewId) { + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, + String text, int accessibilityWindowId, long accessibilityNodeId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findAccessibilityNodeInfosByViewText(text, - accessibilityWindowId, accessibilityViewId, interactionId, this, + final float windowScale = connection.findAccessibilityNodeInfosByText(text, + accessibilityWindowId, accessibilityNodeId, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -283,18 +305,18 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId The id of the window. - * @param accessibilityViewId A unique View accessibility id. + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). * @param action The action to perform. * @return Whether the action was performed. */ public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, - int accessibilityViewId, int action) { + long accessibilityNodeId, int action) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final boolean success = connection.performAccessibilityAction( - accessibilityWindowId, accessibilityViewId, action, interactionId, this, + accessibilityWindowId, accessibilityNodeId, action, interactionId, this, Thread.currentThread().getId()); if (success) { return getPerformAccessibilityActionResult(interactionId); diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 9b0f44a..6939c2c 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -20,7 +20,7 @@ import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.util.SparseIntArray; +import android.util.SparseLongArray; import android.view.View; import java.util.Collections; @@ -98,6 +98,59 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int PROPERTY_SCROLLABLE = 0x00000200; + /** + * Bits that provide the id of a virtual descendant of a view. + */ + private static final long VIRTUAL_DESCENDANT_ID_MASK = 0xffffffff00000000L; + + /** + * Bit shift of {@link #VIRTUAL_DESCENDANT_ID_MASK} to get to the id for a + * virtual descendant of a view. Such a descendant does not exist in the view + * hierarchy and is only reported via the accessibility APIs. + */ + private static final int VIRTUAL_DESCENDANT_ID_SHIFT = 32; + + /** + * Gets the accessibility view id which identifies a View in the view three. + * + * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}. + * @return The accessibility view id part of the node id. + * + * @hide + */ + public static int getAccessibilityViewId(long accessibilityNodeId) { + return (int) accessibilityNodeId; + } + + /** + * Gets the virtual descendant id which identifies an imaginary view in a + * containing View. + * + * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}. + * @return The virtual view id part of the node id. + * + * @hide + */ + public static int getVirtualDescendantId(long accessibilityNodeId) { + return (int) ((accessibilityNodeId & VIRTUAL_DESCENDANT_ID_MASK) + >> VIRTUAL_DESCENDANT_ID_SHIFT); + } + + /** + * Makes a node id by shifting the <code>virtualDescendantId</code> + * by {@link #VIRTUAL_DESCENDANT_ID_SHIFT} and taking + * the bitwise or with the <code>accessibilityViewId</code>. + * + * @param accessibilityViewId A View accessibility id. + * @param virtualDescendantId A virtual descendant id. + * @return The node id. + * + * @hide + */ + public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) { + return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT) | accessibilityViewId; + } + // Housekeeping. private static final int MAX_POOL_SIZE = 50; private static final Object sPoolLock = new Object(); @@ -108,9 +161,10 @@ public class AccessibilityNodeInfo implements Parcelable { private boolean mSealed; // Data. - private int mAccessibilityViewId = UNDEFINED; - private int mAccessibilityWindowId = UNDEFINED; - private int mParentAccessibilityViewId = UNDEFINED; + private int mWindowId = UNDEFINED; + private long mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED); + private long mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED); + private int mBooleanProperties; private final Rect mBoundsInParent = new Rect(); private final Rect mBoundsInScreen = new Rect(); @@ -120,7 +174,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mText; private CharSequence mContentDescription; - private SparseIntArray mChildAccessibilityIds = new SparseIntArray(); + private SparseLongArray mChildIds = new SparseLongArray(); private int mActions; private int mConnectionId = UNDEFINED; @@ -134,13 +188,43 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Sets the source. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> * * @param source The info source. */ public void setSource(View source) { + setSource(source, UNDEFINED); + } + + /** + * Sets the source to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root + * is set as the source. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); - mAccessibilityViewId = source.getAccessibilityViewId(); - mAccessibilityWindowId = source.getAccessibilityWindowId(); + mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } /** @@ -149,7 +233,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return The window id. */ public int getWindowId() { - return mAccessibilityWindowId; + return mWindowId; } /** @@ -158,7 +242,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @return The child count. */ public int getChildCount() { - return mChildAccessibilityIds.size(); + return mChildIds.size(); } /** @@ -177,21 +261,20 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getChild(int index) { enforceSealed(); - final int childAccessibilityViewId = mChildAccessibilityIds.get(index); - if (!canPerformRequestOverConnection(childAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return null; } + final long childId = mChildIds.get(index); AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mAccessibilityWindowId, childAccessibilityViewId); + return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mWindowId, childId); } /** * Adds a child. * <p> - * <strong>Note:</strong> Cannot be called from an - * {@link android.accessibilityservice.AccessibilityService}. - * This class is made immutable before being delivered to an AccessibilityService. + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. * </p> * * @param child The child. @@ -199,10 +282,30 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void addChild(View child) { + addChild(child, UNDEFINED); + } + + /** + * Adds a virtual child which is a descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root + * is added as a child. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + */ + public void addChild(View root, int virtualDescendantId) { enforceNotSealed(); - final int childAccessibilityViewId = child.getAccessibilityViewId(); - final int index = mChildAccessibilityIds.size(); - mChildAccessibilityIds.put(index, childAccessibilityViewId); + final int index = mChildIds.size(); + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + mChildIds.put(index, childNodeId); } /** @@ -250,12 +353,11 @@ public class AccessibilityNodeInfo implements Parcelable { */ public boolean performAction(int action) { enforceSealed(); - if (!canPerformRequestOverConnection(mAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return false; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.performAccessibilityAction(mConnectionId, mAccessibilityWindowId, - mAccessibilityViewId, action); + return client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId, action); } /** @@ -274,12 +376,12 @@ public class AccessibilityNodeInfo implements Parcelable { */ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) { enforceSealed(); - if (!canPerformRequestOverConnection(mAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mSourceNodeId)) { return Collections.emptyList(); } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfosByViewText(mConnectionId, text, - mAccessibilityWindowId, mAccessibilityViewId); + return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId, + mSourceNodeId); } /** @@ -294,12 +396,12 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getParent() { enforceSealed(); - if (!canPerformRequestOverConnection(mParentAccessibilityViewId)) { + if (!canPerformRequestOverConnection(mParentNodeId)) { return null; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mAccessibilityWindowId, mParentAccessibilityViewId); + mWindowId, mParentNodeId); } /** @@ -315,8 +417,33 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setParent(View parent) { + setParent(parent, UNDEFINED); + } + + /** + * Sets the parent to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root + * is set as the parent. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setParent(View root, int virtualDescendantId) { enforceNotSealed(); - mParentAccessibilityViewId = parent.getAccessibilityViewId(); + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mParentNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } /** @@ -829,6 +956,7 @@ public class AccessibilityNodeInfo implements Parcelable { * Returns a cached instance if such is available otherwise a new one * and sets the source. * + * @param source The source view. * @return An instance. * * @see #setSource(View) @@ -840,6 +968,22 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + * @return An instance. + * + * @see #setSource(View, int) + */ + public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setSource(root, virtualDescendantId); + return info; + } + + /** * Returns a cached instance if such is available otherwise a new one. * * @return An instance. @@ -903,16 +1047,16 @@ public class AccessibilityNodeInfo implements Parcelable { */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(isSealed() ? 1 : 0); - parcel.writeInt(mAccessibilityViewId); - parcel.writeInt(mAccessibilityWindowId); - parcel.writeInt(mParentAccessibilityViewId); + parcel.writeLong(mSourceNodeId); + parcel.writeInt(mWindowId); + parcel.writeLong(mParentNodeId); parcel.writeInt(mConnectionId); - SparseIntArray childIds = mChildAccessibilityIds; + SparseLongArray childIds = mChildIds; final int childIdsSize = childIds.size(); parcel.writeInt(childIdsSize); for (int i = 0; i < childIdsSize; i++) { - parcel.writeInt(childIds.valueAt(i)); + parcel.writeLong(childIds.valueAt(i)); } parcel.writeInt(mBoundsInParent.top); @@ -946,9 +1090,9 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void init(AccessibilityNodeInfo other) { mSealed = other.mSealed; - mAccessibilityViewId = other.mAccessibilityViewId; - mParentAccessibilityViewId = other.mParentAccessibilityViewId; - mAccessibilityWindowId = other.mAccessibilityWindowId; + mSourceNodeId = other.mSourceNodeId; + mParentNodeId = other.mParentNodeId; + mWindowId = other.mWindowId; mConnectionId = other.mConnectionId; mBoundsInParent.set(other.mBoundsInParent); mBoundsInScreen.set(other.mBoundsInScreen); @@ -958,7 +1102,7 @@ public class AccessibilityNodeInfo implements Parcelable { mContentDescription = other.mContentDescription; mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; - mChildAccessibilityIds = other.mChildAccessibilityIds.clone(); + mChildIds = other.mChildIds.clone(); } /** @@ -968,15 +1112,15 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void initFromParcel(Parcel parcel) { mSealed = (parcel.readInt() == 1); - mAccessibilityViewId = parcel.readInt(); - mAccessibilityWindowId = parcel.readInt(); - mParentAccessibilityViewId = parcel.readInt(); + mSourceNodeId = parcel.readLong(); + mWindowId = parcel.readInt(); + mParentNodeId = parcel.readLong(); mConnectionId = parcel.readInt(); - SparseIntArray childIds = mChildAccessibilityIds; + SparseLongArray childIds = mChildIds; final int childrenSize = parcel.readInt(); for (int i = 0; i < childrenSize; i++) { - final int childId = parcel.readInt(); + final long childId = parcel.readLong(); childIds.put(i, childId); } @@ -1005,11 +1149,11 @@ public class AccessibilityNodeInfo implements Parcelable { */ private void clear() { mSealed = false; - mAccessibilityViewId = UNDEFINED; - mParentAccessibilityViewId = UNDEFINED; - mAccessibilityWindowId = UNDEFINED; + mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED); + mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED); + mWindowId = UNDEFINED; mConnectionId = UNDEFINED; - mChildAccessibilityIds.clear(); + mChildIds.clear(); mBoundsInParent.set(0, 0, 0, 0); mBoundsInScreen.set(0, 0, 0, 0); mBooleanProperties = 0; @@ -1041,9 +1185,10 @@ public class AccessibilityNodeInfo implements Parcelable { } } - private boolean canPerformRequestOverConnection(int accessibilityViewId) { - return (mConnectionId != UNDEFINED && mAccessibilityWindowId != UNDEFINED - && accessibilityViewId != UNDEFINED); + private boolean canPerformRequestOverConnection(long accessibilityNodeId) { + return (mWindowId != UNDEFINED + && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED + && mConnectionId != UNDEFINED); } @Override @@ -1058,10 +1203,10 @@ public class AccessibilityNodeInfo implements Parcelable { return false; } AccessibilityNodeInfo other = (AccessibilityNodeInfo) object; - if (mAccessibilityViewId != other.mAccessibilityViewId) { + if (mSourceNodeId != other.mSourceNodeId) { return false; } - if (mAccessibilityWindowId != other.mAccessibilityWindowId) { + if (mWindowId != other.mWindowId) { return false; } return true; @@ -1071,8 +1216,9 @@ public class AccessibilityNodeInfo implements Parcelable { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + mAccessibilityViewId; - result = prime * result + mAccessibilityWindowId; + result = prime * result + getAccessibilityViewId(mSourceNodeId); + result = prime * result + getVirtualDescendantId(mSourceNodeId); + result = prime * result + mWindowId; return result; } @@ -1082,9 +1228,10 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append(super.toString()); if (DEBUG) { - builder.append("; accessibilityId: " + mAccessibilityViewId); - builder.append("; parentAccessibilityId: " + mParentAccessibilityViewId); - SparseIntArray childIds = mChildAccessibilityIds; + builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); + builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); + builder.append("; mParentNodeId: " + mParentNodeId); + SparseLongArray childIds = mChildIds; builder.append("; childAccessibilityIds: ["); for (int i = 0, count = childIds.size(); i < count; i++) { builder.append(childIds.valueAt(i)); diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java new file mode 100644 index 0000000..5890417 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2011 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 android.view.accessibility; + +import android.accessibilityservice.AccessibilityService; +import android.view.View; + +import java.util.List; + +/** + * This class is the contract a client should implement to enable support of a + * virtual view hierarchy rooted at a given view for accessibility purposes. A virtual + * view hierarchy is a tree of imaginary Views that is reported as a part of the view + * hierarchy when an {@link AccessibilityService} explores the window content. + * Since the virtual View tree does not exist this class is responsible for + * managing the {@link AccessibilityNodeInfo}s describing that tree to accessibility + * services. + * </p> + * <p> + * The main use case of these APIs is to enable a custom view that draws complex content, + * for example a monthly calendar grid, to be presented as a tree of logical nodes, + * for example month days each containing events, thus conveying its logical structure. + * <p> + * <p> + * A typical use case is to override {@link View#getAccessibilityNodeProvider()} of the + * View that is a root of a virtual View hierarchy to return an instance of this class. + * In such a case this instance is responsible for managing {@link AccessibilityNodeInfo}s + * describing the virtual sub-tree rooted at the View including the one representing the + * View itself. Similarly the returned instance is responsible for performing accessibility + * actions on any virtual view or the root view itself. For example: + * </p> + * <pre> + * getAccessibilityNodeProvider( + * if (mAccessibilityNodeProvider == null) { + * mAccessibilityNodeProvider = new AccessibilityNodeProvider() { + * public boolean performAccessibilityAction(int action, int virtualDescendantId) { + * // Implementation. + * return false; + * } + * + * public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, int virtualDescendantId) { + * // Implementation. + * return null; + * } + * + * public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) { + * // Implementation. + * return null; + * } + * }); + * return mAccessibilityNodeProvider; + * </pre> + */ +public abstract class AccessibilityNodeProvider { + + /** + * Returns an {@link AccessibilityNodeInfo} representing a virtual view, + * i.e. a descendant of the host View, with the given <code>virtualViewId</code> + * or the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * The implementer is responsible for obtaining an accessibility node info from the + * pool of reusable instances and setting the desired properties of the node info + * before returning it. + * </p> + * + * @param virtualViewId A client defined virtual view id. + * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the + * host View. + * + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return null; + } + + /** + * Performs an accessibility action on a virtual view, i.e. a descendant of the + * host View, with the given <code>virtualViewId</code> or the host View itself + * if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * @param action The action to perform. + * @param virtualViewId A client defined virtual view id. + * @return True if the action was performed. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public boolean performAccessibilityAction(int action, int virtualViewId) { + return false; + } + + /** + * Finds {@link AccessibilityNodeInfo}s by text. The match is case insensitive + * containment. The search is relative to the virtual view, i.e. a descendant of the + * host View, with the given <code>virtualViewId</code> or the host View itself + * <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @param text The searched text. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return null; + } +} diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 18d0f6f..07aeb9a 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -77,7 +77,7 @@ public class AccessibilityRecord { int mAddedCount= UNDEFINED; int mRemovedCount = UNDEFINED; - int mSourceViewId = UNDEFINED; + long mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED); int mSourceWindowId = UNDEFINED; CharSequence mClassName; @@ -103,14 +103,28 @@ public class AccessibilityRecord { * @throws IllegalStateException If called from an AccessibilityService. */ public void setSource(View source) { + setSource(source, UNDEFINED); + } + + /** + * Sets the source to be a virtual descendant of the given <code>root</code>. + * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root + * is set as the source. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); - if (source != null) { - mSourceWindowId = source.getAccessibilityWindowId(); - mSourceViewId = source.getAccessibilityViewId(); - } else { - mSourceWindowId = UNDEFINED; - mSourceViewId = UNDEFINED; - } + mSourceWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; + final int rootViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId); } /** @@ -125,12 +139,12 @@ public class AccessibilityRecord { public AccessibilityNodeInfo getSource() { enforceSealed(); if (mConnectionId == UNDEFINED || mSourceWindowId == UNDEFINED - || mSourceViewId == UNDEFINED) { + || AccessibilityNodeInfo.getAccessibilityViewId(mSourceNodeId) == UNDEFINED) { return null; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId, - mSourceViewId); + mSourceNodeId); } /** @@ -383,6 +397,7 @@ public class AccessibilityRecord { public int getMaxScrollX() { return mMaxScrollX; } + /** * Sets the max scroll offset of the source left edge in pixels. * @@ -708,7 +723,7 @@ public class AccessibilityRecord { mParcelableData = record.mParcelableData; mText.addAll(record.mText); mSourceWindowId = record.mSourceWindowId; - mSourceViewId = record.mSourceViewId; + mSourceNodeId = record.mSourceNodeId; mConnectionId = record.mConnectionId; } @@ -733,7 +748,7 @@ public class AccessibilityRecord { mBeforeText = null; mParcelableData = null; mText.clear(); - mSourceViewId = UNDEFINED; + mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED); mSourceWindowId = UNDEFINED; mConnectionId = UNDEFINED; } diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index 535d594..a90c427 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -27,7 +27,7 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback; */ oneway interface IAccessibilityInteractionConnection { - void findAccessibilityNodeInfoByAccessibilityId(int accessibilityViewId, int interactionId, + void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); @@ -35,11 +35,11 @@ oneway interface IAccessibilityInteractionConnection { IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void findAccessibilityNodeInfosByViewText(String text, int accessibilityViewId, + void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void performAccessibilityAction(int accessibilityId, int action, int interactionId, + void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); } diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 0d57c9b..e8c0239 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -323,7 +323,7 @@ public abstract class Animation implements Cloneable { /** * Initialize this animation with the dimensions of the object being * animated as well as the objects parents. (This is to support animation - * sizes being specifed relative to these dimensions.) + * sizes being specified relative to these dimensions.) * * <p>Objects that interpret Animations should call this method when * the sizes of the object being animated and its parent are known, and diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 2cf8ea8..14d3d19 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -224,7 +224,9 @@ public class AnimationSet extends Animation { } boolean changeBounds = (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == 0; - if (changeBounds && a.willChangeTransformationMatrix()) { + + + if (changeBounds && a.willChangeBounds()) { mFlags |= PROPERTY_CHANGE_BOUNDS_MASK; } @@ -346,12 +348,13 @@ public class AnimationSet extends Animation { for (int i = count - 1; i >= 0; --i) { final Animation a = animations.get(i); - - temp.clear(); - final Interpolator interpolator = a.mInterpolator; - a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f) - : 0.0f, temp); - previousTransformation.compose(temp); + if (!a.isFillEnabled() || a.getFillBefore() || a.getStartOffset() == 0) { + temp.clear(); + final Interpolator interpolator = a.mInterpolator; + a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f) + : 0.0f, temp); + previousTransformation.compose(temp); + } } } } diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 32ff647..38043b2 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -133,6 +133,14 @@ public class AnimationUtils { } + /** + * Loads a {@link LayoutAnimationController} object from a resource + * + * @param context Application context used to access resources + * @param id The resource id of the animation to load + * @return The animation object reference by the specified id + * @throws NotFoundException when the layout animation controller cannot be loaded + */ public static LayoutAnimationController loadLayoutAnimation(Context context, int id) throws NotFoundException { diff --git a/core/java/android/view/animation/RotateAnimation.java b/core/java/android/view/animation/RotateAnimation.java index 58bf084..67e0374 100644 --- a/core/java/android/view/animation/RotateAnimation.java +++ b/core/java/android/view/animation/RotateAnimation.java @@ -66,6 +66,8 @@ public class RotateAnimation extends Animation { mPivotYValue = d.value; a.recycle(); + + initializePivotPoint(); } /** @@ -107,6 +109,7 @@ public class RotateAnimation extends Animation { mPivotYType = ABSOLUTE; mPivotXValue = pivotX; mPivotYValue = pivotY; + initializePivotPoint(); } /** @@ -143,6 +146,20 @@ public class RotateAnimation extends Animation { mPivotXType = pivotXType; mPivotYValue = pivotYValue; mPivotYType = pivotYType; + initializePivotPoint(); + } + + /** + * Called at the end of constructor methods to initialize, if possible, values for + * the pivot point. This is only possible for ABSOLUTE pivot values. + */ + private void initializePivotPoint() { + if (mPivotXType == ABSOLUTE) { + mPivotX = mPivotXValue; + } + if (mPivotYType == ABSOLUTE) { + mPivotY = mPivotYValue; + } } @Override diff --git a/core/java/android/view/animation/ScaleAnimation.java b/core/java/android/view/animation/ScaleAnimation.java index 1dd250f..e9a8436 100644 --- a/core/java/android/view/animation/ScaleAnimation.java +++ b/core/java/android/view/animation/ScaleAnimation.java @@ -128,6 +128,8 @@ public class ScaleAnimation extends Animation { mPivotYValue = d.value; a.recycle(); + + initializePivotPoint(); } /** @@ -178,6 +180,7 @@ public class ScaleAnimation extends Animation { mPivotYType = ABSOLUTE; mPivotXValue = pivotX; mPivotYValue = pivotY; + initializePivotPoint(); } /** @@ -218,6 +221,20 @@ public class ScaleAnimation extends Animation { mPivotXType = pivotXType; mPivotYValue = pivotYValue; mPivotYType = pivotYType; + initializePivotPoint(); + } + + /** + * Called at the end of constructor methods to initialize, if possible, values for + * the pivot point. This is only possible for ABSOLUTE pivot values. + */ + private void initializePivotPoint() { + if (mPivotXType == ABSOLUTE) { + mPivotX = mPivotXValue; + } + if (mPivotYType == ABSOLUTE) { + mPivotY = mPivotYValue; + } } @Override diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 5ec1ec3..bd02d62 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -193,10 +193,12 @@ public class BaseInputConnection implements InputConnection { /** * The default implementation performs the deletion around the current * selection position of the editable text. + * @param beforeLength + * @param afterLength */ - public boolean deleteSurroundingText(int leftLength, int rightLength) { - if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength - + " / " + rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength + + " / " + afterLength); final Editable content = getEditable(); if (content == null) return false; @@ -226,17 +228,17 @@ public class BaseInputConnection implements InputConnection { int deleted = 0; - if (leftLength > 0) { - int start = a - leftLength; + if (beforeLength > 0) { + int start = a - beforeLength; if (start < 0) start = 0; content.delete(start, a); deleted = a - start; } - if (rightLength > 0) { + if (afterLength > 0) { b = b - deleted; - int end = b + rightLength; + int end = b + afterLength; if (end > content.length()) end = content.length(); content.delete(b, end); diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index ac378fc..5146567 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -168,6 +168,22 @@ public class EditorInfo implements InputType, Parcelable { public static final int IME_FLAG_NO_ENTER_ACTION = 0x40000000; /** + * Flag of {@link #imeOptions}: used to request that the IME is capable of + * inputting ASCII characters. The intention of this flag is to ensure that + * the user can type Roman alphabet characters in a {@link android.widget.TextView} + * used for, typically, account ID or password input. It is expected that IMEs + * normally are able to input ASCII even without being told so (such IMEs + * already respect this flag in a sense), but there could be some cases they + * aren't when, for instance, only non-ASCII input languagaes like Arabic, + * Greek, Hebrew, Russian are enabled in the IME. Applications need to be + * aware that the flag is not a guarantee, and not all IMEs will respect it. + * However, it is strongly recommended for IME authors to respect this flag + * especially when their IME could end up with a state that has only non-ASCII + * input languages enabled. + */ + public static final int IME_FLAG_FORCE_ASCII = 0x80000000; + + /** * Generic unspecified type for {@link #imeOptions}. */ public static final int IME_NULL = 0x00000000; diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index a6639d1..3563d4d 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -138,19 +138,21 @@ public interface InputConnection { int flags); /** - * Delete <var>leftLength</var> characters of text before the current cursor - * position, and delete <var>rightLength</var> characters of text after the - * current cursor position, excluding composing text. + * Delete <var>beforeLength</var> characters of text before the current cursor + * position, and delete <var>afterLength</var> characters of text after the + * current cursor position, excluding composing text. Before and after refer + * to the order of the characters in the string, not to their visual representation. * - * @param leftLength The number of characters to be deleted before the + * + * @param beforeLength The number of characters to be deleted before the * current cursor position. - * @param rightLength The number of characters to be deleted after the + * @param afterLength The number of characters to be deleted after the * current cursor position. - * + * * @return Returns true on success, false if the input connection is no longer * valid. */ - public boolean deleteSurroundingText(int leftLength, int rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength); /** * Set composing text around the current cursor position with the given text, diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 690ea85..a48473e 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -62,8 +62,8 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.getExtractedText(request, flags); } - public boolean deleteSurroundingText(int leftLength, int rightLength) { - return mTarget.deleteSurroundingText(leftLength, rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + return mTarget.deleteSurroundingText(beforeLength, afterLength); } public boolean setComposingText(CharSequence text, int newCursorPosition) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index b41e6f5..0985e14 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -651,19 +651,7 @@ public final class InputMethodManager { } } - if (mServedInputConnection != null) { - // We need to tell the previously served view that it is no - // longer the input target, so it can reset its state. Schedule - // this call on its window's Handler so it will be on the correct - // thread and outside of our lock. - Handler vh = mServedView.getHandler(); - if (vh != null) { - // This will result in a call to reportFinishInputConnection() - // below. - vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION, - mServedInputConnection)); - } - } + notifyInputConnectionFinished(); mServedView = null; mCompletions = null; @@ -671,7 +659,25 @@ public final class InputMethodManager { clearConnectionLocked(); } } - + + /** + * Notifies the served view that the current InputConnection will no longer be used. + */ + private void notifyInputConnectionFinished() { + if (mServedView != null && mServedInputConnection != null) { + // We need to tell the previously served view that it is no + // longer the input target, so it can reset its state. Schedule + // this call on its window's Handler so it will be on the correct + // thread and outside of our lock. + Handler vh = mServedView.getHandler(); + if (vh != null) { + // This will result in a call to reportFinishInputConnection() below. + vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION, + mServedInputConnection)); + } + } + } + /** * Called from the FINISH_INPUT_CONNECTION message above. * @hide @@ -681,7 +687,7 @@ public final class InputMethodManager { ic.finishComposingText(); } } - + public void displayCompletions(View view, CompletionInfo[] completions) { checkFocus(); synchronized (mH) { @@ -831,7 +837,7 @@ public final class InputMethodManager { * shown with {@link #SHOW_FORCED}. */ public static final int HIDE_NOT_ALWAYS = 0x0002; - + /** * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)} * without a result: request to hide the soft input window from the @@ -993,7 +999,7 @@ public final class InputMethodManager { tba.fieldId = view.getId(); InputConnection ic = view.onCreateInputConnection(tba); if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); - + synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. @@ -1012,6 +1018,8 @@ public final class InputMethodManager { // Hook 'em up and let 'er rip. mCurrentTextBoxAttribute = tba; mServedConnecting = false; + // Notify the served view that its previous input connection is finished + notifyInputConnectionFinished(); mServedInputConnection = ic; IInputContext servedContext; if (ic != null) { @@ -1115,7 +1123,7 @@ public final class InputMethodManager { } } - void scheduleCheckFocusLocked(View view) { + static void scheduleCheckFocusLocked(View view) { Handler vh = view.getHandler(); if (vh != null && !vh.hasMessages(ViewRootImpl.CHECK_FOCUS)) { // This will result in a call to checkFocus() below. diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 93caabe..18dec52 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -31,17 +31,15 @@ import java.util.List; import java.util.Locale; /** - * This class is used to specify meta information of a subtype contained in an input method. - * Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...), and is - * used for IME switch and settings. The input method subtype allows the system to bring up the - * specified subtype of the designated input method directly. + * This class is used to specify meta information of a subtype contained in an input method editor + * (IME). Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...), + * and is used for IME switch and settings. The input method subtype allows the system to bring up + * the specified subtype of the designated IME directly. * - * <p>It should be defined in an XML resource file of the input method - * with the <code><subtype></code> element. - * For more information, see the guide to + * <p>It should be defined in an XML resource file of the input method with the + * <code><subtype></code> element. For more information, see the guide to * <a href="{@docRoot}resources/articles/creating-input-method.html"> * Creating an Input Method</a>.</p> - * */ public final class InputMethodSubtype implements Parcelable { private static final String TAG = InputMethodSubtype.class.getSimpleName(); @@ -59,13 +57,24 @@ public final class InputMethodSubtype implements Parcelable { private HashMap<String, String> mExtraValueHashMapCache; /** - * Constructor - * @param nameId The name of the subtype - * @param iconId The icon of the subtype + * Constructor. + * @param nameId Resource ID of the subtype name string. The string resource may have exactly + * one %s in it. If there is, the %s part will be replaced with the locale's display name by + * the formatter. Please refer to {@link #getDisplayName} for details. + * @param iconId Resource ID of the subtype icon drawable. * @param locale The locale supported by the subtype * @param mode The mode supported by the subtype - * @param extraValue The extra value of the subtype - * @param isAuxiliary true when this subtype is one shot subtype. + * @param extraValue The extra value of the subtype. This string is free-form, but the API + * supplies tools to deal with a key-value comma-separated list; see + * {@link #containsExtraValueKey} and {@link #getExtraValueOf}. + * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary + * subtype will not be shown in the list of enabled IMEs for choosing the current IME in + * the Settings even when this subtype is enabled. Please note that this subtype will still + * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch + * to this subtype while an IME is shown. The framework will never switch the current IME to + * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}. + * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as + * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input). * @hide */ public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue, @@ -74,16 +83,28 @@ public final class InputMethodSubtype implements Parcelable { } /** - * Constructor - * @param nameId The name of the subtype - * @param iconId The icon of the subtype + * Constructor. + * @param nameId Resource ID of the subtype name string. The string resource may have exactly + * one %s in it. If there is, the %s part will be replaced with the locale's display name by + * the formatter. Please refer to {@link #getDisplayName} for details. + * @param iconId Resource ID of the subtype icon drawable. * @param locale The locale supported by the subtype * @param mode The mode supported by the subtype - * @param extraValue The extra value of the subtype - * @param isAuxiliary true when this subtype is one shot subtype. - * @param overridesImplicitlyEnabledSubtype true when this subtype should be selected by default - * if no other subtypes are selected explicitly. Note that a subtype with this parameter being - * true will not be shown in the subtypes list. + * @param extraValue The extra value of the subtype. This string is free-form, but the API + * supplies tools to deal with a key-value comma-separated list; see + * {@link #containsExtraValueKey} and {@link #getExtraValueOf}. + * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary + * subtype will not be shown in the list of enabled IMEs for choosing the current IME in + * the Settings even when this subtype is enabled. Please note that this subtype will still + * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch + * to this subtype while an IME is shown. The framework will never switch the current IME to + * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}. + * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as + * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input). + * @param overridesImplicitlyEnabledSubtype true when this subtype should be enabled by default + * if no other subtypes in the IME are enabled explicitly. Note that a subtype with this + * parameter being true will not be shown in the list of subtypes in each IME's subtype enabler. + * Having an "automatic" subtype is an example use of this flag. */ public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue, boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) { @@ -115,52 +136,60 @@ public final class InputMethodSubtype implements Parcelable { } /** - * @return the name of the subtype + * @return Resource ID of the subtype name string. */ public int getNameResId() { return mSubtypeNameResId; } /** - * @return the icon of the subtype + * @return Resource ID of the subtype icon drawable. */ public int getIconResId() { return mSubtypeIconResId; } /** - * @return the locale of the subtype + * @return The locale of the subtype. This method returns the "locale" string parameter passed + * to the constructor. */ public String getLocale() { return mSubtypeLocale; } /** - * @return the mode of the subtype + * @return The mode of the subtype. */ public String getMode() { return mSubtypeMode; } /** - * @return the extra value of the subtype + * @return The extra value of the subtype. */ public String getExtraValue() { return mSubtypeExtraValue; } /** - * @return true if this subtype is one shot subtype. One shot subtype will not be shown in the - * ime switch list when this subtype is implicitly enabled. The framework will never - * switch the current ime to this subtype by switchToLastInputMethod in InputMethodManager. + * @return true if this subtype is auxiliary, false otherwise. An auxiliary subtype will not be + * shown in the list of enabled IMEs for choosing the current IME in the Settings even when this + * subtype is enabled. Please note that this subtype will still be shown in the list of IMEs in + * the IME switcher to allow the user to tentatively switch to this subtype while an IME is + * shown. The framework will never switch the current IME to this subtype by + * {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}. + * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as + * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input). */ public boolean isAuxiliary() { return mIsAuxiliary; } /** - * @return true when this subtype is selected by default if no other subtypes are selected - * explicitly. Note that a subtype that returns true will not be shown in the subtypes list. + * @return true when this subtype will be enabled by default if no other subtypes in the IME + * are enabled explicitly, false otherwise. Note that a subtype with this method returning true + * will not be shown in the list of subtypes in each IME's subtype enabler. Having an + * "automatic" subtype is an example use of this flag. */ public boolean overridesImplicitlyEnabledSubtype() { return mOverridesImplicitlyEnabledSubtype; @@ -171,10 +200,10 @@ public final class InputMethodSubtype implements Parcelable { * @param packageName The package name of the IME * @param appInfo The application info of the IME * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId) - * can have only one %s in it. If there is, the %s part will be replaced with the locale's - * display name by the formatter. If there is not, this method simply returns the string - * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the - * framework to generate an appropriate display name. + * may have exactly one %s in it. If there is, the %s part will be replaced with the locale's + * display name by the formatter. If there is not, this method returns the string specified by + * mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the framework to + * generate an appropriate display name. */ public CharSequence getDisplayName( Context context, String packageName, ApplicationInfo appInfo) { @@ -215,8 +244,8 @@ public final class InputMethodSubtype implements Parcelable { /** * The string of ExtraValue in subtype should be defined as follows: * example: key0,key1=value1,key2,key3,key4=value4 - * @param key the key of extra value - * @return the subtype contains specified the extra value + * @param key The key of extra value + * @return The subtype contains specified the extra value */ public boolean containsExtraValueKey(String key) { return getExtraValueHashMap().containsKey(key); @@ -225,8 +254,8 @@ public final class InputMethodSubtype implements Parcelable { /** * The string of ExtraValue in subtype should be defined as follows: * example: key0,key1=value1,key2,key3,key4=value4 - * @param key the key of extra value - * @return the value of the specified key + * @param key The key of extra value + * @return The value of the specified key */ public String getExtraValueOf(String key) { return getExtraValueHashMap().get(key); diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java index ca6577f..f6418ce 100644 --- a/core/java/android/view/textservice/SpellCheckerSession.java +++ b/core/java/android/view/textservice/SpellCheckerSession.java @@ -91,14 +91,17 @@ public class SpellCheckerSession { * This meta-data must reference an XML resource. **/ public static final String SERVICE_META_DATA = "android.view.textservice.scs"; + private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck"; private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; + private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; private final InternalListener mInternalListener; private final ITextServicesManager mTextServicesManager; private final SpellCheckerInfo mSpellCheckerInfo; private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; + private final SpellCheckerSubtype mSubtype; private boolean mIsUsed; private SpellCheckerSessionListener mSpellCheckerSessionListener; @@ -111,6 +114,9 @@ public class SpellCheckerSession { case MSG_ON_GET_SUGGESTION_MULTIPLE: handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); break; + case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: + handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj); + break; } } }; @@ -120,7 +126,8 @@ public class SpellCheckerSession { * @hide */ public SpellCheckerSession( - SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { + SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener, + SpellCheckerSubtype subtype) { if (info == null || listener == null || tsm == null) { throw new NullPointerException(); } @@ -130,6 +137,7 @@ public class SpellCheckerSession { mTextServicesManager = tsm; mIsUsed = true; mSpellCheckerSessionListener = listener; + mSubtype = subtype; } /** @@ -170,6 +178,14 @@ public class SpellCheckerSession { } /** + * @hide + */ + public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) { + mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence( + new TextInfo[] {textInfo}, suggestionsLimit); + } + + /** * Get candidate strings for a substring of the specified text. * @param textInfo text metadata for a spell checker * @param suggestionsLimit the number of limit of suggestions returned @@ -198,10 +214,15 @@ public class SpellCheckerSession { mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); } + private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) { + mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos); + } + private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { private static final int TASK_CANCEL = 1; private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; private static final int TASK_CLOSE = 3; + private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<SpellCheckerParams>(); private Handler mHandler; @@ -256,6 +277,20 @@ public class SpellCheckerSession { Log.e(TAG, "Failed to get suggestions " + e); } break; + case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: + if (DBG) { + Log.w(TAG, "Get suggestions from the spell checker."); + } + if (scp.mTextInfos.length != 1) { + throw new IllegalArgumentException(); + } + try { + session.onGetSuggestionsMultipleForSentence( + scp.mTextInfos, scp.mSuggestionsLimit); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get suggestions " + e); + } + break; case TASK_CLOSE: if (DBG) { Log.w(TAG, "Close spell checker tasks."); @@ -331,6 +366,15 @@ public class SpellCheckerSession { suggestionsLimit, sequentialWords)); } + public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) { + if (DBG) { + Log.w(TAG, "getSuggestionsMultipleForSentence"); + } + processOrEnqueueTask( + new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, + textInfos, suggestionsLimit, false)); + } + public void close() { if (DBG) { Log.w(TAG, "close"); @@ -380,6 +424,12 @@ public class SpellCheckerSession { } } } + + @Override + public void onGetSuggestionsForSentence(SuggestionsInfo[] results) { + mHandler.sendMessage( + Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); + } } /** @@ -391,6 +441,10 @@ public class SpellCheckerSession { * @param results an array of results of getSuggestions */ public void onGetSuggestions(SuggestionsInfo[] results); + /** + * @hide + */ + public void onGetSuggestionsForSentence(SuggestionsInfo[] results); } private static class InternalListener extends ITextServicesSessionListener.Stub { @@ -432,4 +486,11 @@ public class SpellCheckerSession { public ISpellCheckerSessionListener getSpellCheckerSessionListener() { return mSpellCheckerSessionListenerImpl; } + + /** + * @hide + */ + public boolean isSentenceSpellCheckSupported() { + return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK); + } } diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.java b/core/java/android/view/textservice/SpellCheckerSubtype.java index aeb3ba6..f235295 100644 --- a/core/java/android/view/textservice/SpellCheckerSubtype.java +++ b/core/java/android/view/textservice/SpellCheckerSubtype.java @@ -21,9 +21,11 @@ import android.content.pm.ApplicationInfo; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Slog; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -33,11 +35,15 @@ import java.util.Locale; * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings. */ public final class SpellCheckerSubtype implements Parcelable { + private static final String TAG = SpellCheckerSubtype.class.getSimpleName(); + private static final String EXTRA_VALUE_PAIR_SEPARATOR = ","; + private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "="; private final int mSubtypeHashCode; private final int mSubtypeNameResId; private final String mSubtypeLocale; private final String mSubtypeExtraValue; + private HashMap<String, String> mExtraValueHashMapCache; /** * Constructor @@ -83,6 +89,46 @@ public final class SpellCheckerSubtype implements Parcelable { return mSubtypeExtraValue; } + private HashMap<String, String> getExtraValueHashMap() { + if (mExtraValueHashMapCache == null) { + mExtraValueHashMapCache = new HashMap<String, String>(); + final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR); + final int N = pairs.length; + for (int i = 0; i < N; ++i) { + final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR); + if (pair.length == 1) { + mExtraValueHashMapCache.put(pair[0], null); + } else if (pair.length > 1) { + if (pair.length > 2) { + Slog.w(TAG, "ExtraValue has two or more '='s"); + } + mExtraValueHashMapCache.put(pair[0], pair[1]); + } + } + } + return mExtraValueHashMapCache; + } + + /** + * The string of ExtraValue in subtype should be defined as follows: + * example: key0,key1=value1,key2,key3,key4=value4 + * @param key the key of extra value + * @return the subtype contains specified the extra value + */ + public boolean containsExtraValueKey(String key) { + return getExtraValueHashMap().containsKey(key); + } + + /** + * The string of ExtraValue in subtype should be defined as follows: + * example: key0,key1=value1,key2,key3,key4=value4 + * @param key the key of extra value + * @return the value of the specified key + */ + public String getExtraValueOf(String key) { + return getExtraValueHashMap().get(key); + } + @Override public int hashCode() { return mSubtypeHashCode; diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java index ddd0361..9b99770 100644 --- a/core/java/android/view/textservice/SuggestionsInfo.java +++ b/core/java/android/view/textservice/SuggestionsInfo.java @@ -21,11 +21,14 @@ import com.android.internal.util.ArrayUtils; import android.os.Parcel; import android.os.Parcelable; +import java.util.Arrays; + /** * This class contains a metadata of suggestions from the text service */ public final class SuggestionsInfo implements Parcelable { private static final String[] EMPTY = ArrayUtils.emptyArray(String.class); + private static final int NOT_A_LENGTH = -1; /** * Flag of the attributes of the suggestions that can be obtained by @@ -47,6 +50,8 @@ public final class SuggestionsInfo implements Parcelable { public static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = 0x0004; private final int mSuggestionsAttributes; private final String[] mSuggestions; + private final int[] mStartPosArray; + private final int[] mLengthArray; private final boolean mSuggestionsAvailable; private int mCookie; private int mSequence; @@ -57,16 +62,7 @@ public final class SuggestionsInfo implements Parcelable { * @param suggestions from the text service */ public SuggestionsInfo(int suggestionsAttributes, String[] suggestions) { - mSuggestionsAttributes = suggestionsAttributes; - if (suggestions == null) { - mSuggestions = EMPTY; - mSuggestionsAvailable = false; - } else { - mSuggestions = suggestions; - mSuggestionsAvailable = true; - } - mCookie = 0; - mSequence = 0; + this(suggestionsAttributes, suggestions, 0, 0); } /** @@ -78,12 +74,46 @@ public final class SuggestionsInfo implements Parcelable { */ public SuggestionsInfo( int suggestionsAttributes, String[] suggestions, int cookie, int sequence) { + this(suggestionsAttributes, suggestions, cookie, sequence, null, null); + } + + /** + * @hide + * Constructor. + * @param suggestionsAttributes from the text service + * @param suggestions from the text service + * @param cookie the cookie of the input TextInfo + * @param sequence the cookie of the input TextInfo + * @param startPosArray the array of start positions of suggestions + * @param lengthArray the array of length of suggestions + */ + public SuggestionsInfo( + int suggestionsAttributes, String[] suggestions, int cookie, int sequence, + int[] startPosArray, int[] lengthArray) { + final int suggestsLen; if (suggestions == null) { mSuggestions = EMPTY; mSuggestionsAvailable = false; + suggestsLen = 0; + mStartPosArray = new int[0]; + mLengthArray = new int[0]; } else { mSuggestions = suggestions; mSuggestionsAvailable = true; + suggestsLen = suggestions.length; + if (startPosArray == null || lengthArray == null) { + mStartPosArray = new int[suggestsLen]; + mLengthArray = new int[suggestsLen]; + for (int i = 0; i < suggestsLen; ++i) { + mStartPosArray[i] = 0; + mLengthArray[i] = NOT_A_LENGTH; + } + } else if (suggestsLen != startPosArray.length || suggestsLen != lengthArray.length) { + throw new IllegalArgumentException(); + } else { + mStartPosArray = Arrays.copyOf(startPosArray, suggestsLen); + mLengthArray = Arrays.copyOf(lengthArray, suggestsLen); + } } mSuggestionsAttributes = suggestionsAttributes; mCookie = cookie; @@ -96,6 +126,10 @@ public final class SuggestionsInfo implements Parcelable { mCookie = source.readInt(); mSequence = source.readInt(); mSuggestionsAvailable = source.readInt() == 1; + mStartPosArray = new int[mSuggestions.length]; + mLengthArray = new int[mSuggestions.length]; + source.readIntArray(mStartPosArray); + source.readIntArray(mLengthArray); } /** @@ -111,6 +145,8 @@ public final class SuggestionsInfo implements Parcelable { dest.writeInt(mCookie); dest.writeInt(mSequence); dest.writeInt(mSuggestionsAvailable ? 1 : 0); + dest.writeIntArray(mStartPosArray); + dest.writeIntArray(mLengthArray); } /** @@ -191,4 +227,24 @@ public final class SuggestionsInfo implements Parcelable { public int describeContents() { return 0; } + + /** + * @hide + */ + public int getSuggestionStartPosAt(int i) { + if (i >= 0 && i < mStartPosArray.length) { + return mStartPosArray[i]; + } + return -1; + } + + /** + * @hide + */ + public int getSuggestionLengthAt(int i) { + if (i >= 0 && i < mLengthArray.length) { + return mLengthArray[i]; + } + return -1; + } } diff --git a/core/java/android/view/textservice/TextServicesManager.java b/core/java/android/view/textservice/TextServicesManager.java index 69f88a5..fc59e6e 100644 --- a/core/java/android/view/textservice/TextServicesManager.java +++ b/core/java/android/view/textservice/TextServicesManager.java @@ -157,7 +157,8 @@ public final class TextServicesManager { if (subtypeInUse == null) { return null; } - final SpellCheckerSession session = new SpellCheckerSession(sci, sService, listener); + final SpellCheckerSession session = new SpellCheckerSession( + sci, sService, listener, subtypeInUse); try { sService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), session.getTextServicesSessionListener(), diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index a810cf6..6fddb1a 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -410,6 +410,7 @@ class BrowserFrame extends Handler { mCommitted = false; // remove pending draw to block update until mFirstLayoutDone is // set to true in didFirstLayout() + mWebViewCore.clearContent(); mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW); } } diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index 5d54180..d7b6adb 100755 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -27,30 +27,47 @@ import java.util.Vector; /** - * This class is used to get Geolocation permissions from, and set them on the - * WebView. For example, it could be used to allow a user to manage Geolocation - * permissions from a browser's UI. + * This class is used to manage permissions for the WebView's Geolocation + * JavaScript API. * - * Permissions are managed on a per-origin basis, as required by the - * Geolocation spec - http://dev.w3.org/geo/api/spec-source.html. An origin - * specifies the scheme, host and port of particular frame. An origin is - * represented here as a string, using the output of - * WebCore::SecurityOrigin::toString. + * Geolocation permissions are applied to an origin, which consists of the + * host, scheme and port of a URI. In order for web content to use the + * Geolocation API, permission must be granted for that content's origin. * - * This class is the Java counterpart of the WebKit C++ GeolocationPermissions - * class. It simply marshalls calls from the UI thread to the WebKit thread. + * This class stores Geolocation permissions. An origin's permission state can + * be either allowed or denied. This class uses Strings to represent + * an origin. * - * Within WebKit, Geolocation permissions may be applied either temporarily - * (for the duration of the page) or permanently. This class deals only with - * permanent permissions. + * When an origin attempts to use the Geolocation API, but no permission state + * is currently set for that origin, + * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) WebChromeClient.onGeolocationPermissionsShowPrompt()} + * is called. This allows the permission state to be set for that origin. + * + * The methods of this class can be used to modify and interrogate the stored + * Geolocation permissions at any time. */ +// This class is the Java counterpart of the WebKit C++ GeolocationPermissions +// class. It simply marshalls calls from the UI thread to the WebKit thread. +// +// Within WebKit, Geolocation permissions may be applied either temporarily +// (for the duration of the page) or permanently. This class deals only with +// permanent permissions. public final class GeolocationPermissions { /** - * Callback interface used by the browser to report a Geolocation permission - * state set by the user in response to a permissions prompt. + * A callback interface used by the host application to set the Geolocation + * permission state for an origin. */ public interface Callback { - public void invoke(String origin, boolean allow, boolean remember); + /** + * Set the Geolocation permission state for the supplied origin. + * @param origin The origin for which permissions are set. + * @param allow Whether or not the origin should be allowed to use the + * Geolocation API. + * @param retain Whether the permission should be retained beyond the + * lifetime of a page currently being displayed by a + * WebView. + */ + public void invoke(String origin, boolean allow, boolean retain); }; // Log tag @@ -82,7 +99,8 @@ public final class GeolocationPermissions { private static final String ALLOWED = "allowed"; /** - * Gets the singleton instance of the class. + * Get the singleton instance of this class. + * @return The singleton {@link GeolocationPermissions} instance. */ public static GeolocationPermissions getInstance() { if (sInstance == null) { @@ -196,15 +214,18 @@ public final class GeolocationPermissions { } /** - * Gets the set of origins for which Geolocation permissions are stored. - * Note that we represent the origins as strings. These are created using - * WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' - * (Database, Geolocation etc) do so, it's safe to match up origins based - * on this string. - * - * Callback is a ValueCallback object whose onReceiveValue method will be - * called asynchronously with the set of origins. + * Get the set of origins for which Geolocation permissions are stored. + * @param callback A {@link ValueCallback} to receive the result of this + * request. This object's + * {@link ValueCallback#onReceiveValue(T) onReceiveValue()} + * method will be invoked asynchronously with a set of + * Strings containing the origins for which Geolocation + * permissions are stored. */ + // Note that we represent the origins as strings. These are created using + // WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' + // (Database, Geolocation etc) do so, it's safe to match up origins based + // on this string. public void getOrigins(ValueCallback<Set<String> > callback) { if (callback != null) { if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { @@ -217,10 +238,14 @@ public final class GeolocationPermissions { } /** - * Gets the permission state for the specified origin. - * - * Callback is a ValueCallback object whose onReceiveValue method will be - * called asynchronously with the permission state for the origin. + * Get the Geolocation permission state for the specified origin. + * @param origin The origin for which Geolocation permission is requested. + * @param callback A {@link ValueCallback} to receive the result of this + * request. This object's + * {@link ValueCallback#onReceiveValue(T) onReceiveValue()} + * method will be invoked asynchronously with a boolean + * indicating whether or not the origin can use the + * Geolocation API. */ public void getAllowed(String origin, ValueCallback<Boolean> callback) { if (callback == null) { @@ -242,27 +267,31 @@ public final class GeolocationPermissions { } /** - * Clears the permission state for the specified origin. This method may be - * called before the WebKit thread has intialized the message handler. - * Messages will be queued until this time. + * Clear the Geolocation permission state for the specified origin. + * @param origin The origin for which Geolocation permissions are cleared. */ + // This method may be called before the WebKit + // thread has intialized the message handler. Messages will be queued until + // this time. public void clear(String origin) { // Called on the UI thread. postMessage(Message.obtain(null, CLEAR, origin)); } /** - * Allows the specified origin. This method may be called before the WebKit - * thread has intialized the message handler. Messages will be queued until - * this time. + * Allow the specified origin to use the Geolocation API. + * @param origin The origin for which Geolocation API use is allowed. */ + // This method may be called before the WebKit + // thread has intialized the message handler. Messages will be queued until + // this time. public void allow(String origin) { // Called on the UI thread. postMessage(Message.obtain(null, ALLOW, origin)); } /** - * Clears the permission state for all origins. + * Clear the Geolocation permission state for all origins. */ public void clearAll() { // Called on the UI thread. diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index e1eff58..bc0557e 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -116,13 +116,12 @@ public class HTML5VideoFullScreen extends HTML5VideoView return mVideoSurfaceView; } - HTML5VideoFullScreen(Context context, int videoLayerId, int position, - boolean autoStart) { + HTML5VideoFullScreen(Context context, int videoLayerId, int position) { mVideoSurfaceView = new VideoSurfaceView(context); mFullScreenMode = FULLSCREEN_OFF; mVideoWidth = 0; mVideoHeight = 0; - init(videoLayerId, position, autoStart); + init(videoLayerId, position); } private void setMediaController(MediaController m) { @@ -186,11 +185,6 @@ public class HTML5VideoFullScreen extends HTML5VideoView // after reading the MetaData if (mMediaController != null) { mMediaController.setEnabled(true); - // If paused , should show the controller for ever! - if (getAutostart()) - mMediaController.show(); - else - mMediaController.show(0); } if (mProgressView != null) { @@ -201,6 +195,13 @@ public class HTML5VideoFullScreen extends HTML5VideoView mVideoHeight = mp.getVideoHeight(); // This will trigger the onMeasure to get the display size right. mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); + // Call into the native to ask for the state, if still in play mode, + // this will trigger the video to play. + mProxy.dispatchOnRestoreState(); + + if (getStartWhenPrepared()) { + mPlayer.start(); + } } public boolean fullScreenExited() { diff --git a/core/java/android/webkit/HTML5VideoInline.java b/core/java/android/webkit/HTML5VideoInline.java index fe5908e..2d5b263 100644 --- a/core/java/android/webkit/HTML5VideoInline.java +++ b/core/java/android/webkit/HTML5VideoInline.java @@ -34,9 +34,8 @@ public class HTML5VideoInline extends HTML5VideoView{ } } - HTML5VideoInline(int videoLayerId, int position, - boolean autoStart) { - init(videoLayerId, position, autoStart); + HTML5VideoInline(int videoLayerId, int position) { + init(videoLayerId, position); mTextureNames = null; } diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java index 67660b8..73166cb 100644 --- a/core/java/android/webkit/HTML5VideoView.java +++ b/core/java/android/webkit/HTML5VideoView.java @@ -52,10 +52,6 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { // Switching between inline and full screen will also create a new instance. protected MediaPlayer mPlayer; - // This will be set up every time we create the Video View object. - // Set to true only when switching into full screen while playing - protected boolean mAutostart; - // We need to save such info. protected Uri mUri; protected Map<String, String> mHeaders; @@ -141,22 +137,17 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { } } - public boolean getAutostart() { - return mAutostart; - } - public boolean getPauseDuringPreparing() { return mPauseDuringPreparing; } // Every time we start a new Video, we create a VideoView and a MediaPlayer - public void init(int videoLayerId, int position, boolean autoStart) { + public void init(int videoLayerId, int position) { mPlayer = new MediaPlayer(); mCurrentState = STATE_INITIALIZED; mProxy = null; mVideoLayerId = videoLayerId; mSaveSeekTime = position; - mAutostart = autoStart; mTimer = null; mPauseDuringPreparing = false; } @@ -203,20 +194,9 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { mPlayer.setOnInfoListener(proxy); } - // Normally called immediately after setVideoURI. But for full screen, - // this should be after surface holder created - public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) { - // SurfaceTexture will be created lazily here for inline mode - decideDisplayMode(); - - setOnCompletionListener(proxy); - setOnPreparedListener(proxy); - setOnErrorListener(proxy); - setOnInfoListener(proxy); - // When there is exception, we could just bail out silently. - // No Video will be played though. Write the stack for debug + public void prepareDataCommon(HTML5VideoViewProxy proxy) { try { - mPlayer.setDataSource(mProxy.getContext(), mUri, mHeaders); + mPlayer.setDataSource(proxy.getContext(), mUri, mHeaders); mPlayer.prepareAsync(); } catch (IllegalArgumentException e) { e.printStackTrace(); @@ -228,6 +208,25 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { mCurrentState = STATE_NOTPREPARED; } + public void reprepareData(HTML5VideoViewProxy proxy) { + mPlayer.reset(); + prepareDataCommon(proxy); + } + + // Normally called immediately after setVideoURI. But for full screen, + // this should be after surface holder created + public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) { + // SurfaceTexture will be created lazily here for inline mode + decideDisplayMode(); + + setOnCompletionListener(proxy); + setOnPreparedListener(proxy); + setOnErrorListener(proxy); + setOnInfoListener(proxy); + + prepareDataCommon(proxy); + } + // Common code public int getVideoLayerId() { @@ -333,4 +332,14 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { return false; } + private boolean m_startWhenPrepared = false; + + public void setStartWhenPrepared(boolean willPlay) { + m_startWhenPrepared = willPlay; + } + + public boolean getStartWhenPrepared() { + return m_startWhenPrepared; + } + } diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index d0237b5..d306c86 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -66,6 +66,7 @@ class HTML5VideoViewProxy extends Handler private static final int POSTER_FETCHED = 202; private static final int PAUSED = 203; private static final int STOPFULLSCREEN = 204; + private static final int RESTORESTATE = 205; // Timer thread -> UI thread private static final int TIMEUPDATE = 300; @@ -144,19 +145,16 @@ class HTML5VideoViewProxy extends Handler HTML5VideoViewProxy proxy, WebView webView) { // Save the inline video info and inherit it in the full screen int savePosition = 0; - boolean savedIsPlaying = false; if (mHTML5VideoView != null) { // If we are playing the same video, then it is better to // save the current position. if (layerId == mHTML5VideoView.getVideoLayerId()) { savePosition = mHTML5VideoView.getCurrentPosition(); - savedIsPlaying = mHTML5VideoView.isPlaying(); } - mHTML5VideoView.pauseAndDispatch(mCurrentProxy); mHTML5VideoView.release(); } mHTML5VideoView = new HTML5VideoFullScreen(proxy.getContext(), - layerId, savePosition, savedIsPlaying); + layerId, savePosition); mCurrentProxy = proxy; mHTML5VideoView.setVideoURI(url, mCurrentProxy); @@ -164,6 +162,16 @@ class HTML5VideoViewProxy extends Handler mHTML5VideoView.enterFullScreenVideoState(layerId, proxy, webView); } + public static void exitFullScreenVideo(HTML5VideoViewProxy proxy, + WebView webView) { + if (!mHTML5VideoView.fullScreenExited() && mHTML5VideoView.isFullScreenMode()) { + WebChromeClient client = webView.getWebChromeClient(); + if (client != null) { + client.onHideCustomView(); + } + } + } + // This is on the UI thread. // When native tell Java to play, we need to check whether or not it is // still the same video by using videoLayerId and treat it differently. @@ -174,6 +182,21 @@ class HTML5VideoViewProxy extends Handler if (mHTML5VideoView != null) { currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); backFromFullScreenMode = mHTML5VideoView.fullScreenExited(); + + // When playing video back to back in full screen mode, + // javascript will switch the src and call play. + // In this case, we can just reuse the same full screen view, + // and play the video after prepared. + if (mHTML5VideoView.isFullScreenMode() + && !backFromFullScreenMode + && currentVideoLayerId != videoLayerId + && mCurrentProxy != proxy) { + mCurrentProxy = proxy; + mHTML5VideoView.setStartWhenPrepared(true); + mHTML5VideoView.setVideoURI(url, proxy); + mHTML5VideoView.reprepareData(proxy); + return; + } } if (backFromFullScreenMode @@ -192,7 +215,7 @@ class HTML5VideoViewProxy extends Handler mHTML5VideoView.release(); } mCurrentProxy = proxy; - mHTML5VideoView = new HTML5VideoInline(videoLayerId, time, false); + mHTML5VideoView = new HTML5VideoInline(videoLayerId, time); mHTML5VideoView.setVideoURI(url, mCurrentProxy); mHTML5VideoView.prepareDataAndDisplayMode(proxy); @@ -235,7 +258,7 @@ class HTML5VideoViewProxy extends Handler } public static void onPrepared() { - if (!mHTML5VideoView.isFullScreenMode() || mHTML5VideoView.getAutostart()) { + if (!mHTML5VideoView.isFullScreenMode()) { mHTML5VideoView.start(); } if (mBaseLayer != 0) { @@ -297,6 +320,11 @@ class HTML5VideoViewProxy extends Handler mWebCoreHandler.sendMessage(msg); } + public void dispatchOnRestoreState() { + Message msg = Message.obtain(mWebCoreHandler, RESTORESTATE); + mWebCoreHandler.sendMessage(msg); + } + public void onTimeupdate() { sendMessage(obtainMessage(TIMEUPDATE)); } @@ -569,6 +597,9 @@ class HTML5VideoViewProxy extends Handler case STOPFULLSCREEN: nativeOnStopFullscreen(mNativePointer); break; + case RESTORESTATE: + nativeOnRestoreState(mNativePointer); + break; } } }; @@ -676,6 +707,10 @@ class HTML5VideoViewProxy extends Handler VideoPlayer.enterFullScreenVideo(layerId, url, this, mWebView); } + public void exitFullScreenVideo() { + VideoPlayer.exitFullScreenVideo(this, mWebView); + } + /** * The factory for HTML5VideoViewProxy instances. * @param webViewCore is the WebViewCore that is requesting the proxy. @@ -696,6 +731,7 @@ class HTML5VideoViewProxy extends Handler private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); private native void nativeOnTimeupdate(int position, int nativePointer); private native void nativeOnStopFullscreen(int nativePointer); + private native void nativeOnRestoreState(int nativePointer); private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture, int baseLayer, int videoLayerId, int textureName, int playerState); diff --git a/core/java/android/webkit/HttpAuthHandlerImpl.java b/core/java/android/webkit/HttpAuthHandlerImpl.java index ac05125..01e8eb8 100644 --- a/core/java/android/webkit/HttpAuthHandlerImpl.java +++ b/core/java/android/webkit/HttpAuthHandlerImpl.java @@ -270,7 +270,6 @@ class HttpAuthHandlerImpl extends HttpAuthHandler { /** * Informs the WebView of a new set of credentials. - * @hide Pending API council review */ public static void onReceivedCredentials(LoadListener loader, String host, String realm, String username, String password) { diff --git a/core/java/android/webkit/JniUtil.java b/core/java/android/webkit/JniUtil.java index 4662040..7b44938 100644 --- a/core/java/android/webkit/JniUtil.java +++ b/core/java/android/webkit/JniUtil.java @@ -91,6 +91,16 @@ class JniUtil { return sCacheDirectory; } + /** + * Called by JNI. Gets the application's package name. + * @return String The application's package name + */ + private static synchronized String getPackageName() { + checkInitialized(); + + return sContext.getPackageName(); + } + private static final String ANDROID_CONTENT = "content:"; /** diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index 30bbb04..ee9b949 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -169,7 +169,9 @@ class Network { if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) return; - NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + final ConnectivityManager connManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo info = connManager.getActiveNetworkInfo(); if (info != null) mRoaming = info.isRoaming(); }; diff --git a/core/java/android/webkit/SearchBox.java b/core/java/android/webkit/SearchBox.java index 6512c4b..38a1740 100644 --- a/core/java/android/webkit/SearchBox.java +++ b/core/java/android/webkit/SearchBox.java @@ -29,7 +29,7 @@ import java.util.List; * SearchBox.query() and receive suggestions by registering a listener on the * SearchBox object. * - * @hide pending API council approval. + * @hide */ public interface SearchBox { /** diff --git a/core/java/android/webkit/SelectActionModeCallback.java b/core/java/android/webkit/SelectActionModeCallback.java index 8c174aa..cdf20f6 100644 --- a/core/java/android/webkit/SelectActionModeCallback.java +++ b/core/java/android/webkit/SelectActionModeCallback.java @@ -17,6 +17,7 @@ package android.webkit; import android.app.SearchManager; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.provider.Browser; @@ -27,11 +28,16 @@ import android.view.MenuItem; class SelectActionModeCallback implements ActionMode.Callback { private WebView mWebView; private ActionMode mActionMode; + private boolean mIsTextSelected = true; void setWebView(WebView webView) { mWebView = webView; } + void setTextSelected(boolean isTextSelected) { + mIsTextSelected = isTextSelected; + } + void finish() { // It is possible that onCreateActionMode was never called, in the case // where there is no ActionBar, for example. @@ -52,17 +58,25 @@ class SelectActionModeCallback implements ActionMode.Callback { mode.setTitle(allowText ? context.getString(com.android.internal.R.string.textSelectionCABTitle) : null); - if (!mode.isUiFocusable()) { - // If the action mode UI we're running in isn't capable of taking window focus - // the user won't be able to type into the find on page UI. Disable this functionality. - // (Note that this should only happen in floating dialog windows.) - // This can be removed once we can handle multiple focusable windows at a time - // in a better way. - final MenuItem findOnPageItem = menu.findItem(com.android.internal.R.id.find); - if (findOnPageItem != null) { - findOnPageItem.setVisible(false); - } - } + // If the action mode UI we're running in isn't capable of taking window focus + // the user won't be able to type into the find on page UI. Disable this functionality. + // (Note that this should only happen in floating dialog windows.) + // This can be removed once we can handle multiple focusable windows at a time + // in a better way. + ClipboardManager cm = (ClipboardManager)(context + .getSystemService(Context.CLIPBOARD_SERVICE)); + boolean isFocusable = mode.isUiFocusable(); + boolean isEditable = mWebView.focusCandidateIsEditableText(); + boolean canPaste = isEditable && cm.hasPrimaryClip() && isFocusable; + boolean canFind = !isEditable && isFocusable; + boolean canCut = isEditable && mIsTextSelected && isFocusable; + boolean canCopy = mIsTextSelected; + boolean canWebSearch = mIsTextSelected; + setMenuVisibility(menu, canFind, com.android.internal.R.id.find); + setMenuVisibility(menu, canPaste, com.android.internal.R.id.paste); + setMenuVisibility(menu, canCut, com.android.internal.R.id.cut); + setMenuVisibility(menu, canCopy, com.android.internal.R.id.copy); + setMenuVisibility(menu, canWebSearch, com.android.internal.R.id.websearch); mActionMode = mode; return true; } @@ -75,11 +89,21 @@ class SelectActionModeCallback implements ActionMode.Callback { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch(item.getItemId()) { + case android.R.id.cut: + mWebView.cutSelection(); + mode.finish(); + break; + case android.R.id.copy: mWebView.copySelection(); mode.finish(); break; + case android.R.id.paste: + mWebView.pasteFromClipboard(); + mode.finish(); + break; + case com.android.internal.R.id.share: String selection = mWebView.getSelection(); Browser.sendString(mWebView.getContext(), selection); @@ -113,4 +137,11 @@ class SelectActionModeCallback implements ActionMode.Callback { public void onDestroyActionMode(ActionMode mode) { mWebView.selectionDone(); } + + private void setMenuVisibility(Menu menu, boolean visible, int resourceId) { + final MenuItem item = menu.findItem(resourceId); + if (item != null) { + item.setVisible(visible); + } + } } diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java index 1a167e8..5c7d97f 100644 --- a/core/java/android/webkit/ValueCallback.java +++ b/core/java/android/webkit/ValueCallback.java @@ -17,11 +17,12 @@ package android.webkit; /** - * A callback interface used to returns values asynchronously + * A callback interface used to provide values asynchronously. */ public interface ValueCallback<T> { /** - * Invoked when we have the result + * Invoked when the value is available. + * @param value The value. */ public void onReceiveValue(T value); }; diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 3d129f7..a6ef0ce 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -250,14 +250,24 @@ public class WebChromeClient { } /** - * Instructs the client to show a prompt to ask the user to set the - * Geolocation permission state for the specified origin. + * Notify the host application that web content from the specified origin + * is attempting to use the Geolocation API, but no permission state is + * currently set for that origin. The host application should invoke the + * specified callback with the desired permission state. See + * {@link GeolocationPermissions} for details. + * @param origin The origin of the web content attempting to use the + * Geolocation API. + * @param callback The callback to use to set the permission state for the + * origin. */ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {} /** - * Instructs the client to hide the Geolocation permissions prompt. + * Notify the host application that a request for Geolocation permissions, + * made with a previous call to + * {@link #onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) onGeolocationPermissionsShowPrompt()} + * has been canceled. Any related UI should therefore be hidden. */ public void onGeolocationPermissionsHidePrompt() {} diff --git a/core/java/android/webkit/WebCoreThreadWatchdog.java b/core/java/android/webkit/WebCoreThreadWatchdog.java new file mode 100644 index 0000000..d100260 --- /dev/null +++ b/core/java/android/webkit/WebCoreThreadWatchdog.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2012 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 android.webkit; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.webkit.WebViewCore.EventHub; + +// A Runnable that will monitor if the WebCore thread is still +// processing messages by pinging it every so often. It is safe +// to call the public methods of this class from any thread. +class WebCoreThreadWatchdog implements Runnable { + + // A message with this id is sent by the WebCore thread to notify the + // Watchdog that the WebCore thread is still processing messages + // (i.e. everything is OK). + private static final int IS_ALIVE = 100; + + // This message is placed in the Watchdog's queue and removed when we + // receive an IS_ALIVE. If it is ever processed, we consider the + // WebCore thread unresponsive. + private static final int TIMED_OUT = 101; + + // Message to tell the Watchdog thread to terminate. + private static final int QUIT = 102; + + // Wait 10s after hearing back from the WebCore thread before checking it's still alive. + private static final int HEARTBEAT_PERIOD = 10 * 1000; + + // If there's no callback from the WebCore thread for 30s, prompt the user the page has + // become unresponsive. + private static final int TIMEOUT_PERIOD = 30 * 1000; + + // After the first timeout, use a shorter period before re-prompting the user. + private static final int SUBSEQUENT_TIMEOUT_PERIOD = 15 * 1000; + + private Context mContext; + private Handler mWebCoreThreadHandler; + private Handler mHandler; + private boolean mPaused; + private boolean mPendingQuit; + + private static WebCoreThreadWatchdog sInstance; + + public synchronized static WebCoreThreadWatchdog start(Context context, + Handler webCoreThreadHandler) { + if (sInstance == null) { + sInstance = new WebCoreThreadWatchdog(context, webCoreThreadHandler); + new Thread(sInstance, "WebCoreThreadWatchdog").start(); + } + return sInstance; + } + + public synchronized static void updateContext(Context context) { + if (sInstance != null) { + sInstance.setContext(context); + } + } + + public synchronized static void pause() { + if (sInstance != null) { + sInstance.pauseWatchdog(); + } + } + + public synchronized static void resume() { + if (sInstance != null) { + sInstance.resumeWatchdog(); + } + } + + public synchronized static void quit() { + if (sInstance != null) { + sInstance.quitWatchdog(); + } + } + + private void setContext(Context context) { + mContext = context; + } + + private WebCoreThreadWatchdog(Context context, Handler webCoreThreadHandler) { + mContext = context; + mWebCoreThreadHandler = webCoreThreadHandler; + } + + private void quitWatchdog() { + if (mHandler == null) { + // The thread hasn't started yet, so set a flag to stop it starting. + mPendingQuit = true; + return; + } + // Clear any pending messages, and then post a quit to the WatchDog handler. + mHandler.removeMessages(TIMED_OUT); + mHandler.removeMessages(IS_ALIVE); + mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT); + mHandler.obtainMessage(QUIT).sendToTarget(); + } + + private void pauseWatchdog() { + mPaused = true; + + if (mHandler == null) { + return; + } + + mHandler.removeMessages(TIMED_OUT); + mHandler.removeMessages(IS_ALIVE); + mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT); + } + + private void resumeWatchdog() { + if (!mPaused) { + // Do nothing if we get a call to resume without being paused. + // This can happen during the initialisation of the WebView. + return; + } + + mPaused = false; + + if (mHandler == null) { + return; + } + + mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT, + mHandler.obtainMessage(IS_ALIVE)).sendToTarget(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD); + } + + private boolean createHandler() { + synchronized (WebCoreThreadWatchdog.class) { + if (mPendingQuit) { + return false; + } + + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case IS_ALIVE: + synchronized(WebCoreThreadWatchdog.class) { + if (mPaused) { + return; + } + + // The WebCore thread still seems alive. Reset the countdown timer. + removeMessages(TIMED_OUT); + sendMessageDelayed(obtainMessage(TIMED_OUT), TIMEOUT_PERIOD); + mWebCoreThreadHandler.sendMessageDelayed( + mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT, + mHandler.obtainMessage(IS_ALIVE)), + HEARTBEAT_PERIOD); + } + break; + + case TIMED_OUT: + new AlertDialog.Builder(mContext) + .setMessage(com.android.internal.R.string.webpage_unresponsive) + .setPositiveButton(com.android.internal.R.string.force_close, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // User chose to force close. + Process.killProcess(Process.myPid()); + } + }) + .setNegativeButton(com.android.internal.R.string.wait, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The user chose to wait. The last HEARTBEAT message + // will still be in the WebCore thread's queue, so all + // we need to do is post another TIMED_OUT so that the + // user will get prompted again if the WebCore thread + // doesn't sort itself out. + sendMessageDelayed(obtainMessage(TIMED_OUT), + SUBSEQUENT_TIMEOUT_PERIOD); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + sendMessageDelayed(obtainMessage(TIMED_OUT), + SUBSEQUENT_TIMEOUT_PERIOD); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + + case QUIT: + Looper.myLooper().quit(); + break; + } + } + }; + + return true; + } + } + + @Override + public void run() { + Looper.prepare(); + + if (!createHandler()) { + return; + } + + // Send the initial control to WebViewCore and start the timeout timer as long as we aren't + // paused. + synchronized (WebCoreThreadWatchdog.class) { + if (!mPaused) { + mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT, + mHandler.obtainMessage(IS_ALIVE)).sendToTarget(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD); + } + } + + Looper.loop(); + } +} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index f947f95..b4c38db 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -142,7 +142,7 @@ public class WebSettings { } // TODO: Keep this up to date - private static final String PREVIOUS_VERSION = "3.1"; + private static final String PREVIOUS_VERSION = "4.0.3"; // WebView associated with this WebSettings. private WebView mWebView; @@ -201,7 +201,7 @@ public class WebSettings { private boolean mXSSAuditorEnabled = false; // HTML5 configuration parameters private long mAppCacheMaxSize = Long.MAX_VALUE; - private String mAppCachePath = ""; + private String mAppCachePath = null; private String mDatabasePath = ""; // The WebCore DatabaseTracker only allows the database path to be set // once. Keep track of when the path has been set. @@ -535,21 +535,6 @@ public class WebSettings { } /** - * If WebView only supports touch, a different navigation model will be - * applied. Otherwise, the navigation to support both touch and keyboard - * will be used. - * @hide - public void setSupportTouchOnly(boolean touchOnly) { - mSupportTounchOnly = touchOnly; - } - */ - - boolean supportTouchOnly() { - // for debug only, use mLightTouchEnabled for mSupportTounchOnly - return mLightTouchEnabled; - } - - /** * Set whether the WebView supports zoom */ public void setSupportZoom(boolean support) { @@ -1370,8 +1355,8 @@ public class WebSettings { } /** - * Tell the WebView to enable Application Caches API. - * @param flag True if the WebView should enable Application Caches. + * Enable or disable the Application Cache API. + * @param flag Whether to enable the Application Cache API. */ public synchronized void setAppCacheEnabled(boolean flag) { if (mAppCacheEnabled != flag) { @@ -1381,15 +1366,19 @@ public class WebSettings { } /** - * Set a custom path to the Application Caches files. The client - * must ensure it exists before this call. - * @param appCachePath String path to the directory containing Application - * Caches files. The appCache path can be the empty string but should not - * be null. Passing null for this parameter will result in a no-op. + * Set the path used by the Application Cache API to store files. This + * setting is applied to all WebViews in the application. In order for the + * Application Cache API to function, this method must be called with a + * path which exists and is writable by the application. This method may + * only be called once: repeated calls are ignored. + * @param path Path to the directory that should be used to store Application + * Cache files. */ - public synchronized void setAppCachePath(String appCachePath) { - if (appCachePath != null && !appCachePath.equals(mAppCachePath)) { - mAppCachePath = appCachePath; + public synchronized void setAppCachePath(String path) { + // We test for a valid path and for repeated setting on the native + // side, but we can avoid syncing in some simple cases. + if (mAppCachePath == null && path != null && !path.isEmpty()) { + mAppCachePath = path; postSync(); } } @@ -1459,7 +1448,7 @@ public class WebSettings { * @param flag True if the WebView should enable WebWorkers. * Note that this flag only affects V8. JSC does not have * an equivalent setting. - * @hide pending api council approval + * @hide */ public synchronized void setWorkersEnabled(boolean flag) { if (mWorkersEnabled != flag) { @@ -1710,7 +1699,7 @@ public class WebSettings { * Specify the maximum decoded image size. The default is * 2 megs for small memory devices and 8 megs for large memory devices. * @param size The maximum decoded size, or zero to set to the default. - * @hide pending api council approval + * @hide */ public void setMaximumDecodedImageSize(long size) { if (mMaximumDecodedImageSize != size) { diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java index 8eb1524..c079404 100644 --- a/core/java/android/webkit/WebStorage.java +++ b/core/java/android/webkit/WebStorage.java @@ -362,7 +362,7 @@ public final class WebStorage { /** * Sets the maximum size of the ApplicationCache. * This should only ever be called on the WebKit thread. - * @hide Pending API council approval + * @hide */ public void setAppCacheMaximumSize(long size) { nativeSetAppCacheMaximumSize(size); diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 24eebd7..cc8eef2 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -20,6 +20,7 @@ import android.annotation.Widget; import android.app.ActivityManager; import android.app.AlertDialog; import android.content.BroadcastReceiver; +import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentCallbacks2; import android.content.Context; @@ -27,7 +28,6 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.DataSetObserver; @@ -57,8 +57,13 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.StrictMode; +import android.os.SystemClock; import android.provider.Settings; import android.speech.tts.TextToSpeech; +import android.text.Editable; +import android.text.InputType; +import android.text.Selection; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; @@ -83,6 +88,7 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; @@ -91,6 +97,7 @@ import android.webkit.WebViewCore.DrawData; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TouchEventData; import android.webkit.WebViewCore.TouchHighlightData; +import android.webkit.WebViewCore.WebKitHitTest; import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; @@ -122,11 +129,6 @@ import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import static javax.microedition.khronos.egl.EGL10.*; - /** * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. @@ -206,10 +208,10 @@ import static javax.microedition.khronos.egl.EGL10.*; * <li>Modifying the {@link android.webkit.WebSettings}, such as * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) * setJavaScriptEnabled()}. </li> - * <li>Adding JavaScript-to-Java interfaces with the {@link - * android.webkit.WebView#addJavascriptInterface} method. - * This lets you bind Java objects into the WebView so they can be - * controlled from the web pages JavaScript.</li> + * <li>Injecting Java objects into the WebView using the + * {@link android.webkit.WebView#addJavascriptInterface} method. This + * method allows you to inject Java objects into a page's JavaScript + * context, so that they can be accessed by JavaScript in the page.</li> * </ul> * * <p>Here's a more complicated example, showing error handling, @@ -324,6 +326,15 @@ import static javax.microedition.khronos.egl.EGL10.*; * property to {@code device-dpi}. This stops Android from performing scaling in your web page and * allows you to make the necessary adjustments for each density via CSS and JavaScript.</p> * + * <h3>HTML5 Video support</h3> + * + * <p>In order to support inline HTML5 video in your application, you need to have hardware + * acceleration turned on, and set a {@link android.webkit.WebChromeClient}. For full screen support, + * implementations of {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} + * and {@link WebChromeClient#onHideCustomView()} are required, + * {@link WebChromeClient#getVideoLoadingProgressView()} is optional. + * </p> + * * */ @Widget @@ -332,6 +343,7 @@ public class WebView extends AbsoluteLayout ViewGroup.OnHierarchyChangeListener { private class InnerGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener { + @Override public void onGlobalLayout() { if (isShown()) { setGLRectViewport(); @@ -340,6 +352,7 @@ public class WebView extends AbsoluteLayout } private class InnerScrollChangedListener implements ViewTreeObserver.OnScrollChangedListener { + @Override public void onScrollChanged() { if (isShown()) { setGLRectViewport(); @@ -347,6 +360,130 @@ public class WebView extends AbsoluteLayout } } + /** + * InputConnection used for ContentEditable. This captures changes + * to the text and sends them either as key strokes or text changes. + */ + private class WebViewInputConnection extends BaseInputConnection { + // Used for mapping characters to keys typed. + private KeyCharacterMap mKeyCharacterMap; + + public WebViewInputConnection() { + super(WebView.this, true); + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + Editable editable = getEditable(); + int start = getComposingSpanStart(editable); + int end = getComposingSpanEnd(editable); + if (start < 0 || end < 0) { + start = Selection.getSelectionStart(editable); + end = Selection.getSelectionEnd(editable); + } + if (end < start) { + int temp = end; + end = start; + start = temp; + } + setNewText(start, end, text); + return super.setComposingText(text, newCursorPosition); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + setComposingText(text, newCursorPosition); + finishComposingText(); + return true; + } + + @Override + public boolean deleteSurroundingText(int leftLength, int rightLength) { + Editable editable = getEditable(); + int cursorPosition = Selection.getSelectionEnd(editable); + int startDelete = Math.max(0, cursorPosition - leftLength); + int endDelete = Math.min(editable.length(), + cursorPosition + rightLength); + setNewText(startDelete, endDelete, ""); + return super.deleteSurroundingText(leftLength, rightLength); + } + + /** + * Sends a text change to webkit indirectly. If it is a single- + * character add or delete, it sends it as a key stroke. If it cannot + * be represented as a key stroke, it sends it as a field change. + * @param start The start offset (inclusive) of the text being changed. + * @param end The end offset (exclusive) of the text being changed. + * @param text The new text to replace the changed text. + */ + private void setNewText(int start, int end, CharSequence text) { + Editable editable = getEditable(); + CharSequence original = editable.subSequence(start, end); + boolean isCharacterAdd = false; + boolean isCharacterDelete = false; + int textLength = text.length(); + int originalLength = original.length(); + if (textLength > originalLength) { + isCharacterAdd = (textLength == originalLength + 1) + && TextUtils.regionMatches(text, 0, original, 0, + originalLength); + } else if (originalLength > textLength) { + isCharacterDelete = (textLength == originalLength - 1) + && TextUtils.regionMatches(text, 0, original, 0, + textLength); + } + if (isCharacterAdd) { + sendCharacter(text.charAt(textLength - 1)); + mTextGeneration++; + } else if (isCharacterDelete) { + sendDeleteKey(); + mTextGeneration++; + } else if (textLength != originalLength || + !TextUtils.regionMatches(text, 0, original, 0, + textLength)) { + // Send a message so that key strokes and text replacement + // do not come out of order. + Message replaceMessage = mPrivateHandler.obtainMessage( + REPLACE_TEXT, start, end, text.toString()); + mPrivateHandler.sendMessage(replaceMessage); + } + } + + /** + * Send a single character to the WebView as a key down and up event. + * @param c The character to be sent. + */ + private void sendCharacter(char c) { + if (mKeyCharacterMap == null) { + mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + } + char[] chars = new char[1]; + chars[0] = c; + KeyEvent[] events = mKeyCharacterMap.getEvents(chars); + if (events != null) { + for (KeyEvent event : events) { + sendKeyEvent(event); + } + } + } + + /** + * Send the delete character as a key down and up event. + */ + private void sendDeleteKey() { + long eventTime = SystemClock.uptimeMillis(); + sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD)); + sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD)); + } + } + + // The listener to capture global layout change event. private InnerGlobalLayoutListener mGlobalLayoutListener = null; @@ -371,6 +508,8 @@ public class WebView extends AbsoluteLayout private final Rect mViewRectViewport = new Rect(); private final RectF mVisibleContentRect = new RectF(); private boolean mGLViewportEmpty = false; + WebViewInputConnection mInputConnection = null; + /** * Transportation object for returning WebView across thread boundaries. @@ -567,6 +706,7 @@ public class WebView extends AbsoluteLayout private boolean mIsPaused; private HitTestResult mInitialHitTestResult; + private WebKitHitTest mFocusedNode; /** * Customizable constant @@ -656,15 +796,13 @@ public class WebView extends AbsoluteLayout private Drawable mSelectHandleLeft; private Drawable mSelectHandleRight; - static final boolean USE_WEBKIT_RINGS = false; + static boolean sDisableNavcache = false; // the color used to highlight the touch rectangles - private static final int HIGHLIGHT_COLOR = 0x6633b5e5; - // the round corner for the highlight path - private static final float TOUCH_HIGHLIGHT_ARC = 5.0f; + static final int HIGHLIGHT_COLOR = 0x6633b5e5; // the region indicating where the user touched on the screen private Region mTouchHighlightRegion = new Region(); // the paint for the touch highlight - private Paint mTouchHightlightPaint; + private Paint mTouchHightlightPaint = new Paint(); // debug only private static final boolean DEBUG_TOUCH_HIGHLIGHT = true; private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000; @@ -732,7 +870,7 @@ public class WebView extends AbsoluteLayout static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128; static final int SET_SCROLLBAR_MODES = 129; static final int SELECTION_STRING_CHANGED = 130; - static final int SET_TOUCH_HIGHLIGHT_RECTS = 131; + static final int HIT_TEST_RESULT = 131; static final int SAVE_WEBARCHIVE_FINISHED = 132; static final int SET_AUTOFILLABLE = 133; @@ -743,9 +881,14 @@ public class WebView extends AbsoluteLayout static final int ENTER_FULLSCREEN_VIDEO = 137; static final int UPDATE_SELECTION = 138; static final int UPDATE_ZOOM_DENSITY = 139; + static final int EXIT_FULLSCREEN_VIDEO = 140; + + static final int COPY_TO_CLIPBOARD = 141; + static final int INIT_EDIT_FIELD = 142; + static final int REPLACE_TEXT = 143; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; - private static final int LAST_PACKAGE_MSG_ID = SET_TOUCH_HIGHLIGHT_RECTS; + private static final int LAST_PACKAGE_MSG_ID = HIT_TEST_RESULT; static final String[] HandlerPrivateDebugString = { "REMEMBER_PASSWORD", // = 1; @@ -849,13 +992,12 @@ public class WebView extends AbsoluteLayout // the alias via which accessibility JavaScript interface is exposed private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility"; - // JavaScript to inject the script chooser which will - // pick the right script for the current URL - private static final String ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT = + // Template for JavaScript that injects a screen-reader. + private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE = "javascript:(function() {" + " var chooser = document.createElement('script');" + " chooser.type = 'text/javascript';" + - " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" + + " chooser.src = '%1s';" + " document.getElementsByTagName('head')[0].appendChild(chooser);" + " })();"; @@ -921,9 +1063,6 @@ public class WebView extends AbsoluteLayout private Rect mScrollingLayerBounds = new Rect(); private boolean mSentAutoScrollMessage = false; - // Temporary hack to work around the context removal upon memory pressure - private static boolean mIncrementEGLContextHack = false; - // used for serializing asynchronously handled touch events. private final TouchEventQueue mTouchEventQueue = new TouchEventQueue(); @@ -952,8 +1091,7 @@ public class WebView extends AbsoluteLayout public void onNewPicture(WebView view, Picture picture); } - // FIXME: Want to make this public, but need to change the API file. - public /*static*/ class HitTestResult { + public static class HitTestResult { /** * Default HitTestResult, where the target is unknown */ @@ -1012,16 +1150,34 @@ public class WebView extends AbsoluteLayout mExtra = extra; } + /** + * Gets the type of the hit test result. + * @return See the XXX_TYPE constants defined in this class. + */ public int getType() { return mType; } + /** + * Gets additional type-dependant information about the result, see + * {@link WebView#getHitTestResult()} for details. + * @return may either be null or contain extra information about this result. + */ public String getExtra() { return mExtra; } } /** + * Refer to {@link WebView#requestFocusNodeHref(Message)} for more information + */ + static class FocusNodeHref { + static final String TITLE = "title"; + static final String URL = "url"; + static final String SRC = "src"; + } + + /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. */ @@ -1053,6 +1209,7 @@ public class WebView extends AbsoluteLayout * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. * @param defStyle The default style resource ID. + * @param privateBrowsing If true the web view will be initialized in private mode. */ public WebView(Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) { @@ -1069,7 +1226,8 @@ public class WebView extends AbsoluteLayout * @param defStyle The default style resource ID. * @param javaScriptInterfaces is a Map of interface names, as keys, and * object implementing those interfaces, as values. - * @hide pending API council approval. + * @param privateBrowsing If true the web view will be initialized in private mode. + * @hide This is an implementation detail. */ protected WebView(Context context, AttributeSet attrs, int defStyle, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { @@ -1232,7 +1390,7 @@ public class WebView extends AbsoluteLayout PackageManager pm = mContext.getPackageManager(); for (String name : sGoogleApps) { try { - PackageInfo pInfo = pm.getPackageInfo(name, + pm.getPackageInfo(name, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); installedPackages.add(name); } catch (PackageManager.NameNotFoundException e) { @@ -1259,6 +1417,7 @@ public class WebView extends AbsoluteLayout private void init() { OnTrimMemoryListener.init(getContext()); + sDisableNavcache = nativeDisableNavcache(); setWillNotDraw(false); setFocusable(true); @@ -1311,7 +1470,7 @@ public class WebView extends AbsoluteLayout final String packageName = ctx.getPackageName(); if (packageName != null) { mTextToSpeech = new TextToSpeech(getContext(), null, null, - packageName + ".**webview**"); + packageName + ".**webview**", true); addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE); } } @@ -1404,23 +1563,27 @@ public class WebView extends AbsoluteLayout .setMessage(com.android.internal.R.string.save_password_message) .setPositiveButton(com.android.internal.R.string.save_password_notnow, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { resumeMsg.sendToTarget(); } }) .setNeutralButton(com.android.internal.R.string.save_password_remember, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { remember.sendToTarget(); } }) .setNegativeButton(com.android.internal.R.string.save_password_never, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { neverRemember.sendToTarget(); } }) .setOnCancelListener(new OnCancelListener() { + @Override public void onCancel(DialogInterface dialog) { resumeMsg.sendToTarget(); } @@ -1506,6 +1669,7 @@ public class WebView extends AbsoluteLayout * * @deprecated This method is now obsolete. */ + @Deprecated public int getVisibleTitleHeight() { checkThread(); return getVisibleTitleHeightImpl(); @@ -1734,7 +1898,7 @@ public class WebView extends AbsoluteLayout * * @param flags JS engine flags in a String * - * @hide pending API solidification + * @hide This is an implementation detail. */ public void setJsFlags(String flags) { checkThread(); @@ -1844,6 +2008,7 @@ public class WebView extends AbsoluteLayout // contains valid data. final File temp = new File(dest.getPath() + ".writing"); new Thread(new Runnable() { + @Override public void run() { FileOutputStream out = null; try { @@ -1913,6 +2078,7 @@ public class WebView extends AbsoluteLayout final FileInputStream in = new FileInputStream(src); final Bundle copy = new Bundle(b); new Thread(new Runnable() { + @Override public void run() { try { final Picture p = Picture.createFromStream(in); @@ -1920,6 +2086,7 @@ public class WebView extends AbsoluteLayout // Post a runnable on the main thread to update the // history picture fields. mPrivateHandler.post(new Runnable() { + @Override public void run() { restoreHistoryPictureFields(p, copy); } @@ -2496,11 +2663,12 @@ public class WebView extends AbsoluteLayout } /** - * Return the reading level scale of the WebView + * Compute the reading level scale of the WebView + * @param scale The current scale. * @return The reading level scale. */ - /*package*/ float getReadingLevelScale() { - return mZoomManager.getReadingLevelScale(); + /*package*/ float computeReadingLevelScale(float scale) { + return mZoomManager.computeReadingLevelScale(scale); } /** @@ -2557,8 +2725,8 @@ public class WebView extends AbsoluteLayout } private HitTestResult hitTestResult(HitTestResult fallback) { - if (mNativeClass == 0) { - return null; + if (mNativeClass == 0 || sDisableNavcache) { + return fallback; } HitTestResult result = new HitTestResult(); @@ -2570,7 +2738,8 @@ public class WebView extends AbsoluteLayout if (text != null) { if (text.startsWith(SCHEME_TEL)) { result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); + result.setExtra(URLDecoder.decode(text + .substring(SCHEME_TEL.length()))); } else if (text.startsWith(SCHEME_MAILTO)) { result.setType(HitTestResult.EMAIL_TYPE); result.setExtra(text.substring(SCHEME_MAILTO.length())); @@ -2640,14 +2809,22 @@ public class WebView extends AbsoluteLayout } int contentX = viewToContentX(mLastTouchX + mScrollX); int contentY = viewToContentY(mLastTouchY + mScrollY); + if (mFocusedNode != null && mFocusedNode.mHitTestX == contentX + && mFocusedNode.mHitTestY == contentY) { + hrefMsg.getData().putString(FocusNodeHref.URL, mFocusedNode.mLinkUrl); + hrefMsg.getData().putString(FocusNodeHref.TITLE, mFocusedNode.mAnchorText); + hrefMsg.getData().putString(FocusNodeHref.SRC, mFocusedNode.mImageUrl); + hrefMsg.sendToTarget(); + return; + } if (nativeHasCursorNode()) { Rect cursorBounds = nativeGetCursorRingBounds(); if (!cursorBounds.contains(contentX, contentY)) { int slop = viewToContentDimension(mNavSlop); cursorBounds.inset(-slop, -slop); if (cursorBounds.contains(contentX, contentY)) { - contentX = (int) cursorBounds.centerX(); - contentY = (int) cursorBounds.centerY(); + contentX = cursorBounds.centerX(); + contentY = cursorBounds.centerY(); } } } @@ -3296,6 +3473,7 @@ public class WebView extends AbsoluteLayout } cancelSelectDialog(); + WebCoreThreadWatchdog.pause(); } } @@ -3328,6 +3506,15 @@ public class WebView extends AbsoluteLayout nativeSetPauseDrawing(mNativeClass, false); } } + // Ensure that the watchdog has a currently valid Context to be able to display + // a prompt dialog. For example, if the Activity was finished whilst the WebCore + // thread was blocked and the Activity is started again, we may reuse the blocked + // thread, but we'll have a new Activity. + WebCoreThreadWatchdog.updateContext(mContext); + // We get a call to onResume for new WebViews (i.e. mIsPaused will be false). We need + // to ensure that the Watchdog thread is running for the new WebView, so call + // it outside the if block above. + WebCoreThreadWatchdog.resume(); } /** @@ -3668,6 +3855,8 @@ public class WebView extends AbsoluteLayout nativeScrollLayer(mCurrentScrollingLayerId, x, y); mScrollingLayerRect.left = x; mScrollingLayerRect.top = y; + mWebViewCore.sendMessage(WebViewCore.EventHub.SCROLL_LAYER, mCurrentScrollingLayerId, + mScrollingLayerRect); onScrollChanged(mScrollX, mScrollY, mScrollX, mScrollY); invalidate(); } @@ -3818,7 +4007,7 @@ public class WebView extends AbsoluteLayout if (onDeviceScriptInjectionEnabled) { ensureAccessibilityScriptInjectorInstance(false); // neither script injected nor script injection opted out => we inject - loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + loadUrl(getScreenReaderInjectingJs()); // TODO: Set this flag after successfull script injection. Maybe upon injection // the chooser should update the meta tag and we check it to declare success mAccessibilityScriptInjected = true; @@ -3832,7 +4021,7 @@ public class WebView extends AbsoluteLayout } else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED) { ensureAccessibilityScriptInjectorInstance(false); // the URL provides accessibility but we still need to add our generic script - loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + loadUrl(getScreenReaderInjectingJs()); } else { Log.e(LOGTAG, "Unknown URL value for the \"axs\" URL parameter: " + axsParameterValue); } @@ -3854,6 +4043,17 @@ public class WebView extends AbsoluteLayout } /** + * Gets JavaScript that injects a screen-reader. + * + * @return The JavaScript snippet. + */ + private String getScreenReaderInjectingJs() { + String screenReaderUrl = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL); + return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl); + } + + /** * Gets the "axs" URL parameter value. * * @param url A url to fetch the paramter from. @@ -4003,7 +4203,7 @@ public class WebView extends AbsoluteLayout * Gets the WebViewClient * @return the current WebViewClient instance. * - *@hide pending API council approval. + * @hide This is an implementation detail. */ public WebViewClient getWebViewClient() { return mCallbackProxy.getWebViewClient(); @@ -4035,7 +4235,7 @@ public class WebView extends AbsoluteLayout * Gets the chrome handler. * @return the current WebChromeClient instance. * - * @hide API council approval. + * @hide This is an implementation detail. */ public WebChromeClient getWebChromeClient() { return mCallbackProxy.getWebChromeClient(); @@ -4089,34 +4289,39 @@ public class WebView extends AbsoluteLayout } /** - * Use this function to bind an object to JavaScript so that the - * methods can be accessed from JavaScript. + * This method injects the supplied Java object into the WebView. The + * object is injected into the JavaScript context of the main frame, using + * the supplied name. This allows the Java object to be accessed from + * JavaScript. Note that that injected objects will not appear in + * JavaScript until the page is next (re)loaded. For example: + * <pre> webView.addJavascriptInterface(new Object(), "injectedObject"); + * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null); + * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre> * <p><strong>IMPORTANT:</strong> * <ul> - * <li> Using addJavascriptInterface() allows JavaScript to control your - * application. This can be a very useful feature or a dangerous security - * issue. When the HTML in the WebView is untrustworthy (for example, part - * or all of the HTML is provided by some person or process), then an - * attacker could inject HTML that will execute your code and possibly any - * code of the attacker's choosing.<br> - * Do not use addJavascriptInterface() unless all of the HTML in this - * WebView was written by you.</li> - * <li> The Java object that is bound runs in another thread and not in - * the thread that it was constructed in.</li> + * <li> addJavascriptInterface() can be used to allow JavaScript to control + * the host application. This is a powerful feature, but also presents a + * security risk. Use of this method in a WebView containing untrusted + * content could allow an attacker to manipulate the host application in + * unintended ways, executing Java code with the permissions of the host + * application. Use extreme care when using this method in a WebView which + * could contain untrusted content. + * <li> JavaScript interacts with Java object on a private, background + * thread of the WebView. Care is therefore required to maintain thread + * safety.</li> * </ul></p> - * @param obj The class instance to bind to JavaScript, null instances are - * ignored. - * @param interfaceName The name to used to expose the instance in - * JavaScript. + * @param object The Java object to inject into the WebView's JavaScript + * context. Null values are ignored. + * @param name The name used to expose the instance in JavaScript. */ - public void addJavascriptInterface(Object obj, String interfaceName) { + public void addJavascriptInterface(Object object, String name) { checkThread(); - if (obj == null) { + if (object == null) { return; } WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); - arg.mObject = obj; - arg.mInterfaceName = interfaceName; + arg.mObject = object; + arg.mInterfaceName = name; mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } @@ -4245,6 +4450,9 @@ public class WebView extends AbsoluteLayout @Override protected void onDraw(Canvas canvas) { + if (inFullScreenMode()) { + return; // no need to draw anything if we aren't visible. + } // if mNativeClass is 0, the WebView is either destroyed or not // initialized. In either case, just draw the background color and return if (mNativeClass == 0) { @@ -4261,13 +4469,6 @@ public class WebView extends AbsoluteLayout } if (canvas.isHardwareAccelerated()) { - if (mIncrementEGLContextHack == false) { - mIncrementEGLContextHack = true; - EGL10 egl = (EGL10) EGLContext.getEGL(); - EGLDisplay eglDisplay = egl.eglGetDisplay(EGL_DEFAULT_DISPLAY); - int[] version = new int[2]; - egl.eglInitialize(eglDisplay, version); - } mZoomManager.setHardwareAccelerated(); } @@ -4285,7 +4486,7 @@ public class WebView extends AbsoluteLayout || mTouchMode == TOUCH_SHORTPRESS_MODE || mTouchMode == TOUCH_DONE_MODE); boolean drawNativeRings = !drawJavaRings; - if (USE_WEBKIT_RINGS) { + if (sDisableNavcache) { drawNativeRings = !drawJavaRings && !isInTouchMode(); } drawContent(canvas, drawNativeRings); @@ -4307,10 +4508,6 @@ public class WebView extends AbsoluteLayout Rect r = mTouchHighlightRegion.getBounds(); postInvalidateDelayed(delay, r.left, r.top, r.right, r.bottom); } else { - if (mTouchHightlightPaint == null) { - mTouchHightlightPaint = new Paint(); - mTouchHightlightPaint.setColor(HIGHLIGHT_COLOR); - } RegionIterator iter = new RegionIterator(mTouchHighlightRegion); Rect r = new Rect(); while (iter.next(r)) { @@ -4340,8 +4537,8 @@ public class WebView extends AbsoluteLayout } private void removeTouchHighlight() { - mWebViewCore.removeMessages(EventHub.GET_TOUCH_HIGHLIGHT_RECTS); - mPrivateHandler.removeMessages(SET_TOUCH_HIGHLIGHT_RECTS); + mWebViewCore.removeMessages(EventHub.HIT_TEST); + mPrivateHandler.removeMessages(HIT_TEST_RESULT); setTouchHighlightRects(null); } @@ -4403,6 +4600,11 @@ public class WebView extends AbsoluteLayout final boolean isSelecting = selectText(); if (isSelecting) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else if (focusCandidateIsEditableText()) { + mSelectCallback = new SelectActionModeCallback(); + mSelectCallback.setWebView(this); + mSelectCallback.setTextSelected(false); + startActionMode(mSelectCallback); } return isSelecting; } @@ -4410,7 +4612,7 @@ public class WebView extends AbsoluteLayout /** * Select the word at the last click point. * - * @hide pending API council approval + * @hide This is an implementation detail. */ public boolean selectText() { int x = viewToContentX(mLastTouchX + mScrollX); @@ -4518,7 +4720,7 @@ public class WebView extends AbsoluteLayout boolean isPictureAfterFirstLayout, boolean registerPageSwapCallback) { if (mNativeClass == 0) return; - nativeSetBaseLayer(layer, invalRegion, showVisualIndicator, + nativeSetBaseLayer(mNativeClass, layer, invalRegion, showVisualIndicator, isPictureAfterFirstLayout, registerPageSwapCallback); if (mHTML5VideoViewProxy != null) { mHTML5VideoViewProxy.setBaseLayer(layer); @@ -4826,9 +5028,19 @@ public class WebView extends AbsoluteLayout @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - InputConnection connection = super.onCreateInputConnection(outAttrs); - outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN; - return connection; + outAttrs.inputType = EditorInfo.IME_FLAG_NO_FULLSCREEN + | EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE + | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; + outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; + + if (mInputConnection == null) { + mInputConnection = new WebViewInputConnection(); + } + outAttrs.initialCapsMode = mInputConnection.getCursorCapsMode(InputType.TYPE_CLASS_TEXT); + return mInputConnection; } /** @@ -5023,6 +5235,7 @@ public class WebView extends AbsoluteLayout mWebSettings = getSettings(); } + @Override public void run() { ArrayList<String> pastEntries = new ArrayList<String>(); @@ -5107,17 +5320,6 @@ public class WebView extends AbsoluteLayout canProvideGamma, gamma); } - /** - * Dump the V8 counters to standard output. - * Note that you need a build with V8 and WEBCORE_INSTRUMENTATION set to - * true. Otherwise, this will do nothing. - * - * @hide debug only - */ - public void dumpV8Counters() { - mWebViewCore.sendMessage(EventHub.DUMP_V8COUNTERS); - } - // This is used to determine long press with the center key. Does not // affect long press with the trackball/touch. private boolean mGotCenterDown = false; @@ -5307,9 +5509,6 @@ public class WebView extends AbsoluteLayout case KeyEvent.KEYCODE_8: dumpRenderTree(keyCode == KeyEvent.KEYCODE_7); break; - case KeyEvent.KEYCODE_9: - nativeInstrumentReport(); - return true; } } @@ -5562,7 +5761,7 @@ public class WebView extends AbsoluteLayout /** * Select all of the text in this WebView. * - * @hide pending API council approval. + * @hide This is an implementation detail. */ public void selectAll() { if (0 == mNativeClass) return; // client isn't initialized @@ -5605,7 +5804,7 @@ public class WebView extends AbsoluteLayout /** * Copy the selection to the clipboard * - * @hide pending API council approval. + * @hide This is an implementation detail. */ public boolean copySelection() { boolean copiedSomething = false; @@ -5621,13 +5820,50 @@ public class WebView extends AbsoluteLayout ClipboardManager cm = (ClipboardManager)getContext() .getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(selection); + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.COPY_TEXT, handles); } invalidate(); // remove selection region and pointer return copiedSomething; } /** - * @hide pending API Council approval. + * Cut the selected text into the clipboard + * + * @hide This is an implementation detail + */ + public void cutSelection() { + copySelection(); + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles); + } + + /** + * Paste text from the clipboard to the cursor position. + * + * @hide This is an implementation detail + */ + public void pasteFromClipboard() { + ClipboardManager cm = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = cm.getPrimaryClip(); + if (clipData != null) { + ClipData.Item clipItem = clipData.getItemAt(0); + CharSequence pasteText = clipItem.getText(); + if (pasteText != null) { + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles); + mWebViewCore.sendMessage(EventHub.INSERT_TEXT, + pasteText.toString()); + } + } + } + + /** + * @hide This is an implementation detail. */ public SearchBox getSearchBox() { if ((mWebViewCore == null) || (mWebViewCore.getBrowserFrame() == null)) { @@ -5699,6 +5935,8 @@ public class WebView extends AbsoluteLayout * @deprecated WebView no longer needs to implement * ViewGroup.OnHierarchyChangeListener. This method does nothing now. */ + @Override + // Cannot add @hide as this can always be accessed via the interface. @Deprecated public void onChildViewAdded(View parent, View child) {} @@ -5706,6 +5944,8 @@ public class WebView extends AbsoluteLayout * @deprecated WebView no longer needs to implement * ViewGroup.OnHierarchyChangeListener. This method does nothing now. */ + @Override + // Cannot add @hide as this can always be accessed via the interface. @Deprecated public void onChildViewRemoved(View p, View child) {} @@ -5713,6 +5953,8 @@ public class WebView extends AbsoluteLayout * @deprecated WebView should not have implemented * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now. */ + @Override + // Cannot add @hide as this can always be accessed via the interface. @Deprecated public void onGlobalFocusChanged(View oldFocus, View newFocus) { } @@ -5842,7 +6084,8 @@ public class WebView extends AbsoluteLayout } calcOurContentVisibleRectF(mVisibleContentRect); nativeUpdateDrawGLFunction(mGLViewportEmpty ? null : mGLRectViewport, - mGLViewportEmpty ? null : mViewRectViewport, mVisibleContentRect); + mGLViewportEmpty ? null : mViewRectViewport, + mVisibleContentRect); } /** @@ -5989,6 +6232,7 @@ public class WebView extends AbsoluteLayout if (inFullScreenMode()) { mFullScreenHolder.hide(); mFullScreenHolder = null; + invalidate(); } } @@ -6106,7 +6350,7 @@ public class WebView extends AbsoluteLayout nativeSetIsScrolling(false); } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); - if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (sDisableNavcache) { removeTouchHighlight(); } if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { @@ -6130,7 +6374,7 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage( EventHub.UPDATE_FRAME_CACHE_IF_LOADING); } - if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (sDisableNavcache) { TouchHighlightData data = new TouchHighlightData(); data.mX = contentX; data.mY = contentY; @@ -6142,13 +6386,14 @@ public class WebView extends AbsoluteLayout if (!mBlockWebkitViewMessages) { mTouchHighlightRequested = System.currentTimeMillis(); mWebViewCore.sendMessageAtFrontOfQueue( - EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data); + EventHub.HIT_TEST, data); } if (DEBUG_TOUCH_HIGHLIGHT) { if (getSettings().getNavDump()) { - mTouchHighlightX = (int) x + mScrollX; - mTouchHighlightY = (int) y + mScrollY; + mTouchHighlightX = x + mScrollX; + mTouchHighlightY = y + mScrollY; mPrivateHandler.postDelayed(new Runnable() { + @Override public void run() { mTouchHighlightX = mTouchHighlightY = 0; invalidate(); @@ -6229,7 +6474,7 @@ public class WebView extends AbsoluteLayout if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mTouchMode = TOUCH_INIT_MODE; } - if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (sDisableNavcache) { removeTouchHighlight(); } } @@ -6740,8 +6985,6 @@ public class WebView extends AbsoluteLayout int oldY = mScrollY; int rangeX = computeMaxScrollX(); int rangeY = computeMaxScrollY(); - int overscrollDistance = mOverscrollDistance; - // Check for the original scrolling layer in case we change // directions. mTouchMode might be TOUCH_DRAG_MODE if we have // reached the edge of a layer but mScrollingLayer will be non-zero @@ -6833,7 +7076,7 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); - if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (sDisableNavcache) { removeTouchHighlight(); } mHeldMotionless = MOTIONLESS_TRUE; @@ -7390,7 +7633,7 @@ public class WebView extends AbsoluteLayout * and calls showCursorTimed on the native side */ private void updateSelection() { - if (mNativeClass == 0) { + if (mNativeClass == 0 || sDisableNavcache) { return; } mPrivateHandler.removeMessages(UPDATE_SELECTION); @@ -7454,8 +7697,8 @@ public class WebView extends AbsoluteLayout return false; } mDragFromTextInput = true; - event.offsetLocation((float) (mWebTextView.getLeft() - mScrollX), - (float) (mWebTextView.getTop() - mScrollY)); + event.offsetLocation((mWebTextView.getLeft() - mScrollX), + (mWebTextView.getTop() - mScrollY)); boolean result = onTouchEvent(event); mDragFromTextInput = false; return result; @@ -7498,7 +7741,7 @@ public class WebView extends AbsoluteLayout int contentX = viewToContentX(mLastTouchX + mScrollX); int contentY = viewToContentY(mLastTouchY + mScrollY); int slop = viewToContentDimension(mNavSlop); - if (USE_WEBKIT_RINGS && !mTouchHighlightRegion.isEmpty()) { + if (sDisableNavcache && !mTouchHighlightRegion.isEmpty()) { // set mTouchHighlightRequested to 0 to cause an immediate // drawing of the touch rings mTouchHighlightRequested = 0; @@ -7510,8 +7753,7 @@ public class WebView extends AbsoluteLayout } }, ViewConfiguration.getPressedStateDuration()); } - if (getSettings().supportTouchOnly()) { - removeTouchHighlight(); + if (sDisableNavcache) { WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData(); // use "0" as generation id to inform WebKit to use the same x/y as // it used when processing GET_TOUCH_HIGHLIGHT_RECTS @@ -8396,9 +8638,8 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_SHORTPRESS: { - mInitialHitTestResult = null; // set by updateSelection() if (mTouchMode == TOUCH_INIT_MODE) { - if (!getSettings().supportTouchOnly() + if (!sDisableNavcache && mPreventDefault != PREVENT_DEFAULT_YES) { mTouchMode = TOUCH_SHORTPRESS_START_MODE; updateSelection(); @@ -8413,7 +8654,7 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_LONGPRESS: { - if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (sDisableNavcache) { removeTouchHighlight(); } if (inFullScreenMode() || mDeferTouchProcess) { @@ -8652,6 +8893,12 @@ public class WebView extends AbsoluteLayout } break; + case EXIT_FULLSCREEN_VIDEO: + if (mHTML5VideoViewProxy != null) { + mHTML5VideoViewProxy.exitFullScreenVideo(); + } + break; + case SHOW_FULLSCREEN: { View view = (View) msg.obj; int orientation = msg.arg1; @@ -8664,6 +8911,7 @@ public class WebView extends AbsoluteLayout mFullScreenHolder = new PluginFullScreenHolder(WebView.this, orientation, npp); mFullScreenHolder.setContentView(view); mFullScreenHolder.show(); + invalidate(); break; } @@ -8743,10 +8991,28 @@ public class WebView extends AbsoluteLayout } break; - case SET_TOUCH_HIGHLIGHT_RECTS: - @SuppressWarnings("unchecked") - ArrayList<Rect> rects = (ArrayList<Rect>) msg.obj; - setTouchHighlightRects(rects); + case HIT_TEST_RESULT: + WebKitHitTest hit = (WebKitHitTest) msg.obj; + mFocusedNode = hit; + setTouchHighlightRects(hit); + if (hit == null) { + mInitialHitTestResult = null; + } else { + mInitialHitTestResult = new HitTestResult(); + if (hit.mLinkUrl != null) { + mInitialHitTestResult.mType = HitTestResult.SRC_ANCHOR_TYPE; + mInitialHitTestResult.mExtra = hit.mLinkUrl; + if (hit.mImageUrl != null) { + mInitialHitTestResult.mType = HitTestResult.SRC_IMAGE_ANCHOR_TYPE; + mInitialHitTestResult.mExtra = hit.mImageUrl; + } + } else if (hit.mImageUrl != null) { + mInitialHitTestResult.mType = HitTestResult.IMAGE_TYPE; + mInitialHitTestResult.mExtra = hit.mImageUrl; + } else if (hit.mEditable) { + mInitialHitTestResult.mType = HitTestResult.EDIT_TEXT_TYPE; + } + } break; case SAVE_WEBARCHIVE_FINISHED: @@ -8776,6 +9042,35 @@ public class WebView extends AbsoluteLayout nativeSelectAt(msg.arg1, msg.arg2); break; + case COPY_TO_CLIPBOARD: + copyToClipboard((String) msg.obj); + break; + + case INIT_EDIT_FIELD: + if (mInputConnection != null) { + mTextGeneration = 0; + String text = (String)msg.obj; + mInputConnection.beginBatchEdit(); + Editable editable = mInputConnection.getEditable(); + editable.replace(0, editable.length(), text); + int start = msg.arg1; + int end = msg.arg2; + mInputConnection.setComposingRegion(end, end); + mInputConnection.setSelection(start, end); + mInputConnection.endBatchEdit(); + } + break; + + case REPLACE_TEXT:{ + String text = (String)msg.obj; + int start = msg.arg1; + int end = msg.arg2; + int cursorPosition = start + text.length(); + replaceTextfieldText(start, end, text, + cursorPosition, cursorPosition); + break; + } + default: super.handleMessage(msg); break; @@ -8783,10 +9078,14 @@ public class WebView extends AbsoluteLayout } } - private void setTouchHighlightRects(ArrayList<Rect> rects) { - invalidate(mTouchHighlightRegion.getBounds()); - mTouchHighlightRegion.setEmpty(); + private void setTouchHighlightRects(WebKitHitTest hit) { + Rect[] rects = hit != null ? hit.mTouchRects : null; + if (!mTouchHighlightRegion.isEmpty()) { + invalidate(mTouchHighlightRegion.getBounds()); + mTouchHighlightRegion.setEmpty(); + } if (rects != null) { + mTouchHightlightPaint.setColor(hit.mTapHighlightColor); for (Rect rect : rects) { Rect viewRect = contentToViewRect(rect); // some sites, like stories in nytimes.com, set @@ -8897,10 +9196,13 @@ public class WebView extends AbsoluteLayout */ private void updateTextSelectionFromMessage(int nodePointer, int textGeneration, WebViewCore.TextSelectionData data) { - if (inEditingMode() - && mWebTextView.isSameTextField(nodePointer) - && textGeneration == mTextGeneration) { - mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd); + if (textGeneration == mTextGeneration) { + if (inEditingMode() + && mWebTextView.isSameTextField(nodePointer)) { + mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd); + } else if (mInputConnection != null){ + mInputConnection.setSelection(data.mStart, data.mEnd); + } } } @@ -9009,7 +9311,7 @@ public class WebView extends AbsoluteLayout if (position < 0 || position >= getCount()) { return null; } - return (Container) getItem(position); + return getItem(position); } @Override @@ -9108,6 +9410,7 @@ public class WebView extends AbsoluteLayout } } + @Override public void run() { final ListView listView = (ListView) LayoutInflater.from(mContext) .inflate(com.android.internal.R.layout.select_dialog, null); @@ -9118,6 +9421,7 @@ public class WebView extends AbsoluteLayout if (mMultiple) { b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { mWebViewCore.sendMessage( EventHub.LISTBOX_CHOICES, @@ -9126,6 +9430,7 @@ public class WebView extends AbsoluteLayout }}); b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { mWebViewCore.sendMessage( EventHub.SINGLE_LISTBOX_CHOICE, -2, 0); @@ -9149,6 +9454,7 @@ public class WebView extends AbsoluteLayout } } else { listView.setOnItemClickListener(new OnItemClickListener() { + @Override public void onItemClick(AdapterView<?> parent, View v, int position, long id) { // Rather than sending the message right away, send it @@ -9169,6 +9475,7 @@ public class WebView extends AbsoluteLayout } } mListBoxDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override public void onCancel(DialogInterface dialog) { mWebViewCore.sendMessage( EventHub.SINGLE_LISTBOX_CHOICE, -2, 0); @@ -9445,6 +9752,18 @@ public class WebView extends AbsoluteLayout } /** + * Copy text into the clipboard. This is called indirectly from + * WebViewCore. + * @param text The text to put into the clipboard. + */ + private void copyToClipboard(String text) { + ClipboardManager cm = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getTitle(), text); + cm.setPrimaryClip(clip); + } + + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. * @hide @@ -9484,7 +9803,12 @@ public class WebView extends AbsoluteLayout /** @hide call pageSwapCallback upon next page swap */ protected void registerPageSwapCallback() { - nativeRegisterPageSwapCallback(); + nativeRegisterPageSwapCallback(mNativeClass); + } + + /** @hide discard all textures from tiles */ + protected void discardAllTextures() { + nativeDiscardAllTextures(); } /** @@ -9525,6 +9849,23 @@ public class WebView extends AbsoluteLayout return nativeTileProfilingGetFloat(frame, tile, key); } + /** + * Checks the focused content for an editable text field. This can be + * text input or ContentEditable. + * @return true if the focused item is an editable text field. + */ + boolean focusCandidateIsEditableText() { + boolean isEditable = false; + // TODO: reverse sDisableNavcache so that its name is positive + boolean isNavcacheEnabled = !sDisableNavcache; + if (isNavcacheEnabled) { + isEditable = nativeFocusCandidateIsEditableText(mNativeClass); + } else if (mFocusedNode != null) { + isEditable = mFocusedNode.mEditable; + } + return isEditable; + } + private native int nativeCacheHitFramePointer(); private native boolean nativeCacheHitIsPlugin(); private native Rect nativeCacheHitNodeBounds(); @@ -9570,6 +9911,7 @@ public class WebView extends AbsoluteLayout /* package */ native boolean nativeFocusCandidateIsPassword(); private native boolean nativeFocusCandidateIsRtlText(); private native boolean nativeFocusCandidateIsTextInput(); + private native boolean nativeFocusCandidateIsEditableText(int nativeClass); /* package */ native int nativeFocusCandidateMaxLength(); /* package */ native boolean nativeFocusCandidateIsAutoComplete(); /* package */ native boolean nativeFocusCandidateIsSpellcheck(); @@ -9602,7 +9944,6 @@ public class WebView extends AbsoluteLayout private native void nativeHideCursor(); private native boolean nativeHitSelection(int x, int y); private native String nativeImageURI(int x, int y); - private native void nativeInstrumentReport(); private native Rect nativeLayerBounds(int layer); /* package */ native boolean nativeMoveCursorToNextTextInput(); // return true if the page has been scrolled @@ -9635,7 +9976,8 @@ public class WebView extends AbsoluteLayout private native void nativeSetFindIsEmpty(); private native void nativeSetFindIsUp(boolean isUp); private native void nativeSetHeightCanMeasure(boolean measure); - private native void nativeSetBaseLayer(int layer, Region invalRegion, + private native void nativeSetBaseLayer(int nativeInstance, + int layer, Region invalRegion, boolean showVisualIndicator, boolean isPictureAfterFirstLayout, boolean registerPageSwapCallback); private native int nativeGetBaseLayer(); @@ -9649,7 +9991,8 @@ public class WebView extends AbsoluteLayout private native void nativeStopGL(); private native Rect nativeSubtractLayers(Rect content); private native int nativeTextGeneration(); - private native void nativeRegisterPageSwapCallback(); + private native void nativeRegisterPageSwapCallback(int nativeInstance); + private native void nativeDiscardAllTextures(); private native void nativeTileProfilingStart(); private native float nativeTileProfilingStop(); private native void nativeTileProfilingClear(); @@ -9690,4 +10033,5 @@ public class WebView extends AbsoluteLayout */ private static native void nativeOnTrimMemory(int level); private static native void nativeSetPauseDrawing(int instance, boolean pause); + private static native boolean nativeDisableNavcache(); } diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 2d15afb..fe51581 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -26,6 +26,7 @@ import android.graphics.Region; import android.media.MediaFile; import android.net.ProxyProperties; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -37,18 +38,15 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; -import android.webkit.DeviceMotionService; -import android.webkit.DeviceMotionAndOrientationManager; -import android.webkit.DeviceOrientationService; -import android.webkit.JniUtil; +import android.webkit.WebView.FocusNodeHref; + +import junit.framework.Assert; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Set; -import junit.framework.Assert; - /** * @hide */ @@ -170,6 +168,10 @@ public final class WebViewCore { "creation."); Log.e(LOGTAG, Log.getStackTraceString(e)); } + + // Start the singleton watchdog which will monitor the WebCore thread + // to verify it's still processing messages. + WebCoreThreadWatchdog.start(context, sWebCoreHandler); } } // Create an EventHub to handle messages before and after the thread is @@ -382,8 +384,9 @@ public final class WebViewCore { mCallbackProxy.onExceededDatabaseQuota(url, databaseIdentifier, currentQuota, estimatedSize, getUsedQuota(), new WebStorage.QuotaUpdater() { + @Override public void updateQuota(long quota) { - nativeSetNewStorageLimit(quota); + nativeSetNewStorageLimit(mNativeClass, quota); } }); } @@ -396,14 +399,16 @@ public final class WebViewCore { protected void reachedMaxAppCacheSize(long spaceNeeded) { mCallbackProxy.onReachedMaxAppCacheSize(spaceNeeded, getUsedQuota(), new WebStorage.QuotaUpdater() { + @Override public void updateQuota(long quota) { - nativeSetNewStorageLimit(quota); + nativeSetNewStorageLimit(mNativeClass, quota); } }); } protected void populateVisitedLinks() { ValueCallback callback = new ValueCallback<String[]>() { + @Override public void onReceiveValue(String[] value) { sendMessage(EventHub.POPULATE_VISITED_LINKS, (Object)value); } @@ -420,14 +425,15 @@ public final class WebViewCore { protected void geolocationPermissionsShowPrompt(String origin) { mCallbackProxy.onGeolocationPermissionsShowPrompt(origin, new GeolocationPermissions.Callback() { - public void invoke(String origin, boolean allow, boolean remember) { - GeolocationPermissionsData data = new GeolocationPermissionsData(); - data.mOrigin = origin; - data.mAllow = allow; - data.mRemember = remember; - // Marshall to WebCore thread. - sendMessage(EventHub.GEOLOCATION_PERMISSIONS_PROVIDE, data); - } + @Override + public void invoke(String origin, boolean allow, boolean remember) { + GeolocationPermissionsData data = new GeolocationPermissionsData(); + data.mOrigin = origin; + data.mAllow = allow; + data.mRemember = remember; + // Marshall to WebCore thread. + sendMessage(EventHub.GEOLOCATION_PERMISSIONS_PROVIDE, data); + } }); } @@ -497,6 +503,24 @@ public final class WebViewCore { message.sendToTarget(); } + /** + * Notify the webview that we want to exit the video fullscreen. + * This is called through JNI by webcore. + */ + protected void exitFullscreenVideo() { + if (mWebView == null) return; + Message message = Message.obtain(mWebView.mPrivateHandler, + WebView.EXIT_FULLSCREEN_VIDEO); + message.sendToTarget(); + } + + /** + * Clear the picture set. To be called only on the WebCore thread. + */ + /* package */ void clearContent() { + nativeClearContent(mNativeClass); + } + //------------------------------------------------------------------------- // JNI methods //------------------------------------------------------------------------- @@ -506,15 +530,16 @@ public final class WebViewCore { /** * Empty the picture set. */ - private native void nativeClearContent(); + private native void nativeClearContent(int nativeClass); - private native void nativeContentInvalidateAll(); + private native void nativeContentInvalidateAll(int nativeClass); /** * Redraw a portion of the picture set. The Point wh returns the * width and height of the overall picture. */ - private native int nativeRecordContent(Region invalRegion, Point wh); + private native int nativeRecordContent(int nativeClass, Region invalRegion, + Point wh); /** * Update the layers' content @@ -526,25 +551,27 @@ public final class WebViewCore { */ private native void nativeNotifyAnimationStarted(int nativeClass); - private native boolean nativeFocusBoundsChanged(); + private native boolean nativeFocusBoundsChanged(int nativeClass); /** * Splits slow parts of the picture set. Called from the webkit thread after * WebView.nativeDraw() returns content to be split. */ - private native void nativeSplitContent(int content); + private native void nativeSplitContent(int nativeClass, int content); - private native boolean nativeKey(int keyCode, int unichar, - int repeatCount, boolean isShift, boolean isAlt, boolean isSym, - boolean isDown); + private native boolean nativeKey(int nativeClass, int keyCode, + int unichar, int repeatCount, boolean isShift, boolean isAlt, + boolean isSym, boolean isDown); - private native void nativeClick(int framePtr, int nodePtr, boolean fake); + private native void nativeClick(int nativeClass, int framePtr, int nodePtr, + boolean fake); - private native void nativeSendListBoxChoices(boolean[] choices, int size); + private native void nativeSendListBoxChoices(int nativeClass, + boolean[] choices, int size); - private native void nativeSendListBoxChoice(int choice); + private native void nativeSendListBoxChoice(int nativeClass, int choice); - private native void nativeCloseIdleConnections(); + private native void nativeCloseIdleConnections(int nativeClass); /* Tell webkit what its width and height are, for the purposes of layout/line-breaking. These coordinates are in document space, @@ -554,77 +581,84 @@ public final class WebViewCore { fixed size, textWrapWidth can be different from width with zooming. should this be called nativeSetViewPortSize? */ - private native void nativeSetSize(int width, int height, int textWrapWidth, - float scale, int screenWidth, int screenHeight, int anchorX, - int anchorY, boolean ignoreHeight); + private native void nativeSetSize(int nativeClass, int width, int height, + int textWrapWidth, float scale, int screenWidth, int screenHeight, + int anchorX, int anchorY, boolean ignoreHeight); - private native int nativeGetContentMinPrefWidth(); + private native int nativeGetContentMinPrefWidth(int nativeClass); // Start: functions that deal with text editing private native void nativeReplaceTextfieldText( - int oldStart, int oldEnd, String replace, int newStart, int newEnd, - int textGeneration); + int nativeClass, int oldStart, int oldEnd, String replace, + int newStart, int newEnd, int textGeneration); - private native void passToJs(int gen, - String currentText, int keyCode, int keyValue, boolean down, - boolean cap, boolean fn, boolean sym); + private native void passToJs(int nativeClass, + int gen, String currentText, int keyCode, int keyValue, + boolean down, boolean cap, boolean fn, boolean sym); - private native void nativeSetFocusControllerActive(boolean active); + private native void nativeSetFocusControllerActive(int nativeClass, + boolean active); - private native void nativeSaveDocumentState(int frame); + private native void nativeSaveDocumentState(int nativeClass, int frame); - private native void nativeMoveFocus(int framePtr, int nodePointer); - private native void nativeMoveMouse(int framePtr, int x, int y); + private native void nativeMoveFocus(int nativeClass, int framePtr, + int nodePointer); + private native void nativeMoveMouse(int nativeClass, int framePtr, int x, + int y); - private native void nativeMoveMouseIfLatest(int moveGeneration, - int framePtr, int x, int y); + private native void nativeMoveMouseIfLatest(int nativeClass, + int moveGeneration, int framePtr, int x, int y); - private native String nativeRetrieveHref(int x, int y); - private native String nativeRetrieveAnchorText(int x, int y); - private native String nativeRetrieveImageSource(int x, int y); - private native void nativeStopPaintingCaret(); - private native void nativeTouchUp(int touchGeneration, - int framePtr, int nodePtr, int x, int y); + private native String nativeRetrieveHref(int nativeClass, int x, int y); + private native String nativeRetrieveAnchorText(int nativeClass, + int x, int y); + private native String nativeRetrieveImageSource(int nativeClass, + int x, int y); + private native void nativeStopPaintingCaret(int nativeClass); + private native void nativeTouchUp(int nativeClass, + int touchGeneration, int framePtr, int nodePtr, int x, int y); - private native boolean nativeHandleTouchEvent(int action, int[] idArray, - int[] xArray, int[] yArray, int count, int actionIndex, int metaState); + private native boolean nativeHandleTouchEvent(int nativeClass, int action, + int[] idArray, int[] xArray, int[] yArray, int count, + int actionIndex, int metaState); - private native void nativeUpdateFrameCache(); + private native void nativeUpdateFrameCache(int nativeClass); - private native void nativeSetBackgroundColor(int color); + private native void nativeSetBackgroundColor(int nativeClass, int color); - private native void nativeDumpDomTree(boolean useFile); + private native void nativeDumpDomTree(int nativeClass, boolean useFile); - private native void nativeDumpRenderTree(boolean useFile); + private native void nativeDumpRenderTree(int nativeClass, boolean useFile); - private native void nativeDumpNavTree(); + private native void nativeDumpNavTree(int nativeClass); - private native void nativeDumpV8Counters(); - - private native void nativeSetJsFlags(String flags); + private native void nativeSetJsFlags(int nativeClass, String flags); /** * Delete text from start to end in the focused textfield. If there is no * focus, or if start == end, silently fail. If start and end are out of * order, swap them. - * @param start Beginning of selection to delete. - * @param end End of selection to delete. - * @param textGeneration Text generation number when delete was pressed. + * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass + * @param start Beginning of selection to delete. + * @param end End of selection to delete. + * @param textGeneration Text generation number when delete was pressed. */ - private native void nativeDeleteSelection(int start, int end, - int textGeneration); + private native void nativeDeleteSelection(int nativeClass, int start, + int end, int textGeneration); /** * Set the selection to (start, end) in the focused textfield. If start and * end are out of order, swap them. - * @param start Beginning of selection. - * @param end End of selection. + * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass + * @param start Beginning of selection. + * @param end End of selection. */ - private native void nativeSetSelection(int start, int end); + private native void nativeSetSelection(int nativeClass, int start, int end); // Register a scheme to be treated as local scheme so that it can access // local asset files for resources - private native void nativeRegisterURLSchemeAsLocal(String scheme); + private native void nativeRegisterURLSchemeAsLocal(int nativeClass, + String scheme); /* * Inform webcore that the user has decided whether to allow or deny new @@ -632,34 +666,39 @@ public final class WebViewCore { * the main thread should wake up now. * @param limit Is the new quota for an origin or new app cache max size. */ - private native void nativeSetNewStorageLimit(long limit); + private native void nativeSetNewStorageLimit(int nativeClass, long limit); /** * Provide WebCore with a Geolocation permission state for the specified * origin. + * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass * @param origin The origin for which Geolocation permissions are provided. * @param allow Whether Geolocation permissions are allowed. * @param remember Whether this decision should be remembered beyond the * life of the current page. */ - private native void nativeGeolocationPermissionsProvide(String origin, boolean allow, boolean remember); + private native void nativeGeolocationPermissionsProvide(int nativeClass, + String origin, boolean allow, boolean remember); /** * Provide WebCore with the previously visted links from the history database + * @param nativeClass TODO */ - private native void nativeProvideVisitedHistory(String[] history); + private native void nativeProvideVisitedHistory(int nativeClass, + String[] history); /** * Modifies the current selection. * * Note: Accessibility support. - * + * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass * @param direction The direction in which to alter the selection. * @param granularity The granularity of the selection modification. * * @return The selection string. */ - private native String nativeModifySelection(int direction, int granularity); + private native String nativeModifySelection(int nativeClass, int direction, + int granularity); // EventHub for processing messages private final EventHub mEventHub; @@ -672,6 +711,7 @@ public final class WebViewCore { private static final int REDUCE_PRIORITY = 1; private static final int RESUME_PRIORITY = 2; + @Override public void run() { Looper.prepare(); Assert.assertNull(sWebCoreHandler); @@ -720,6 +760,13 @@ public final class WebViewCore { } BrowserFrame.sJavaBridge.updateProxy((ProxyProperties)msg.obj); break; + + case EventHub.HEARTBEAT: + // Ping back the watchdog to let it know we're still processing + // messages. + Message m = (Message)msg.obj; + m.sendToTarget(); + break; } } }; @@ -814,6 +861,23 @@ public final class WebViewCore { Rect mNativeLayerRect; } + static class WebKitHitTest { + String mLinkUrl; + String mAnchorText; + String mImageUrl; + String mAltDisplayString; + String mTitle; + Rect[] mTouchRects; + boolean mEditable; + int mTapHighlightColor = WebView.HIGHLIGHT_COLOR; + + // These are the input values that produced this hit test + int mHitTestX; + int mHitTestY; + int mHitTestSlop; + boolean mHitTestMovedMouse; + } + static class AutoFillData { public AutoFillData() { mQueryId = WebTextView.FORM_NOT_AUTOFILLABLE; @@ -958,6 +1022,8 @@ public final class WebViewCore { static final int SET_BACKGROUND_COLOR = 126; static final int SET_MOVE_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; + static final int DELETE_SURROUNDING_TEXT = 129; + static final int WEBKIT_DRAW = 130; static final int POST_URL = 132; @@ -1007,7 +1073,6 @@ public final class WebViewCore { static final int DUMP_DOMTREE = 170; static final int DUMP_RENDERTREE = 171; static final int DUMP_NAVTREE = 172; - static final int DUMP_V8COUNTERS = 173; static final int SET_JS_FLAGS = 174; static final int CONTENT_INVALIDATE_ALL = 175; @@ -1025,7 +1090,7 @@ public final class WebViewCore { static final int ADD_PACKAGE_NAME = 185; static final int REMOVE_PACKAGE_NAME = 186; - static final int GET_TOUCH_HIGHLIGHT_RECTS = 187; + static final int HIT_TEST = 187; // accessibility support static final int MODIFY_SELECTION = 190; @@ -1042,9 +1107,18 @@ public final class WebViewCore { static final int NOTIFY_ANIMATION_STARTED = 196; + static final int HEARTBEAT = 197; + + static final int SCROLL_LAYER = 198; + // private message ids private static final int DESTROY = 200; + // for cut & paste + static final int COPY_TEXT = 210; + static final int DELETE_TEXT = 211; + static final int INSERT_TEXT = 212; + // Private handler for WebCore messages. private Handler mHandler; // Message queue for containing messages before the WebCore thread is @@ -1120,18 +1194,19 @@ public final class WebViewCore { mSettings.onDestroyed(); mNativeClass = 0; mWebView = null; + WebCoreThreadWatchdog.quit(); } break; case REVEAL_SELECTION: - nativeRevealSelection(); + nativeRevealSelection(mNativeClass); break; case REQUEST_LABEL: if (mWebView != null) { int nodePointer = msg.arg2; - String label = nativeRequestLabel(msg.arg1, - nodePointer); + String label = nativeRequestLabel(mNativeClass, + msg.arg1, nodePointer); if (label != null && label.length() > 0) { Message.obtain(mWebView.mPrivateHandler, WebView.RETURN_LABEL, nodePointer, @@ -1141,7 +1216,7 @@ public final class WebViewCore { break; case UPDATE_FRAME_CACHE_IF_LOADING: - nativeUpdateFrameCacheIfLoading(); + nativeUpdateFrameCacheIfLoading(mNativeClass); break; case SCROLL_TEXT_INPUT: @@ -1151,7 +1226,7 @@ public final class WebViewCore { } else { xPercent = ((Float) msg.obj).floatValue(); } - nativeScrollFocusedTextInput(xPercent, msg.arg2); + nativeScrollFocusedTextInput(mNativeClass, xPercent, msg.arg2); break; case LOAD_URL: { @@ -1186,7 +1261,8 @@ public final class WebViewCore { !scheme.startsWith("ftp") && !scheme.startsWith("about") && !scheme.startsWith("javascript")) { - nativeRegisterURLSchemeAsLocal(scheme); + nativeRegisterURLSchemeAsLocal(mNativeClass, + scheme); } } } @@ -1195,7 +1271,7 @@ public final class WebViewCore { loadParams.mMimeType, loadParams.mEncoding, loadParams.mHistoryUrl); - nativeContentInvalidateAll(); + nativeContentInvalidateAll(mNativeClass); break; case STOP_LOADING: @@ -1224,11 +1300,11 @@ public final class WebViewCore { break; case FAKE_CLICK: - nativeClick(msg.arg1, msg.arg2, true); + nativeClick(mNativeClass, msg.arg1, msg.arg2, true); break; case CLICK: - nativeClick(msg.arg1, msg.arg2, false); + nativeClick(mNativeClass, msg.arg1, msg.arg2, false); break; case VIEW_SIZE_CHANGED: { @@ -1239,14 +1315,14 @@ public final class WebViewCore { // note: these are in document coordinates // (inv-zoom) Point pt = (Point) msg.obj; - nativeSetScrollOffset(msg.arg1, msg.arg2 == 1, - pt.x, pt.y); + nativeSetScrollOffset(mNativeClass, msg.arg1, + msg.arg2 == 1, pt.x, pt.y); break; case SET_GLOBAL_BOUNDS: Rect r = (Rect) msg.obj; - nativeSetGlobalBounds(r.left, r.top, r.width(), - r.height()); + nativeSetGlobalBounds(mNativeClass, r.left, r.top, + r.width(), r.height()); break; case GO_BACK_FORWARD: @@ -1275,7 +1351,7 @@ public final class WebViewCore { WebViewWorker.getHandler().sendEmptyMessage( WebViewWorker.MSG_PAUSE_CACHE_TRANSACTION); } else { - nativeCloseIdleConnections(); + nativeCloseIdleConnections(mNativeClass); } break; @@ -1289,16 +1365,16 @@ public final class WebViewCore { break; case ON_PAUSE: - nativePause(); + nativePause(mNativeClass); break; case ON_RESUME: - nativeResume(); + nativeResume(mNativeClass); break; case FREE_MEMORY: clearCache(false); - nativeFreeMemory(); + nativeFreeMemory(mNativeClass); break; case SET_NETWORK_STATE: @@ -1331,9 +1407,9 @@ public final class WebViewCore { case REPLACE_TEXT: ReplaceTextData rep = (ReplaceTextData) msg.obj; - nativeReplaceTextfieldText(msg.arg1, msg.arg2, - rep.mReplace, rep.mNewStart, rep.mNewEnd, - rep.mTextGeneration); + nativeReplaceTextfieldText(mNativeClass, msg.arg1, + msg.arg2, rep.mReplace, rep.mNewStart, + rep.mNewEnd, rep.mTextGeneration); break; case PASS_TO_JS: { @@ -1342,19 +1418,19 @@ public final class WebViewCore { int keyCode = evt.getKeyCode(); int keyValue = evt.getUnicodeChar(); int generation = msg.arg1; - passToJs(generation, + passToJs(mNativeClass, + generation, jsData.mCurrentText, keyCode, keyValue, - evt.isDown(), - evt.isShiftPressed(), evt.isAltPressed(), - evt.isSymPressed()); + evt.isDown(), evt.isShiftPressed(), + evt.isAltPressed(), evt.isSymPressed()); break; } case SAVE_DOCUMENT_STATE: { CursorData cDat = (CursorData) msg.obj; - nativeSaveDocumentState(cDat.mFrame); + nativeSaveDocumentState(mNativeClass, cDat.mFrame); break; } @@ -1363,7 +1439,7 @@ public final class WebViewCore { // FIXME: This will not work for connections currently in use, as // they cache the certificate responses. See http://b/5324235. SslCertLookupTable.getInstance().clear(); - nativeCloseIdleConnections(); + nativeCloseIdleConnections(mNativeClass); } else { Network.getInstance(mContext).clearUserSslPrefTable(); } @@ -1372,10 +1448,12 @@ public final class WebViewCore { case TOUCH_UP: TouchUpData touchUpData = (TouchUpData) msg.obj; if (touchUpData.mNativeLayer != 0) { - nativeScrollLayer(touchUpData.mNativeLayer, + nativeScrollLayer(mNativeClass, + touchUpData.mNativeLayer, touchUpData.mNativeLayerRect); } - nativeTouchUp(touchUpData.mMoveGeneration, + nativeTouchUp(mNativeClass, + touchUpData.mMoveGeneration, touchUpData.mFrame, touchUpData.mNode, touchUpData.mX, touchUpData.mY); break; @@ -1390,11 +1468,13 @@ public final class WebViewCore { yArray[c] = ted.mPoints[c].y; } if (ted.mNativeLayer != 0) { - nativeScrollLayer(ted.mNativeLayer, - ted.mNativeLayerRect); + nativeScrollLayer(mNativeClass, + ted.mNativeLayer, ted.mNativeLayerRect); } - ted.mNativeResult = nativeHandleTouchEvent(ted.mAction, ted.mIds, - xArray, yArray, count, ted.mActionIndex, ted.mMetaState); + ted.mNativeResult = nativeHandleTouchEvent( + mNativeClass, ted.mAction, ted.mIds, xArray, + yArray, count, ted.mActionIndex, + ted.mMetaState); Message.obtain( mWebView.mPrivateHandler, WebView.PREVENT_TOUCH_ID, @@ -1405,7 +1485,7 @@ public final class WebViewCore { } case SET_ACTIVE: - nativeSetFocusControllerActive(msg.arg1 == 1); + nativeSetFocusControllerActive(mNativeClass, msg.arg1 == 1); break; case ADD_JS_INTERFACE: @@ -1431,39 +1511,38 @@ public final class WebViewCore { case SET_MOVE_FOCUS: CursorData focusData = (CursorData) msg.obj; - nativeMoveFocus(focusData.mFrame, focusData.mNode); + nativeMoveFocus(mNativeClass, focusData.mFrame, focusData.mNode); break; case SET_MOVE_MOUSE: CursorData cursorData = (CursorData) msg.obj; - nativeMoveMouse(cursorData.mFrame, - cursorData.mX, cursorData.mY); + nativeMoveMouse(mNativeClass, + cursorData.mFrame, cursorData.mX, cursorData.mY); break; case SET_MOVE_MOUSE_IF_LATEST: CursorData cData = (CursorData) msg.obj; - nativeMoveMouseIfLatest(cData.mMoveGeneration, - cData.mFrame, - cData.mX, cData.mY); + nativeMoveMouseIfLatest(mNativeClass, + cData.mMoveGeneration, + cData.mFrame, cData.mX, cData.mY); if (msg.arg1 == 1) { - nativeStopPaintingCaret(); + nativeStopPaintingCaret(mNativeClass); } break; case REQUEST_CURSOR_HREF: { + WebKitHitTest hit = performHitTest(msg.arg1, msg.arg2, 1, false); Message hrefMsg = (Message) msg.obj; - hrefMsg.getData().putString("url", - nativeRetrieveHref(msg.arg1, msg.arg2)); - hrefMsg.getData().putString("title", - nativeRetrieveAnchorText(msg.arg1, msg.arg2)); - hrefMsg.getData().putString("src", - nativeRetrieveImageSource(msg.arg1, msg.arg2)); + Bundle data = hrefMsg.getData(); + data.putString(FocusNodeHref.URL,hit.mLinkUrl); + data.putString(FocusNodeHref.TITLE, hit.mAnchorText); + data.putString(FocusNodeHref.SRC, hit.mImageUrl); hrefMsg.sendToTarget(); break; } case UPDATE_CACHE_AND_TEXT_ENTRY: - nativeUpdateFrameCache(); + nativeUpdateFrameCache(mNativeClass); // FIXME: this should provide a minimal rectangle if (mWebView != null) { mWebView.postInvalidate(); @@ -1481,17 +1560,18 @@ public final class WebViewCore { case DELETE_SELECTION: TextSelectionData deleteSelectionData = (TextSelectionData) msg.obj; - nativeDeleteSelection(deleteSelectionData.mStart, - deleteSelectionData.mEnd, msg.arg1); + nativeDeleteSelection(mNativeClass, + deleteSelectionData.mStart, deleteSelectionData.mEnd, msg.arg1); break; case SET_SELECTION: - nativeSetSelection(msg.arg1, msg.arg2); + nativeSetSelection(mNativeClass, msg.arg1, msg.arg2); break; case MODIFY_SELECTION: - String modifiedSelectionString = nativeModifySelection(msg.arg1, - msg.arg2); + String modifiedSelectionString = + nativeModifySelection(mNativeClass, msg.arg1, + msg.arg2); mWebView.mPrivateHandler.obtainMessage(WebView.SELECTION_STRING_CHANGED, modifiedSelectionString).sendToTarget(); break; @@ -1504,40 +1584,36 @@ public final class WebViewCore { for (int c = 0; c < choicesSize; c++) { choicesArray[c] = choices.get(c); } - nativeSendListBoxChoices(choicesArray, - choicesSize); + nativeSendListBoxChoices(mNativeClass, + choicesArray, choicesSize); break; case SINGLE_LISTBOX_CHOICE: - nativeSendListBoxChoice(msg.arg1); + nativeSendListBoxChoice(mNativeClass, msg.arg1); break; case SET_BACKGROUND_COLOR: - nativeSetBackgroundColor(msg.arg1); + nativeSetBackgroundColor(mNativeClass, msg.arg1); break; case DUMP_DOMTREE: - nativeDumpDomTree(msg.arg1 == 1); + nativeDumpDomTree(mNativeClass, msg.arg1 == 1); break; case DUMP_RENDERTREE: - nativeDumpRenderTree(msg.arg1 == 1); + nativeDumpRenderTree(mNativeClass, msg.arg1 == 1); break; case DUMP_NAVTREE: - nativeDumpNavTree(); - break; - - case DUMP_V8COUNTERS: - nativeDumpV8Counters(); + nativeDumpNavTree(mNativeClass); break; case SET_JS_FLAGS: - nativeSetJsFlags((String)msg.obj); + nativeSetJsFlags(mNativeClass, (String)msg.obj); break; case CONTENT_INVALIDATE_ALL: - nativeContentInvalidateAll(); + nativeContentInvalidateAll(mNativeClass); break; case SAVE_WEBARCHIVE: @@ -1552,12 +1628,12 @@ public final class WebViewCore { case GEOLOCATION_PERMISSIONS_PROVIDE: GeolocationPermissionsData data = (GeolocationPermissionsData) msg.obj; - nativeGeolocationPermissionsProvide(data.mOrigin, - data.mAllow, data.mRemember); + nativeGeolocationPermissionsProvide(mNativeClass, + data.mOrigin, data.mAllow, data.mRemember); break; case SPLIT_PICTURE_SET: - nativeSplitContent(msg.arg1); + nativeSplitContent(mNativeClass, msg.arg1); mWebView.mPrivateHandler.obtainMessage( WebView.REPLACE_BASE_CONTENT, msg.arg1, 0); mSplitPictureIsScheduled = false; @@ -1567,7 +1643,7 @@ public final class WebViewCore { // Clear the view so that onDraw() will draw nothing // but white background // (See public method WebView.clearView) - nativeClearContent(); + clearContent(); break; case MESSAGE_RELAY: @@ -1575,15 +1651,15 @@ public final class WebViewCore { break; case POPULATE_VISITED_LINKS: - nativeProvideVisitedHistory((String[])msg.obj); + nativeProvideVisitedHistory(mNativeClass, (String[])msg.obj); break; case VALID_NODE_BOUNDS: { MotionUpData motionUpData = (MotionUpData) msg.obj; if (!nativeValidNodeAndBounds( - motionUpData.mFrame, motionUpData.mNode, - motionUpData.mBounds)) { - nativeUpdateFrameCache(); + mNativeClass, motionUpData.mFrame, + motionUpData.mNode, motionUpData.mBounds)) { + nativeUpdateFrameCache(mNativeClass); } Message message = mWebView.mPrivateHandler .obtainMessage(WebView.DO_MOTION_UP, @@ -1594,11 +1670,11 @@ public final class WebViewCore { } case HIDE_FULLSCREEN: - nativeFullScreenPluginHidden(msg.arg1); + nativeFullScreenPluginHidden(mNativeClass, msg.arg1); break; case PLUGIN_SURFACE_READY: - nativePluginSurfaceReady(); + nativePluginSurfaceReady(mNativeClass); break; case NOTIFY_ANIMATION_STARTED: @@ -1614,16 +1690,15 @@ public final class WebViewCore { (Set<String>) msg.obj); break; - case GET_TOUCH_HIGHLIGHT_RECTS: + case HIT_TEST: TouchHighlightData d = (TouchHighlightData) msg.obj; if (d.mNativeLayer != 0) { - nativeScrollLayer(d.mNativeLayer, - d.mNativeLayerRect); + nativeScrollLayer(mNativeClass, + d.mNativeLayer, d.mNativeLayerRect); } - ArrayList<Rect> rects = nativeGetTouchHighlightRects - (d.mX, d.mY, d.mSlop); + WebKitHitTest hit = performHitTest(d.mX, d.mY, d.mSlop, true); mWebView.mPrivateHandler.obtainMessage( - WebView.SET_TOUCH_HIGHLIGHT_RECTS, rects) + WebView.HIT_TEST_RESULT, hit) .sendToTarget(); break; @@ -1632,7 +1707,7 @@ public final class WebViewCore { break; case AUTOFILL_FORM: - nativeAutoFillForm(msg.arg1); + nativeAutoFillForm(mNativeClass, msg.arg1); mWebView.mPrivateHandler.obtainMessage(WebView.AUTOFILL_COMPLETE, null) .sendToTarget(); break; @@ -1645,6 +1720,33 @@ public final class WebViewCore { mBrowserFrame.stringByEvaluatingJavaScriptFromString((String) msg.obj); } break; + case SCROLL_LAYER: + int nativeLayer = msg.arg1; + Rect rect = (Rect) msg.obj; + nativeScrollLayer(mNativeClass, nativeLayer, + rect); + break; + + case DELETE_TEXT: { + int[] handles = (int[]) msg.obj; + nativeDeleteText(mNativeClass, handles[0], + handles[1], handles[2], handles[3]); + break; + } + case COPY_TEXT: { + int[] handles = (int[]) msg.obj; + String copiedText = nativeGetText(mNativeClass, + handles[0], handles[1], handles[2], + handles[3]); + if (copiedText != null) { + mWebView.mPrivateHandler.obtainMessage(WebView.COPY_TO_CLIPBOARD, copiedText) + .sendToTarget(); + } + break; + } + case INSERT_TEXT: + nativeInsertText(mNativeClass, (String) msg.obj); + break; } } }; @@ -1820,6 +1922,15 @@ public final class WebViewCore { // WebViewCore private methods //------------------------------------------------------------------------- + private WebKitHitTest performHitTest(int x, int y, int slop, boolean moveMouse) { + WebKitHitTest hit = nativeHitTest(mNativeClass, x, y, slop, moveMouse); + hit.mHitTestX = x; + hit.mHitTestY = y; + hit.mHitTestSlop = slop; + hit.mHitTestMovedMouse = moveMouse; + return hit; + } + private void clearCache(boolean includeDiskFiles) { mBrowserFrame.clearCache(); if (includeDiskFiles) { @@ -1853,9 +1964,9 @@ public final class WebViewCore { unicodeChar = evt.getCharacters().codePointAt(0); } - if (!nativeKey(keyCode, unicodeChar, evt.getRepeatCount(), evt.isShiftPressed(), - evt.isAltPressed(), evt.isSymPressed(), - isDown) && keyCode != KeyEvent.KEYCODE_ENTER) { + if (!nativeKey(mNativeClass, keyCode, unicodeChar, evt.getRepeatCount(), + evt.isShiftPressed(), evt.isAltPressed(), + evt.isSymPressed(), isDown) && keyCode != KeyEvent.KEYCODE_ENTER) { if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { if (DebugFlags.WEB_VIEW_CORE) { @@ -1901,9 +2012,9 @@ public final class WebViewCore { float ratio = (heightWidthRatio > 0) ? heightWidthRatio : (float) h / w; height = Math.round(ratio * width); } - nativeSetSize(width, height, textwrapWidth, scale, w, - data.mActualViewHeight > 0 ? data.mActualViewHeight : h, - data.mAnchorX, data.mAnchorY, data.mIgnoreHeight); + int screenHeight = data.mActualViewHeight > 0 ? data.mActualViewHeight : h; + nativeSetSize(mNativeClass, width, height, textwrapWidth, scale, + w, screenHeight, data.mAnchorX, data.mAnchorY, data.mIgnoreHeight); // Remember the current width and height boolean needInvalidate = (mCurrentViewWidth == 0); mCurrentViewWidth = w; @@ -2039,7 +2150,8 @@ public final class WebViewCore { mDrawIsScheduled = false; DrawData draw = new DrawData(); if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start"); - draw.mBaseLayer = nativeRecordContent(draw.mInvalRegion, draw.mContentSize); + draw.mBaseLayer = nativeRecordContent(mNativeClass, draw.mInvalRegion, + draw.mContentSize); if (draw.mBaseLayer == 0) { if (mWebView != null && !mWebView.isPaused()) { if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort, resending draw message"); @@ -2055,14 +2167,14 @@ public final class WebViewCore { private void webkitDraw(DrawData draw) { if (mWebView != null) { - draw.mFocusSizeChanged = nativeFocusBoundsChanged(); + draw.mFocusSizeChanged = nativeFocusBoundsChanged(mNativeClass); draw.mViewSize = new Point(mCurrentViewWidth, mCurrentViewHeight); if (mSettings.getUseWideViewPort()) { draw.mMinPrefWidth = Math.max( mViewportWidth == -1 ? WebView.DEFAULT_VIEWPORT_WIDTH : (mViewportWidth == 0 ? mCurrentViewWidth : mViewportWidth), - nativeGetContentMinPrefWidth()); + nativeGetContentMinPrefWidth(mNativeClass)); } if (mInitialViewState != null) { draw.mViewState = mInitialViewState; @@ -2113,7 +2225,7 @@ public final class WebViewCore { Log.w(LOGTAG, "Cannot pauseUpdatePicture, core destroyed or not initialized!"); return; } - core.nativeSetIsPaused(true); + core.nativeSetIsPaused(core.mNativeClass, true); core.mDrawIsPaused = true; } } @@ -2131,7 +2243,7 @@ public final class WebViewCore { Log.w(LOGTAG, "Cannot resumeUpdatePicture, core destroyed!"); return; } - core.nativeSetIsPaused(false); + core.nativeSetIsPaused(core.mNativeClass, false); core.mDrawIsPaused = false; // always redraw on resume to reenable gif animations core.mDrawIsScheduled = false; @@ -2255,7 +2367,7 @@ public final class WebViewCore { return mWebView; } - private native void setViewportSettingsFromNative(); + private native void setViewportSettingsFromNative(int nativeClass); // called by JNI private void didFirstLayout(boolean standardLoad) { @@ -2277,9 +2389,9 @@ public final class WebViewCore { } // remove the touch highlight when moving to a new page - if (WebView.USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) { + if (WebView.sDisableNavcache) { mWebView.mPrivateHandler.sendEmptyMessage( - WebView.SET_TOUCH_HIGHLIGHT_RECTS); + WebView.HIT_TEST_RESULT); } // reset the scroll position, the restored offset and scales @@ -2300,7 +2412,7 @@ public final class WebViewCore { return; } // set the viewport settings from WebKit - setViewportSettingsFromNative(); + setViewportSettingsFromNative(mNativeClass); // clamp initial scale if (mViewportInitialScale > 0) { @@ -2338,11 +2450,7 @@ public final class WebViewCore { // adjust the default scale to match the densityDpi float adjust = 1.0f; if (mViewportDensityDpi == -1) { - // convert default zoom scale to a integer (percentage) to avoid any - // issues with floating point comparisons - if (mWebView != null && (int)(mWebView.getDefaultZoomScale() * 100) != 100) { - adjust = mWebView.getDefaultZoomScale(); - } + adjust = mContext.getResources().getDisplayMetrics().density; } else if (mViewportDensityDpi > 0) { adjust = (float) mContext.getResources().getDisplayMetrics().densityDpi / mViewportDensityDpi; @@ -2521,7 +2629,7 @@ public final class WebViewCore { if (mSettings.isNarrowColumnLayout()) { // In case of automatic text reflow in fixed view port mode. mInitialViewState.mTextWrapScale = - mWebView.getReadingLevelScale(); + mWebView.computeReadingLevelScale(data.mScale); } } else { // Scale is given such as when page is restored, use it. @@ -2614,18 +2722,31 @@ public final class WebViewCore { WebView.FIND_AGAIN).sendToTarget(); } - private native void nativeUpdateFrameCacheIfLoading(); - private native void nativeRevealSelection(); - private native String nativeRequestLabel(int framePtr, int nodePtr); + // called by JNI + private void initEditField(String text, int start, int end) { + if (mWebView == null) { + return; + } + Message.obtain(mWebView.mPrivateHandler, + WebView.INIT_EDIT_FIELD, start, end, text).sendToTarget(); + } + + private native void nativeUpdateFrameCacheIfLoading(int nativeClass); + private native void nativeRevealSelection(int nativeClass); + private native String nativeRequestLabel(int nativeClass, int framePtr, + int nodePtr); /** * Scroll the focused textfield to (xPercent, y) in document space */ - private native void nativeScrollFocusedTextInput(float xPercent, int y); + private native void nativeScrollFocusedTextInput(int nativeClass, + float xPercent, int y); // these must be in document space (i.e. not scaled/zoomed). - private native void nativeSetScrollOffset(int gen, boolean sendScrollEvent, int dx, int dy); + private native void nativeSetScrollOffset(int nativeClass, int gen, + boolean sendScrollEvent, int dx, int dy); - private native void nativeSetGlobalBounds(int x, int y, int w, int h); + private native void nativeSetGlobalBounds(int nativeClass, int x, int y, + int w, int h); // called by JNI private void requestListBox(String[] array, int[] enabledArray, @@ -2860,18 +2981,49 @@ public final class WebViewCore { return mDeviceOrientationService; } - private native void nativeSetIsPaused(boolean isPaused); - private native void nativePause(); - private native void nativeResume(); - private native void nativeFreeMemory(); - private native void nativeFullScreenPluginHidden(int npp); - private native void nativePluginSurfaceReady(); - private native boolean nativeValidNodeAndBounds(int frame, int node, - Rect bounds); + private native void nativeSetIsPaused(int nativeClass, boolean isPaused); + private native void nativePause(int nativeClass); + private native void nativeResume(int nativeClass); + private native void nativeFreeMemory(int nativeClass); + private native void nativeFullScreenPluginHidden(int nativeClass, int npp); + private native void nativePluginSurfaceReady(int nativeClass); + private native boolean nativeValidNodeAndBounds(int nativeClass, int frame, + int node, Rect bounds); - private native ArrayList<Rect> nativeGetTouchHighlightRects(int x, int y, - int slop); + private native WebKitHitTest nativeHitTest(int nativeClass, int x, int y, + int slop, boolean moveMouse); - private native void nativeAutoFillForm(int queryId); - private native void nativeScrollLayer(int layer, Rect rect); + private native void nativeAutoFillForm(int nativeClass, int queryId); + private native void nativeScrollLayer(int nativeClass, int layer, Rect rect); + + /** + * Deletes editable text between two points. Note that the selection may + * differ from the WebView's selection because the algorithms for selecting + * text differs for non-LTR text. Any text that isn't editable will be + * left unchanged. + * @param nativeClass The pointer to the native class (mNativeClass) + * @param startX The X position of the top-left selection point. + * @param startY The Y position of the top-left selection point. + * @param endX The X position of the bottom-right selection point. + * @param endY The Y position of the bottom-right selection point. + */ + private native void nativeDeleteText(int nativeClass, + int startX, int startY, int endX, int endY); + /** + * Inserts text at the current cursor position. If the currently-focused + * node does not have a cursor position then this function does nothing. + */ + private native void nativeInsertText(int nativeClass, String text); + /** + * Gets the text between two selection points. Note that the selection + * may differ from the WebView's selection because the algorithms for + * selecting text differs for non-LTR text. + * @param nativeClass The pointer to the native class (mNativeClass) + * @param startX The X position of the top-left selection point. + * @param startY The Y position of the top-left selection point. + * @param endX The X position of the bottom-right selection point. + * @param endY The Y position of the bottom-right selection point. + */ + private native String nativeGetText(int nativeClass, + int startX, int startY, int endX, int endY); } diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java index 14bdc42..8ffba64 100644 --- a/core/java/android/webkit/ZoomManager.java +++ b/core/java/android/webkit/ZoomManager.java @@ -316,7 +316,12 @@ class ZoomManager { * Returns the zoom scale used for reading text on a double-tap. */ public final float getReadingLevelScale() { - return mDisplayDensity * mDoubleTapZoomFactor; + return computeScaleWithLimits(computeReadingLevelScale(getZoomOverviewScale())); + } + + /* package */ final float computeReadingLevelScale(float scale) { + return Math.max(mDisplayDensity * mDoubleTapZoomFactor, + scale + MIN_DOUBLE_TAP_SCALE_INCREMENT); } public final float getInvDefaultScale() { @@ -678,7 +683,7 @@ class ZoomManager { } zoomToOverview(); } else { - zoomToReadingLevelOrMore(); + zoomToReadingLevel(); } } @@ -709,9 +714,8 @@ class ZoomManager { !mWebView.getSettings().getUseFixedViewport()); } - private void zoomToReadingLevelOrMore() { - final float zoomScale = Math.max(getReadingLevelScale(), - mActualScale + MIN_DOUBLE_TAP_SCALE_INCREMENT); + private void zoomToReadingLevel() { + final float readingScale = getReadingLevelScale(); int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); if (left != WebView.NO_LEFTEDGE) { @@ -721,13 +725,13 @@ class ZoomManager { // Re-calculate the zoom center so that the new scroll x will be // on the left edge. if (viewLeft > 0) { - mZoomCenterX = viewLeft * zoomScale / (zoomScale - mActualScale); + mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale); } else { mWebView.scrollBy(viewLeft, 0); mZoomCenterX = 0; } } - startZoomAnimation(zoomScale, + startZoomAnimation(readingScale, !mWebView.getSettings().getUseFixedViewport()); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 38bb2e1..e94b1cb 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -20,7 +20,6 @@ import com.android.internal.R; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -1297,6 +1296,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te super.sendAccessibilityEvent(eventType); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AbsListView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AbsListView.class.getName()); + } + /** * Indicates whether the children's drawing cache is used during a scroll. * By default, the drawing cache is enabled but this will consume more memory. @@ -5572,6 +5583,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * Hints the RemoteViewsAdapter, if it exists, about which views are currently + * being displayed by the AbsListView. + */ + void setVisibleRangeHint(int start, int end) { + if (mRemoteAdapter != null) { + mRemoteAdapter.setVisibleRangeHint(start, end); + } + } + + /** * Sets the recycler listener to be notified whenever a View is set aside in * the recycler for later reuse. This listener can be used to free resources * associated to the View. diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index bdaf89e..e36afa3 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -25,6 +25,8 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; public abstract class AbsSeekBar extends ProgressBar { private Drawable mThumb; @@ -464,4 +466,15 @@ public abstract class AbsSeekBar extends ProgressBar { return super.onKeyDown(keyCode, event); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AbsSeekBar.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AbsSeekBar.class.getName()); + } } diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index 3d79205..efdfae3 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -28,6 +28,8 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * An abstract base class for spinner widgets. SDK users will probably not @@ -40,7 +42,6 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { int mHeightMeasureSpec; int mWidthMeasureSpec; - boolean mBlockLayoutRequests; int mSelectionLeftPadding = 0; int mSelectionTopPadding = 0; @@ -463,4 +464,16 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { scrapHeap.clear(); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AbsSpinner.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AbsSpinner.class.getName()); + } } diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index 60b24bc..be6b4e2 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -33,8 +33,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; import android.widget.ActivityChooserModel.ActivityChooserModelClient; /** @@ -366,7 +364,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod getListPopupWindow().dismiss(); ViewTreeObserver viewTreeObserver = getViewTreeObserver(); if (viewTreeObserver.isAlive()) { - viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); + viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener); } } return true; @@ -400,7 +398,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } ViewTreeObserver viewTreeObserver = getViewTreeObserver(); if (viewTreeObserver.isAlive()) { - viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); + viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener); } mIsAttachedToWindow = false; } @@ -547,6 +545,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod position = mAdapter.getShowDefaultActivity() ? position : position + 1; Intent launchIntent = mAdapter.getDataModel().chooseActivity(position); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } } @@ -564,6 +563,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity); Intent launchIntent = mAdapter.getDataModel().chooseActivity(index); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } } else if (view == mExpandActivityOverflowButton) { diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 40df168..97a864c 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -913,6 +913,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AdapterView.class.getName()); info.setScrollable(isScrollableForAccessibility()); View selectedView = getSelectedView(); if (selectedView != null) { @@ -923,6 +924,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(AdapterView.class.getName()); event.setScrollable(isScrollableForAccessibility()); View selectedView = getSelectedView(); if (selectedView != null) { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index c83c780..bb00049 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -29,6 +29,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import java.util.ArrayList; import java.util.HashMap; @@ -555,6 +557,11 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> mCurrentWindowStart = newWindowStart; mCurrentWindowEnd = newWindowEnd; mCurrentWindowStartUnbounded = newWindowStartUnbounded; + if (mRemoteViewsAdapter != null) { + int adapterStart = modulo(mCurrentWindowStart, adapterCount); + int adapterEnd = modulo(mCurrentWindowEnd, adapterCount); + mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd); + } } requestLayout(); invalidate(); @@ -1045,4 +1052,16 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> */ public void fyiWillBeAdvancedByHostKThx() { } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AdapterViewAnimator.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AdapterViewAnimator.class.getName()); + } } diff --git a/core/java/android/widget/AdapterViewFlipper.java b/core/java/android/widget/AdapterViewFlipper.java index 4419886..5096227 100644 --- a/core/java/android/widget/AdapterViewFlipper.java +++ b/core/java/android/widget/AdapterViewFlipper.java @@ -16,7 +16,6 @@ package android.widget; -import android.animation.ObjectAnimator; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,7 +26,8 @@ import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.RemotableViewMethod; -import android.view.animation.AlphaAnimation; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -268,4 +268,16 @@ public class AdapterViewFlipper extends AdapterViewAnimator { mAdvancedByHost = true; updateRunning(false); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(AdapterViewFlipper.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(AdapterViewFlipper.class.getName()); + } } diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 07523e3..f7a6b27 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -1085,10 +1085,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe for (int i = 0; i < count; i++) { if (adapter.isEnabled(i)) { - realCount++; Object item = adapter.getItem(i); long id = adapter.getItemId(i); - completions[i] = new CompletionInfo(id, i, convertSelectionToString(item)); + completions[realCount] = new CompletionInfo(id, realCount, + convertSelectionToString(item)); + realCount++; } } diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java index 8d58a6d..99f4cae 100644 --- a/core/java/android/widget/Button.java +++ b/core/java/android/widget/Button.java @@ -18,9 +18,8 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; -import android.util.Log; -import android.view.MotionEvent; -import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -107,4 +106,16 @@ public class Button extends TextView { public Button(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Button.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Button.class.getName()); + } } diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index e0403ff..85252af 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -39,6 +39,8 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.AbsListView.OnScrollListener; import com.android.internal.R; @@ -431,6 +433,18 @@ public class CalendarView extends FrameLayout { setCurrentLocale(newConfig.locale); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(CalendarView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(CalendarView.class.getName()); + } + /** * Gets the minimal date supported by this {@link CalendarView} in milliseconds * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index 2788846..0685eea 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -19,6 +19,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -78,4 +79,16 @@ public class CheckBox extends CompoundButton { event.getText().add(mContext.getString(R.string.checkbox_not_checked)); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(CheckBox.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(CheckBox.class.getName()); + } } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 0a54743..5c7e5a3 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -220,6 +220,7 @@ public class CheckedTextView extends TextView implements Checkable { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(CheckedTextView.class.getName()); event.setChecked(mChecked); } @@ -236,6 +237,7 @@ public class CheckedTextView extends TextView implements Checkable { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(CheckedTextView.class.getName()); info.setChecked(mChecked); } } diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 7e66722..0370049 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -25,6 +25,8 @@ import android.os.SystemClock; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import java.util.Formatter; @@ -276,4 +278,16 @@ public class Chronometer extends TextView { mOnChronometerTickListener.onChronometerTick(this); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Chronometer.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Chronometer.class.getName()); + } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index d3cdad8..02c4c4f 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -211,12 +211,14 @@ public abstract class CompoundButton extends Button implements Checkable { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(CompoundButton.class.getName()); event.setChecked(mChecked); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(CompoundButton.class.getName()); info.setCheckable(true); info.setChecked(mChecked); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 0f462ff..110c8f3 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -31,6 +31,7 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.NumberPicker.OnValueChangeListener; @@ -391,6 +392,18 @@ public class DatePicker extends FrameLayout { } @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(DatePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(DatePicker.class.getName()); + } + + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setCurrentLocale(newConfig.locale); diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java index 379883a..add9d9b 100644 --- a/core/java/android/widget/DigitalClock.java +++ b/core/java/android/widget/DigitalClock.java @@ -24,6 +24,8 @@ import android.os.SystemClock; import android.provider.Settings; import android.text.format.DateFormat; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import java.util.Calendar; @@ -126,4 +128,16 @@ public class DigitalClock extends TextView { setFormat(); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(DigitalClock.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(DigitalClock.class.getName()); + } } diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 0da68a4..2fd8768 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -24,6 +24,8 @@ import android.text.TextUtils; import android.text.method.ArrowKeyMovementMethod; import android.text.method.MovementMethod; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /* @@ -114,4 +116,16 @@ public class EditText extends TextView { } super.setEllipsize(ellipsis); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(EditText.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(EditText.class.getName()); + } } diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index ead9b4f..badfaa7 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -30,6 +30,8 @@ import android.view.ContextMenu; import android.view.SoundEffectConstants; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ExpandableListConnector.PositionMetadata; import java.util.ArrayList; @@ -1167,4 +1169,15 @@ public class ExpandableListView extends ListView { } } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ExpandableListView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ExpandableListView.class.getName()); + } } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index 74a57b0..da98884 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -29,6 +29,8 @@ import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -555,6 +557,19 @@ public class FrameLayout extends ViewGroup { return new LayoutParams(p); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(FrameLayout.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(FrameLayout.class.getName()); + } + /** * Per-child layout information for layouts that support margins. * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes} diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index 5e37fa8..03fdc39 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -32,6 +32,8 @@ import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Transformation; import com.android.internal.R; @@ -1355,6 +1357,18 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Gallery.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Gallery.class.getName()); + } + /** * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to * initiate a fling. Each frame of the fling is handled in {@link #run()}. diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 7cf5168..984ec79 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -27,6 +27,9 @@ import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + import com.android.internal.R; import java.lang.reflect.Array; @@ -839,9 +842,11 @@ public class GridLayout extends ViewGroup { * @hide */ @Override - protected void onChildVisibilityChanged(View child, int visibility) { - super.onChildVisibilityChanged(child, visibility); - invalidateStructure(); + protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) { + super.onChildVisibilityChanged(child, oldVisibility, newVisibility); + if (oldVisibility == GONE || newVisibility == GONE) { + invalidateStructure(); + } } // Measurement @@ -1041,6 +1046,18 @@ public class GridLayout extends ViewGroup { } } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(GridLayout.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(GridLayout.class.getName()); + } + // Inner classes /* diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 5d406de..be2df8e 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -27,6 +27,8 @@ import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.GridLayoutAnimationController; import android.widget.RemoteViews.RemoteView; @@ -290,6 +292,7 @@ public class GridView extends AbsListView { pos += mNumColumns; } + setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; } @@ -382,6 +385,7 @@ public class GridView extends AbsListView { mFirstPosition = Math.max(0, pos + 1); } + setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; } @@ -2116,5 +2120,16 @@ public class GridView extends AbsListView { } return result; } -} + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(GridView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(GridView.class.getName()); + } +} diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 1683d20..0b4ebf4 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -721,12 +721,14 @@ public class HorizontalScrollView extends FrameLayout { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(HorizontalScrollView.class.getName()); info.setScrollable(getScrollRange() > 0); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(HorizontalScrollView.class.getName()); event.setScrollable(getScrollRange() > 0); event.setScrollX(mScrollX); event.setScrollY(mScrollY); diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index d680fad..59a8f28 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -21,6 +21,8 @@ import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import java.util.Map; @@ -90,4 +92,16 @@ public class ImageButton extends ImageView { protected boolean onSetAlpha(int alpha) { return false; } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ImageButton.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ImageButton.class.getName()); + } } diff --git a/core/java/android/widget/ImageSwitcher.java b/core/java/android/widget/ImageSwitcher.java index bcb750a..c048970 100644 --- a/core/java/android/widget/ImageSwitcher.java +++ b/core/java/android/widget/ImageSwitcher.java @@ -16,12 +16,12 @@ package android.widget; -import java.util.Map; - import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; public class ImageSwitcher extends ViewSwitcher @@ -55,5 +55,16 @@ public class ImageSwitcher extends ViewSwitcher image.setImageDrawable(drawable); showNext(); } -} + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ImageSwitcher.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ImageSwitcher.class.getName()); + } +} diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 73e1273..07ae93b 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -37,6 +37,7 @@ import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -1060,4 +1061,16 @@ public class ImageView extends View { mDrawable.setVisible(false, false); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ImageView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ImageView.class.getName()); + } } diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 427fd3e..b5deec7 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -27,6 +27,8 @@ import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -1729,7 +1731,19 @@ public class LinearLayout extends ViewGroup { protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LinearLayout.LayoutParams; } - + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(LinearLayout.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(LinearLayout.class.getName()); + } + /** * Per-child layout information associated with ViewLinearLayout. * diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 7f7a3a7..e20d12a 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -32,13 +32,13 @@ import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.FocusFinder; import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import java.util.ArrayList; @@ -678,6 +678,7 @@ public class ListView extends AbsListView { pos++; } + setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; } @@ -711,7 +712,7 @@ public class ListView extends AbsListView { } mFirstPosition = pos + 1; - + setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; } @@ -3609,4 +3610,16 @@ public class ListView extends AbsListView { } return new long[0]; } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ListView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ListView.class.getName()); + } } diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index f2ea3fc..fc35f05 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -31,6 +31,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.SeekBar.OnSeekBarChangeListener; import com.android.internal.policy.PolicyManager; @@ -592,6 +594,18 @@ public class MediaController extends FrameLayout { super.setEnabled(enabled); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(MediaController.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(MediaController.class.getName()); + } + private View.OnClickListener mRewListener = new View.OnClickListener() { public void onClick(View v) { int pos = mPlayer.getCurrentPosition(); diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 134e4c4..0b30c84 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -23,7 +23,8 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.method.QwertyKeyListener; import android.util.AttributeSet; -import android.widget.MultiAutoCompleteTextView.Tokenizer; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * An editable text view, extending {@link AutoCompleteTextView}, that @@ -196,6 +197,18 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { editable.replace(start, end, mTokenizer.terminateToken(text)); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(MultiAutoCompleteTextView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(MultiAutoCompleteTextView.class.getName()); + } + public static interface Tokenizer { /** * Returns the start of the token that ends at offset diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 13375bf..d395fb2 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -47,6 +47,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodManager; @@ -54,12 +55,12 @@ import com.android.internal.R; /** * A widget that enables the user to select a number form a predefined range. - * The widget presents an input filed and up and down buttons for selecting the + * The widget presents an input field and up and down buttons for selecting the * current value. Pressing/long pressing the up and down buttons increments and - * decrements the current value respectively. Touching the input filed shows a + * decrements the current value respectively. Touching the input field shows a * scroll wheel, tapping on which while shown and not moving allows direct edit * of the current value. Sliding motions up or down hide the buttons and the - * input filed, show the scroll wheel, and rotate the latter. Flinging is + * input field, show the scroll wheel, and rotate the latter. Flinging is * also supported. The widget enables mapping from positions to strings such * that instead the position index the corresponding string is displayed. * <p> @@ -70,6 +71,11 @@ import com.android.internal.R; public class NumberPicker extends LinearLayout { /** + * The number of items show in the selector wheel. + */ + public static final int SELECTOR_WHEEL_ITEM_COUNT = 5; + + /** * The default update interval during long press. */ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; @@ -583,10 +589,7 @@ public class NumberPicker extends LinearLayout { OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } + hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { changeCurrentByOne(true); @@ -598,6 +601,7 @@ public class NumberPicker extends LinearLayout { OnLongClickListener onLongClickListener = new OnLongClickListener() { public boolean onLongClick(View v) { + hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { postChangeCurrentByOneFromLongPress(true); @@ -786,6 +790,7 @@ public class NumberPicker extends LinearLayout { } mBeginEditOnUpEvent = scrollersFinished; mAdjustScrollerOnUpEvent = true; + hideSoftInput(); hideInputControls(); return true; } @@ -795,6 +800,7 @@ public class NumberPicker extends LinearLayout { } mAdjustScrollerOnUpEvent = false; setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); + hideSoftInput(); hideInputControls(); return true; case MotionEvent.ACTION_MOVE: @@ -804,6 +810,7 @@ public class NumberPicker extends LinearLayout { mBeginEditOnUpEvent = false; onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE); + hideSoftInput(); hideInputControls(); return true; } @@ -1062,6 +1069,16 @@ public class NumberPicker extends LinearLayout { } /** + * Hides the soft input of it is active for the input text. + */ + private void hideSoftInput() { + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { + inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } + } + + /** * Computes the max width if no such specified as an attribute. */ private void tryComputeMaxWidth() { @@ -1125,14 +1142,17 @@ public class NumberPicker extends LinearLayout { * items shown on the selector wheel) the selector wheel wrapping is * enabled. * </p> - * + * <p> + * <strong>Note:</strong> If the number of items, i.e. the range + * ({@link #getMaxValue()} - {@link #getMinValue()}) is less than + * {@link #SELECTOR_WHEEL_ITEM_COUNT}, the selector wheel will not + * wrap. Hence, in such a case calling this method is a NOP. + * </p> * @param wrapSelectorWheel Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelectorWheel) { - if (wrapSelectorWheel && (mMaxValue - mMinValue) < mSelectorIndices.length) { - throw new IllegalStateException("Range less than selector items count."); - } - if (wrapSelectorWheel != mWrapSelectorWheel) { + final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; + if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { mWrapSelectorWheel = wrapSelectorWheel; updateIncrementAndDecrementButtonsVisibilityState(); } @@ -1371,6 +1391,18 @@ public class NumberPicker extends LinearLayout { // perceive this widget as several controls rather as a whole. } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(NumberPicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(NumberPicker.class.getName()); + } + /** * Makes a measure spec that tries greedily to use the max value. * diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index df88fec..ace3f60 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -45,6 +45,7 @@ import android.view.View; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -1124,10 +1125,17 @@ public class ProgressBar extends View { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(ProgressBar.class.getName()); event.setItemCount(mMax); event.setCurrentItemIndex(mProgress); } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ProgressBar.class.getName()); + } + /** * Schedule a command for sending an accessibility event. * </br> diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index adc0fb0..786afe2 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -36,6 +36,8 @@ import android.provider.ContactsContract.RawContacts; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * Widget used to show an image with the standard QuickContact badge @@ -228,6 +230,18 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(QuickContactBadge.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(QuickContactBadge.class.getName()); + } + /** * Set a list of specific MIME-types to exclude and not display. For * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE} diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java index 9fa649f..b6dac3e 100644 --- a/core/java/android/widget/RadioButton.java +++ b/core/java/android/widget/RadioButton.java @@ -19,6 +19,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -85,4 +86,16 @@ public class RadioButton extends CompoundButton { event.getText().add(mContext.getString(R.string.radiobutton_not_selected)); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(RadioButton.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RadioButton.class.getName()); + } } diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java index 393346a..7f53ffd 100644 --- a/core/java/android/widget/RadioGroup.java +++ b/core/java/android/widget/RadioGroup.java @@ -23,6 +23,8 @@ import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** @@ -236,6 +238,18 @@ public class RadioGroup extends LinearLayout { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(RadioGroup.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RadioGroup.class.getName()); + } + /** * <p>This set of layout parameters defaults the width and the height of * the children to {@link #WRAP_CONTENT} when they are not specified in the diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java index 9e6ff4b..e69577b 100644 --- a/core/java/android/widget/RatingBar.java +++ b/core/java/android/widget/RatingBar.java @@ -21,6 +21,8 @@ import android.content.res.TypedArray; import android.graphics.drawable.shapes.RectShape; import android.graphics.drawable.shapes.Shape; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -324,4 +326,15 @@ public class RatingBar extends AbsSeekBar { super.setMax(max); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(RatingBar.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RatingBar.class.getName()); + } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 12a93ac..e4b8f34 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -18,10 +18,10 @@ package android.widget; import com.android.internal.R; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.HashMap; import java.util.SortedSet; import java.util.TreeSet; @@ -40,6 +40,7 @@ import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import static android.util.Log.d; @@ -151,6 +152,15 @@ public class RelativeLayout extends ViewGroup { private static final int VERB_COUNT = 16; + + private static final int[] RULES_VERTICAL = { + ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM + }; + + private static final int[] RULES_HORIZONTAL = { + LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT + }; + private View mBaselineView = null; private boolean mHasBaselineAlignedChild; @@ -284,14 +294,13 @@ public class RelativeLayout extends ViewGroup { if (DEBUG_GRAPH) { d(LOG_TAG, "=== Sorted vertical children"); - graph.log(getResources(), ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM); + graph.log(getResources(), RULES_VERTICAL); d(LOG_TAG, "=== Sorted horizontal children"); - graph.log(getResources(), LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT); + graph.log(getResources(), RULES_HORIZONTAL); } - graph.getSortedViews(mSortedVerticalChildren, ABOVE, BELOW, ALIGN_BASELINE, - ALIGN_TOP, ALIGN_BOTTOM); - graph.getSortedViews(mSortedHorizontalChildren, LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT); + graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL); + graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL); if (DEBUG_GRAPH) { d(LOG_TAG, "=== Ordered list of vertical children"); @@ -977,6 +986,18 @@ public class RelativeLayout extends ViewGroup { return false; } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(RelativeLayout.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RelativeLayout.class.getName()); + } + /** * Compares two views in left-to-right and top-to-bottom fashion. */ @@ -1216,7 +1237,7 @@ public class RelativeLayout extends ViewGroup { * Temporary data structure used to build the list of roots * for this graph. */ - private LinkedList<Node> mRoots = new LinkedList<Node>(); + private ArrayDeque<Node> mRoots = new ArrayDeque<Node>(); /** * Clears the graph. @@ -1261,18 +1282,18 @@ public class RelativeLayout extends ViewGroup { * @param rules The list of rules to take into account. */ void getSortedViews(View[] sorted, int... rules) { - final LinkedList<Node> roots = findRoots(rules); + final ArrayDeque<Node> roots = findRoots(rules); int index = 0; - while (roots.size() > 0) { - final Node node = roots.removeFirst(); + Node node; + while ((node = roots.pollLast()) != null) { final View view = node.view; final int key = view.getId(); sorted[index++] = view; - final HashSet<Node> dependents = node.dependents; - for (Node dependent : dependents) { + final HashMap<Node, DependencyGraph> dependents = node.dependents; + for (Node dependent : dependents.keySet()) { final SparseArray<Node> dependencies = dependent.dependencies; dependencies.remove(key); @@ -1297,7 +1318,7 @@ public class RelativeLayout extends ViewGroup { * * @return A list of node, each being a root of the graph */ - private LinkedList<Node> findRoots(int[] rulesFilter) { + private ArrayDeque<Node> findRoots(int[] rulesFilter) { final SparseArray<Node> keyNodes = mKeyNodes; final ArrayList<Node> nodes = mNodes; final int count = nodes.size(); @@ -1330,20 +1351,20 @@ public class RelativeLayout extends ViewGroup { continue; } // Add the current node as a dependent - dependency.dependents.add(node); + dependency.dependents.put(node, this); // Add a dependency to the current node node.dependencies.put(rule, dependency); } } } - final LinkedList<Node> roots = mRoots; + final ArrayDeque<Node> roots = mRoots; roots.clear(); // Finds all the roots in the graph: all nodes with no dependencies for (int i = 0; i < count; i++) { final Node node = nodes.get(i); - if (node.dependencies.size() == 0) roots.add(node); + if (node.dependencies.size() == 0) roots.addLast(node); } return roots; @@ -1356,7 +1377,7 @@ public class RelativeLayout extends ViewGroup { * @param rules The list of rules to take into account. */ void log(Resources resources, int... rules) { - final LinkedList<Node> roots = findRoots(rules); + final ArrayDeque<Node> roots = findRoots(rules); for (Node node : roots) { printNode(resources, node); } @@ -1382,7 +1403,7 @@ public class RelativeLayout extends ViewGroup { if (node.dependents.size() == 0) { printViewId(resources, node.view); } else { - for (Node dependent : node.dependents) { + for (Node dependent : node.dependents.keySet()) { StringBuilder buffer = new StringBuilder(); appendViewId(resources, node, buffer); printdependents(resources, dependent, buffer); @@ -1397,7 +1418,7 @@ public class RelativeLayout extends ViewGroup { if (node.dependents.size() == 0) { d(LOG_TAG, buffer.toString()); } else { - for (Node dependent : node.dependents) { + for (Node dependent : node.dependents.keySet()) { StringBuilder subBuffer = new StringBuilder(buffer); printdependents(resources, dependent, subBuffer); } @@ -1420,7 +1441,7 @@ public class RelativeLayout extends ViewGroup { * The list of dependents for this node; a dependent is a node * that needs this node to be processed first. */ - final HashSet<Node> dependents = new HashSet<Node>(); + final HashMap<Node, DependencyGraph> dependents = new HashMap<Node, DependencyGraph>(); /** * The list of dependencies for this node. diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 7b43032..586fdf4 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -68,6 +68,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback private RemoteViewsAdapterServiceConnection mServiceConnection; private WeakReference<RemoteAdapterConnectionCallback> mCallback; private FixedSizeRemoteViewsCache mCache; + private int mVisibleWindowLowerBound; + private int mVisibleWindowUpperBound; // A flag to determine whether we should notify data set changed after we connect private boolean mNotifyDataSetChangedAfterOnServiceConnected = false; @@ -765,7 +767,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } if (position > -1) { // Load the item, and notify any existing RemoteViewsFrameLayouts - updateRemoteViews(position, isRequested); + updateRemoteViews(position, isRequested, true); // Queue up for the next one to load loadNextIndexInBackground(); @@ -827,8 +829,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } } - private void updateRemoteViews(final int position, boolean isRequested) { - if (!mServiceConnection.isConnected()) return; + private void updateRemoteViews(final int position, boolean isRequested, boolean + notifyWhenLoaded) { IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); // Load the item information from the remote service @@ -864,12 +866,14 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // there is new data for it. final RemoteViews rv = remoteViews; final int typeId = mCache.getMetaDataAt(position).typeId; - mMainQueue.post(new Runnable() { - @Override - public void run() { - mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId); - } - }); + if (notifyWhenLoaded) { + mMainQueue.post(new Runnable() { + @Override + public void run() { + mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId); + } + }); + } } } @@ -929,6 +933,16 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback return typeId; } + /** + * This method allows an AdapterView using this Adapter to provide information about which + * views are currently being displayed. This allows for certain optimizations and preloading + * which wouldn't otherwise be possible. + */ + public void setVisibleRangeHint(int lowerBound, int upperBound) { + mVisibleWindowLowerBound = lowerBound; + mVisibleWindowUpperBound = upperBound; + } + public View getView(int position, View convertView, ViewGroup parent) { // "Request" an index so that we can queue it for loading, initiate subsequent // preloading, etc. @@ -1059,6 +1073,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // Re-request the new metadata (only after the notification to the factory) updateTemporaryMetaData(); + // Pre-load (our best guess of) the views which are currently visible in the AdapterView. + // This mitigates flashing and flickering of loading views when a widget notifies that + // its data has changed. + for (int i = mVisibleWindowLowerBound; i <= mVisibleWindowUpperBound; i++) { + updateRemoteViews(i, false, false); + } + // Propagate the notification back to the base adapter mMainQueue.post(new Runnable() { @Override diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 767eaee..3ffc0fe 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -721,12 +721,14 @@ public class ScrollView extends FrameLayout { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ScrollView.class.getName()); info.setScrollable(getScrollRange() > 0); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(ScrollView.class.getName()); final boolean scrollable = getScrollRange() > 0; event.setScrollable(scrollable); event.setScrollX(mScrollX); diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 9d2ff2e..99cd0b8 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -35,7 +35,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.speech.RecognizerIntent; import android.text.Editable; import android.text.InputType; @@ -51,6 +50,8 @@ import android.view.CollapsibleActionView; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView.OnItemClickListener; @@ -1206,6 +1207,18 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { setIconified(false); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(SearchView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(SearchView.class.getName()); + } + private void adjustDropDownSizeAndPosition() { if (mDropDownAnchor.getWidth() > 1) { Resources res = getContext().getResources(); diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java index c76728f..2737f94 100644 --- a/core/java/android/widget/SeekBar.java +++ b/core/java/android/widget/SeekBar.java @@ -18,6 +18,8 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; @@ -117,5 +119,16 @@ public class SeekBar extends AbsSeekBar { mOnSeekBarChangeListener.onStopTrackingTouch(this); } } - + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(SeekBar.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(SeekBar.class.getName()); + } } diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index bb27b73..22e9ef1 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -279,6 +279,7 @@ public class ShareActionProvider extends ActionProvider { final int itemId = item.getItemId(); Intent launchIntent = dataModel.chooseActivity(itemId); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } return true; diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index bdeb5c2..14edd10 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -32,6 +32,7 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * SlidingDrawer hides content out of the screen and allows the user to drag a handle @@ -810,6 +811,18 @@ public class SlidingDrawer extends ViewGroup { } } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(SlidingDrawer.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(SlidingDrawer.class.getName()); + } + private void closeDrawer() { moveHandle(COLLAPSED_FULL_CLOSED); mContent.setVisibility(View.GONE); diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index da3134a..570f0f9 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -102,7 +102,8 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextServicesManager = (TextServicesManager) mTextView.getContext(). getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); - if (!mTextServicesManager.isSpellCheckerEnabled()) { + if (!mTextServicesManager.isSpellCheckerEnabled() + || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { mSpellCheckerSession = null; } else { mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( @@ -120,9 +121,6 @@ public class SpellChecker implements SpellCheckerSessionListener { // Remove existing misspelled SuggestionSpans mTextView.removeMisspelledSpans((Editable) mTextView.getText()); - - // This class is the listener for locale change: warn other locale-aware objects - mTextView.onLocaleChanged(); } private void setLocale(Locale locale) { @@ -152,7 +150,7 @@ public class SpellChecker implements SpellCheckerSessionListener { final int length = mSpellParsers.length; for (int i = 0; i < length; i++) { - mSpellParsers[i].finish(); + mSpellParsers[i].stop(); } if (mSpellRunnable != null) { @@ -217,6 +215,7 @@ public class SpellChecker implements SpellCheckerSessionListener { if (!isSessionActive()) return; + // Find first available SpellParser from pool final int length = mSpellParsers.length; for (int i = 0; i < length; i++) { final SpellParser spellParser = mSpellParsers[i]; @@ -278,6 +277,12 @@ public class SpellChecker implements SpellCheckerSessionListener { } @Override + public void onGetSuggestionsForSentence(SuggestionsInfo[] results) { + // TODO: Handle the position and length for each suggestion + onGetSuggestions(results); + } + + @Override public void onGetSuggestions(SuggestionsInfo[] results) { Editable editable = (Editable) mTextView.getText(); @@ -337,56 +342,15 @@ public class SpellChecker implements SpellCheckerSessionListener { final int end = editable.getSpanEnd(spellCheckSpan); if (start < 0 || end <= start) return; // span was removed in the meantime - // Other suggestion spans may exist on that region, with identical suggestions, filter - // them out to avoid duplicates. - SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class); - final int length = suggestionSpans.length; - for (int i = 0; i < length; i++) { - final int spanStart = editable.getSpanStart(suggestionSpans[i]); - final int spanEnd = editable.getSpanEnd(suggestionSpans[i]); - if (spanStart != start || spanEnd != end) { - // Nulled (to avoid new array allocation) if not on that exact same region - suggestionSpans[i] = null; - } - } - final int suggestionsCount = suggestionsInfo.getSuggestionsCount(); - String[] suggestions; if (suggestionsCount <= 0) { // A negative suggestion count is possible - suggestions = ArrayUtils.emptyArray(String.class); - } else { - int numberOfSuggestions = 0; - suggestions = new String[suggestionsCount]; - - for (int i = 0; i < suggestionsCount; i++) { - final String spellSuggestion = suggestionsInfo.getSuggestionAt(i); - if (spellSuggestion == null) break; - boolean suggestionFound = false; - - for (int j = 0; j < length && !suggestionFound; j++) { - if (suggestionSpans[j] == null) break; - - String[] suggests = suggestionSpans[j].getSuggestions(); - for (int k = 0; k < suggests.length; k++) { - if (spellSuggestion.equals(suggests[k])) { - // The suggestion is already provided by an other SuggestionSpan - suggestionFound = true; - break; - } - } - } - - if (!suggestionFound) { - suggestions[numberOfSuggestions++] = spellSuggestion; - } - } + return; + } - if (numberOfSuggestions != suggestionsCount) { - String[] newSuggestions = new String[numberOfSuggestions]; - System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions); - suggestions = newSuggestions; - } + String[] suggestions = new String[suggestionsCount]; + for (int i = 0; i < suggestionsCount; i++) { + suggestions[i] = suggestionsInfo.getSuggestionAt(i); } SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, @@ -400,18 +364,25 @@ public class SpellChecker implements SpellCheckerSessionListener { private Object mRange = new Object(); public void init(int start, int end) { - ((Editable) mTextView.getText()).setSpan(mRange, start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - public void finish() { - ((Editable) mTextView.getText()).removeSpan(mRange); + setRangeSpan((Editable) mTextView.getText(), start, end); } public boolean isFinished() { return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0; } + public void stop() { + removeRangeSpan((Editable) mTextView.getText()); + } + + private void setRangeSpan(Editable editable, int start, int end) { + editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private void removeRangeSpan(Editable editable) { + editable.removeSpan(mRange); + } + public void parse() { Editable editable = (Editable) mTextView.getText(); // Iterate over the newly added text and schedule new SpellCheckSpans @@ -433,7 +404,7 @@ public class SpellChecker implements SpellCheckerSessionListener { wordEnd = mWordIterator.getEnd(wordStart); } if (wordEnd == BreakIterator.DONE) { - editable.removeSpan(mRange); + removeRangeSpan(editable); return; } @@ -511,9 +482,10 @@ public class SpellChecker implements SpellCheckerSessionListener { } if (scheduleOtherSpellCheck) { - editable.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // Update range span: start new spell check from last wordStart + setRangeSpan(editable, wordStart, end); } else { - editable.removeSpan(mRange); + removeRangeSpan(editable); } spellCheck(); diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index ec3790e..ecf19b3 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -29,6 +29,8 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** @@ -456,12 +458,24 @@ public class Spinner extends AbsSpinner implements OnClickListener { return handled; } - + public void onClick(DialogInterface dialog, int which) { setSelection(which); dialog.dismiss(); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Spinner.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Spinner.class.getName()); + } + /** * Sets the prompt to display when the dialog is shown. * @param prompt the prompt to set diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 03e6e99..22df3bc 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -40,6 +40,8 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.LinearInterpolator; import android.widget.RemoteViews.RemoteView; @@ -1216,6 +1218,18 @@ public class StackView extends AdapterViewAnimator { measureChildren(); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(StackView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(StackView.class.getName()); + } + class LayoutParams extends ViewGroup.LayoutParams { int horizontalOffset; int verticalOffset; diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index 02c9d03..334b9c4 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -35,6 +35,7 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -651,4 +652,16 @@ public class Switch extends CompoundButton { mThumbDrawable.jumpToCurrentState(); mTrackDrawable.jumpToCurrentState(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Switch.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Switch.class.getName()); + } } diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 88d7230..9b292be 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -33,6 +33,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import java.util.ArrayList; import java.util.List; @@ -321,6 +323,18 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TabHost.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TabHost.class.getName()); + } + public void setCurrentTab(int index) { if (index < 0 || index >= mTabSpecs.size()) { return; diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 80bfe99..8901037 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -29,6 +29,7 @@ import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * @@ -416,10 +417,28 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(TabWidget.class.getName()); event.setItemCount(getTabCount()); event.setCurrentItemIndex(mSelectedTab); } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + // this class fires events only when tabs are focused or selected + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { + event.recycle(); + return; + } + super.sendAccessibilityEventUnchecked(event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TabWidget.class.getName()); + } + /** * Sets the current tab and focuses the UI on it. * This method makes sure that the focused tab matches the selected @@ -485,16 +504,6 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { mSelectedTab = -1; } - @Override - public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { - // this class fires events only when tabs are focused or selected - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { - event.recycle(); - return; - } - super.sendAccessibilityEventUnchecked(event); - } - /** * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. */ diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index 842b087..f5d3746 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -24,6 +24,8 @@ import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import java.util.regex.Pattern; @@ -658,6 +660,18 @@ public class TableLayout extends LinearLayout { return new LayoutParams(p); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TableLayout.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TableLayout.class.getName()); + } + /** * <p>This set of layout parameters enforces the width of each child to be * {@link #MATCH_PARENT} and the height of each child to be diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index 3fd4631..01c4c2c 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -24,6 +24,8 @@ import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** @@ -377,6 +379,18 @@ public class TableRow extends LinearLayout { return new LayoutParams(p); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TableRow.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TableRow.class.getName()); + } + /** * <p>Set of layout parameters used in table rows.</p> * diff --git a/core/java/android/widget/TextSwitcher.java b/core/java/android/widget/TextSwitcher.java index a8794a3..1aefd2b 100644 --- a/core/java/android/widget/TextSwitcher.java +++ b/core/java/android/widget/TextSwitcher.java @@ -21,6 +21,8 @@ import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * Specialized {@link android.widget.ViewSwitcher} that contains @@ -88,4 +90,16 @@ public class TextSwitcher extends ViewSwitcher { public void setCurrentText(CharSequence text) { ((TextView)getCurrentView()).setText(text); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TextSwitcher.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TextSwitcher.class.getName()); + } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index b9d3d43..3ce0a3e 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -84,6 +84,7 @@ import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.method.TransformationMethod2; import android.text.method.WordIterator; +import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.EasyEditSpan; import android.text.style.ParagraphStyle; @@ -101,9 +102,11 @@ import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; import android.view.ActionMode.Callback; +import android.view.DisplayList; import android.view.DragEvent; import android.view.Gravity; import android.view.HapticFeedbackConstants; +import android.view.HardwareCanvas; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -253,10 +256,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private float mShadowRadius, mShadowDx, mShadowDy; - private static final int PREDRAW_NOT_REGISTERED = 0; - private static final int PREDRAW_PENDING = 1; - private static final int PREDRAW_DONE = 2; - private int mPreDrawState = PREDRAW_NOT_REGISTERED; + private boolean mPreDrawRegistered; private TextUtils.TruncateAt mEllipsize = null; @@ -283,6 +283,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private Drawables mDrawables; + private DisplayList mTextDisplayList; + private boolean mTextDisplayListIsValid; + private CharSequence mError; private boolean mErrorWasChanged; private ErrorPopup mPopup; @@ -336,6 +339,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; + private Runnable mShowSuggestionRunnable; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; @@ -351,7 +355,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private float mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; - private final int mSquaredTouchSlopDistance; // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; @@ -437,15 +440,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, null); } - public TextView(Context context, - AttributeSet attrs) { + public TextView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.textViewStyle); } @SuppressWarnings("deprecation") - public TextView(Context context, - AttributeSet attrs, - int defStyle) { + public TextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mText = ""; @@ -1128,10 +1128,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLongClickable(longClickable); prepareCursorControllers(); - - final ViewConfiguration viewConfiguration = ViewConfiguration.get(context); - final int touchSlop = viewConfiguration.getScaledTouchSlop(); - mSquaredTouchSlopDistance = touchSlop * touchSlop; } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -1202,14 +1198,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener imm.hideSoftInputFromWindow(getWindowToken(), 0); } } + super.setEnabled(enabled); - prepareCursorControllers(); + if (enabled) { // Make sure IME is updated with current editor info. InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) imm.restartInput(this); } + prepareCursorControllers(); + // start or stop the cursor blinking as appropriate makeBlink(); } @@ -3193,8 +3192,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int n = mFilters.length; for (int i = 0; i < n; i++) { - CharSequence out = mFilters[i].filter(text, 0, text.length(), - EMPTY_SPANNED, 0, 0); + CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0); if (out != null) { text = out; } @@ -4386,26 +4384,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void registerForPreDraw() { - final ViewTreeObserver observer = getViewTreeObserver(); - - if (mPreDrawState == PREDRAW_NOT_REGISTERED) { - observer.addOnPreDrawListener(this); - mPreDrawState = PREDRAW_PENDING; - } else if (mPreDrawState == PREDRAW_DONE) { - mPreDrawState = PREDRAW_PENDING; + if (!mPreDrawRegistered) { + getViewTreeObserver().addOnPreDrawListener(this); + mPreDrawRegistered = true; } - - // else state is PREDRAW_PENDING, so keep waiting. } /** * {@inheritDoc} */ public boolean onPreDraw() { - if (mPreDrawState != PREDRAW_PENDING) { - return true; - } - if (mLayout == null) { assumeLayout(); } @@ -4456,7 +4444,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener startSelectionActionMode(); } - mPreDrawState = PREDRAW_DONE; + getViewTreeObserver().removeOnPreDrawListener(this); + mPreDrawRegistered = false; + return !changed; } @@ -4491,10 +4481,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - final ViewTreeObserver observer = getViewTreeObserver(); - if (mPreDrawState != PREDRAW_NOT_REGISTERED) { - observer.removeOnPreDrawListener(this); - mPreDrawState = PREDRAW_NOT_REGISTERED; + if (mPreDrawRegistered) { + getViewTreeObserver().removeOnPreDrawListener(this); + mPreDrawRegistered = false; } if (mError != null) { @@ -4513,10 +4502,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mSelectionModifierCursorController.onDetached(); } + if (mShowSuggestionRunnable != null) { + removeCallbacks(mShowSuggestionRunnable); + } + hideControllers(); resetResolvedDrawables(); + if (mTextDisplayList != null) { + mTextDisplayList.invalidate(); + } + if (mSpellChecker != null) { mSpellChecker.closeSession(); // Forces the creation of a new SpellChecker next time this window is created. @@ -4759,12 +4756,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onDraw(Canvas canvas) { - if (mPreDrawState == PREDRAW_DONE) { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnPreDrawListener(this); - mPreDrawState = PREDRAW_NOT_REGISTERED; - } - if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return; restartMarqueeIfNeeded(); @@ -4967,17 +4958,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - /* Comment out until we decide what to do about animations - boolean isLinearTextOn = false; - if (currentTransformation != null) { - isLinearTextOn = mTextPaint.isLinearTextOn(); - Matrix m = currentTransformation.getMatrix(); - if (!m.isIdentity()) { - // mTextPaint.setLinearTextOn(true); - } - } - */ - final InputMethodState ims = mInputMethodState; final int cursorOffsetVertical = voffsetCursor - voffsetText; if (ims != null && ims.mBatchEditNesting == 0) { @@ -5035,18 +5015,38 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener highlight = null; } - layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); + if (canHaveDisplayList() && canvas.isHardwareAccelerated()) { + final int width = mRight - mLeft; + final int height = mBottom - mTop; - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); + if (mTextDisplayList == null || !mTextDisplayList.isValid() || + !mTextDisplayListIsValid) { + if (mTextDisplayList == null) { + mTextDisplayList = getHardwareRenderer().createDisplayList(); + } + + final HardwareCanvas hardwareCanvas = mTextDisplayList.start(); + try { + hardwareCanvas.setViewport(width, height); + // The dirty rect should always be null for a display list + hardwareCanvas.onPreDraw(null); + layout.draw(hardwareCanvas, highlight, mHighlightPaint, cursorOffsetVertical); + } finally { + hardwareCanvas.onPostDraw(); + mTextDisplayList.end(); + mTextDisplayListIsValid = true; + } + } + ((HardwareCanvas) canvas).drawDisplayList(mTextDisplayList, + mScrollX + width, mScrollY + height, null); + } else { layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); } - /* Comment out until we decide what to do about animations - if (currentTransformation != null) { - mTextPaint.setLinearTextOn(isLinearTextOn); + if (mMarquee != null && mMarquee.shouldDrawGhost()) { + canvas.translate((int) mMarquee.getGhostOffset(), 0.0f); + layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); } - */ canvas.restore(); } @@ -5251,10 +5251,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener state.handleUpEvent(event); } if (event.isTracking() && !event.isCanceled()) { - if (isInSelectionMode) { - stopSelectionActionMode(); - return true; - } + stopSelectionActionMode(); + return true; } } } @@ -5599,11 +5597,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); } - @Override public boolean onCheckIsTextEditor() { + @Override + public boolean onCheckIsTextEditor() { return mInputType != EditorInfo.TYPE_NULL; } - @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { if (mInputMethodState == null) { mInputMethodState = new InputMethodState(); @@ -6083,6 +6083,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mSavedMarqueeModeLayout = mLayout = mHintLayout = null; + mBoring = mHintBoring = null; + // Since it depends on the value of mLayout prepareCursorControllers(); } @@ -6775,6 +6777,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) mTextDisplayListIsValid = false; + } + /** * Returns true if anything changed. */ @@ -7557,6 +7565,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ protected void onSelectionChanged(int selStart, int selEnd) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + mTextDisplayListIsValid = false; } /** @@ -7636,6 +7645,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } updateSpellCheckSpans(start, start + after, false); + mTextDisplayListIsValid = false; // Hide the controllers as soon as text is modified (typing, procedural...) // We do not hide the span controllers, since they can be added when a new text is @@ -7738,7 +7748,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) { + if (what instanceof UpdateAppearance || what instanceof ParagraphStyle || + what instanceof CharacterStyle) { if (ims == null || ims.mBatchEditNesting == 0) { invalidate(); mHighlightPathBogus = true; @@ -7746,6 +7757,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { ims.mContentChanged = true; } + mTextDisplayListIsValid = false; } if (MetaKeyKeyListener.isMetaTracker(buf, what)) { @@ -8311,6 +8323,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener getSelectionController().onTouchEvent(event); } + if (mShowSuggestionRunnable != null) { + removeCallbacks(mShowSuggestionRunnable); + } + if (action == MotionEvent.ACTION_DOWN) { mLastDownPositionX = event.getX(); mLastDownPositionY = event.getY(); @@ -8334,7 +8350,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && - !shouldIgnoreActionUpEvent() && isFocused(); + !mIgnoreActionUpEvent && isFocused(); if ((mMovement != null || onCheckIsTextEditor()) && isEnabled() && mText instanceof Spannable && mLayout != null) { @@ -8351,7 +8367,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(), getSelectionEnd(), ClickableSpan.class); - if (links.length != 0) { + if (links.length > 0) { links[0].onClick(this); handled = true; } @@ -8368,13 +8384,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); hideControllers(); if (!selectAllGotFocus && mText.length() > 0) { + // Move cursor + final int offset = getOffsetForPosition(event.getX(), event.getY()); + Selection.setSelection((Spannable) mText, offset); if (mSpellChecker != null) { // When the cursor moves, the word that was typed may need spell check mSpellChecker.onSelectionChanged(); } if (!extractedTextModeWillBeStarted()) { if (isCursorInsideEasyCorrectionSpan()) { - showSuggestions(); + if (mShowSuggestionRunnable == null) { + mShowSuggestionRunnable = new Runnable() { + public void run() { + showSuggestions(); + } + }; + } + postDelayed(mShowSuggestionRunnable, + ViewConfiguration.getDoubleTapTimeout()); } else if (hasInsertionController()) { getInsertionController().show(); } @@ -8510,17 +8537,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mIgnoreActionUpEvent = true; } - /** - * This method is only valid during a touch event. - * - * @return true when the ACTION_UP event should be ignored, false otherwise. - * - * @hide - */ - public boolean shouldIgnoreActionUpEvent() { - return mIgnoreActionUpEvent; - } - @Override public boolean onTrackballEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && @@ -8579,7 +8595,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @return True when the TextView isFocused and has a valid zero-length selection (cursor). */ private boolean shouldBlink() { - if (!isFocused()) return false; + if (!isCursorVisible() || !isFocused()) return false; final int start = getSelectionStart(); if (start < 0) return false; @@ -8591,13 +8607,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void makeBlink() { - if (isCursorVisible()) { - if (shouldBlink()) { - mShowCursor = SystemClock.uptimeMillis(); - if (mBlink == null) mBlink = new Blink(this); - mBlink.removeCallbacks(mBlink); - mBlink.postAtTime(mBlink, mShowCursor + BLINK); - } + if (shouldBlink()) { + mShowCursor = SystemClock.uptimeMillis(); + if (mBlink == null) mBlink = new Blink(this); + mBlink.removeCallbacks(mBlink); + mBlink.postAtTime(mBlink, mShowCursor + BLINK); } else { if (mBlink != null) mBlink.removeCallbacks(mBlink); } @@ -8903,14 +8917,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wordIterator.setCharSequence(mText, minOffset, maxOffset); selectionStart = wordIterator.getBeginning(minOffset); - if (selectionStart == BreakIterator.DONE) return false; - selectionEnd = wordIterator.getEnd(maxOffset); - if (selectionEnd == BreakIterator.DONE) return false; - if (selectionStart == selectionEnd) { + if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || + selectionStart == selectionEnd) { // Possible when the word iterator does not properly handle the text's language - long range = getCharRange(selectionStart); + long range = getCharRange(minOffset); selectionStart = extractRangeStartFromLong(range); selectionEnd = extractRangeEndFromLong(range); } @@ -9003,6 +9015,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + event.setClassName(TextView.class.getName()); final boolean isPassword = hasPasswordTransformationMethod(); event.setPassword(isPassword); @@ -9017,11 +9030,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TextView.class.getName()); final boolean isPassword = hasPasswordTransformationMethod(); + info.setPassword(isPassword); + if (!isPassword) { info.setText(getTextForAccessibility()); } - info.setPassword(isPassword); } @Override @@ -9219,7 +9234,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean vibrate = true; if (super.performLongClick()) { - mDiscardNextActionUp = true; handled = true; } @@ -9722,11 +9736,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void show() { if (!(mText instanceof Editable)) return; - updateSuggestions(); - mCursorWasVisibleBeforeSuggestions = mCursorVisible; - setCursorVisible(false); - mIsShowingUp = true; - super.show(); + if (updateSuggestions()) { + mCursorWasVisibleBeforeSuggestions = mCursorVisible; + setCursorVisible(false); + mIsShowingUp = true; + super.show(); + } } @Override @@ -9782,11 +9797,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener super.hide(); } - private void updateSuggestions() { + private boolean updateSuggestions() { Spannable spannable = (Spannable) TextView.this.mText; SuggestionSpan[] suggestionSpans = getSuggestionSpans(); final int nbSpans = suggestionSpans.length; + // Suggestions are shown after a delay: the underlying spans may have been removed + if (nbSpans == 0) return false; mNumberOfSuggestions = 0; int spanUnionStart = mText.length(); @@ -9812,17 +9829,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener String[] suggestions = suggestionSpan.getSuggestions(); int nbSuggestions = suggestions.length; for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = suggestionSpan; - suggestionInfo.suggestionIndex = suggestionIndex; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - suggestions[suggestionIndex]); + String suggestion = suggestions[suggestionIndex]; + + boolean suggestionIsDuplicate = false; + for (int i = 0; i < mNumberOfSuggestions; i++) { + if (mSuggestionInfos[i].text.toString().equals(suggestion)) { + SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan; + final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); + final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); + if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { + suggestionIsDuplicate = true; + break; + } + } + } - mNumberOfSuggestions++; - if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { - // Also end outer for loop - spanIndex = nbSpans; - break; + if (!suggestionIsDuplicate) { + SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; + suggestionInfo.suggestionSpan = suggestionSpan; + suggestionInfo.suggestionIndex = suggestionIndex; + suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion); + + mNumberOfSuggestions++; + + if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) { + // Also end outer for loop + spanIndex = nbSpans; + break; + } } } } @@ -9831,7 +9865,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); } - // Add to dictionary item if there is a span with the misspelled flag + // Add "Add to dictionary" item if there is a span with the misspelled flag if (misspelledSpan != null) { final int misspelledStart = spannable.getSpanStart(misspelledSpan); final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); @@ -9872,6 +9906,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mSuggestionsAdapter.notifyDataSetChanged(); + return true; } private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, @@ -9889,8 +9924,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // Add the text before and after the span. - suggestionInfo.text.insert(0, mText.toString().substring(unionStart, spanStart)); - suggestionInfo.text.append(mText.toString().substring(spanEnd, unionEnd)); + final String textAsString = text.toString(); + suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart)); + suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd)); } @Override @@ -10128,8 +10164,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean willExtract = extractedTextModeWillBeStarted(); - // Do not start the action mode when extracted text will show up full screen, thus - // immediately hiding the newly created action bar, which would be visually distracting. + // Do not start the action mode when extracted text will show up full screen, which would + // immediately hide the newly created action bar and would be visually distracting. if (!willExtract) { ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); mSelectionActionMode = startActionMode(actionModeCallback); @@ -10155,7 +10191,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - private void stopSelectionActionMode() { + /** + * @hide + */ + protected void stopSelectionActionMode() { if (mSelectionActionMode != null) { // This will hide the mSelectionModifierCursorController mSelectionActionMode.finish(); @@ -10769,7 +10808,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final float deltaX = mDownPositionX - ev.getRawX(); final float deltaY = mDownPositionY - ev.getRawY(); final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + final int touchSlop = viewConfiguration.getScaledTouchSlop(); + + if (distanceSquared < touchSlop * touchSlop) { if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { // Tapping on the handle dismisses the displayed action popup mActionPopupWindow.hide(); @@ -10983,7 +11027,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Double tap detection private long mPreviousTapUpTime = 0; - private float mPreviousTapPositionX, mPreviousTapPositionY; + private float mDownPositionX, mDownPositionY; + private boolean mGestureStayedInTapRegion; SelectionModifierCursorController() { resetTouchOffsets(); @@ -11046,20 +11091,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); // Double tap detection - long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; - if (duration <= ViewConfiguration.getDoubleTapTimeout() && - isPositionOnText(x, y)) { - final float deltaX = x - mPreviousTapPositionX; - final float deltaY = y - mPreviousTapPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { - startSelectionActionMode(); - mDiscardNextActionUp = true; + if (mGestureStayedInTapRegion) { + long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; + if (duration <= ViewConfiguration.getDoubleTapTimeout()) { + final float deltaX = x - mDownPositionX; + final float deltaY = y - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); + boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; + + if (stayedInArea && isPositionOnText(x, y)) { + startSelectionActionMode(); + mDiscardNextActionUp = true; + } } } - mPreviousTapPositionX = x; - mPreviousTapPositionY = y; + mDownPositionX = x; + mDownPositionY = y; + mGestureStayedInTapRegion = true; break; case MotionEvent.ACTION_POINTER_DOWN: @@ -11072,6 +11125,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } break; + case MotionEvent.ACTION_MOVE: + if (mGestureStayedInTapRegion) { + final float deltaX = event.getX() - mDownPositionX; + final float deltaY = event.getY() - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); + + if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { + mGestureStayedInTapRegion = false; + } + } + break; + case MotionEvent.ACTION_UP: mPreviousTapUpTime = SystemClock.uptimeMillis(); break; @@ -11375,6 +11444,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case TEXT_DIRECTION_RTL: mTextDir = TextDirectionHeuristics.RTL; break; + case TEXT_DIRECTION_LOCALE: + mTextDir = TextDirectionHeuristics.LOCALE; + break; } } @@ -11504,13 +11576,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mUserSetTextScaleX; private final Paint mHighlightPaint; private int mHighlightColor = 0x6633B5E5; - /** - * This is temporarily visible to fix bug 3085564 in webView. Do not rely on - * this field being protected. Will be restored as private when lineHeight - * feature request 3215097 is implemented - * @hide - */ - protected Layout mLayout; + private Layout mLayout; private long mShowCursor; private Blink mBlink; diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index afca2db..8f10fff 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -27,6 +27,7 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.NumberPicker.OnValueChangeListener; @@ -476,6 +477,18 @@ public class TimePicker extends FrameLayout { event.getText().add(selectedDateUtterance); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TimePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TimePicker.class.getName()); + } + private void updateHourControl() { if (is24HourView()) { mHourSpinner.setMinValue(0); diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java index a754268..a0edafe 100644 --- a/core/java/android/widget/ToggleButton.java +++ b/core/java/android/widget/ToggleButton.java @@ -23,6 +23,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.R; @@ -161,4 +162,16 @@ public class ToggleButton extends CompoundButton { event.getText().add(mContext.getString(R.string.togglebutton_not_pressed)); } } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ToggleButton.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ToggleButton.class.getName()); + } } diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java index eab6f2d..e707ea3 100644 --- a/core/java/android/widget/TwoLineListItem.java +++ b/core/java/android/widget/TwoLineListItem.java @@ -16,14 +16,12 @@ package android.widget; -import com.android.internal.R; - - import android.annotation.Widget; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RelativeLayout; /** @@ -86,4 +84,16 @@ public class TwoLineListItem extends RelativeLayout { public TextView getText2() { return mText2; } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(TwoLineListItem.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TwoLineListItem.class.getName()); + } } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 8e438ff..0fba498 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -34,6 +34,8 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.MediaController.MediaPlayerControl; import java.io.IOException; @@ -124,6 +126,18 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { setMeasuredDimension(width, height); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(VideoView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(VideoView.class.getName()); + } + public int resolveAdjustedSize(int desiredSize, int measureSpec) { int result = desiredSize; int specMode = MeasureSpec.getMode(measureSpec); @@ -380,7 +394,6 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } new AlertDialog.Builder(mContext) - .setTitle(com.android.internal.R.string.VideoView_error_title) .setMessage(messageId) .setPositiveButton(com.android.internal.R.string.VideoView_error_button, new DialogInterface.OnClickListener() { diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java index 3c683d6..6a68240 100644 --- a/core/java/android/widget/ViewAnimator.java +++ b/core/java/android/widget/ViewAnimator.java @@ -22,6 +22,8 @@ import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -185,6 +187,10 @@ public class ViewAnimator extends FrameLayout { } else { child.setVisibility(View.GONE); } + if (index >= 0 && mWhichChild >= index) { + // Added item above current one, increment the index of the displayed child + setDisplayedChild(mWhichChild + 1); + } } @Override @@ -337,4 +343,16 @@ public class ViewAnimator extends FrameLayout { public int getBaseline() { return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ViewAnimator.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ViewAnimator.class.getName()); + } } diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index c6f6e81..061bb00 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -25,6 +25,8 @@ import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; /** @@ -139,6 +141,18 @@ public class ViewFlipper extends ViewAnimator { updateRunning(); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ViewFlipper.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ViewFlipper.class.getName()); + } + /** * Internal method to start or stop dispatching flip {@link Message} based * on {@link #mRunning} and {@link #mVisible} state. diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java index 71ae624..0376918 100644 --- a/core/java/android/widget/ViewSwitcher.java +++ b/core/java/android/widget/ViewSwitcher.java @@ -20,6 +20,8 @@ import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * {@link ViewAnimator} that switches between two views, and has a factory @@ -66,6 +68,18 @@ public class ViewSwitcher extends ViewAnimator { super.addView(child, index, params); } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ViewSwitcher.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ViewSwitcher.class.getName()); + } + /** * Returns the next view to be displayed. * diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java index eb372ca..af17c94 100644 --- a/core/java/android/widget/ZoomButton.java +++ b/core/java/android/widget/ZoomButton.java @@ -23,6 +23,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; public class ZoomButton extends ImageButton implements OnLongClickListener { @@ -96,4 +98,16 @@ public class ZoomButton extends ImageButton implements OnLongClickListener { clearFocus(); return super.dispatchUnhandledMove(focused, direction); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ZoomButton.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ZoomButton.class.getName()); + } } diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java index a12aee5..8897875 100644 --- a/core/java/android/widget/ZoomControls.java +++ b/core/java/android/widget/ZoomControls.java @@ -22,6 +22,8 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import com.android.internal.R; @@ -106,4 +108,16 @@ public class ZoomControls extends LinearLayout { public boolean hasFocus() { return mZoomIn.hasFocus() || mZoomOut.hasFocus(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(ZoomControls.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ZoomControls.class.getName()); + } } diff --git a/core/java/com/android/internal/backup/BackupConstants.java b/core/java/com/android/internal/backup/BackupConstants.java index 906b5d5..4c276b7 100644 --- a/core/java/com/android/internal/backup/BackupConstants.java +++ b/core/java/com/android/internal/backup/BackupConstants.java @@ -24,4 +24,5 @@ public class BackupConstants { public static final int TRANSPORT_ERROR = 1; public static final int TRANSPORT_NOT_INITIALIZED = 2; public static final int AGENT_ERROR = 3; + public static final int AGENT_UNKNOWN = 4; } diff --git a/core/java/com/android/internal/net/DNParser.java b/core/java/com/android/internal/net/DNParser.java deleted file mode 100644 index 5254207..0000000 --- a/core/java/com/android/internal/net/DNParser.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.internal.net; - - -import android.util.Log; - -import java.io.IOException; - -import javax.security.auth.x500.X500Principal; - -/** - * A simple distinguished name(DN) parser. - * - * <p>This class is based on org.apache.harmony.security.x509.DNParser. It's customized to remove - * external references which are unnecessary for our requirements. - * - * <p>This class is only meant for extracting a string value from a DN. e.g. it doesn't support - * values in the hex-string style. - * - * <p>This class is used by {@link DomainNameValidator} only. However, in order to make this - * class visible from unit tests, it's made public. - * - * @hide - */ -public final class DNParser { - private static final String TAG = "DNParser"; - - /** DN to be parsed. */ - private final String dn; - - // length of distinguished name string - private final int length; - - private int pos, beg, end; - - // tmp vars to store positions of the currently parsed item - private int cur; - - // distinguished name chars - private char[] chars; - - /** - * Exception message thrown when we failed to parse DN, which shouldn't happen because we - * only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed. - */ - private static final String ERROR_PARSE_ERROR = "Failed to parse DN"; - - /** - * Constructor. - * - * @param principal - {@link X500Principal} to be parsed - */ - public DNParser(X500Principal principal) { - this.dn = principal.getName(X500Principal.RFC2253); - this.length = dn.length(); - } - - // gets next attribute type: (ALPHA 1*keychar) / oid - private String nextAT() throws IOException { - - // skip preceding space chars, they can present after - // comma or semicolon (compatibility with RFC 1779) - for (; pos < length && chars[pos] == ' '; pos++) { - } - if (pos == length) { - return null; // reached the end of DN - } - - // mark the beginning of attribute type - beg = pos; - - // attribute type chars - pos++; - for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { - // we don't follow exact BNF syntax here: - // accept any char except space and '=' - } - if (pos >= length) { - // unexpected end of DN - throw new IOException(ERROR_PARSE_ERROR); - } - - // mark the end of attribute type - end = pos; - - // skip trailing space chars between attribute type and '=' - // (compatibility with RFC 1779) - if (chars[pos] == ' ') { - for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { - } - - if (chars[pos] != '=' || pos == length) { - // unexpected end of DN - throw new IOException(ERROR_PARSE_ERROR); - } - } - - pos++; //skip '=' char - - // skip space chars between '=' and attribute value - // (compatibility with RFC 1779) - for (; pos < length && chars[pos] == ' '; pos++) { - } - - // in case of oid attribute type skip its prefix: "oid." or "OID." - // (compatibility with RFC 1779) - if ((end - beg > 4) && (chars[beg + 3] == '.') - && (chars[beg] == 'O' || chars[beg] == 'o') - && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') - && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { - beg += 4; - } - - return new String(chars, beg, end - beg); - } - - // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION - private String quotedAV() throws IOException { - - pos++; - beg = pos; - end = beg; - while (true) { - - if (pos == length) { - // unexpected end of DN - throw new IOException(ERROR_PARSE_ERROR); - } - - if (chars[pos] == '"') { - // enclosing quotation was found - pos++; - break; - } else if (chars[pos] == '\\') { - chars[end] = getEscaped(); - } else { - // shift char: required for string with escaped chars - chars[end] = chars[pos]; - } - pos++; - end++; - } - - // skip trailing space chars before comma or semicolon. - // (compatibility with RFC 1779) - for (; pos < length && chars[pos] == ' '; pos++) { - } - - return new String(chars, beg, end - beg); - } - - // gets hex string attribute value: "#" hexstring - private String hexAV() throws IOException { - - if (pos + 4 >= length) { - // encoded byte array must be not less then 4 c - throw new IOException(ERROR_PARSE_ERROR); - } - - beg = pos; // store '#' position - pos++; - while (true) { - - // check for end of attribute value - // looks for space and component separators - if (pos == length || chars[pos] == '+' || chars[pos] == ',' - || chars[pos] == ';') { - end = pos; - break; - } - - if (chars[pos] == ' ') { - end = pos; - pos++; - // skip trailing space chars before comma or semicolon. - // (compatibility with RFC 1779) - for (; pos < length && chars[pos] == ' '; pos++) { - } - break; - } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { - chars[pos] += 32; //to low case - } - - pos++; - } - - // verify length of hex string - // encoded byte array must be not less then 4 and must be even number - int hexLen = end - beg; // skip first '#' char - if (hexLen < 5 || (hexLen & 1) == 0) { - throw new IOException(ERROR_PARSE_ERROR); - } - - // get byte encoding from string representation - byte[] encoded = new byte[hexLen / 2]; - for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { - encoded[i] = (byte) getByte(p); - } - - return new String(chars, beg, hexLen); - } - - // gets string attribute value: *( stringchar / pair ) - private String escapedAV() throws IOException { - - beg = pos; - end = pos; - while (true) { - - if (pos >= length) { - // the end of DN has been found - return new String(chars, beg, end - beg); - } - - switch (chars[pos]) { - case '+': - case ',': - case ';': - // separator char has beed found - return new String(chars, beg, end - beg); - case '\\': - // escaped char - chars[end++] = getEscaped(); - pos++; - break; - case ' ': - // need to figure out whether space defines - // the end of attribute value or not - cur = end; - - pos++; - chars[end++] = ' '; - - for (; pos < length && chars[pos] == ' '; pos++) { - chars[end++] = ' '; - } - if (pos == length || chars[pos] == ',' || chars[pos] == '+' - || chars[pos] == ';') { - // separator char or the end of DN has beed found - return new String(chars, beg, cur - beg); - } - break; - default: - chars[end++] = chars[pos]; - pos++; - } - } - } - - // returns escaped char - private char getEscaped() throws IOException { - - pos++; - if (pos == length) { - throw new IOException(ERROR_PARSE_ERROR); - } - - switch (chars[pos]) { - case '"': - case '\\': - case ',': - case '=': - case '+': - case '<': - case '>': - case '#': - case ';': - case ' ': - case '*': - case '%': - case '_': - //FIXME: escaping is allowed only for leading or trailing space char - return chars[pos]; - default: - // RFC doesn't explicitly say that escaped hex pair is - // interpreted as UTF-8 char. It only contains an example of such DN. - return getUTF8(); - } - } - - // decodes UTF-8 char - // see http://www.unicode.org for UTF-8 bit distribution table - private char getUTF8() throws IOException { - - int res = getByte(pos); - pos++; //FIXME tmp - - if (res < 128) { // one byte: 0-7F - return (char) res; - } else if (res >= 192 && res <= 247) { - - int count; - if (res <= 223) { // two bytes: C0-DF - count = 1; - res = res & 0x1F; - } else if (res <= 239) { // three bytes: E0-EF - count = 2; - res = res & 0x0F; - } else { // four bytes: F0-F7 - count = 3; - res = res & 0x07; - } - - int b; - for (int i = 0; i < count; i++) { - pos++; - if (pos == length || chars[pos] != '\\') { - return 0x3F; //FIXME failed to decode UTF-8 char - return '?' - } - pos++; - - b = getByte(pos); - pos++; //FIXME tmp - if ((b & 0xC0) != 0x80) { - return 0x3F; //FIXME failed to decode UTF-8 char - return '?' - } - - res = (res << 6) + (b & 0x3F); - } - return (char) res; - } else { - return 0x3F; //FIXME failed to decode UTF-8 char - return '?' - } - } - - // Returns byte representation of a char pair - // The char pair is composed of DN char in - // specified 'position' and the next char - // According to BNF syntax: - // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" - // / "a" / "b" / "c" / "d" / "e" / "f" - private int getByte(int position) throws IOException { - - if ((position + 1) >= length) { - // to avoid ArrayIndexOutOfBoundsException - throw new IOException(ERROR_PARSE_ERROR); - } - - int b1, b2; - - b1 = chars[position]; - if (b1 >= '0' && b1 <= '9') { - b1 = b1 - '0'; - } else if (b1 >= 'a' && b1 <= 'f') { - b1 = b1 - 87; // 87 = 'a' - 10 - } else if (b1 >= 'A' && b1 <= 'F') { - b1 = b1 - 55; // 55 = 'A' - 10 - } else { - throw new IOException(ERROR_PARSE_ERROR); - } - - b2 = chars[position + 1]; - if (b2 >= '0' && b2 <= '9') { - b2 = b2 - '0'; - } else if (b2 >= 'a' && b2 <= 'f') { - b2 = b2 - 87; // 87 = 'a' - 10 - } else if (b2 >= 'A' && b2 <= 'F') { - b2 = b2 - 55; // 55 = 'A' - 10 - } else { - throw new IOException(ERROR_PARSE_ERROR); - } - - return (b1 << 4) + b2; - } - - /** - * Parses the DN and returns the attribute value for an attribute type. - * - * @param attributeType attribute type to look for (e.g. "ca") - * @return value of the attribute that first found, or null if none found - */ - public String find(String attributeType) { - try { - // Initialize internal state. - pos = 0; - beg = 0; - end = 0; - cur = 0; - chars = dn.toCharArray(); - - String attType = nextAT(); - if (attType == null) { - return null; - } - while (true) { - String attValue = ""; - - if (pos == length) { - return null; - } - - switch (chars[pos]) { - case '"': - attValue = quotedAV(); - break; - case '#': - attValue = hexAV(); - break; - case '+': - case ',': - case ';': // compatibility with RFC 1779: semicolon can separate RDNs - //empty attribute value - break; - default: - attValue = escapedAV(); - } - - if (attributeType.equalsIgnoreCase(attType)) { - return attValue; - } - - if (pos >= length) { - return null; - } - - if (chars[pos] == ',' || chars[pos] == ';') { - } else if (chars[pos] != '+') { - throw new IOException(ERROR_PARSE_ERROR); - } - - pos++; - attType = nextAT(); - if (attType == null) { - throw new IOException(ERROR_PARSE_ERROR); - } - } - } catch (IOException e) { - // Parse error shouldn't happen, because we only handle DNs that - // X500Principal.getName() returns, which shouldn't be malformed. - Log.e(TAG, "Failed to parse DN: " + dn); - return null; - } - } -} diff --git a/core/java/com/android/internal/net/DomainNameValidator.java b/core/java/com/android/internal/net/DomainNameValidator.java deleted file mode 100644 index 3950655..0000000 --- a/core/java/com/android/internal/net/DomainNameValidator.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (C) 2010 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.internal.net; - -import android.net.NetworkUtils; -import android.util.Log; - -import java.net.InetAddress; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import javax.security.auth.x500.X500Principal; - -/** @hide */ -public class DomainNameValidator { - private final static String TAG = "DomainNameValidator"; - - private static final boolean DEBUG = false; - private static final boolean LOG_ENABLED = false; - - private static final int ALT_DNS_NAME = 2; - private static final int ALT_IPA_NAME = 7; - - /** - * Checks the site certificate against the domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - public static boolean match(X509Certificate certificate, String thisDomain) { - if (certificate == null || thisDomain == null || thisDomain.length() == 0) { - return false; - } - - thisDomain = thisDomain.toLowerCase(); - if (!isIpAddress(thisDomain)) { - return matchDns(certificate, thisDomain); - } else { - return matchIpAddress(certificate, thisDomain); - } - } - - /** - * @return True iff the domain name is specified as an IP address - */ - private static boolean isIpAddress(String domain) { - boolean rval = (domain != null && domain.length() != 0); - if (rval) { - try { - // do a quick-dirty IP match first to avoid DNS lookup - rval = domain.equals( - NetworkUtils.numericToInetAddress(domain).getHostAddress()); - } catch (IllegalArgumentException e) { - if (LOG_ENABLED) { - Log.v(TAG, "DomainNameValidator.isIpAddress(): " + e); - } - - rval = false; - } - } - - return rval; - } - - /** - * Checks the site certificate against the IP domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) { - if (LOG_ENABLED) { - Log.v(TAG, "DomainNameValidator.matchIpAddress(): this domain: " + thisDomain); - } - - try { - Collection subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - Iterator i = subjectAltNames.iterator(); - while (i.hasNext()) { - List altNameEntry = (List)(i.next()); - if (altNameEntry != null && 2 <= altNameEntry.size()) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null) { - if (altNameType.intValue() == ALT_IPA_NAME) { - String altName = (String)(altNameEntry.get(1)); - if (altName != null) { - if (LOG_ENABLED) { - Log.v(TAG, "alternative IP: " + altName); - } - if (thisDomain.equalsIgnoreCase(altName)) { - return true; - } - } - } - } - } - } - } - } catch (CertificateParsingException e) {} - - return false; - } - - /** - * Checks the site certificate against the DNS domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchDns(X509Certificate certificate, String thisDomain) { - boolean hasDns = false; - try { - Collection subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - Iterator i = subjectAltNames.iterator(); - while (i.hasNext()) { - List altNameEntry = (List)(i.next()); - if (altNameEntry != null && 2 <= altNameEntry.size()) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null) { - if (altNameType.intValue() == ALT_DNS_NAME) { - hasDns = true; - String altName = (String)(altNameEntry.get(1)); - if (altName != null) { - if (matchDns(thisDomain, altName)) { - return true; - } - } - } - } - } - } - } - } catch (CertificateParsingException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "failed to parse certificate"; - } - - Log.w(TAG, "DomainNameValidator.matchDns(): " + errorMessage); - return false; - } - - if (!hasDns) { - final String cn = new DNParser(certificate.getSubjectX500Principal()) - .find("cn"); - if (LOG_ENABLED) { - Log.v(TAG, "Validating subject: DN:" - + certificate.getSubjectX500Principal().getName(X500Principal.CANONICAL) - + " CN:" + cn); - } - if (cn != null) { - return matchDns(thisDomain, cn); - } - } - - return false; - } - - /** - * @param thisDomain The domain name of the site being visited - * @param thatDomain The domain name from the certificate - * @return True iff thisDomain matches thatDomain as specified by RFC2818 - */ - // not private for testing - public static boolean matchDns(String thisDomain, String thatDomain) { - if (LOG_ENABLED) { - Log.v(TAG, "DomainNameValidator.matchDns():" + - " this domain: " + thisDomain + - " that domain: " + thatDomain); - } - - if (thisDomain == null || thisDomain.length() == 0 || - thatDomain == null || thatDomain.length() == 0) { - return false; - } - - thatDomain = thatDomain.toLowerCase(); - - // (a) domain name strings are equal, ignoring case: X matches X - boolean rval = thisDomain.equals(thatDomain); - if (!rval) { - String[] thisDomainTokens = thisDomain.split("\\."); - String[] thatDomainTokens = thatDomain.split("\\."); - - int thisDomainTokensNum = thisDomainTokens.length; - int thatDomainTokensNum = thatDomainTokens.length; - - // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X - if (thisDomainTokensNum >= thatDomainTokensNum) { - for (int i = thatDomainTokensNum - 1; i >= 0; --i) { - rval = thisDomainTokens[i].equals(thatDomainTokens[i]); - if (!rval) { - // (c) OR we have a special *-match: - // *.Y.X matches Z.Y.X but *.X doesn't match Z.Y.X - rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum); - if (rval) { - rval = thatDomainTokens[0].equals("*"); - if (!rval) { - // (d) OR we have a *-component match: - // f*.com matches foo.com but not bar.com - rval = domainTokenMatch( - thisDomainTokens[0], thatDomainTokens[0]); - } - } - break; - } - } - } else { - // (e) OR thatHost has a '*.'-prefix of thisHost: - // *.Y.X matches Y.X - rval = thatDomain.equals("*." + thisDomain); - } - } - - return rval; - } - - /** - * @param thisDomainToken The domain token from the current domain name - * @param thatDomainToken The domain token from the certificate - * @return True iff thisDomainToken matches thatDomainToken, using the - * wildcard match as specified by RFC2818-3.1. For example, f*.com must - * match foo.com but not bar.com - */ - private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) { - if (thisDomainToken != null && thatDomainToken != null) { - int starIndex = thatDomainToken.indexOf('*'); - if (starIndex >= 0) { - if (thatDomainToken.length() - 1 <= thisDomainToken.length()) { - String prefix = thatDomainToken.substring(0, starIndex); - String suffix = thatDomainToken.substring(starIndex + 1); - - return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix); - } - } - } - - return false; - } -} diff --git a/core/java/com/android/internal/os/AtomicFile.java b/core/java/com/android/internal/os/AtomicFile.java index b093977..445d10a 100644 --- a/core/java/com/android/internal/os/AtomicFile.java +++ b/core/java/com/android/internal/os/AtomicFile.java @@ -28,6 +28,17 @@ import java.io.IOException; /** * Helper class for performing atomic operations on a file, by creating a * backup file until a write has successfully completed. + * <p> + * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + * </p><p> + * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + * </p> */ public class AtomicFile { private final File mBaseName; diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index fec4cbc..86118b1 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -19,6 +19,7 @@ package com.android.internal.os; import static android.net.NetworkStats.IFACE_ALL; import static android.net.NetworkStats.UID_ALL; import static android.text.format.DateUtils.SECOND_IN_MILLIS; +import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; @@ -35,6 +36,7 @@ import android.os.ParcelFormatException; import android.os.Parcelable; import android.os.Process; import android.os.SystemClock; +import android.os.SystemProperties; import android.os.WorkSource; import android.telephony.ServiceState; import android.telephony.SignalStrength; @@ -5713,11 +5715,17 @@ public final class BatteryStatsImpl extends BatteryStats { synchronized (this) { if (mNetworkSummaryCache == null || mNetworkSummaryCache.getElapsedRealtimeAge() > SECOND_IN_MILLIS) { - try { - mNetworkSummaryCache = mNetworkStatsFactory.readNetworkStatsSummary(); - } catch (IllegalStateException e) { - // log problem and return empty object - Log.wtf(TAG, "problem reading network stats", e); + mNetworkSummaryCache = null; + + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + try { + mNetworkSummaryCache = mNetworkStatsFactory.readNetworkStatsSummary(); + } catch (IllegalStateException e) { + Log.wtf(TAG, "problem reading network stats", e); + } + } + + if (mNetworkSummaryCache == null) { mNetworkSummaryCache = new NetworkStats(SystemClock.elapsedRealtime(), 0); } } @@ -5730,12 +5738,18 @@ public final class BatteryStatsImpl extends BatteryStats { synchronized (this) { if (mNetworkDetailCache == null || mNetworkDetailCache.getElapsedRealtimeAge() > SECOND_IN_MILLIS) { - try { - mNetworkDetailCache = mNetworkStatsFactory - .readNetworkStatsDetail().groupedByUid(); - } catch (IllegalStateException e) { - // log problem and return empty object - Log.wtf(TAG, "problem reading network stats", e); + mNetworkDetailCache = null; + + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + try { + mNetworkDetailCache = mNetworkStatsFactory + .readNetworkStatsDetail().groupedByUid(); + } catch (IllegalStateException e) { + Log.wtf(TAG, "problem reading network stats", e); + } + } + + if (mNetworkDetailCache == null) { mNetworkDetailCache = new NetworkStats(SystemClock.elapsedRealtime(), 0); } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 9c45dc6..6a99a2b 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -243,7 +243,7 @@ public class ZygoteInit { private static void preloadClasses() { final VMRuntime runtime = VMRuntime.getRuntime(); - InputStream is = ZygoteInit.class.getClassLoader().getResourceAsStream( + InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream( PRELOADED_CLASSES); if (is == null) { Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + "."); diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl index 3c61968..ba0aa1a 100644 --- a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl +++ b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl @@ -24,6 +24,7 @@ import android.view.textservice.TextInfo; oneway interface ISpellCheckerSession { void onGetSuggestionsMultiple( in TextInfo[] textInfos, int suggestionsLimit, boolean multipleWords); + void onGetSuggestionsMultipleForSentence(in TextInfo[] textInfos, int suggestionsLimit); void onCancel(); void onClose(); } diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl index 796b06e..b44dbc8 100644 --- a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl +++ b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl @@ -23,4 +23,5 @@ import android.view.textservice.SuggestionsInfo; */ oneway interface ISpellCheckerSessionListener { void onGetSuggestions(in SuggestionsInfo[] results); + void onGetSuggestionsForSentence(in SuggestionsInfo[] results); } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 3d22929..d1aa1ce 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -17,7 +17,6 @@ package com.android.internal.util; import java.lang.reflect.Array; -import java.util.Collection; // XXX these should be changed to reflect the actual memory allocator we use. // it looks like right now objects want to be powers of 2 minus 8 @@ -142,4 +141,64 @@ public class ArrayUtils } return false; } + + public static long total(long[] array) { + long total = 0; + for (long value : array) { + total += value; + } + return total; + } + + /** + * Appends an element to a copy of the array and returns the copy. + * @param array The original array, or null to represent an empty array. + * @param element The element to add. + * @return A new array that contains all of the elements of the original array + * with the specified element added at the end. + */ + @SuppressWarnings("unchecked") + public static <T> T[] appendElement(Class<T> kind, T[] array, T element) { + final T[] result; + final int end; + if (array != null) { + end = array.length; + result = (T[])Array.newInstance(kind, end + 1); + System.arraycopy(array, 0, result, 0, end); + } else { + end = 0; + result = (T[])Array.newInstance(kind, 1); + } + result[end] = element; + return result; + } + + /** + * Removes an element from a copy of the array and returns the copy. + * If the element is not present, then the original array is returned unmodified. + * @param array The original array, or null to represent an empty array. + * @param element The element to remove. + * @return A new array that contains all of the elements of the original array + * except the first copy of the specified element removed. If the specified element + * was not present, then returns the original array. Returns null if the result + * would be an empty array. + */ + @SuppressWarnings("unchecked") + public static <T> T[] removeElement(Class<T> kind, T[] array, T element) { + if (array != null) { + final int length = array.length; + for (int i = 0; i < length; i++) { + if (array[i] == element) { + if (length == 1) { + return null; + } + T[] result = (T[])Array.newInstance(kind, length - 1); + System.arraycopy(array, 0, result, 0, i); + System.arraycopy(array, i + 1, result, i, length - i - 1); + return result; + } + } + } + return array; + } } diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java new file mode 100644 index 0000000..8a8f315 --- /dev/null +++ b/core/java/com/android/internal/util/FileRotator.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2012 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.internal.util; + +import android.os.FileUtils; +import android.util.Slog; + +import com.android.internal.util.FileRotator.Rewriter; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import libcore.io.IoUtils; + +/** + * Utility that rotates files over time, similar to {@code logrotate}. There is + * a single "active" file, which is periodically rotated into historical files, + * and eventually deleted entirely. Files are stored under a specific directory + * with a well-known prefix. + * <p> + * Instead of manipulating files directly, users implement interfaces that + * perform operations on {@link InputStream} and {@link OutputStream}. This + * enables atomic rewriting of file contents in + * {@link #rewriteActive(Rewriter, long)}. + * <p> + * Users must periodically call {@link #maybeRotate(long)} to perform actual + * rotation. Not inherently thread safe. + */ +public class FileRotator { + private static final String TAG = "FileRotator"; + private static final boolean LOGD = true; + + private final File mBasePath; + private final String mPrefix; + private final long mRotateAgeMillis; + private final long mDeleteAgeMillis; + + private static final String SUFFIX_BACKUP = ".backup"; + private static final String SUFFIX_NO_BACKUP = ".no_backup"; + + // TODO: provide method to append to active file + + /** + * External class that reads data from a given {@link InputStream}. May be + * called multiple times when reading rotated data. + */ + public interface Reader { + public void read(InputStream in) throws IOException; + } + + /** + * External class that writes data to a given {@link OutputStream}. + */ + public interface Writer { + public void write(OutputStream out) throws IOException; + } + + /** + * External class that reads existing data from given {@link InputStream}, + * then writes any modified data to {@link OutputStream}. + */ + public interface Rewriter extends Reader, Writer { + public void reset(); + public boolean shouldWrite(); + } + + /** + * Create a file rotator. + * + * @param basePath Directory under which all files will be placed. + * @param prefix Filename prefix used to identify this rotator. + * @param rotateAgeMillis Age in milliseconds beyond which an active file + * may be rotated into a historical file. + * @param deleteAgeMillis Age in milliseconds beyond which a rotated file + * may be deleted. + */ + public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) { + mBasePath = Preconditions.checkNotNull(basePath); + mPrefix = Preconditions.checkNotNull(prefix); + mRotateAgeMillis = rotateAgeMillis; + mDeleteAgeMillis = deleteAgeMillis; + + // ensure that base path exists + mBasePath.mkdirs(); + + // recover any backup files + for (String name : mBasePath.list()) { + if (!name.startsWith(mPrefix)) continue; + + if (name.endsWith(SUFFIX_BACKUP)) { + if (LOGD) Slog.d(TAG, "recovering " + name); + + final File backupFile = new File(mBasePath, name); + final File file = new File( + mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length())); + + // write failed with backup; recover last file + backupFile.renameTo(file); + + } else if (name.endsWith(SUFFIX_NO_BACKUP)) { + if (LOGD) Slog.d(TAG, "recovering " + name); + + final File noBackupFile = new File(mBasePath, name); + final File file = new File( + mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length())); + + // write failed without backup; delete both + noBackupFile.delete(); + file.delete(); + } + } + } + + /** + * Delete all files managed by this rotator. + */ + public void deleteAll() { + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // delete each file that matches parser + new File(mBasePath, name).delete(); + } + } + + /** + * Process currently active file, first reading any existing data, then + * writing modified data. Maintains a backup during write, which is restored + * if the write fails. + */ + public void rewriteActive(Rewriter rewriter, long currentTimeMillis) + throws IOException { + final String activeName = getActiveName(currentTimeMillis); + rewriteSingle(rewriter, activeName); + } + + @Deprecated + public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis) + throws IOException { + rewriteActive(new Rewriter() { + /** {@inheritDoc} */ + public void reset() { + // ignored + } + + /** {@inheritDoc} */ + public void read(InputStream in) throws IOException { + reader.read(in); + } + + /** {@inheritDoc} */ + public boolean shouldWrite() { + return true; + } + + /** {@inheritDoc} */ + public void write(OutputStream out) throws IOException { + writer.write(out); + } + }, currentTimeMillis); + } + + /** + * Process all files managed by this rotator, usually to rewrite historical + * data. Each file is processed atomically. + */ + public void rewriteAll(Rewriter rewriter) throws IOException { + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // process each file that matches parser + rewriteSingle(rewriter, name); + } + } + + /** + * Process a single file atomically, first reading any existing data, then + * writing modified data. Maintains a backup during write, which is restored + * if the write fails. + */ + private void rewriteSingle(Rewriter rewriter, String name) throws IOException { + if (LOGD) Slog.d(TAG, "rewriting " + name); + + final File file = new File(mBasePath, name); + final File backupFile; + + rewriter.reset(); + + if (file.exists()) { + // read existing data + readFile(file, rewriter); + + // skip when rewriter has nothing to write + if (!rewriter.shouldWrite()) return; + + // backup existing data during write + backupFile = new File(mBasePath, name + SUFFIX_BACKUP); + file.renameTo(backupFile); + + try { + writeFile(file, rewriter); + + // write success, delete backup + backupFile.delete(); + } catch (IOException e) { + // write failed, delete file and restore backup + file.delete(); + backupFile.renameTo(file); + throw e; + } + + } else { + // create empty backup during write + backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP); + backupFile.createNewFile(); + + try { + writeFile(file, rewriter); + + // write success, delete empty backup + backupFile.delete(); + } catch (IOException e) { + // write failed, delete file and empty backup + file.delete(); + backupFile.delete(); + throw e; + } + } + } + + /** + * Read any rotated data that overlap the requested time range. + */ + public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis) + throws IOException { + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // read file when it overlaps + if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) { + if (LOGD) Slog.d(TAG, "reading matching " + name); + + final File file = new File(mBasePath, name); + readFile(file, reader); + } + } + } + + /** + * Return the currently active file, which may not exist yet. + */ + private String getActiveName(long currentTimeMillis) { + String oldestActiveName = null; + long oldestActiveStart = Long.MAX_VALUE; + + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // pick the oldest active file which covers current time + if (info.isActive() && info.startMillis < currentTimeMillis + && info.startMillis < oldestActiveStart) { + oldestActiveName = name; + oldestActiveStart = info.startMillis; + } + } + + if (oldestActiveName != null) { + return oldestActiveName; + } else { + // no active file found above; create one starting now + info.startMillis = currentTimeMillis; + info.endMillis = Long.MAX_VALUE; + return info.build(); + } + } + + /** + * Examine all files managed by this rotator, renaming or deleting if their + * age matches the configured thresholds. + */ + public void maybeRotate(long currentTimeMillis) { + final long rotateBefore = currentTimeMillis - mRotateAgeMillis; + final long deleteBefore = currentTimeMillis - mDeleteAgeMillis; + + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + if (info.isActive()) { + if (info.startMillis <= rotateBefore) { + // found active file; rotate if old enough + if (LOGD) Slog.d(TAG, "rotating " + name); + + info.endMillis = currentTimeMillis; + + final File file = new File(mBasePath, name); + final File destFile = new File(mBasePath, info.build()); + file.renameTo(destFile); + } + } else if (info.endMillis <= deleteBefore) { + // found rotated file; delete if old enough + if (LOGD) Slog.d(TAG, "deleting " + name); + + final File file = new File(mBasePath, name); + file.delete(); + } + } + } + + private static void readFile(File file, Reader reader) throws IOException { + final FileInputStream fis = new FileInputStream(file); + final BufferedInputStream bis = new BufferedInputStream(fis); + try { + reader.read(bis); + } finally { + IoUtils.closeQuietly(bis); + } + } + + private static void writeFile(File file, Writer writer) throws IOException { + final FileOutputStream fos = new FileOutputStream(file); + final BufferedOutputStream bos = new BufferedOutputStream(fos); + try { + writer.write(bos); + bos.flush(); + } finally { + FileUtils.sync(fos); + IoUtils.closeQuietly(bos); + } + } + + /** + * Details for a rotated file, either parsed from an existing filename, or + * ready to be built into a new filename. + */ + private static class FileInfo { + public final String prefix; + + public long startMillis; + public long endMillis; + + public FileInfo(String prefix) { + this.prefix = Preconditions.checkNotNull(prefix); + } + + /** + * Attempt parsing the given filename. + * + * @return Whether parsing was successful. + */ + public boolean parse(String name) { + startMillis = endMillis = -1; + + final int dotIndex = name.lastIndexOf('.'); + final int dashIndex = name.lastIndexOf('-'); + + // skip when missing time section + if (dotIndex == -1 || dashIndex == -1) return false; + + // skip when prefix doesn't match + if (!prefix.equals(name.substring(0, dotIndex))) return false; + + try { + startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex)); + + if (name.length() - dashIndex == 1) { + endMillis = Long.MAX_VALUE; + } else { + endMillis = Long.parseLong(name.substring(dashIndex + 1)); + } + + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Build current state into filename. + */ + public String build() { + final StringBuilder name = new StringBuilder(); + name.append(prefix).append('.').append(startMillis).append('-'); + if (endMillis != Long.MAX_VALUE) { + name.append(endMillis); + } + return name.toString(); + } + + /** + * Test if current file is active (no end timestamp). + */ + public boolean isActive() { + return endMillis == Long.MAX_VALUE; + } + } +} diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java new file mode 100644 index 0000000..3dd2284 --- /dev/null +++ b/core/java/com/android/internal/util/IndentingPrintWriter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 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.internal.util; + +import java.io.PrintWriter; +import java.io.Writer; + +/** + * Lightweight wrapper around {@link PrintWriter} that automatically indents + * newlines based on internal state. Delays writing indent until first actual + * write on a newline, enabling indent modification after newline. + */ +public class IndentingPrintWriter extends PrintWriter { + private final String mIndent; + + private StringBuilder mBuilder = new StringBuilder(); + private String mCurrent = new String(); + private boolean mEmptyLine = true; + + public IndentingPrintWriter(Writer writer, String indent) { + super(writer); + mIndent = indent; + } + + public void increaseIndent() { + mBuilder.append(mIndent); + mCurrent = mBuilder.toString(); + } + + public void decreaseIndent() { + mBuilder.delete(0, mIndent.length()); + mCurrent = mBuilder.toString(); + } + + @Override + public void println() { + super.println(); + mEmptyLine = true; + } + + @Override + public void write(char[] buf, int offset, int count) { + if (mEmptyLine) { + mEmptyLine = false; + super.print(mCurrent); + } + super.write(buf, offset, count); + } +} diff --git a/core/java/com/android/internal/view/BaseInputHandler.java b/core/java/com/android/internal/view/BaseInputHandler.java deleted file mode 100644 index 74b4b06..0000000 --- a/core/java/com/android/internal/view/BaseInputHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2010 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.internal.view; - -import android.view.InputHandler; -import android.view.InputQueue; -import android.view.KeyEvent; -import android.view.MotionEvent; - -/** - * Base do-nothing implementation of an input handler. - * @hide - */ -public abstract class BaseInputHandler implements InputHandler { - public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) { - finishedCallback.finished(false); - } - - public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) { - finishedCallback.finished(false); - } -} diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index a235d9a..9024d8d 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -387,9 +387,9 @@ public class InputConnectionWrapper implements InputConnection { } } - public boolean deleteSurroundingText(int leftLength, int rightLength) { + public boolean deleteSurroundingText(int beforeLength, int afterLength) { try { - mIInputContext.deleteSurroundingText(leftLength, rightLength); + mIInputContext.deleteSurroundingText(beforeLength, afterLength); return true; } catch (RemoteException e) { return false; diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index b689f53..517ce4e 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -300,6 +300,7 @@ public class ActionBarView extends AbsActionBarView { mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle); mProgressView.setId(R.id.progress_horizontal); mProgressView.setMax(10000); + mProgressView.setVisibility(GONE); addView(mProgressView); } @@ -307,6 +308,7 @@ public class ActionBarView extends AbsActionBarView { mIndeterminateProgressView = new ProgressBar(mContext, null, 0, mIndeterminateProgressStyle); mIndeterminateProgressView.setId(R.id.progress_circular); + mIndeterminateProgressView.setVisibility(GONE); addView(mIndeterminateProgressView); } diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java index daefc9a..af3fd42 100644 --- a/core/java/com/android/internal/widget/DigitalClock.java +++ b/core/java/com/android/internal/widget/DigitalClock.java @@ -228,7 +228,7 @@ public class DigitalClock extends RelativeLayout { updateTime(); } - private void updateTime() { + public void updateTime() { mCalendar.setTimeInMillis(System.currentTimeMillis()); CharSequence newTime = DateFormat.format(mFormat, mCalendar); diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 32e733b..9579bce 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -35,6 +35,11 @@ public class EditableInputConnection extends BaseInputConnection { private final TextView mTextView; + // Keeps track of nested begin/end batch edit to ensure this connection always has a + // balanced impact on its associated TextView. + // A negative value means that this connection has been finished by the InputMethodManager. + private int mBatchEditNesting; + public EditableInputConnection(TextView textview) { super(textview, true); mTextView = textview; @@ -48,19 +53,35 @@ public class EditableInputConnection extends BaseInputConnection { } return null; } - + @Override public boolean beginBatchEdit() { - mTextView.beginBatchEdit(); - return true; + synchronized(this) { + if (mBatchEditNesting >= 0) { + mTextView.beginBatchEdit(); + mBatchEditNesting++; + return true; + } + } + return false; } - + @Override public boolean endBatchEdit() { - mTextView.endBatchEdit(); - return true; + synchronized(this) { + if (mBatchEditNesting > 0) { + // When the connection is reset by the InputMethodManager and finishComposingText + // is called, some endBatchEdit calls may still be asynchronously received from the + // IME. Do not take these into account, thus ensuring that this IC's final + // contribution to mTextView's nested batch edit count is zero. + mTextView.endBatchEdit(); + mBatchEditNesting--; + return true; + } + } + return false; } - + @Override public boolean clearMetaKeyStates(int states) { final Editable content = getEditable(); @@ -76,7 +97,24 @@ public class EditableInputConnection extends BaseInputConnection { } return true; } - + + @Override + public boolean finishComposingText() { + final boolean superResult = super.finishComposingText(); + synchronized(this) { + if (mBatchEditNesting < 0) { + // The connection was already finished + return false; + } + while (mBatchEditNesting > 0) { + endBatchEdit(); + } + // Will prevent any further calls to begin or endBatchEdit + mBatchEditNesting = -1; + } + return superResult; + } + @Override public boolean commitCompletion(CompletionInfo text) { if (DEBUG) Log.v(TAG, "commitCompletion " + text); diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 905a171..6893ffb 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -404,7 +404,7 @@ public class LockPatternUtils { saveLockPassword(null, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); setLockPatternEnabled(false); saveLockPattern(null); - setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); setLong(PASSWORD_TYPE_ALTERNATE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); } diff --git a/core/java/com/android/server/NetworkManagementSocketTagger.java b/core/java/com/android/server/NetworkManagementSocketTagger.java index 8445ad1..c77992d 100644 --- a/core/java/com/android/server/NetworkManagementSocketTagger.java +++ b/core/java/com/android/server/NetworkManagementSocketTagger.java @@ -80,14 +80,15 @@ public final class NetworkManagementSocketTagger extends SocketTagger { } private void tagSocketFd(FileDescriptor fd, int tag, int uid) { - int errno; if (tag == -1 && uid == -1) return; - errno = native_tagSocketFd(fd, tag, uid); - if (errno < 0) { - Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", " - + tag + ", " + - + uid + ") failed with errno" + errno); + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + final int errno = native_tagSocketFd(fd, tag, uid); + if (errno < 0) { + Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", " + + tag + ", " + + + uid + ") failed with errno" + errno); + } } } @@ -101,12 +102,13 @@ public final class NetworkManagementSocketTagger extends SocketTagger { private void unTagSocketFd(FileDescriptor fd) { final SocketTags options = threadSocketTags.get(); - int errno; if (options.statsTag == -1 && options.statsUid == -1) return; - errno = native_untagSocketFd(fd); - if (errno < 0) { - Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno); + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + final int errno = native_untagSocketFd(fd); + if (errno < 0) { + Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno); + } } } @@ -116,16 +118,21 @@ public final class NetworkManagementSocketTagger extends SocketTagger { } public static void setKernelCounterSet(int uid, int counterSet) { - int errno = native_setCounterSet(counterSet, uid); - if (errno < 0) { - Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno " + errno); + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + final int errno = native_setCounterSet(counterSet, uid); + if (errno < 0) { + Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno " + + errno); + } } } public static void resetKernelUidStats(int uid) { - int errno = native_deleteTagData(0, uid); - if (errno < 0) { - Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno); + if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) { + int errno = native_deleteTagData(0, uid); + if (errno < 0) { + Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno); + } } } diff --git a/core/java/com/google/android/mms/pdu/PduParser.java b/core/java/com/google/android/mms/pdu/PduParser.java index f7f71ed..015d864 100755 --- a/core/java/com/google/android/mms/pdu/PduParser.java +++ b/core/java/com/google/android/mms/pdu/PduParser.java @@ -934,6 +934,9 @@ public class PduParser { int temp = pduDataStream.read(); assert(-1 != temp); int first = temp & 0xFF; + if (first == 0) { + return null; // Blank subject, bail. + } pduDataStream.reset(); if (first < TEXT_MIN) { |
