diff options
Diffstat (limited to 'core/java')
228 files changed, 26544 insertions, 17923 deletions
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 1d9e0f1..2ead976 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -1657,7 +1657,7 @@ public class AccountManagerService } boolean needsProvisioning; try { - needsProvisioning = telephony.getCdmaNeedsProvisioning(); + needsProvisioning = telephony.needsOtaServiceProvisioning(); } catch (RemoteException e) { Log.w(TAG, "exception while checking provisioning", e); // default to NOT wiping out the passwords diff --git a/core/java/android/animation/Animatable.java b/core/java/android/animation/Animatable.java new file mode 100644 index 0000000..68415f0 --- /dev/null +++ b/core/java/android/animation/Animatable.java @@ -0,0 +1,146 @@ +/* + * 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.animation; + +import java.util.ArrayList; + +/** + * This is the superclass for classes which provide basic support for animations which can be + * started, ended, and have <code>AnimatableListeners</code> added to them. + */ +public abstract class Animatable { + + + /** + * The set of listeners to be sent events through the life of an animation. + */ + ArrayList<AnimatableListener> mListeners = null; + + /** + * Starts this animation. If the animation has a nonzero startDelay, the animation will start + * running after that delay elapses. Note that the animation does not start synchronously with + * this call, because all animation events are posted to a central timing loop so that animation + * times are all synchronized on a single timing pulse on the UI thread. So the animation will + * start the next time that event handler processes events. + */ + public void start() { + } + + /** + * Cancels the animation. Unlike {@link #end()}, <code>cancel()</code> causes the animation to + * stop in its tracks, sending an {@link AnimatableListener#onAnimationCancel(Animatable)} to + * its listeners, followed by an {@link AnimatableListener#onAnimationEnd(Animatable)} message. + */ + public void cancel() { + } + + /** + * Ends the animation. This causes the animation to assign the end value of the property being + * animated, then calling the {@link AnimatableListener#onAnimationEnd(Animatable)} method on + * its listeners. + */ + public void end() { + } + + /** + * Adds a listener to the set of listeners that are sent events through the life of an + * animation, such as start, repeat, and end. + * + * @param listener the listener to be added to the current set of listeners for this animation. + */ + public void addListener(AnimatableListener listener) { + if (mListeners == null) { + mListeners = new ArrayList<AnimatableListener>(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set listening to this animation. + * + * @param listener the listener to be removed from the current set of listeners for this + * animation. + */ + public void removeListener(AnimatableListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + if (mListeners.size() == 0) { + mListeners = null; + } + } + + /** + * Gets the set of {@link AnimatableListener} objects that are currently + * listening for events on this <code>Animatable</code> object. + * + * @return ArrayList<AnimatableListener> The set of listeners. + */ + public ArrayList<AnimatableListener> getListeners() { + return mListeners; + } + + /** + * Removes all listeners from this object. This is equivalent to calling + * <code>getListeners()</code> followed by calling <code>clear()</code> on the + * returned list of listeners. + */ + public void removeAllListeners() { + if (mListeners != null) { + mListeners.clear(); + mListeners = null; + } + } + + /** + * <p>An animation listener receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * repetition of the animation.</p> + */ + public static interface AnimatableListener { + /** + * <p>Notifies the start of the animation.</p> + * + * @param animation The started animation. + */ + void onAnimationStart(Animatable animation); + + /** + * <p>Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.</p> + * + * @param animation The animation which reached its end. + */ + void onAnimationEnd(Animatable animation); + + /** + * <p>Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.</p> + * + * @param animation The animation which was canceled. + */ + void onAnimationCancel(Animatable animation); + + /** + * <p>Notifies the repetition of the animation.</p> + * + * @param animation The animation which was repeated. + */ + void onAnimationRepeat(Animatable animation); + } +} diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java new file mode 100755 index 0000000..b6c4763 --- /dev/null +++ b/core/java/android/animation/Animator.java @@ -0,0 +1,773 @@ +/* + * 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.animation; + +import android.os.Handler; +import android.os.Message; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * This class provides a simple timing engine for running animations + * which calculate animated values and set them on target objects. + * + * There is a single timing pulse that all animations use. It runs in a + * custom handler to ensure that property changes happen on the UI thread. + */ +public class Animator extends Animatable { + + /** + * Internal constants + */ + + /* + * The default amount of time in ms between animation frames + */ + private static final long DEFAULT_FRAME_DELAY = 30; + + /** + * 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 + */ + private static final int ANIMATION_START = 0; + private static final int ANIMATION_FRAME = 1; + + /** + * Values used with internal variable mPlayingState to indicate the current state of an + * animation. + */ + private static final int STOPPED = 0; // Not yet playing + private static final int RUNNING = 1; // Playing normally + private static final int CANCELED = 2; // cancel() called - need to end it + private static final int ENDED = 3; // end() called - need to end it + + /** + * Internal variables + */ + + + // The first time that the animation's animateFrame() method is called. This time is used to + // determine elapsed time (and therefore the elapsed fraction) in subsequent calls + // to animateFrame() + private long mStartTime; + + // The static sAnimationHandler processes the internal timing loop on which all animations + // are based + private static AnimationHandler sAnimationHandler; + + // The static list of all active animations + private static final ArrayList<Animator> sAnimations = new ArrayList<Animator>(); + + // The set of animations to be started on the next animation frame + private static final ArrayList<Animator> sPendingAnimations = new ArrayList<Animator>(); + + // The time interpolator to be used if none is set on the animation + private static final Interpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); + + // type evaluators for the three primitive types handled by this implementation + private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); + private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); + private static final TypeEvaluator sDoubleEvaluator = new DoubleEvaluator(); + + /** + * Used to indicate whether the animation is currently playing in reverse. This causes the + * elapsed fraction to be inverted to calculate the appropriate values. + */ + private boolean mPlayingBackwards = false; + + /** + * This variable tracks the current iteration that is playing. When mCurrentIteration exceeds the + * repeatCount (if repeatCount!=INFINITE), the animation ends + */ + private int mCurrentIteration = 0; + + /** + * Tracks whether a startDelay'd animation has begun playing through the startDelay. + */ + private boolean mStartedDelay = false; + + /** + * Tracks the time at which the animation began playing through its startDelay. This is + * different from the mStartTime variable, which is used to track when the animation became + * active (which is when the startDelay expired and the animation was added to the active + * animations list). + */ + private long mDelayStartTime; + + /** + * Flag that represents the current state of the animation. Used to figure out when to start + * an animation (if state == STOPPED). Also used to end an animation that + * has been cancel()'d or end()'d since the last animation frame. Possible values are + * STOPPED, RUNNING, ENDED, CANCELED. + */ + private int mPlayingState = STOPPED; + + /** + * Internal collections used to avoid set collisions as animations start and end while being + * processed. + */ + private static final ArrayList<Animator> sEndingAnims = new ArrayList<Animator>(); + private static final ArrayList<Animator> sDelayedAnims = new ArrayList<Animator>(); + private static final ArrayList<Animator> sReadyAnims = new ArrayList<Animator>(); + + // + // Backing variables + // + + // How long the animation should last in ms + private long mDuration; + + // The value that the animation should start from, set in the constructor + private Object mValueFrom; + + // The value that the animation should animate to, set in the constructor + private Object mValueTo; + + // 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; + + /** + * The type of repetition that will occur when repeatMode is nonzero. RESTART means the + * animation will start from the beginning on every new cycle. REVERSE means the animation + * will reverse directions on each iteration. + */ + private int mRepeatMode = RESTART; + + /** + * The time interpolator to be used. The elapsed fraction of the animation will be passed + * through this interpolator to calculate the interpolated fraction, which is then used to + * calculate the animated values. + */ + private Interpolator mInterpolator = sDefaultInterpolator; + + /** + * The type evaluator used to calculate the animated values. This evaluator is determined + * automatically based on the type of the start/end objects passed into the constructor, + * but the system only knows about the primitive types int, double, and float. Any other + * type will need to set the evaluator to a custom evaluator for that type. + */ + private TypeEvaluator mEvaluator; + + /** + * The set of listeners to be sent events through the life of an animation. + */ + private ArrayList<AnimatorUpdateListener> mUpdateListeners = null; + + /** + * The current value calculated by the animation. The value is calculated in animateFraction(), + * prior to calling the setter (if set) and sending out the onAnimationUpdate() callback + * to the update listeners. + */ + private Object mAnimatedValue = null; + + /** + * The type of the values, as determined by the valueFrom/valueTo properties. + */ + Class mValueType; + + /** + * Public constants + */ + + /** + * When the animation reaches the end and <code>repeatCount</code> is INFINITE + * or a positive value, the animation restarts from the beginning. + */ + public static final int RESTART = 1; + /** + * When the animation reaches the end and <code>repeatCount</code> is INFINITE + * or a positive value, the animation reverses direction on every iteration. + */ + public static final int REVERSE = 2; + /** + * This value used used with the {@link #setRepeatCount(int)} property to repeat + * the animation indefinitely. + */ + public static final int INFINITE = -1; + + private Animator(long duration, Object valueFrom, Object valueTo, Class valueType) { + mDuration = duration; + mValueFrom = valueFrom; + mValueTo= valueTo; + this.mValueType = valueType; + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero <code>startDelay</code>, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. + * + * <p>Overrides of this method should call the superclass method to ensure + * that internal mechanisms for the animation are set up correctly.</p> + */ + void initAnimation() { + if (mEvaluator == null) { + mEvaluator = (mValueType == int.class) ? sIntEvaluator : + (mValueType == double.class) ? sDoubleEvaluator : sFloatEvaluator; + } + mPlayingBackwards = false; + mCurrentIteration = 0; + } + + /** + * A constructor that takes <code>float</code> values. + * + * @param duration The length of the animation, in milliseconds. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public Animator(long duration, float valueFrom, float valueTo) { + this(duration, valueFrom, valueTo, float.class); + } + + /** + * A constructor that takes <code>int</code> values. + * + * @param duration The length of the animation, in milliseconds. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public Animator(long duration, int valueFrom, int valueTo) { + this(duration, valueFrom, valueTo, int.class); + } + + /** + * A constructor that takes <code>double</code> values. + * + * @param duration The length of the animation, in milliseconds. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public Animator(long duration, double valueFrom, double valueTo) { + this(duration, valueFrom, valueTo, double.class); + } + + /** + * A constructor that takes <code>Object</code> values. + * + * @param duration The length of the animation, in milliseconds. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public Animator(long duration, Object valueFrom, Object valueTo) { + this(duration, valueFrom, valueTo, + (valueFrom != null) ? valueFrom.getClass() : valueTo.getClass()); + } + + /** + * This custom, static handler handles the timing pulse that is shared by + * all active animations. This approach ensures that the setting of animation + * values will happen on the UI thread and that all animations will share + * the same times for calculating their values, which makes synchronizing + * animations possible. + * + */ + private static class AnimationHandler extends Handler { + /** + * 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 + * 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; + switch (msg.what) { + // TODO: should we avoid sending frame message when starting if we + // were already running? + case ANIMATION_START: + if (sAnimations.size() > 0 || sDelayedAnims.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 (sPendingAnimations.size() > 0) { + ArrayList<Animator> pendingCopy = + (ArrayList<Animator>) sPendingAnimations.clone(); + sPendingAnimations.clear(); + int count = pendingCopy.size(); + for (int i = 0; i < count; ++i) { + Animator anim = pendingCopy.get(i); + // If the animation has a startDelay, place it on the delayed list + if (anim.mStartDelay == 0) { + anim.startAnimation(); + } else { + sDelayedAnims.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(); + + // First, process animations currently sitting on the delayed queue, adding + // them to the active animations if they are ready + int numDelayedAnims = sDelayedAnims.size(); + for (int i = 0; i < numDelayedAnims; ++i) { + Animator anim = sDelayedAnims.get(i); + if (anim.delayedAnimationFrame(currentTime)) { + sReadyAnims.add(anim); + } + } + int numReadyAnims = sReadyAnims.size(); + if (numReadyAnims > 0) { + for (int i = 0; i < numReadyAnims; ++i) { + Animator anim = sReadyAnims.get(i); + anim.startAnimation(); + sDelayedAnims.remove(anim); + } + sReadyAnims.clear(); + } + + // Now process all active animations. The return value from animationFrame() + // tells the handler whether it should now be ended + int numAnims = sAnimations.size(); + for (int i = 0; i < numAnims; ++i) { + Animator anim = sAnimations.get(i); + if (anim.animationFrame(currentTime)) { + sEndingAnims.add(anim); + } + } + if (sEndingAnims.size() > 0) { + for (int i = 0; i < sEndingAnims.size(); ++i) { + sEndingAnims.get(i).endAnimation(); + } + sEndingAnims.clear(); + } + + // If there are still active or delayed animations, call the handler again + // after the frameDelay + if (callAgain && (!sAnimations.isEmpty() || !sDelayedAnims.isEmpty())) { + sendEmptyMessageDelayed(ANIMATION_FRAME, sFrameDelay); + } + break; + } + } + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + * + * @return the number of milliseconds to delay running the animation + */ + public long getStartDelay() { + return mStartDelay; + } + + /** + * The amount of time, in milliseconds, to delay starting the animation after + * {@link #start()} is called. + + * @param startDelay The amount of the delay, in milliseconds + */ + public void setStartDelay(long startDelay) { + this.mStartDelay = startDelay; + } + + /** + * 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. + * + * @return the requested time between frames, in milliseconds + */ + public static long getFrameDelay() { + return sFrameDelay; + } + + /** + * Gets the value that this animation will start from. + * + * @return Object The starting value for the animation. + */ + public Object getValueFrom() { + return mValueFrom; + } + + /** + * Sets the value that this animation will start from. + */ + public void setValueFrom(Object valueFrom) { + mValueFrom = valueFrom; + } + + /** + * Gets the value that this animation will animate to. + * + * @return Object The ending value for the animation. + */ + public Object getValueTo() { + return mValueTo; + } + + /** + * Sets the value that this animation will animate to. + * + * @return Object The ending value for the animation. + */ + public void setValueTo(Object valueTo) { + mValueTo = valueTo; + } + + /** + * 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. + * + * @param frameDelay the requested time between frames, in milliseconds + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + + /** + * The most recent value calculated by this <code>Animator</code> for the property + * being animated. This value is only sensible while the animation is running. The main + * purpose for this read-only property is to retrieve the value from the <code>Animator</code> + * during a call to {@link AnimatorUpdateListener#onAnimationUpdate(Animator)}, which + * is called during each animation frame, immediately after the value is calculated. + * + * @return animatedValue The value most recently calculated by this <code>Animator</code> for + * the property specified in the constructor. + */ + public Object getAnimatedValue() { + return mAnimatedValue; + } + + /** + * Sets how many times the animation should be repeated. If the repeat + * count is 0, the animation is never repeated. If the repeat count is + * greater than 0 or {@link #INFINITE}, the repeat mode will be taken + * into account. The repeat count is 0 by default. + * + * @param value the number of times the animation should be repeated + */ + public void setRepeatCount(int value) { + mRepeatCount = value; + } + /** + * Defines how many times the animation should repeat. The default value + * is 0. + * + * @return the number of times the animation should repeat, or {@link #INFINITE} + */ + public int getRepeatCount() { + return mRepeatCount; + } + + /** + * Defines what this animation should do when it reaches the end. This + * setting is applied only when the repeat count is either greater than + * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}. + * + * @param value {@link #RESTART} or {@link #REVERSE} + */ + public void setRepeatMode(int value) { + mRepeatMode = value; + } + + /** + * Defines what this animation should do when it reaches the end. + * + * @return either one of {@link #REVERSE} or {@link #RESTART} + */ + public int getRepeatMode() { + return mRepeatMode; + } + + /** + * Adds a listener to the set of listeners that are sent update events through the life of + * an animation. This method is called on all listeners for every frame of the animation, + * after the values for the animation have been calculated. + * + * @param listener the listener to be added to the current set of listeners for this animation. + */ + public void addUpdateListener(AnimatorUpdateListener listener) { + if (mUpdateListeners == null) { + mUpdateListeners = new ArrayList<AnimatorUpdateListener>(); + } + mUpdateListeners.add(listener); + } + + /** + * Removes a listener from the set listening to frame updates for this animation. + * + * @param listener the listener to be removed from the current set of update listeners + * for this animation. + */ + public void removeUpdateListener(AnimatorUpdateListener listener) { + if (mUpdateListeners == null) { + return; + } + mUpdateListeners.remove(listener); + if (mUpdateListeners.size() == 0) { + mUpdateListeners = null; + } + } + + + /** + * The time interpolator used in calculating the elapsed fraction of this animation. The + * interpolator determines whether the animation runs with linear or non-linear motion, + * such as acceleration and deceleration. The default value is + * {@link android.view.animation.AccelerateDecelerateInterpolator} + * + * @param value the interpolator to be used by this animation + */ + public void setInterpolator(Interpolator value) { + if (value != null) { + mInterpolator = value; + } + } + + /** + * The type evaluator to be used when calculating the animated values of this animation. + * The system will automatically assign a float, int, or double evaluator based on the type + * of <code>startValue</code> and <code>endValue</code> in the constructor. But if these values + * are not one of these primitive types, or if different evaluation is desired (such as is + * necessary with int values that represent colors), a custom evaluator needs to be assigned. + * For example, when running an animation on color values, the {@link RGBEvaluator} + * should be used to get correct RGB color interpolation. + * + * @param value the evaluator to be used this animation + */ + public void setEvaluator(TypeEvaluator value) { + if (value != null) { + mEvaluator = value; + } + } + + public void start() { + sPendingAnimations.add(this); + if (sAnimationHandler == null) { + sAnimationHandler = new AnimationHandler(); + } + // TODO: does this put too many messages on the queue if the handler + // is already running? + sAnimationHandler.sendEmptyMessage(ANIMATION_START); + } + + public void cancel() { + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + // Just set the CANCELED flag - this causes the animation to end the next time a frame + // is processed. + mPlayingState = CANCELED; + } + + public void end() { + // Just set the ENDED flag - this causes the animation to end the next time a frame + // is processed. + mPlayingState = ENDED; + } + + /** + * Called internally to end an animation by removing it from the animations list. Must be + * called on the UI thread. + */ + private void endAnimation() { + sAnimations.remove(this); + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } + } + mPlayingState = STOPPED; + } + + /** + * Called internally to start an animation by adding it to the active animations list. Must be + * called on the UI thread. + */ + private void startAnimation() { + initAnimation(); + sAnimations.add(this); + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationStart(this); + } + } + } + + /** + * Internal function called to process an animation frame on an animation that is currently + * sleeping through its <code>startDelay</code> phase. The return value indicates whether it + * should be woken up and put on the active animations queue. + * + * @param currentTime The current animation time, used to calculate whether the animation + * has exceeded its <code>startDelay</code> and should be started. + * @return True if the animation's <code>startDelay</code> has been exceeded and the animation + * should be added to the set of active animations. + */ + private boolean delayedAnimationFrame(long currentTime) { + if (!mStartedDelay) { + mStartedDelay = true; + mDelayStartTime = currentTime; + } else { + long deltaTime = currentTime - mDelayStartTime; + if (deltaTime > mStartDelay) { + // startDelay ended - start the anim and record the + // mStartTime appropriately + mStartTime = currentTime - (deltaTime - mStartDelay); + mPlayingState = RUNNING; + return true; + } + } + return false; + } + + /** + * This internal function processes a single animation frame for a given animation. The + * currentTime parameter is the timing pulse sent by the handler, used to calculate the + * elapsed duration, and therefore + * the elapsed fraction, of the animation. The return value indicates whether the animation + * should be ended (which happens when the elapsed time of the animation exceeds the + * animation's duration, including the repeatCount). + * + * @param currentTime The current time, as tracked by the static timing handler + * @return true if the animation's duration, including any repetitions due to + * <code>repeatCount</code> has been exceeded and the animation should be ended. + */ + private boolean animationFrame(long currentTime) { + + boolean done = false; + + if (mPlayingState == STOPPED) { + mPlayingState = RUNNING; + mStartTime = currentTime; + } + switch (mPlayingState) { + case RUNNING: + float fraction = (float)(currentTime - mStartTime) / mDuration; + if (fraction >= 1f) { + if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { + // Time to repeat + if (mListeners != null) { + for (AnimatableListener listener : mListeners) { + listener.onAnimationRepeat(this); + } + } + ++mCurrentIteration; + if (mRepeatMode == REVERSE) { + mPlayingBackwards = mPlayingBackwards ? false : true; + } + // TODO: doesn't account for fraction going Wayyyyy over 1, like 2+ + fraction = fraction - 1f; + mStartTime += mDuration; + } else { + done = true; + fraction = Math.min(fraction, 1.0f); + } + } + if (mPlayingBackwards) { + fraction = 1f - fraction; + } + animateValue(fraction); + break; + case ENDED: + // The final value set on the target varies, depending on whether the animation + // was supposed to repeat an odd number of times + if (mRepeatCount > 0 && (mRepeatCount & 0x01) == 1) { + animateValue(0f); + } else { + animateValue(1f); + } + // Fall through to set done flag + case CANCELED: + done = true; + break; + } + + return done; + } + + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the <code>end()</code> + * function is called, to set the final value on the property. + * + * <p>Overrides of this method must call the superclass to perform the calculation + * of the animated value.</p> + * + * @param fraction The elapsed fraction of the animation. + */ + void animateValue(float fraction) { + fraction = mInterpolator.getInterpolation(fraction); + mAnimatedValue = mEvaluator.evaluate(fraction, mValueFrom, mValueTo); + if (mUpdateListeners != null) { + int numListeners = mUpdateListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mUpdateListeners.get(i).onAnimationUpdate(this); + } + } + } + + /** + * Implementors of this interface can add themselves as update listeners + * to an <code>Animator</code> instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * <code>Animator</code>. + */ + public static interface AnimatorUpdateListener { + /** + * <p>Notifies the occurrence of another frame of the animation.</p> + * + * @param animation The animation which was repeated. + */ + void onAnimationUpdate(Animator animation); + + } +}
\ No newline at end of file diff --git a/core/java/android/animation/DoubleEvaluator.java b/core/java/android/animation/DoubleEvaluator.java new file mode 100644 index 0000000..86e3f22 --- /dev/null +++ b/core/java/android/animation/DoubleEvaluator.java @@ -0,0 +1,42 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>double</code> values. + */ +public class DoubleEvaluator implements TypeEvaluator { + /** + * This function returns the result of linearly interpolating the start and end values, with + * <code>fraction</code> representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>, + * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>, + * and <code>t</code> is <code>fraction</code>. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type <code>double</code> or + * <code>Double</code> + * @param endValue The end value; should be of type <code>double</code> or + * <code>Double</code> + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + double startDouble = (Double) startValue; + return startDouble + fraction * ((Double) endValue - startDouble); + } +}
\ No newline at end of file diff --git a/core/java/android/animation/FloatEvaluator.java b/core/java/android/animation/FloatEvaluator.java new file mode 100644 index 0000000..29a6f71 --- /dev/null +++ b/core/java/android/animation/FloatEvaluator.java @@ -0,0 +1,42 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>float</code> values. + */ +public class FloatEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * <code>fraction</code> representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>, + * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>, + * and <code>t</code> is <code>fraction</code>. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type <code>float</code> or + * <code>Float</code> + * @param endValue The end value; should be of type <code>float</code> or <code>Float</code> + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + float startFloat = (Float) startValue; + return startFloat + fraction * ((Float) endValue - startFloat); + } +}
\ No newline at end of file diff --git a/core/java/android/animation/IntEvaluator.java b/core/java/android/animation/IntEvaluator.java new file mode 100644 index 0000000..7a2911a --- /dev/null +++ b/core/java/android/animation/IntEvaluator.java @@ -0,0 +1,42 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>int</code> values. + */ +public class IntEvaluator implements TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * <code>fraction</code> representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>, + * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>, + * and <code>t</code> is <code>fraction</code>. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value; should be of type <code>int</code> or + * <code>Integer</code> + * @param endValue The end value; should be of type <code>int</code> or <code>Integer</code> + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + int startInt = (Integer) startValue; + return (int) (startInt + fraction * ((Integer) endValue - startInt)); + } +}
\ No newline at end of file diff --git a/core/java/android/animation/PropertyAnimator.java b/core/java/android/animation/PropertyAnimator.java new file mode 100644 index 0000000..99799f0 --- /dev/null +++ b/core/java/android/animation/PropertyAnimator.java @@ -0,0 +1,395 @@ +/* + * 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.animation; + +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This subclass of {@link Animator} provides support for animating properties on target objects. + * The constructors of this class take parameters to define the target object that will be animated + * as well as the name of the property that will be animated. Appropriate set/get functions + * are then determined internally and the animation will call these functions as necessary to + * animate the property. + */ +public final class PropertyAnimator extends Animator { + + // The target object on which the property exists, set in the constructor + private Object mTarget; + + private String mPropertyName; + + private Method mGetter = null; + + // The property setter that is assigned internally, based on the propertyName passed into + // the constructor + private Method mSetter; + + // These maps hold all property entries for a particular class. This map + // is used to speed up property/setter/getter lookups for a given class/property + // combination. No need to use reflection on the combination more than once. + private static final HashMap<Object, HashMap<String, Method>> sSetterPropertyMap = + new HashMap<Object, HashMap<String, Method>>(); + private static final HashMap<Object, HashMap<String, Method>> sGetterPropertyMap = + new HashMap<Object, HashMap<String, Method>>(); + + // This lock is used to ensure that only one thread is accessing the property maps + // at a time. + private ReentrantReadWriteLock propertyMapLock = new ReentrantReadWriteLock(); + + + /** + * Sets the name of the property that will be animated. This name is used to derive + * a setter function that will be called to set animated values. + * For example, a property name of <code>foo</code> will result + * in a call to the function <code>setFoo()</code> on the target object. If either + * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will + * also be derived and called. + * + * <p>Note that the setter function derived from this property name + * must take the same parameter type as the + * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to + * the setter function will fail.</p> + * + * @param propertyName The name of the property being animated. + */ + public void setPropertyName(String propertyName) { + mPropertyName = propertyName; + } + + /** + * Gets the name of the property that will be animated. This name will be used to derive + * a setter function that will be called to set animated values. + * For example, a property name of <code>foo</code> will result + * in a call to the function <code>setFoo()</code> on the target object. If either + * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will + * also be derived and called. + */ + public String getPropertyName() { + return mPropertyName; + } + + /** + * Sets the <code>Method</code> that is called with the animated values calculated + * during the animation. Setting the setter method is an alternative to supplying a + * {@link #setPropertyName(String) propertyName} from which the method is derived. This + * approach is more direct, and is especially useful when a function must be called that does + * not correspond to the convention of <code>setName()</code>. For example, if a function + * called <code>offset()</code> is to be called with the animated values, there is no way + * to tell <code>PropertyAnimator</code> how to call that function simply through a property + * name, so a setter method should be supplied instead. + * + * <p>Note that the setter function must take the same parameter type as the + * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to + * the setter function will fail.</p> + * + * @param setter The setter method that should be called with the animated values. + */ + public void setSetter(Method setter) { + mSetter = setter; + } + + /** + * Gets the <code>Method</code> that is called with the animated values calculated + * during the animation. + */ + public Method getSetter() { + return mSetter; + } + + /** + * Sets the <code>Method</code> that is called to get unsupplied <code>valueFrom</code> or + * <code>valueTo</code> properties. Setting the getter method is an alternative to supplying a + * {@link #setPropertyName(String) propertyName} from which the method is derived. This + * approach is more direct, and is especially useful when a function must be called that does + * not correspond to the convention of <code>setName()</code>. For example, if a function + * called <code>offset()</code> is to be called to get an initial value, there is no way + * to tell <code>PropertyAnimator</code> how to call that function simply through a property + * name, so a getter method should be supplied instead. + * + * <p>Note that the getter method is only called whether supplied here or derived + * from the property name, if one of <code>valueFrom</code> or <code>valueTo</code> are + * null. If both of those values are non-null, then there is no need to get one of the + * values and the getter is not called. + * + * <p>Note that the getter function must return the same parameter type as the + * <code>valueFrom</code> and <code>valueTo</code> properties (whichever of them are + * non-null), otherwise the call to the getter function will fail.</p> + * + * @param getter The getter method that should be called to get initial animation values. + */ + public void setGetter(Method getter) { + mGetter = getter; + } + + /** + * Gets the <code>Method</code> that is called to get unsupplied <code>valueFrom</code> or + * <code>valueTo</code> properties. + */ + public Method getGetter() { + return mGetter; + } + + /** + * Determine the setter or getter function using the JavaBeans convention of setFoo or + * getFoo for a property named 'foo'. This function figures out what the name of the + * function should be and uses reflection to find the Method with that name on the + * target object. + * + * @param prefix "set" or "get", depending on whether we need a setter or getter. + * @return Method the method associated with mPropertyName. + */ + private Method getPropertyFunction(String prefix) { + // TODO: faster implementation... + Method returnVal = null; + String firstLetter = mPropertyName.substring(0, 1); + String theRest = mPropertyName.substring(1); + firstLetter = firstLetter.toUpperCase(); + String setterName = prefix + firstLetter + theRest; + Class args[] = new Class[1]; + args[0] = mValueType; + try { + returnVal = mTarget.getClass().getMethod(setterName, args); + } catch (NoSuchMethodException e) { + Log.e("PropertyAnimator", + "Couldn't find setter for property " + mPropertyName + ": " + e); + } + return returnVal; + } + + /** + * A constructor that takes <code>float</code> values. When this constructor + * is called, the system expects to find a setter for <code>propertyName</code> on + * the target object that takes a <code>float</code> value. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have a public function on it called <code>setName()</code>, where <code>name</code> is + * the name of the property passed in as the <code>propertyName</code> parameter. + * @param propertyName The name of the property on the <code>target</code> object + * that will be animated. Given this name, the constructor will search for a + * setter on the target object with the name <code>setPropertyName</code>. For example, + * if the constructor is called with <code>propertyName = "foo"</code>, then the + * target object should have a setter function with the name <code>setFoo()</code>. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public PropertyAnimator(int duration, Object target, String propertyName, + float valueFrom, float valueTo) { + super(duration, valueFrom, valueTo); + mTarget = target; + mPropertyName = propertyName; + } + + /** + * A constructor that takes <code>int</code> values. When this constructor + * is called, the system expects to find a setter for <code>propertyName</code> on + * the target object that takes a <code>int</code> value. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have a public function on it called <code>setName()</code>, where <code>name</code> is + * the name of the property passed in as the <code>propertyName</code> parameter. + * @param propertyName The name of the property on the <code>target</code> object + * that will be animated. Given this name, the constructor will search for a + * setter on the target object with the name <code>setPropertyName</code>. For example, + * if the constructor is called with <code>propertyName = "foo"</code>, then the + * target object should have a setter function with the name <code>setFoo()</code>. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public PropertyAnimator(int duration, Object target, String propertyName, + int valueFrom, int valueTo) { + super(duration, valueFrom, valueTo); + mTarget = target; + mPropertyName = propertyName; + } + + /** + * A constructor that takes <code>double</code> values. When this constructor + * is called, the system expects to find a setter for <code>propertyName</code> on + * the target object that takes a <code>double</code> value. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have a public function on it called <code>setName()</code>, where <code>name</code> is + * the name of the property passed in as the <code>propertyName</code> parameter. + * @param propertyName The name of the property on the <code>target</code> object + * that will be animated. Given this name, the constructor will search for a + * setter on the target object with the name <code>setPropertyName</code>. For example, + * if the constructor is called with <code>propertyName = "foo"</code>, then the + * target object should have a setter function with the name <code>setFoo()</code>. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public PropertyAnimator(int duration, Object target, String propertyName, + double valueFrom, double valueTo) { + super(duration, valueFrom, valueTo); + mTarget = target; + mPropertyName = propertyName; + } + + /** + * A constructor that takes <code>Object</code> values. When this constructor + * is called, the system expects to find a setter for <code>propertyName</code> on + * the target object that takes a value of the same type as the <code>Object</code>s. + * + * @param duration The length of the animation, in milliseconds. + * @param target The object whose property is to be animated. This object should + * have a public function on it called <code>setName()</code>, where <code>name</code> is + * the name of the property passed in as the <code>propertyName</code> parameter. + * @param propertyName The name of the property on the <code>target</code> object + * that will be animated. Given this name, the constructor will search for a + * setter on the target object with the name <code>setPropertyName</code>. For example, + * if the constructor is called with <code>propertyName = "foo"</code>, then the + * target object should have a setter function with the name <code>setFoo()</code>. + * @param valueFrom The initial value of the property when the animation begins. + * @param valueTo The value to which the property will animate. + */ + public PropertyAnimator(int duration, Object target, String propertyName, + Object valueFrom, Object valueTo) { + super(duration, valueFrom, valueTo); + mTarget = target; + mPropertyName = propertyName; + } + + /** + * This function is called immediately before processing the first animation + * frame of an animation. If there is a nonzero <code>startDelay</code>, the + * function is called after that delay ends. + * It takes care of the final initialization steps for the + * animation. This includes setting mEvaluator, if the user has not yet + * set it up, and the setter/getter methods, if the user did not supply + * them. + * + * <p>Overriders of this method should call the superclass method to cause + * internal mechanisms to be set up correctly.</p> + */ + @Override + void initAnimation() { + super.initAnimation(); + if (mSetter == null) { + try { + // Have to lock property map prior to reading it, to guard against + // another thread putting something in there after we've checked it + // but before we've added an entry to it + propertyMapLock.writeLock().lock(); + HashMap<String, Method> propertyMap = sSetterPropertyMap.get(mTarget); + if (propertyMap != null) { + mSetter = propertyMap.get(mPropertyName); + if (mSetter != null) { + return; + } + } + mSetter = getPropertyFunction("set"); + if (propertyMap == null) { + propertyMap = new HashMap<String, Method>(); + sSetterPropertyMap.put(mTarget, propertyMap); + } + propertyMap.put(mPropertyName, mSetter); + } finally { + propertyMapLock.writeLock().unlock(); + } + } + if (getValueFrom() == null || getValueTo() == null) { + // Need to set up the getter if not set by the user, then call it + // to get the initial values + if (mGetter == null) { + try { + propertyMapLock.writeLock().lock(); + HashMap<String, Method> propertyMap = sGetterPropertyMap.get(mTarget); + if (propertyMap != null) { + mGetter = propertyMap.get(mPropertyName); + if (mGetter != null) { + return; + } + } + mGetter = getPropertyFunction("get"); + if (propertyMap == null) { + propertyMap = new HashMap<String, Method>(); + sGetterPropertyMap.put(mTarget, propertyMap); + } + propertyMap.put(mPropertyName, mGetter); + } finally { + propertyMapLock.writeLock().unlock(); + } + } + try { + if (getValueFrom() == null) { + setValueFrom(mGetter.invoke(mTarget)); + } + if (getValueTo() == null) { + setValueTo(mGetter.invoke(mTarget)); + } + } catch (IllegalArgumentException e) { + Log.e("PropertyAnimator", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyAnimator", e.toString()); + } catch (InvocationTargetException e) { + Log.e("PropertyAnimator", e.toString()); + } + } + } + + + /** + * The target object whose property will be animated by this animation + * + * @return The object being animated + */ + public Object getTarget() { + return mTarget; + } + + /** + * This method is called with the elapsed fraction of the animation during every + * animation frame. This function turns the elapsed fraction into an interpolated fraction + * and then into an animated value (from the evaluator. The function is called mostly during + * animation updates, but it is also called when the <code>end()</code> + * function is called, to set the final value on the property. + * + * <p>Overrides of this method must call the superclass to perform the calculation + * of the animated value.</p> + * + * @param fraction The elapsed fraction of the animation. + */ + @Override + void animateValue(float fraction) { + super.animateValue(fraction); + if (mSetter != null) { + try { + mSetter.invoke(mTarget, getAnimatedValue()); + } catch (InvocationTargetException e) { + Log.e("PropertyAnimator", e.toString()); + } catch (IllegalAccessException e) { + Log.e("PropertyAnimator", e.toString()); + } + } + } + + @Override + public String toString() { + return "Animator: target: " + this.mTarget + "\n" + + " property: " + mPropertyName + "\n" + + " from: " + getValueFrom() + "\n" + + " to: " + getValueTo(); + } +} diff --git a/core/java/android/animation/RGBEvaluator.java b/core/java/android/animation/RGBEvaluator.java new file mode 100644 index 0000000..bae0af0 --- /dev/null +++ b/core/java/android/animation/RGBEvaluator.java @@ -0,0 +1,59 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between integer + * values that represent ARGB colors. + */ +public class RGBEvaluator implements TypeEvaluator { + + /** + * This function returns the calculated in-between value for a color + * given integers that represent the start and end values in the four + * bytes of the 32-bit int. Each channel is separately linearly interpolated + * and the resulting calculated values are recombined into the return value. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @param endValue A 32-bit int value representing colors in the + * separate bytes of the parameter + * @return A value that is calculated to be the linearly interpolated + * result, derived by separating the start and end values into separate + * color channels and interpolating each one separately, recombining the + * resulting values in the same way. + */ + public Object evaluate(float fraction, Object startValue, Object endValue) { + int startInt = (Integer) startValue; + int startA = (startInt >> 24); + int startR = (startInt >> 16) & 0xff; + int startG = (startInt >> 8) & 0xff; + int startB = startInt & 0xff; + + int endInt = (Integer) endValue; + int endA = (endInt >> 24); + int endR = (endInt >> 16) & 0xff; + int endG = (endInt >> 8) & 0xff; + int endB = endInt & 0xff; + + return (int)((startA + (int)(fraction * (endA - startA))) << 24) | + (int)((startR + (int)(fraction * (endR - startR))) << 16) | + (int)((startG + (int)(fraction * (endG - startG))) << 8) | + (int)((startB + (int)(fraction * (endB - startB)))); + } +}
\ No newline at end of file diff --git a/core/java/android/animation/Sequencer.java b/core/java/android/animation/Sequencer.java new file mode 100644 index 0000000..00ab6a3 --- /dev/null +++ b/core/java/android/animation/Sequencer.java @@ -0,0 +1,681 @@ +/* + * 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.animation; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class plays a set of {@link Animatable} objects in the specified order. Animations + * can be set up to play together, in sequence, or after a specified delay. + * + * <p>There are two different approaches to adding animations to a <code>Sequencer</code>: + * either the {@link Sequencer#playTogether(Animatable...) playTogether()} or + * {@link Sequencer#playSequentially(Animatable...) playSequentially()} methods can be called to add + * a set of animations all at once, or the {@link Sequencer#play(Animatable)} can be + * used in conjunction with methods in the {@link android.animation.Sequencer.Builder Builder} + * class to add animations + * one by one.</p> + * + * <p>It is possible to set up a <code>Sequencer</code> with circular dependencies between + * its animations. For example, an animation a1 could be set up to start before animation a2, a2 + * before a3, and a3 before a1. The results of this configuration are undefined, but will typically + * result in none of the affected animations being played. Because of this (and because + * circular dependencies do not make logical sense anyway), circular dependencies + * should be avoided, and the dependency flow of animations should only be in one direction. + */ +public final class Sequencer extends Animatable { + + /** + * Tracks aniamtions currently being played, so that we know what to + * cancel or end when cancel() or end() is called on this Sequencer + */ + private final ArrayList<Animatable> mPlayingSet = new ArrayList<Animatable>(); + + /** + * Contains all nodes, mapped to their respective Animatables. When new + * dependency information is added for an Animatable, we want to add it + * to a single node representing that Animatable, not create a new Node + * if one already exists. + */ + private final HashMap<Animatable, Node> mNodeMap = new HashMap<Animatable, Node>(); + + /** + * Set of all nodes created for this Sequencer. This list is used upon + * starting the sequencer, and the nodes are placed in sorted order into the + * sortedNodes collection. + */ + private final ArrayList<Node> mNodes = new ArrayList<Node>(); + + /** + * The sorted list of nodes. This is the order in which the animations will + * be played. The details about when exactly they will be played depend + * on the dependency relationships of the nodes. + */ + private final ArrayList<Node> mSortedNodes = new ArrayList<Node>(); + + /** + * The set of listeners to be sent events through the life of an animation. + */ + private ArrayList<AnimatableListener> mListeners = null; + + /** + * Flag indicating whether the nodes should be sorted prior to playing. This + * flag allows us to cache the previous sorted nodes so that if the sequence + * is replayed with no changes, it does not have to re-sort the nodes again. + */ + private boolean mNeedsSort = true; + + private SequencerAnimatableListener mSequenceListener = null; + + /** + * Sets up this Sequencer to play all of the supplied animations at the same time. + * + * @param sequenceItems The animations that will be started simultaneously. + */ + public void playTogether(Animatable... sequenceItems) { + if (sequenceItems != null) { + mNeedsSort = true; + Builder builder = play(sequenceItems[0]); + for (int i = 1; i < sequenceItems.length; ++i) { + builder.with(sequenceItems[i]); + } + } + } + + /** + * Sets up this Sequencer to play each of the supplied animations when the + * previous animation ends. + * + * @param sequenceItems The aniamtions that will be started one after another. + */ + public void playSequentially(Animatable... sequenceItems) { + if (sequenceItems != null) { + mNeedsSort = true; + if (sequenceItems.length == 1) { + play(sequenceItems[0]); + } else { + for (int i = 0; i < sequenceItems.length - 1; ++i) { + play(sequenceItems[i]).before(sequenceItems[i+1]); + } + } + } + } + + /** + * This method creates a <code>Builder</code> object, which is used to + * set up playing constraints. This initial <code>play()</code> method + * tells the <code>Builder</code> the animation that is the dependency for + * the succeeding commands to the <code>Builder</code>. For example, + * calling <code>play(a1).with(a2)</code> sets up the Sequence to play + * <code>a1</code> and <code>a2</code> at the same time, + * <code>play(a1).before(a2)</code> sets up the Sequence to play + * <code>a1</code> first, followed by <code>a2</code>, and + * <code>play(a1).after(a2)</code> sets up the Sequence to play + * <code>a2</code> first, followed by <code>a1</code>. + * + * <p>Note that <code>play()</code> is the only way to tell the + * <code>Builder</code> the animation upon which the dependency is created, + * so successive calls to the various functions in <code>Builder</code> + * will all refer to the initial parameter supplied in <code>play()</code> + * as the dependency of the other animations. For example, calling + * <code>play(a1).before(a2).before(a3)</code> will play both <code>a2</code> + * and <code>a3</code> when a1 ends; it does not set up a dependency between + * <code>a2</code> and <code>a3</code>.</p> + * + * @param anim The animation that is the dependency used in later calls to the + * methods in the returned <code>Builder</code> object. A null parameter will result + * in a null <code>Builder</code> return value. + * @return Builder The object that constructs the sequence based on the dependencies + * outlined in the calls to <code>play</code> and the other methods in the + * <code>Builder</code object. + */ + public Builder play(Animatable anim) { + if (anim != null) { + mNeedsSort = true; + return new Builder(anim); + } + return null; + } + + /** + * {@inheritDoc} + * + * <p>Note that canceling a <code>Sequencer</code> also cancels all of the animations that it is + * responsible for.</p> + */ + @SuppressWarnings("unchecked") + @Override + public void cancel() { + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } + if (mPlayingSet.size() > 0) { + for (Animatable item : mPlayingSet) { + item.cancel(); + } + mPlayingSet.clear(); + } + } + + /** + * {@inheritDoc} + * + * <p>Note that ending a <code>Sequencer</code> also ends all of the animations that it is + * responsible for.</p> + */ + @Override + public void end() { + if (mPlayingSet.size() > 0) { + for (Animatable item : mPlayingSet) { + item.end(); + } + mPlayingSet.clear(); + } + } + + /** + * {@inheritDoc} + * + * <p>Starting this <code>Sequencer</code> will, in turn, start the animations for which + * it is responsible. The details of when exactly those animations are started depends on + * the dependency relationships that have been set up between the animations. + */ + @SuppressWarnings("unchecked") + @Override + public void start() { + // First, sort the nodes (if necessary). This will ensure that sortedNodes + // contains the animation nodes in the correct order. + sortNodes(); + + // nodesToStart holds the list of nodes to be started immediately. We don't want to + // start the animations in the loop directly because we first need to set up + // dependencies on all of the nodes. For example, we don't want to start an animation + // when some other animation also wants to start when the first animation begins. + ArrayList<Node> nodesToStart = new ArrayList<Node>(); + for (Node node : mSortedNodes) { + if (mSequenceListener == null) { + mSequenceListener = new SequencerAnimatableListener(this); + } + node.animation.addListener(mSequenceListener); + if (node.dependencies == null || node.dependencies.size() == 0) { + nodesToStart.add(node); + } else { + for (Dependency dependency : node.dependencies) { + dependency.node.animation.addListener( + new DependencyListener(node, dependency.rule)); + } + node.tmpDependencies = (ArrayList<Dependency>) node.dependencies.clone(); + } + } + // Now that all dependencies are set up, start the animations that should be started. + for (Node node : nodesToStart) { + node.animation.start(); + mPlayingSet.add(node.animation); + } + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationStart(this); + } + } + } + + /** + * This class is the mechanism by which animations are started based on events in other + * animations. If an animation has multiple dependencies on other animations, then + * all dependencies must be satisfied before the animation is started. + */ + private static class DependencyListener implements AnimatableListener { + + // The node upon which the dependency is based. + private Node mNode; + + // The Dependency rule (WITH or AFTER) that the listener should wait for on + // the node + private int mRule; + + public DependencyListener(Node node, int rule) { + this.mNode = node; + this.mRule = rule; + } + + /** + * If an animation that is being listened for is canceled, then this removes + * the listener on that animation, to avoid triggering further animations down + * the line when the animation ends. + */ + public void onAnimationCancel(Animatable animation) { + Dependency dependencyToRemove = null; + for (Dependency dependency : mNode.tmpDependencies) { + if (dependency.node.animation == animation) { + // animation canceled - remove the dependency and listener + dependencyToRemove = dependency; + animation.removeListener(this); + break; + } + } + mNode.tmpDependencies.remove(dependencyToRemove); + } + + /** + * An end event is received - see if this is an event we are listening for + */ + public void onAnimationEnd(Animatable animation) { + if (mRule == Dependency.AFTER) { + startIfReady(animation); + } + } + + /** + * Ignore repeat events for now + */ + public void onAnimationRepeat(Animatable animation) { + } + + /** + * A start event is received - see if this is an event we are listening for + */ + public void onAnimationStart(Animatable animation) { + if (mRule == Dependency.WITH) { + startIfReady(animation); + } + } + + /** + * Check whether the event received is one that the node was waiting for. + * If so, mark it as complete and see whether it's time to start + * the animation. + * @param dependencyAnimation the animation that sent the event. + */ + private void startIfReady(Animatable dependencyAnimation) { + Dependency dependencyToRemove = null; + for (Dependency dependency : mNode.tmpDependencies) { + if (dependency.rule == mRule && + dependency.node.animation == dependencyAnimation) { + // rule fired - remove the dependency and listener and check to + // see whether it's time to start the animation + dependencyToRemove = dependency; + dependencyAnimation.removeListener(this); + break; + } + } + mNode.tmpDependencies.remove(dependencyToRemove); + if (mNode.tmpDependencies.size() == 0) { + // all dependencies satisfied: start the animation + mNode.animation.start(); + } + } + + } + + private class SequencerAnimatableListener implements AnimatableListener { + + private Sequencer mSequencer; + + SequencerAnimatableListener(Sequencer sequencer) { + mSequencer = sequencer; + } + + public void onAnimationCancel(Animatable animation) { + if (mPlayingSet.size() == 0) { + if (mListeners != null) { + for (AnimatableListener listener : mListeners) { + listener.onAnimationCancel(mSequencer); + } + } + } + } + + @SuppressWarnings("unchecked") + public void onAnimationEnd(Animatable animation) { + animation.removeListener(this); + mPlayingSet.remove(animation); + if (mPlayingSet.size() == 0) { + // If this was the last child animation to end, then notify listeners that this + // sequence ended + if (mListeners != null) { + ArrayList<AnimatableListener> tmpListeners = + (ArrayList<AnimatableListener>) mListeners.clone(); + for (AnimatableListener listener : tmpListeners) { + listener.onAnimationEnd(mSequencer); + } + } + } + } + + // Nothing to do + public void onAnimationRepeat(Animatable animation) { + } + + // Nothing to do + public void onAnimationStart(Animatable animation) { + } + + } + + /** + * This method sorts the current set of nodes, if needed. The sort is a simple + * DependencyGraph sort, which goes like this: + * - All nodes without dependencies become 'roots' + * - while roots list is not null + * - for each root r + * - add r to sorted list + * - remove r as a dependency from any other node + * - any nodes with no dependencies are added to the roots list + */ + private void sortNodes() { + if (mNeedsSort) { + mSortedNodes.clear(); + ArrayList<Node> roots = new ArrayList<Node>(); + for (Node node : mNodes) { + if (node.dependencies == null || node.dependencies.size() == 0) { + roots.add(node); + } + } + ArrayList<Node> tmpRoots = new ArrayList<Node>(); + while (roots.size() > 0) { + for (Node root : roots) { + mSortedNodes.add(root); + if (root.nodeDependents != null) { + for (Node node : root.nodeDependents) { + node.nodeDependencies.remove(root); + if (node.nodeDependencies.size() == 0) { + tmpRoots.add(node); + } + } + } + } + roots.addAll(tmpRoots); + tmpRoots.clear(); + } + mNeedsSort = false; + if (mSortedNodes.size() != mNodes.size()) { + throw new IllegalStateException("Circular dependencies cannot exist" + + " in Sequencer"); + } + } else { + // Doesn't need sorting, but still need to add in the nodeDependencies list + // because these get removed as the event listeners fire and the dependencies + // are satisfied + for (Node node : mNodes) { + if (node.dependencies != null && node.dependencies.size() > 0) { + for (Dependency dependency : node.dependencies) { + if (node.nodeDependencies == null) { + node.nodeDependencies = new ArrayList<Node>(); + } + if (!node.nodeDependencies.contains(dependency.node)) { + node.nodeDependencies.add(dependency.node); + } + } + } + } + } + } + + /** + * Dependency holds information about the node that some other node is + * dependent upon and the nature of that dependency. + * + */ + private static class Dependency { + static final int WITH = 0; // dependent node must start with this dependency node + static final int AFTER = 1; // dependent node must start when this dependency node finishes + + // The node that the other node with this Dependency is dependent upon + public Node node; + + // The nature of the dependency (WITH or AFTER) + public int rule; + + public Dependency(Node node, int rule) { + this.node = node; + this.rule = rule; + } + } + + /** + * A Node is an embodiment of both the Animatable that it wraps as well as + * any dependencies that are associated with that Animation. This includes + * both dependencies upon other nodes (in the dependencies list) as + * well as dependencies of other nodes upon this (in the nodeDependents list). + */ + private static class Node { + public Animatable animation; + + /** + * These are the dependencies that this node's animation has on other + * nodes. For example, if this node's animation should begin with some + * other animation ends, then there will be an item in this node's + * dependencies list for that other animation's node. + */ + public ArrayList<Dependency> dependencies = null; + + /** + * tmpDependencies is a runtime detail. We use the dependencies list for sorting. + * But we also use the list to keep track of when multiple dependencies are satisfied, + * but removing each dependency as it is satisfied. We do not want to remove + * the dependency itself from the list, because we need to retain that information + * if the sequencer is launched in the future. So we create a copy of the dependency + * list when the sequencer starts and use this tmpDependencies list to track the + * list of satisfied dependencies. + */ + public ArrayList<Dependency> tmpDependencies = null; + + /** + * nodeDependencies is just a list of the nodes that this Node is dependent upon. + * This information is used in sortNodes(), to determine when a node is a root. + */ + public ArrayList<Node> nodeDependencies = null; + + /** + * nodeDepdendents is the list of nodes that have this node as a dependency. This + * is a utility field used in sortNodes to facilitate removing this node as a + * dependency when it is a root node. + */ + public ArrayList<Node> nodeDependents = null; + + /** + * Constructs the Node with the animation that it encapsulates. A Node has no + * dependencies by default; dependencies are added via the addDependency() + * method. + * + * @param animation The animation that the Node encapsulates. + */ + public Node(Animatable animation) { + this.animation = animation; + } + + /** + * Add a dependency to this Node. The dependency includes information about the + * node that this node is dependency upon and the nature of the dependency. + * @param dependency + */ + public void addDependency(Dependency dependency) { + if (dependencies == null) { + dependencies = new ArrayList<Dependency>(); + nodeDependencies = new ArrayList<Node>(); + } + dependencies.add(dependency); + if (!nodeDependencies.contains(dependency.node)) { + nodeDependencies.add(dependency.node); + } + Node dependencyNode = dependency.node; + if (dependencyNode.nodeDependents == null) { + dependencyNode.nodeDependents = new ArrayList<Node>(); + } + dependencyNode.nodeDependents.add(this); + } + } + + /** + * The <code>Builder</code> object is a utility class to facilitate adding animations to a + * <code>Sequencer</code> along with the relationships between the various animations. The + * intention of the <code>Builder</code> methods, along with the {@link + * Sequencer#play(Animatable) play()} method of <code>Sequencer</code> is to make it possible to + * express the dependency relationships of animations in a natural way. Developers can also use + * the {@link Sequencer#playTogether(Animatable...) playTogether()} and {@link + * Sequencer#playSequentially(Animatable...) playSequentially()} methods if these suit the need, + * but it might be easier in some situations to express the sequence of animations in pairs. + * <p/> + * <p>The <code>Builder</code> object cannot be constructed directly, but is rather constructed + * internally via a call to {@link Sequencer#play(Animatable)}.</p> + * <p/> + * <p>For example, this sets up a Sequencer to play anim1 and anim2 at the same time, anim3 to + * play when anim2 finishes, and anim4 to play when anim3 finishes:</p> + * <pre> + * Sequencer s = new Sequencer(); + * s.play(anim1).with(anim2); + * s.play(anim2).before(anim3); + * s.play(anim4).after(anim3); + * </pre> + * <p/> + * <p>Note in the example that both {@link Builder#before(Animatable)} and {@link + * Builder#after(Animatable)} are used. These are just different ways of expressing the same + * relationship and are provided to make it easier to say things in a way that is more natural, + * depending on the situation.</p> + * <p/> + * <p>It is possible to make several calls into the same <code>Builder</code> object to express + * multiple relationships. However, note that it is only the animation passed into the initial + * {@link Sequencer#play(Animatable)} method that is the dependency in any of the successive + * calls to the <code>Builder</code> object. For example, the following code starts both anim2 + * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and + * anim3: + * <pre> + * Sequencer s = new Sequencer(); + * s.play(anim1).before(anim2).before(anim3); + * </pre> + * If the desired result is to play anim1 then anim2 then anim3, this code expresses the + * relationship correctly:</p> + * <pre> + * Sequencer s = new Sequencer(); + * s.play(anim1).before(anim2); + * s.play(anim2).before(anim3); + * </pre> + * <p/> + * <p>Note that it is possible to express relationships that cannot be resolved and will not + * result in sensible results. For example, <code>play(anim1).after(anim1)</code> makes no + * sense. In general, circular dependencies like this one (or more indirect ones where a depends + * on b, which depends on c, which depends on a) should be avoided. Only create sequences that + * can boil down to a simple, one-way relationship of animations starting with, before, and + * after other, different, animations.</p> + */ + public class Builder { + + /** + * This tracks the current node being processed. It is supplied to the play() method + * of Sequencer and passed into the constructor of Builder. + */ + private Node mCurrentNode; + + /** + * package-private constructor. Builders are only constructed by Sequencer, when the + * play() method is called. + * + * @param anim The animation that is the dependency for the other animations passed into + * the other methods of this Builder object. + */ + Builder(Animatable anim) { + mCurrentNode = mNodeMap.get(anim); + if (mCurrentNode == null) { + mCurrentNode = new Node(anim); + mNodeMap.put(anim, mCurrentNode); + mNodes.add(mCurrentNode); + } + } + + /** + * Sets up the given animation to play at the same time as the animation supplied in the + * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object. + * + * @param anim The animation that will play when the animation supplied to the + * {@link Sequencer#play(Animatable)} method starts. + */ + public void with(Animatable anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH); + node.addDependency(dependency); + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object + * ends. + * + * @param anim The animation that will play when the animation supplied to the + * {@link Sequencer#play(Animatable)} method ends. + */ + public void before(Animatable anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER); + node.addDependency(dependency); + } + + /** + * Sets up the given animation to play when the animation supplied in the + * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object + * to start when the animation supplied in this method call ends. + * + * @param anim The animation whose end will cause the animation supplied to the + * {@link Sequencer#play(Animatable)} method to play. + */ + public void after(Animatable anim) { + Node node = mNodeMap.get(anim); + if (node == null) { + node = new Node(anim); + mNodeMap.put(anim, node); + mNodes.add(node); + } + Dependency dependency = new Dependency(node, Dependency.AFTER); + mCurrentNode.addDependency(dependency); + } + + /** + * Sets up the animation supplied in the + * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object + * to play when the given amount of time elapses. + * + * @param delay The number of milliseconds that should elapse before the + * animation starts. + */ + public void after(long delay) { + // setup dummy Animator just to run the clock + Animator anim = new Animator(delay, 0, 1); + Node node = new Node(anim); + mNodes.add(node); + Dependency dependency = new Dependency(node, Dependency.AFTER); + mCurrentNode.addDependency(dependency); + } + + } + +} diff --git a/core/java/android/animation/TypeEvaluator.java b/core/java/android/animation/TypeEvaluator.java new file mode 100644 index 0000000..6150e00 --- /dev/null +++ b/core/java/android/animation/TypeEvaluator.java @@ -0,0 +1,44 @@ +/* + * 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.animation; + +/** + * Interface for use with the {@link Animator#setEvaluator(TypeEvaluator)} function. Evaluators + * allow developers to create animations on arbitrary property types, by allowing them to supply + * custom evaulators for types that are not automatically understood and used by the animation + * system. + * + * @see Animator#setEvaluator(TypeEvaluator) + */ +public interface TypeEvaluator { + + /** + * This function returns the result of linearly interpolating the start and end values, with + * <code>fraction</code> representing the proportion between the start and end values. The + * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>, + * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>, + * and <code>t</code> is <code>fraction</code>. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + public Object evaluate(float fraction, Object startValue, Object endValue); + +}
\ No newline at end of file diff --git a/core/java/android/animation/package.html b/core/java/android/animation/package.html new file mode 100644 index 0000000..1c9bf9d --- /dev/null +++ b/core/java/android/animation/package.html @@ -0,0 +1,5 @@ +<html> +<body> + {@hide} +</body> +</html> diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java new file mode 100644 index 0000000..3cd2b9e --- /dev/null +++ b/core/java/android/app/ActionBar.java @@ -0,0 +1,531 @@ +/* + * 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.app; + +import android.graphics.drawable.Drawable; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.SpinnerAdapter; + +/** + * This is the public interface to the contextual ActionBar. + * The ActionBar acts as a replacement for the title bar in Activities. + * It provides facilities for creating toolbar actions as well as + * methods of navigating around an application. + */ +public abstract class ActionBar { + /** + * Standard navigation mode. Consists of either a logo or icon + * and title text with an optional subtitle. Clicking any of these elements + * will dispatch onActionItemSelected to the registered Callback with + * a MenuItem with item ID android.R.id.home. + */ + public static final int NAVIGATION_MODE_STANDARD = 0; + + /** + * Dropdown list navigation mode. Instead of static title text this mode + * presents a dropdown menu for navigation within the activity. + */ + public static final int NAVIGATION_MODE_DROPDOWN_LIST = 1; + + /** + * Tab navigation mode. Instead of static title text this mode + * presents a series of tabs for navigation within the activity. + */ + public static final int NAVIGATION_MODE_TABS = 2; + + /** + * Custom navigation mode. This navigation mode is set implicitly whenever + * a custom navigation view is set. See {@link #setCustomNavigationMode(View)}. + */ + public static final int NAVIGATION_MODE_CUSTOM = 3; + + /** + * Use logo instead of icon if available. This flag will cause appropriate + * navigation modes to use a wider logo in place of the standard icon. + */ + public static final int DISPLAY_USE_LOGO = 0x1; + + /** + * Hide 'home' elements in this action bar, leaving more space for other + * navigation elements. This includes logo and icon. + */ + public static final int DISPLAY_HIDE_HOME = 0x2; + + /** + * Set the action bar into custom navigation mode, supplying a view + * for custom navigation. + * + * Custom navigation views appear between the application icon and + * any action buttons and may use any space available there. Common + * use cases for custom navigation views might include an auto-suggesting + * address bar for a browser or other navigation mechanisms that do not + * translate well to provided navigation modes. + * + * @param view Custom navigation view to place in the ActionBar. + */ + public abstract void setCustomNavigationMode(View view); + + /** + * Set the action bar into dropdown navigation mode and supply an adapter + * that will provide views for navigation choices. + * + * @param adapter An adapter that will provide views both to display + * the current navigation selection and populate views + * within the dropdown navigation menu. + * @param callback A NavigationCallback that will receive events when the user + * selects a navigation item. + */ + public abstract void setDropdownNavigationMode(SpinnerAdapter adapter, + NavigationCallback callback); + + /** + * Set the action bar into standard navigation mode, supplying a title and subtitle. + * + * Standard navigation mode is default. The title is automatically set to the + * name of your Activity. Subtitles are displayed underneath the title, usually + * in a smaller font or otherwise less prominently than the title. Subtitles are + * good for extended descriptions of activity state. + * + * @param title The action bar's title. null is treated as an empty string. + * @param subtitle The action bar's subtitle. null will remove the subtitle entirely. + */ + public abstract void setStandardNavigationMode(CharSequence title, CharSequence subtitle); + + /** + * Set the action bar into standard navigation mode, supplying a title and subtitle. + * + * Standard navigation mode is default. The title is automatically set to the + * name of your Activity on startup if an action bar is present. + * + * @param title The action bar's title. null is treated as an empty string. + */ + public abstract void setStandardNavigationMode(CharSequence title); + + /** + * Set the action bar into standard navigation mode, using the currently set title + * and/or subtitle. + * + * Standard navigation mode is default. The title is automatically set to the name of + * your Activity on startup if an action bar is present. + */ + public abstract void setStandardNavigationMode(); + + /** + * Set the action bar's title. This will only be displayed in standard navigation mode. + * + * @param title Title to set + */ + public abstract void setTitle(CharSequence title); + + /** + * Set the action bar's subtitle. This will only be displayed in standard navigation mode. + * Set to null to disable the subtitle entirely. + * + * @param subtitle Subtitle to set + */ + public abstract void setSubtitle(CharSequence subtitle); + + /** + * Set display options. This changes all display option bits at once. To change + * a limited subset of display options, see {@link #setDisplayOptions(int, int)}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + */ + public abstract void setDisplayOptions(int options); + + /** + * Set selected display options. Only the options specified by mask will be changed. + * To change all display option bits at once, see {@link #setDisplayOptions(int)}. + * + * <p>Example: setDisplayOptions(0, DISPLAY_HIDE_HOME) will disable the + * {@link #DISPLAY_HIDE_HOME} option. + * setDisplayOptions(DISPLAY_HIDE_HOME, DISPLAY_HIDE_HOME | DISPLAY_USE_LOGO) + * will enable {@link #DISPLAY_HIDE_HOME} and disable {@link #DISPLAY_USE_LOGO}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + * @param mask A bit mask declaring which display options should be changed. + */ + public abstract void setDisplayOptions(int options, int mask); + + /** + * Set the ActionBar's background. + * + * @param d Background drawable + */ + public abstract void setBackgroundDrawable(Drawable d); + + /** + * @return The current custom navigation view. + */ + public abstract View getCustomNavigationView(); + + /** + * Returns the current ActionBar title in standard mode. + * Returns null if {@link #getNavigationMode()} would not return + * {@link #NAVIGATION_MODE_STANDARD}. + * + * @return The current ActionBar title or null. + */ + public abstract CharSequence getTitle(); + + /** + * Returns the current ActionBar subtitle in standard mode. + * Returns null if {@link #getNavigationMode()} would not return + * {@link #NAVIGATION_MODE_STANDARD}. + * + * @return The current ActionBar subtitle or null. + */ + public abstract CharSequence getSubtitle(); + + /** + * Returns the current navigation mode. The result will be one of: + * <ul> + * <li>{@link #NAVIGATION_MODE_STANDARD}</li> + * <li>{@link #NAVIGATION_MODE_DROPDOWN_LIST}</li> + * <li>{@link #NAVIGATION_MODE_TABS}</li> + * <li>{@link #NAVIGATION_MODE_CUSTOM}</li> + * </ul> + * + * @return The current navigation mode. + * + * @see #setStandardNavigationMode() + * @see #setStandardNavigationMode(CharSequence) + * @see #setStandardNavigationMode(CharSequence, CharSequence) + * @see #setDropdownNavigationMode(SpinnerAdapter) + * @see #setTabNavigationMode() + * @see #setCustomNavigationMode(View) + */ + public abstract int getNavigationMode(); + + /** + * @return The current set of display options. + */ + public abstract int getDisplayOptions(); + + /** + * Start a context mode controlled by <code>callback</code>. + * The {@link ContextModeCallback} will receive lifecycle events for the duration + * of the context mode. + * + * @param callback Callback handler that will manage this context mode. + */ + public abstract void startContextMode(ContextModeCallback callback); + + /** + * Finish the current context mode. + */ + public abstract void finishContextMode(); + + /** + * Set the action bar into tabbed navigation mode. + * + * @see #addTab(Tab) + * @see #insertTab(Tab, int) + * @see #removeTab(Tab) + * @see #removeTabAt(int) + */ + public abstract void setTabNavigationMode(); + + /** + * Set the action bar into tabbed navigation mode. + * + * @param containerViewId Id of the container view where tab content fragments should appear. + * + * @see #addTab(Tab) + * @see #insertTab(Tab, int) + * @see #removeTab(Tab) + * @see #removeTabAt(int) + */ + public abstract void setTabNavigationMode(int containerViewId); + + /** + * Create and return a new {@link Tab}. + * This tab will not be included in the action bar until it is added. + * + * @return A new Tab + * + * @see #addTab(Tab) + * @see #insertTab(Tab, int) + */ + public abstract Tab newTab(); + + /** + * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list. + * + * @param tab Tab to add + */ + public abstract void addTab(Tab tab); + + /** + * Insert a tab for use in tabbed navigation mode. The tab will be inserted at + * <code>position</code>. + * + * @param tab The tab to add + * @param position The new position of the tab + */ + public abstract void insertTab(Tab tab, int position); + + /** + * Remove a tab from the action bar. + * + * @param tab The tab to remove + */ + public abstract void removeTab(Tab tab); + + /** + * Remove a tab from the action bar. + * + * @param position Position of the tab to remove + */ + public abstract void removeTabAt(int position); + + /** + * Select the specified tab. If it is not a child of this action bar it will be added. + * + * @param tab Tab to select + */ + public abstract void selectTab(Tab tab); + + /** + * Select the tab at <code>position</code> + * + * @param position Position of the tab to select + */ + public abstract void selectTabAt(int position); + + /** + * Represents a contextual mode of the Action Bar. Context modes can be used for + * modal interactions with activity content and replace the normal Action Bar until finished. + * Examples of good contextual modes include selection modes, search, content editing, etc. + */ + public static abstract class ContextMode { + /** + * Set the title of the context mode. This method will have no visible effect if + * a custom view has been set. + * + * @param title Title string to set + * + * @see #setCustomView(View) + */ + public abstract void setTitle(CharSequence title); + + /** + * Set the subtitle of the context mode. This method will have no visible effect if + * a custom view has been set. + * + * @param subtitle Subtitle string to set + * + * @see #setCustomView(View) + */ + public abstract void setSubtitle(CharSequence subtitle); + + /** + * Set a custom view for this context mode. The custom view will take the place of + * the title and subtitle. Useful for things like search boxes. + * + * @param view Custom view to use in place of the title/subtitle. + * + * @see #setTitle(CharSequence) + * @see #setSubtitle(CharSequence) + */ + public abstract void setCustomView(View view); + + /** + * Invalidate the context mode and refresh menu content. The context mode's + * {@link ContextModeCallback} will have its + * {@link ContextModeCallback#onPrepareContextMode(ContextMode, Menu)} method called. + * If it returns true the menu will be scanned for updated content and any relevant changes + * will be reflected to the user. + */ + public abstract void invalidate(); + + /** + * Finish and close this context mode. The context mode's {@link ContextModeCallback} will + * have its {@link ContextModeCallback#onDestroyContextMode(ContextMode)} method called. + */ + public abstract void finish(); + + /** + * Returns the menu of actions that this context mode presents. + * @return The context mode's menu. + */ + public abstract Menu getMenu(); + + /** + * Returns the current title of this context mode. + * @return Title text + */ + public abstract CharSequence getTitle(); + + /** + * Returns the current subtitle of this context mode. + * @return Subtitle text + */ + public abstract CharSequence getSubtitle(); + + /** + * Returns the current custom view for this context mode. + * @return The current custom view + */ + public abstract View getCustomView(); + } + + /** + * Callback interface for ActionBar context modes. Supplied to + * {@link ActionBar#startContextMode(ContextModeCallback)}, a ContextModeCallback + * configures and handles events raised by a user's interaction with a context mode. + * + * <p>A context mode's lifecycle is as follows: + * <ul> + * <li>{@link ContextModeCallback#onCreateContextMode(ContextMode, Menu)} once on initial + * creation</li> + * <li>{@link ContextModeCallback#onPrepareContextMode(ContextMode, Menu)} after creation + * and any time the {@link ContextMode} is invalidated</li> + * <li>{@link ContextModeCallback#onContextItemClicked(ContextMode, MenuItem)} any time a + * contextual action button is clicked</li> + * <li>{@link ContextModeCallback#onDestroyContextMode(ContextMode)} when the context mode + * is closed</li> + * </ul> + */ + public interface ContextModeCallback { + /** + * Called when a context mode is first created. The menu supplied will be used to generate + * action buttons for the context mode. + * + * @param mode ContextMode being created + * @param menu Menu used to populate contextual action buttons + * @return true if the context mode should be created, false if entering this context mode + * should be aborted. + */ + public boolean onCreateContextMode(ContextMode mode, Menu menu); + + /** + * Called to refresh a context mode's action menu whenever it is invalidated. + * + * @param mode ContextMode being prepared + * @param menu Menu used to populate contextual action buttons + * @return true if the menu or context mode was updated, false otherwise. + */ + public boolean onPrepareContextMode(ContextMode mode, Menu menu); + + /** + * Called to report a user click on a contextual action button. + * + * @param mode The current ContextMode + * @param item The item that was clicked + * @return true if this callback handled the event, false if the standard MenuItem + * invocation should continue. + */ + public boolean onContextItemClicked(ContextMode mode, MenuItem item); + + /** + * Called when a context mode is about to be exited and destroyed. + * + * @param mode The current ContextMode being destroyed + */ + public void onDestroyContextMode(ContextMode mode); + } + + /** + * Callback interface for ActionBar navigation events. + */ + public interface NavigationCallback { + /** + * This method is called whenever a navigation item in your action bar + * is selected. + * + * @param itemPosition Position of the item clicked. + * @param itemId ID of the item clicked. + * @return True if the event was handled, false otherwise. + */ + public boolean onNavigationItemSelected(int itemPosition, long itemId); + } + + /** + * A tab in the action bar. + * + * <p>Tabs manage the hiding and showing of {@link Fragment}s. + */ + public static abstract class Tab { + /** + * An invalid position for a tab. + * + * @see #getPosition() + */ + public static final int INVALID_POSITION = -1; + + /** + * Return the current position of this tab in the action bar. + * + * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in + * the action bar. + */ + public abstract int getPosition(); + + /** + * Return the icon associated with this tab. + * + * @return The tab's icon + */ + public abstract Drawable getIcon(); + + /** + * Return the text of this tab. + * + * @return The tab's text + */ + public abstract CharSequence getText(); + + /** + * Set the icon displayed on this tab. + * + * @param icon The drawable to use as an icon + */ + public abstract void setIcon(Drawable icon); + + /** + * Set the text displayed on this tab. Text may be truncated if there is not + * room to display the entire string. + * + * @param text The text to display + */ + public abstract void setText(CharSequence text); + + /** + * Returns the fragment that will be shown when this tab is selected. + * + * @return Fragment associated with this tab + */ + public abstract Fragment getFragment(); + + /** + * Set the fragment that will be shown when this tab is selected. + * + * @param fragment Fragment to associate with this tab + */ + public abstract void setFragment(Fragment fragment); + + /** + * Select this tab. Only valid if the tab has been added to the action bar. + */ + public abstract void select(); + } +} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index f7ccc12..91e4cd5 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,19 +16,21 @@ package android.app; -import com.android.internal.policy.PolicyManager; +import java.util.ArrayList; +import java.util.HashMap; import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.IIntentSender; +import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -39,6 +41,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Parcelable; import android.os.RemoteException; import android.text.Selection; import android.text.SpannableStringBuilder; @@ -51,6 +54,7 @@ import android.util.Log; import android.util.SparseArray; import android.view.ContextMenu; import android.view.ContextThemeWrapper; +import android.view.InflateException; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -70,8 +74,9 @@ import android.widget.AdapterView; import android.widget.FrameLayout; import android.widget.LinearLayout; -import java.util.ArrayList; -import java.util.HashMap; +import com.android.internal.app.ActionBarImpl; +import com.android.internal.policy.PolicyManager; +import com.android.internal.widget.ActionBarView; /** * An activity is a single, focused thing that the user can do. Almost all @@ -607,6 +612,7 @@ public class Activity extends ContextThemeWrapper private static long sInstanceCount = 0; private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState"; + private static final String FRAGMENTS_TAG = "android:fragments"; private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds"; private static final String SAVED_DIALOGS_TAG = "android:savedDialogs"; private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; @@ -628,18 +634,27 @@ public class Activity extends ContextThemeWrapper private ComponentName mComponent; /*package*/ ActivityInfo mActivityInfo; /*package*/ ActivityThread mMainThread; - /*package*/ Object mLastNonConfigurationInstance; - /*package*/ HashMap<String,Object> mLastNonConfigurationChildInstances; Activity mParent; boolean mCalled; + boolean mStarted; private boolean mResumed; private boolean mStopped; boolean mFinished; boolean mStartedActivity; + /** true if the activity is being destroyed in order to recreate it with a new configuration */ + /*package*/ boolean mChangingConfigurations = false; /*package*/ int mConfigChangeFlags; /*package*/ Configuration mCurrentConfig; private SearchManager mSearchManager; + static final class NonConfigurationInstances { + Object activity; + HashMap<String, Object> children; + ArrayList<Fragment> fragments; + SparseArray<LoaderManager> loaders; + } + /* package */ NonConfigurationInstances mLastNonConfigurationInstances; + private Window mWindow; private WindowManager mWindowManager; @@ -647,10 +662,16 @@ public class Activity extends ContextThemeWrapper /*package*/ boolean mWindowAdded = false; /*package*/ boolean mVisibleFromServer = false; /*package*/ boolean mVisibleFromClient = true; + /*package*/ ActionBar mActionBar = null; private CharSequence mTitle; private int mTitleColor = 0; + final FragmentManager mFragments = new FragmentManager(); + + SparseArray<LoaderManager> mAllLoaderManagers; + LoaderManager mLoaderManager; + private static final class ManagedCursor { ManagedCursor(Cursor cursor) { mCursor = cursor; @@ -677,7 +698,7 @@ public class Activity extends ContextThemeWrapper protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused}; private Thread mUiThread; - private final Handler mHandler = new Handler(); + final Handler mHandler = new Handler(); // Used for debug only /* @@ -748,6 +769,29 @@ public class Activity extends ContextThemeWrapper } /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + mLoaderManager = getLoaderManager(-1, false); + return mLoaderManager; + } + + LoaderManager getLoaderManager(int index, boolean started) { + if (mAllLoaderManagers == null) { + mAllLoaderManagers = new SparseArray<LoaderManager>(); + } + LoaderManager lm = mAllLoaderManagers.get(index); + if (lm == null) { + lm = new LoaderManager(started); + mAllLoaderManagers.put(index, lm); + } + return lm; + } + + /** * Calls {@link android.view.Window#getCurrentFocus} on the * Window of this Activity to return the currently focused view. * @@ -801,6 +845,15 @@ public class Activity extends ContextThemeWrapper protected void onCreate(Bundle savedInstanceState) { mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); + if (mLastNonConfigurationInstances != null) { + mAllLoaderManagers = mLastNonConfigurationInstances.loaders; + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreAllState(p, mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.fragments : null); + } + mFragments.dispatchCreate(); mCalled = true; } @@ -915,6 +968,10 @@ public class Activity extends ContextThemeWrapper mTitleReady = true; onTitleChanged(getTitle(), getTitleColor()); } + if (mWindow != null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) { + // Invalidate the action bar menu so that it can initialize properly. + mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR); + } mCalled = true; } @@ -933,6 +990,10 @@ public class Activity extends ContextThemeWrapper */ protected void onStart() { mCalled = true; + mStarted = true; + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } } /** @@ -1085,6 +1146,10 @@ public class Activity extends ContextThemeWrapper */ protected void onSaveInstanceState(Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); + Parcelable p = mFragments.saveAllState(); + if (p != null) { + outState.putParcelable(FRAGMENTS_TAG, p); + } } /** @@ -1407,7 +1472,8 @@ public class Activity extends ContextThemeWrapper * {@link #onRetainNonConfigurationInstance()}. */ public Object getLastNonConfigurationInstance() { - return mLastNonConfigurationInstance; + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.activity : null; } /** @@ -1463,8 +1529,9 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationChildInstances()} */ - HashMap<String,Object> getLastNonConfigurationChildInstances() { - return mLastNonConfigurationChildInstances; + HashMap<String, Object> getLastNonConfigurationChildInstances() { + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.children : null; } /** @@ -1478,11 +1545,62 @@ public class Activity extends ContextThemeWrapper return null; } + NonConfigurationInstances retainNonConfigurationInstances() { + Object activity = onRetainNonConfigurationInstance(); + HashMap<String, Object> children = onRetainNonConfigurationChildInstances(); + ArrayList<Fragment> fragments = mFragments.retainNonConfig(); + boolean retainLoaders = false; + if (mAllLoaderManagers != null) { + // prune out any loader managers that were already stopped, so + // have nothing useful to retain. + for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { + LoaderManager lm = mAllLoaderManagers.valueAt(i); + if (lm.mRetaining) { + retainLoaders = true; + } else { + mAllLoaderManagers.removeAt(i); + } + } + } + if (activity == null && children == null && fragments == null && !retainLoaders) { + return null; + } + + NonConfigurationInstances nci = new NonConfigurationInstances(); + nci.activity = activity; + nci.children = children; + nci.fragments = fragments; + nci.loaders = mAllLoaderManagers; + return nci; + } + public void onLowMemory() { mCalled = true; } /** + * Start a series of edit operations on the Fragments associated with + * this activity. + */ + public FragmentTransaction openFragmentTransaction() { + return new BackStackEntry(mFragments); + } + + void invalidateFragmentIndex(int index) { + if (mAllLoaderManagers != null) { + mAllLoaderManagers.remove(index); + } + } + + /** + * Called when a Fragment is being attached to this activity, immediately + * after the call to its {@link Fragment#onAttach Fragment.onAttach()} + * method and before {@link Fragment#onCreate Fragment.onCreate()}. + */ + public void onAttachFragment(Fragment fragment) { + } + + /** * Wrapper around * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)} * that gives the resulting {@link Cursor} to call @@ -1544,40 +1662,6 @@ public class Activity extends ContextThemeWrapper } /** - * Wrapper around {@link Cursor#commitUpdates()} that takes care of noting - * that the Cursor needs to be requeried. You can call this method in - * {@link #onPause} or {@link #onStop} to have the system call - * {@link Cursor#requery} for you if the activity is later resumed. This - * allows you to avoid determing when to do the requery yourself (which is - * required for the Cursor to see any data changes that were committed with - * it). - * - * @param c The Cursor whose changes are to be committed. - * - * @see #managedQuery(android.net.Uri , String[], String, String[], String) - * @see #startManagingCursor - * @see Cursor#commitUpdates() - * @see Cursor#requery - * @hide - */ - @Deprecated - public void managedCommitUpdates(Cursor c) { - synchronized (mManagedCursors) { - final int N = mManagedCursors.size(); - for (int i=0; i<N; i++) { - ManagedCursor mc = mManagedCursors.get(i); - if (mc.mCursor == c) { - c.commitUpdates(); - mc.mUpdated = true; - return; - } - } - throw new RuntimeException( - "Cursor " + c + " is not currently managed"); - } - } - - /** * This method allows the activity to take care of managing the given * {@link Cursor}'s lifecycle for you based on the activity's lifecycle. * That is, when the activity is stopped it will automatically call @@ -1655,7 +1739,52 @@ public class Activity extends ContextThemeWrapper public View findViewById(int id) { return getWindow().findViewById(id); } - + + /** + * Retrieve a reference to this activity's ActionBar. + * + * <p><em>Note:</em> The ActionBar is initialized when a content view + * is set. This function will return null if called before {@link #setContentView} + * or {@link #addContentView}. + * @return The Activity's ActionBar, or null if it does not have one. + */ + public ActionBar getActionBar() { + return mActionBar; + } + + /** + * Creates a new ActionBar, locates the inflated ActionBarView, + * initializes the ActionBar with the view, and sets mActionBar. + */ + private void initActionBar() { + Window window = getWindow(); + if (!window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) { + return; + } + + mActionBar = new ActionBarImpl(this); + } + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentById(int id) { + return mFragments.findFragmentById(id); + } + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentByTag(String tag) { + return mFragments.findFragmentByTag(tag); + } + /** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. @@ -1664,6 +1793,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); + initActionBar(); } /** @@ -1675,6 +1805,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view) { getWindow().setContentView(view); + initActionBar(); } /** @@ -1687,6 +1818,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); + initActionBar(); } /** @@ -1698,6 +1830,7 @@ public class Activity extends ContextThemeWrapper */ public void addContentView(View view, ViewGroup.LayoutParams params) { getWindow().addContentView(view, params); + initActionBar(); } /** @@ -1921,12 +2054,25 @@ public class Activity extends ContextThemeWrapper } /** + * Pop the last fragment transition from the local activity's fragment + * back stack. If there is nothing to pop, false is returned. + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to (but not including) that + * state will be popped. If null, only the top state is popped. + */ + public boolean popBackStack(String name) { + return mFragments.popBackStackState(mHandler, name); + } + + /** * Called when the activity has detected the user's press of the back * key. The default implementation simply finishes the current activity, * but you can override this to do whatever you want. */ public void onBackPressed() { - finish(); + if (!popBackStack(null)) { + finish(); + } } /** @@ -2164,7 +2310,9 @@ public class Activity extends ContextThemeWrapper */ public boolean onCreatePanelMenu(int featureId, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL) { - return onCreateOptionsMenu(menu); + boolean show = onCreateOptionsMenu(menu); + show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); + return show; } return false; } @@ -2181,6 +2329,7 @@ public class Activity extends ContextThemeWrapper public boolean onPreparePanel(int featureId, View view, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { boolean goforit = onPrepareOptionsMenu(menu); + goforit |= mFragments.dispatchPrepareOptionsMenu(menu); return goforit && menu.hasVisibleItems(); } return true; @@ -2211,11 +2360,17 @@ public class Activity extends ContextThemeWrapper // doesn't call through to superclass's implmeentation of each // of these methods below EventLog.writeEvent(50000, 0, item.getTitleCondensed()); - return onOptionsItemSelected(item); + if (onOptionsItemSelected(item)) { + return true; + } + return mFragments.dispatchOptionsItemSelected(item); case Window.FEATURE_CONTEXT_MENU: EventLog.writeEvent(50000, 1, item.getTitleCondensed()); - return onContextItemSelected(item); + if (onContextItemSelected(item)) { + return true; + } + return mFragments.dispatchContextItemSelected(item); default: return false; @@ -2234,6 +2389,7 @@ public class Activity extends ContextThemeWrapper public void onPanelClosed(int featureId, Menu menu) { switch (featureId) { case Window.FEATURE_OPTIONS_PANEL: + mFragments.dispatchOptionsMenuClosed(menu); onOptionsMenuClosed(menu); break; @@ -2244,6 +2400,15 @@ public class Activity extends ContextThemeWrapper } /** + * Declare that the options menu has changed, so should be recreated. + * The {@link #onCreateOptionsMenu(Menu)} method will be called the next + * time it needs to be displayed. + */ + public void invalidateOptionsMenu() { + mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); + } + + /** * Initialize the contents of the Activity's standard options menu. You * should place your menu items in to <var>menu</var>. * @@ -3085,6 +3250,36 @@ public class Activity extends ContextThemeWrapper } /** + * This is called when a Fragment in this activity calls its + * {@link Fragment#startActivity} or {@link Fragment#startActivityForResult} + * method. + * + * <p>This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param fragment The fragment making the call. + * @param intent The intent to start. + * @param requestCode Reply request code. < 0 if reply is not requested. + * + * @throws android.content.ActivityNotFoundException + * + * @see Fragment#startActivity + * @see Fragment#startActivityForResult + */ + public void startActivityFromFragment(Fragment fragment, Intent intent, + int requestCode) { + Instrumentation.ActivityResult ar = + mInstrumentation.execStartActivity( + this, mMainThread.getApplicationThread(), mToken, fragment, + intent, requestCode); + if (ar != null) { + mMainThread.sendActivityResult( + mToken, fragment.mWho, requestCode, + ar.getResultCode(), ar.getResultData()); + } + } + + /** * Like {@link #startActivityFromChild(Activity, Intent, int)}, but * taking a IntentSender; see * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)} @@ -3243,6 +3438,19 @@ public class Activity extends ContextThemeWrapper } /** + * Check to see whether this activity is in the process of being destroyed in order to be + * recreated with a new configuration. This is often used in + * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed + * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}. + * + * @return If the activity is being torn down in order to be recreated with a new configuration, + * returns true; else returns false. + */ + public boolean isChangingConfigurations() { + return mChangingConfigurations; + } + + /** * Call this when your activity is done and should be closed. The * ActivityResult is propagated back to whoever launched you via * onActivityResult(). @@ -3343,8 +3551,7 @@ public class Activity extends ContextThemeWrapper * @see #createPendingResult * @see #setResult(int) */ - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) { } /** @@ -3728,15 +3935,69 @@ public class Activity extends ContextThemeWrapper } /** - * Stub implementation of {@link android.view.LayoutInflater.Factory#onCreateView} used when - * inflating with the LayoutInflater returned by {@link #getSystemService}. This - * implementation simply returns null for all view names. + * Standard implementation of + * {@link android.view.LayoutInflater.Factory#onCreateView} used when + * inflating with the LayoutInflater returned by {@link #getSystemService}. + * This implementation handles <fragment> tags to embed fragments inside + * of the activity. * * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */ public View onCreateView(String name, Context context, AttributeSet attrs) { - return null; + if (!"fragment".equals(name)) { + return null; + } + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment); + String fname = a.getString(com.android.internal.R.styleable.Fragment_name); + int id = a.getResourceId(com.android.internal.R.styleable.Fragment_id, 0); + String tag = a.getString(com.android.internal.R.styleable.Fragment_tag); + a.recycle(); + + if (id == 0) { + throw new IllegalArgumentException(attrs.getPositionDescription() + + ": Must specify unique android:id for " + fname); + } + + try { + // If we restored from a previous state, we may already have + // instantiated this fragment from the state and should use + // that instance instead of making a new one. + Fragment fragment = mFragments.findFragmentById(id); + if (FragmentManager.DEBUG) Log.v(TAG, "onCreateView: id=0x" + + Integer.toHexString(id) + " fname=" + fname + + " existing=" + fragment); + if (fragment == null) { + fragment = Fragment.instantiate(this, fname); + fragment.mFromLayout = true; + fragment.mFragmentId = id; + fragment.mTag = tag; + fragment.mImmediateActivity = this; + mFragments.addFragment(fragment, true); + } + // If this fragment is newly instantiated (either right now, or + // from last saved state), then give it the attributes to + // initialize itself. + if (!fragment.mRetaining) { + fragment.onInflate(this, attrs, fragment.mSavedFragmentState); + } + if (fragment.mView == null) { + throw new IllegalStateException("Fragment " + fname + + " did not create a view."); + } + fragment.mView.setId(id); + if (fragment.mView.getTag() == null) { + fragment.mView.setTag(tag); + } + return fragment.mView; + } catch (Exception e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating fragment " + fname); + ie.initCause(e); + throw ie; + } } /** @@ -3787,23 +4048,25 @@ public class Activity extends ContextThemeWrapper final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, - Activity parent, String id, Object lastNonConfigurationInstance, + Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id, - lastNonConfigurationInstance, null, config); + lastNonConfigurationInstances, config); } final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, - Object lastNonConfigurationInstance, - HashMap<String,Object> lastNonConfigurationChildInstances, + NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attachBaseContext(context); + mFragments.attachActivity(this); + mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); + mWindow.getLayoutInflater().setFactory(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } @@ -3820,8 +4083,7 @@ public class Activity extends ContextThemeWrapper mTitle = title; mParent = parent; mEmbeddedID = id; - mLastNonConfigurationInstance = lastNonConfigurationInstance; - mLastNonConfigurationChildInstances = lastNonConfigurationChildInstances; + mLastNonConfigurationInstances = lastNonConfigurationInstances; mWindow.setWindowManager(null, mToken, mComponent.flattenToString()); if (mParent != null) { @@ -3835,14 +4097,26 @@ public class Activity extends ContextThemeWrapper return mParent != null ? mParent.getActivityToken() : mToken; } + final void performCreate(Bundle icicle) { + onCreate(icicle); + mFragments.dispatchActivityCreated(); + } + final void performStart() { mCalled = false; + mFragments.execPendingActions(); mInstrumentation.callActivityOnStart(this); if (!mCalled) { throw new SuperNotCalledException( "Activity " + mComponent.toShortString() + " did not call through to super.onStart()"); } + mFragments.dispatchStart(); + if (mAllLoaderManagers != null) { + for (int i=mAllLoaderManagers.size()-1; i>=0; i--) { + mAllLoaderManagers.valueAt(i).finishRetain(); + } + } } final void performRestart() { @@ -3874,7 +4148,9 @@ public class Activity extends ContextThemeWrapper final void performResume() { performRestart(); - mLastNonConfigurationInstance = null; + mFragments.execPendingActions(); + + mLastNonConfigurationInstances = null; // First call onResume() -before- setting mResumed, so we don't // send out any status bar / menu notifications the client makes. @@ -3889,6 +4165,10 @@ public class Activity extends ContextThemeWrapper // Now really resume, and install the current status bar and menu. mResumed = true; mCalled = false; + + mFragments.dispatchResume(); + mFragments.execPendingActions(); + onPostResume(); if (!mCalled) { throw new SuperNotCalledException( @@ -3898,6 +4178,7 @@ public class Activity extends ContextThemeWrapper } final void performPause() { + mFragments.dispatchPause(); onPause(); } @@ -3907,11 +4188,24 @@ public class Activity extends ContextThemeWrapper } final void performStop() { + if (mStarted) { + mStarted = false; + if (mLoaderManager != null) { + if (!mChangingConfigurations) { + mLoaderManager.doStop(); + } else { + mLoaderManager.doRetain(); + } + } + } + if (!mStopped) { if (mWindow != null) { mWindow.closeAllPanels(); } + mFragments.dispatchStop(); + mCalled = false; mInstrumentation.callActivityOnStop(this); if (!mCalled) { @@ -3936,6 +4230,11 @@ public class Activity extends ContextThemeWrapper mResumed = false; } + final void performDestroy() { + mFragments.dispatchDestroy(); + onDestroy(); + } + final boolean isResumed() { return mResumed; } @@ -3947,6 +4246,11 @@ public class Activity extends ContextThemeWrapper + ", resCode=" + resultCode + ", data=" + data); if (who == null) { onActivityResult(requestCode, resultCode, data); + } else { + Fragment frag = mFragments.findFragmentByWho(who); + if (frag != null) { + frag.onActivityResult(requestCode, resultCode, data); + } } } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 1fe85e6..43a08b5 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -1294,6 +1294,19 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case DUMP_HEAP_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String process = data.readString(); + boolean managed = data.readInt() != 0; + String path = data.readString(); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + boolean res = dumpHeap(process, managed, path, fd); + reply.writeNoException(); + reply.writeInt(res ? 1 : 0); + return true; + } + } return super.onTransact(code, data, reply, flags); @@ -2874,6 +2887,28 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - + + public boolean dumpHeap(String process, boolean managed, + String path, ParcelFileDescriptor fd) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(process); + data.writeInt(managed ? 1 : 0); + data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + mRemote.transact(DUMP_HEAP_TRANSACTION, data, reply, 0); + reply.readException(); + boolean res = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return res; + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 0fb2b49..c800fbe 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.content.res.AssetManager; @@ -195,8 +196,7 @@ public final class ActivityThread { Window window; Activity parent; String embeddedID; - Object lastNonConfigurationInstance; - HashMap<String,Object> lastNonConfigurationChildInstances; + Activity.NonConfigurationInstances lastNonConfigurationInstances; boolean paused; boolean stopped; boolean hideForNow; @@ -357,11 +357,16 @@ public final class ActivityThread { ParcelFileDescriptor fd; } + private static final class DumpHeapData { + String path; + ParcelFileDescriptor fd; + } + private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; private static final String TWO_COUNT_COLUMNS = "%17s %8d %17s %8d"; - private static final String DB_INFO_FORMAT = " %8d %8d %10d %s"; + private static final String DB_INFO_FORMAT = " %4d %6d %8d %14s %s"; // Formatting for checkin service - update version if row format changes private static final int ACTIVITY_THREAD_CHECKIN_VERSION = 1; @@ -624,6 +629,13 @@ public final class ActivityThread { queueOrSendMessage(H.PROFILER_CONTROL, pcd, start ? 1 : 0); } + public void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) { + DumpHeapData dhd = new DumpHeapData(); + dhd.path = path; + dhd.fd = fd; + queueOrSendMessage(H.DUMP_HEAP, dhd, managed ? 1 : 0); + } + public void setSchedulingGroup(int group) { // Note: do this immediately, since going into the foreground // should happen regardless of what pending work we have to do @@ -761,7 +773,7 @@ public final class ActivityThread { for (int i = 0; i < stats.dbStats.size(); i++) { DbStats dbStats = stats.dbStats.get(i); printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize, - dbStats.lookaside, dbStats.dbName); + dbStats.lookaside, dbStats.cache, dbStats.dbName); pw.print(','); } @@ -812,11 +824,12 @@ public final class ActivityThread { int N = stats.dbStats.size(); if (N > 0) { pw.println(" DATABASES"); - printRow(pw, " %8s %8s %10s %s", "Pagesize", "Dbsize", "Lookaside", "Dbname"); + printRow(pw, " %4s %6s %8s %14s %s", "pgsz", "dbsz", "lkaside", "cache", + "Dbname"); for (int i = 0; i < N; i++) { DbStats dbStats = stats.dbStats.get(i); printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize, - dbStats.lookaside, dbStats.dbName); + dbStats.lookaside, dbStats.cache, dbStats.dbName); } } @@ -874,6 +887,7 @@ public final class ActivityThread { public static final int ENABLE_JIT = 132; public static final int DISPATCH_PACKAGE_BROADCAST = 133; public static final int SCHEDULE_CRASH = 134; + public static final int DUMP_HEAP = 135; String codeToString(int code) { if (localLOGV) { switch (code) { @@ -912,6 +926,7 @@ public final class ActivityThread { case ENABLE_JIT: return "ENABLE_JIT"; case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST"; case SCHEDULE_CRASH: return "SCHEDULE_CRASH"; + case DUMP_HEAP: return "DUMP_HEAP"; } } return "(unknown)"; @@ -1037,13 +1052,35 @@ public final class ActivityThread { break; case SCHEDULE_CRASH: throw new RemoteServiceException((String)msg.obj); + case DUMP_HEAP: + handleDumpHeap(msg.arg1 != 0, (DumpHeapData)msg.obj); + break; } } void maybeSnapshot() { if (mBoundApplication != null) { - SamplingProfilerIntegration.writeSnapshot( - mBoundApplication.processName); + // convert the *private* ActivityThread.PackageInfo to *public* known + // android.content.pm.PackageInfo + String packageName = mBoundApplication.info.mPackageName; + android.content.pm.PackageInfo packageInfo = null; + try { + Context context = getSystemContext(); + if(context == null) { + Log.e(TAG, "cannot get a valid context"); + return; + } + PackageManager pm = context.getPackageManager(); + if(pm == null) { + Log.e(TAG, "cannot get a valid PackageManager"); + return; + } + packageInfo = pm.getPackageInfo( + packageName, PackageManager.GET_ACTIVITIES); + } catch (NameNotFoundException e) { + Log.e(TAG, "cannot get package info for " + packageName, e); + } + SamplingProfilerIntegration.writeSnapshot(mBoundApplication.processName, packageInfo); } } } @@ -1434,7 +1471,7 @@ public final class ActivityThread { public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, - Object lastNonConfigurationInstance) { + Activity.NonConfigurationInstances lastNonConfigurationInstances) { ActivityClientRecord r = new ActivityClientRecord(); r.token = token; r.ident = 0; @@ -1443,7 +1480,7 @@ public final class ActivityThread { r.parent = parent; r.embeddedID = id; r.activityInfo = activityInfo; - r.lastNonConfigurationInstance = lastNonConfigurationInstance; + r.lastNonConfigurationInstances = lastNonConfigurationInstances; if (localLOGV) { ComponentName compname = intent.getComponent(); String name; @@ -1565,14 +1602,12 @@ public final class ActivityThread { + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, - r.embeddedID, r.lastNonConfigurationInstance, - r.lastNonConfigurationChildInstances, config); + r.embeddedID, r.lastNonConfigurationInstances, config); if (customIntent != null) { activity.mIntent = customIntent; } - r.lastNonConfigurationInstance = null; - r.lastNonConfigurationChildInstances = null; + r.lastNonConfigurationInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { @@ -2541,6 +2576,9 @@ public final class ActivityThread { if (finishing) { r.activity.mFinished = true; } + if (getNonConfigInstance) { + r.activity.mChangingConfigurations = true; + } if (!r.paused) { try { r.activity.mCalled = false; @@ -2581,8 +2619,8 @@ public final class ActivityThread { } if (getNonConfigInstance) { try { - r.lastNonConfigurationInstance - = r.activity.onRetainNonConfigurationInstance(); + r.lastNonConfigurationInstances + = r.activity.retainNonConfigurationInstances(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -2591,22 +2629,10 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - try { - r.lastNonConfigurationChildInstances - = r.activity.onRetainNonConfigurationChildInstances(); - } catch (Exception e) { - if (!mInstrumentation.onException(r.activity, e)) { - throw new RuntimeException( - "Unable to retain child activities " - + safeToComponentShortString(r.intent) - + ": " + e.toString(), e); - } - } - } try { r.activity.mCalled = false; - r.activity.onDestroy(); + mInstrumentation.callActivityOnDestroy(r.activity); if (!r.activity.mCalled) { throw new SuperNotCalledException( "Activity " + safeToComponentShortString(r.intent) + @@ -3018,6 +3044,25 @@ public final class ActivityThread { } } + final void handleDumpHeap(boolean managed, DumpHeapData dhd) { + if (managed) { + try { + Debug.dumpHprofData(dhd.path, dhd.fd.getFileDescriptor()); + } catch (IOException e) { + Slog.w(TAG, "Managed heap dump failed on path " + dhd.path + + " -- can the process access this path?"); + } finally { + try { + dhd.fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure closing profile fd", e); + } + } + } else { + Debug.dumpNativeHeap(dhd.fd.getFileDescriptor()); + } + } + final void handleDispatchPackageBroadcast(int cmd, String[] packages) { boolean hasPkgInfo = false; if (packages != null) { diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 1c20062..dc2145f 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -403,6 +403,17 @@ public abstract class ApplicationThreadNative extends Binder scheduleCrash(msg); return true; } + + case DUMP_HEAP_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + boolean managed = data.readInt() != 0; + String path = data.readString(); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + dumpHeap(managed, path, fd); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -829,5 +840,22 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + + public void dumpHeap(boolean managed, String path, + ParcelFileDescriptor fd) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeInt(managed ? 1 : 0); + data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + mRemote.transact(DUMP_HEAP_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/app/BackStackEntry.java b/core/java/android/app/BackStackEntry.java new file mode 100644 index 0000000..c958e26 --- /dev/null +++ b/core/java/android/app/BackStackEntry.java @@ -0,0 +1,466 @@ +/* + * 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.app; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.util.ArrayList; + +final class BackStackState implements Parcelable { + final int[] mOps; + final int mTransition; + final int mTransitionStyle; + final String mName; + + public BackStackState(FragmentManager fm, BackStackEntry bse) { + int numRemoved = 0; + BackStackEntry.Op op = bse.mHead; + while (op != null) { + if (op.removed != null) numRemoved += op.removed.size(); + op = op.next; + } + mOps = new int[bse.mNumOp*5 + numRemoved]; + + op = bse.mHead; + int pos = 0; + while (op != null) { + mOps[pos++] = op.cmd; + mOps[pos++] = op.fragment.mIndex; + mOps[pos++] = op.enterAnim; + mOps[pos++] = op.exitAnim; + if (op.removed != null) { + final int N = op.removed.size(); + mOps[pos++] = N; + for (int i=0; i<N; i++) { + mOps[pos++] = op.removed.get(i).mIndex; + } + } else { + mOps[pos++] = 0; + } + op = op.next; + } + mTransition = bse.mTransition; + mTransitionStyle = bse.mTransitionStyle; + mName = bse.mName; + } + + public BackStackState(Parcel in) { + mOps = in.createIntArray(); + mTransition = in.readInt(); + mTransitionStyle = in.readInt(); + mName = in.readString(); + } + + public BackStackEntry instantiate(FragmentManager fm) { + BackStackEntry bse = new BackStackEntry(fm); + int pos = 0; + while (pos < mOps.length) { + BackStackEntry.Op op = new BackStackEntry.Op(); + op.cmd = mOps[pos++]; + Fragment f = fm.mActive.get(mOps[pos++]); + f.mBackStackNesting++; + op.fragment = f; + op.enterAnim = mOps[pos++]; + op.exitAnim = mOps[pos++]; + final int N = mOps[pos++]; + if (N > 0) { + op.removed = new ArrayList<Fragment>(N); + for (int i=0; i<N; i++) { + op.removed.add(fm.mActive.get(mOps[pos++])); + } + } + bse.addOp(op); + } + bse.mTransition = mTransition; + bse.mTransitionStyle = mTransitionStyle; + bse.mName = mName; + return bse; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeIntArray(mOps); + dest.writeInt(mTransition); + dest.writeInt(mTransitionStyle); + dest.writeString(mName); + } + + public static final Parcelable.Creator<BackStackState> CREATOR + = new Parcelable.Creator<BackStackState>() { + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} + +/** + * @hide Entry of an operation on the fragment back stack. + */ +final class BackStackEntry implements FragmentTransaction, Runnable { + static final String TAG = "BackStackEntry"; + + final FragmentManager mManager; + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REPLACE = 2; + static final int OP_REMOVE = 3; + static final int OP_HIDE = 4; + static final int OP_SHOW = 5; + + static final class Op { + Op next; + Op prev; + int cmd; + Fragment fragment; + int enterAnim; + int exitAnim; + ArrayList<Fragment> removed; + } + + Op mHead; + Op mTail; + int mNumOp; + int mEnterAnim; + int mExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + String mName; + boolean mCommitted; + + public BackStackEntry(FragmentManager manager) { + mManager = manager; + } + + void addOp(Op op) { + if (mHead == null) { + mHead = mTail = op; + } else { + op.prev = mTail; + mTail.next = op; + mTail = op; + } + op.enterAnim = mEnterAnim; + op.exitAnim = mExitAnim; + mNumOp++; + } + + public FragmentTransaction add(Fragment fragment, String tag) { + doAddOp(0, fragment, tag, OP_ADD); + return this; + } + + public FragmentTransaction add(int containerViewId, Fragment fragment) { + doAddOp(containerViewId, fragment, null, OP_ADD); + return this; + } + + public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { + doAddOp(containerViewId, fragment, tag, OP_ADD); + return this; + } + + private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) { + if (fragment.mImmediateActivity != null) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + fragment.mImmediateActivity = mManager.mActivity; + + if (tag != null) { + if (fragment.mTag != null && !tag.equals(fragment.mTag)) { + throw new IllegalStateException("Can't change tag of fragment " + + fragment + ": was " + fragment.mTag + + " now " + tag); + } + fragment.mTag = tag; + } + + if (containerViewId != 0) { + if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { + throw new IllegalStateException("Can't change container ID of fragment " + + fragment + ": was " + fragment.mFragmentId + + " now " + containerViewId); + } + fragment.mContainerId = fragment.mFragmentId = containerViewId; + } + + Op op = new Op(); + op.cmd = opcmd; + op.fragment = fragment; + addOp(op); + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment) { + return replace(containerViewId, fragment, null); + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) { + if (containerViewId == 0) { + throw new IllegalArgumentException("Must use non-zero containerViewId"); + } + + doAddOp(containerViewId, fragment, tag, OP_REPLACE); + return this; + } + + public FragmentTransaction remove(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + fragment.mImmediateActivity = null; + + Op op = new Op(); + op.cmd = OP_REMOVE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction hide(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_HIDE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction show(Fragment fragment) { + if (fragment.mImmediateActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_SHOW; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction setCustomAnimations(int enter, int exit) { + mEnterAnim = enter; + mExitAnim = exit; + return this; + } + + public FragmentTransaction setTransition(int transition) { + mTransition = transition; + return this; + } + + public FragmentTransaction setTransitionStyle(int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + public FragmentTransaction addToBackStack(String name) { + mAddToBackStack = true; + mName = name; + return this; + } + + public void commit() { + if (mCommitted) throw new IllegalStateException("commit already called"); + if (FragmentManager.DEBUG) Log.v(TAG, "Commit: " + this); + mCommitted = true; + mManager.enqueueAction(this); + } + + public void run() { + if (FragmentManager.DEBUG) Log.v(TAG, "Run: " + this); + + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i=0; i<mManager.mAdded.size(); i++) { + Fragment old = mManager.mAdded.get(i); + if (FragmentManager.DEBUG) Log.v(TAG, + "OP_REPLACE: adding=" + f + " old=" + old); + if (old.mContainerId == f.mContainerId) { + if (op.removed == null) { + op.removed = new ArrayList<Fragment>(); + } + op.removed.add(old); + if (mAddToBackStack) { + old.mBackStackNesting++; + } + old.mNextAnim = op.exitAnim; + mManager.removeFragment(old, mTransition, mTransitionStyle); + } + } + } + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REMOVE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.exitAnim; + mManager.removeFragment(f, mTransition, mTransitionStyle); + } break; + case OP_HIDE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.exitAnim; + mManager.hideFragment(f, mTransition, mTransitionStyle); + } break; + case OP_SHOW: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.enterAnim; + mManager.showFragment(f, mTransition, mTransitionStyle); + } break; + default: { + throw new IllegalArgumentException("Unknown cmd: " + op.cmd); + } + } + + op = op.next; + } + + mManager.moveToState(mManager.mCurState, mTransition, + mTransitionStyle, true); + if (mManager.mNeedMenuInvalidate && mManager.mActivity != null) { + mManager.mActivity.invalidateOptionsMenu(); + mManager.mNeedMenuInvalidate = false; + } + + if (mAddToBackStack) { + mManager.addBackStackState(this); + } + } + + public void popFromBackStack() { + if (FragmentManager.DEBUG) Log.v(TAG, "popFromBackStack: " + this); + + Op op = mTail; + while (op != null) { + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.removeFragment(f, + FragmentManager.reverseTransit(mTransition), + mTransitionStyle); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.removeFragment(f, + FragmentManager.reverseTransit(mTransition), + mTransitionStyle); + if (op.removed != null) { + for (int i=0; i<op.removed.size(); i++) { + Fragment old = op.removed.get(i); + if (mAddToBackStack) { + old.mBackStackNesting--; + } + mManager.addFragment(old, false); + } + } + } break; + case OP_REMOVE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.addFragment(f, false); + } break; + case OP_HIDE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.showFragment(f, + FragmentManager.reverseTransit(mTransition), mTransitionStyle); + } break; + case OP_SHOW: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.hideFragment(f, + FragmentManager.reverseTransit(mTransition), mTransitionStyle); + } break; + default: { + throw new IllegalArgumentException("Unknown cmd: " + op.cmd); + } + } + + op = op.prev; + } + + mManager.moveToState(mManager.mCurState, + FragmentManager.reverseTransit(mTransition), mTransitionStyle, true); + if (mManager.mNeedMenuInvalidate && mManager.mActivity != null) { + mManager.mActivity.invalidateOptionsMenu(); + mManager.mNeedMenuInvalidate = false; + } + } + + public String getName() { + return mName; + } + + public int getTransition() { + return mTransition; + } + + public int getTransitionStyle() { + return mTransitionStyle; + } +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 9deaa31..a2a74f8 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -56,6 +56,7 @@ import android.content.pm.ServiceInfo; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; @@ -542,6 +543,15 @@ class ContextImpl extends Context { } @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + File f = validateFilePath(name, true); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f.getPath(), factory, errorHandler); + setFilePermissionsFromMode(f.getPath(), mode, 0); + return db; + } + + @Override public boolean deleteDatabase(String name) { try { File f = validateFilePath(name, false); @@ -619,7 +629,8 @@ class ContextImpl extends Context { + " Is this really what you want?"); } mMainThread.getInstrumentation().execStartActivity( - getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1); + getOuterContext(), mMainThread.getApplicationThread(), null, + (Activity)null, intent, -1); } @Override @@ -2757,6 +2768,13 @@ class ContextImpl extends Context { return v != null ? v : defValue; } } + + public Set<String> getStringSet(String key, Set<String> defValues) { + synchronized (this) { + Set<String> v = (Set<String>) mMap.get(key); + return v != null ? v : defValues; + } + } public int getInt(String key, int defValue) { synchronized (this) { @@ -2799,6 +2817,12 @@ class ContextImpl extends Context { return this; } } + public Editor putStringSet(String key, Set<String> values) { + synchronized (this) { + mModified.put(key, values); + return this; + } + } public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java new file mode 100644 index 0000000..51cce5e --- /dev/null +++ b/core/java/android/app/Fragment.java @@ -0,0 +1,770 @@ +/* + * 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.app; + +import android.content.ComponentCallbacks; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; +import android.view.animation.Animation; +import android.widget.AdapterView; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; + +final class FragmentState implements Parcelable { + static final String VIEW_STATE_TAG = "android:view_state"; + + final String mClassName; + final int mIndex; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + public FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mIndex = frag.mIndex; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + } + + public FragmentState(Parcel in) { + mClassName = in.readString(); + mIndex = in.readInt(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mSavedFragmentState = in.readBundle(); + } + + public Fragment instantiate(Activity activity) { + if (mInstance != null) { + return mInstance; + } + + try { + mInstance = Fragment.instantiate(activity, mClassName); + } catch (Exception e) { + throw new RuntimeException("Unable to restore fragment " + mClassName, e); + } + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(activity.getClassLoader()); + mInstance.mSavedFragmentState = mSavedFragmentState; + mInstance.mSavedViewState + = mSavedFragmentState.getSparseParcelableArray(VIEW_STATE_TAG); + } + mInstance.setIndex(mIndex); + mInstance.mFromLayout = mFromLayout; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + + return mInstance; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeInt(mIndex); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeBundle(mSavedFragmentState); + } + + public static final Parcelable.Creator<FragmentState> CREATOR + = new Parcelable.Creator<FragmentState>() { + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} + +/** + * A Fragment is a piece of an application's user interface or behavior + * that can be placed in an {@link Activity}. + */ +public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener { + private static final HashMap<String, Class<?>> sClassMap = + new HashMap<String, Class<?>>(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. + static final int STARTED = 3; // Created and started, not resumed. + static final int RESUMED = 4; // Created started and resumed. + + int mState = INITIALIZING; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray<Parcelable> mSavedViewState; + + // Index into active fragment array. + int mIndex = -1; + + // Internal unique name for this fragment; + String mWho; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // True if the fragment is in the resumed state. + boolean mResumed; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // Set as soon as a fragment is added to a transaction (or removed), + // to be able to do validation. + Activity mImmediateActivity; + + // Activity this fragment is attached to. + Activity mActivity; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment is being retained across the current config change. + boolean mRetaining; + + // If set this fragment has menu items to contribute. + boolean mHasMenu; + + // Used to verify that subclasses call through to super class. + boolean mCalled; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + LoaderManager mLoaderManager; + boolean mStarted; + + /** + * Default constructor. <strong>Every</string> fragment must have an + * empty constructor, so it can be instantiated when restoring its + * activity's state. It is strongly recommended that subclasses do not + * have other constructors with parameters, since these constructors + * will not be called when the fragment is re-instantiated; instead, + * retrieve such parameters from the activity in {@link #onAttach(Activity)}. + */ + public Fragment() { + } + + static Fragment instantiate(Activity activity, String fname) + throws NoSuchMethodException, ClassNotFoundException, + IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException { + Class<?> clazz = sClassMap.get(fname); + + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = activity.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + return (Fragment)clazz.newInstance(); + } + + void restoreViewState() { + if (mSavedViewState != null) { + mView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + } + + void setIndex(int index) { + mIndex = index; + mWho = "android:fragment:" + mIndex; + } + + void clearIndex() { + mIndex = -1; + mWho = null; + } + + /** + * Subclasses can not override equals(). + */ + @Override final public boolean equals(Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override final public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Fragment{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + final public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + final public String getTag() { + return mTag; + } + + /** + * Return the Activity this fragment is currently associated with. + */ + final public Activity getActivity() { + return mActivity; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + final public boolean isAdded() { + return mActivity != null && mActivity.mFragments.mAdded.contains(this); + } + + /** + * Return true if the fragment is in the resumed state. This is true + * for the duration of {@link #onResume()} and {@link #onPause()} as well. + */ + final public boolean isResumed() { + return mResumed; + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + final public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + final public boolean isHidden() { + return mHidden; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false if it is not + * visible. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). This can only + * be used with fragments not in the back stack. If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + * <ul> + * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + * <li> {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + * <li> {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} <b>will</b> + * still be called. + * </ul> + */ + public void setRetainInstance(boolean retain) { + mRetainInstance = retain; + } + + final public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Report that this fragment would like to participate in populating + * the options menu by receiving a call to {@link #onCreateOptionsMenu} + * and related methods. + * + * @param hasMenu If true, the fragment has menu items to contribute. + */ + public void setHasOptionsMenu(boolean hasMenu) { + if (mHasMenu != hasMenu) { + mHasMenu = hasMenu; + if (isAdded() && !isHidden()) { + mActivity.invalidateOptionsMenu(); + } + } + } + + /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted); + return mLoaderManager; + } + + /** + * Call {@link Activity#startActivity(Intent)} on the fragment's + * containing Activity. + */ + public void startActivity(Intent intent) { + mActivity.startActivityFromFragment(this, intent, -1); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's + * containing Activity. + */ + public void startActivityForResult(Intent intent, int requestCode) { + mActivity.startActivityFromFragment(this, intent, requestCode); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * will be called both the first time the fragment is created, as well + * later when it is being re-created from its saved state (which is also + * given here). + * + * XXX This is kind-of yucky... maybe we could just supply the + * AttributeSet to onCreate()? + * + * @param activity The Activity that is inflating the fragment. + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onInflate(Activity activity, AttributeSet attrs, + Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + */ + public void onAttach(Activity activity) { + mCalled = true; + } + + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + * <p>Note that this can be called while the fragment's activity is + * still in the process of being created. As such, you can not rely + * on things like the activity's content view hierarchy being initialized + * at this point. If you want to do work once the activity itself is + * created, see {@link #onActivityCreated(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onCreate(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. + * + * <p>If you return a View from here, you will later be called in + * {@link #onDestroyView} when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return null; + } + + public View getView() { + return mView; + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onActivityCreated(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + public void onStart() { + mCalled = true; + mStarted = true; + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + public void onResume() { + mCalled = true; + } + + public void onSaveInstanceState(Bundle outState) { + } + + public void onConfigurationChanged(Configuration newConfig) { + mCalled = true; + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + public void onStop() { + mCalled = true; + } + + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the view previously created by {@link #onCreateView} has + * been detached from the fragment. The next time the fragment needs + * to be displayed, a new view will be created. This is called + * after {@link #onStop()} and before {@link #onDestroy()}; it is only + * called if {@link #onCreateView} returns a non-null View. + */ + public void onDestroyView() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + public void onDestroy() { + mCalled = true; + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + public void onDetach() { + mCalled = true; + } + + /** + * Initialize the contents of the Activity's standard options menu. You + * should place your menu items in to <var>menu</var>. For this method + * to be called, you must have first called {@link #setHasOptionsMenu}. See + * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu} + * for more information. + * + * @param menu The options menu in which you place your items. + * + * @see #setHasOptionsMenu + * @see #onPrepareOptionsMenu + * @see #onOptionsItemSelected + */ + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + } + + /** + * Prepare the Screen's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. See + * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu} + * for more information. + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + * + * @see #setHasOptionsMenu + * @see #onCreateOptionsMenu + */ + public void onPrepareOptionsMenu(Menu menu) { + } + + /** + * This hook is called whenever an item in your options menu is selected. + * The default implementation simply returns false to have the normal + * processing happen (calling the item's Runnable or sending a message to + * its Handler as appropriate). You can use this method for any items + * for which you would like to do processing without those other + * facilities. + * + * <p>Derived classes should call through to the base class for it to + * perform the default menu handling. + * + * @param item The menu item that was selected. + * + * @return boolean Return false to allow normal menu processing to + * proceed, true to consume it here. + * + * @see #onCreateOptionsMenu + */ + public boolean onOptionsItemSelected(MenuItem item) { + return false; + } + + /** + * This hook is called whenever the options menu is being closed (either by the user canceling + * the menu with the back/menu button, or when an item is selected). + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + */ + public void onOptionsMenuClosed(Menu menu) { + } + + /** + * Called when a context menu for the {@code view} is about to be shown. + * Unlike {@link #onCreateOptionsMenu}, this will be called every + * time the context menu is about to be shown and should be populated for + * the view (or item inside the view for {@link AdapterView} subclasses, + * this can be found in the {@code menuInfo})). + * <p> + * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an + * item has been selected. + * <p> + * The default implementation calls up to + * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though + * you can not call this implementation if you don't want that behavior. + * <p> + * It is not safe to hold onto the context menu after this method returns. + * {@inheritDoc} + */ + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + getActivity().onCreateContextMenu(menu, v, menuInfo); + } + + /** + * Registers a context menu to be shown for the given view (multiple views + * can show the context menu). This method will set the + * {@link OnCreateContextMenuListener} on the view to this fragment, so + * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be + * called when it is time to show the context menu. + * + * @see #unregisterForContextMenu(View) + * @param view The view that should show a context menu. + */ + public void registerForContextMenu(View view) { + view.setOnCreateContextMenuListener(this); + } + + /** + * Prevents a context menu to be shown for the given view. This method will + * remove the {@link OnCreateContextMenuListener} on the view. + * + * @see #registerForContextMenu(View) + * @param view The view that should stop showing a context menu. + */ + public void unregisterForContextMenu(View view) { + view.setOnCreateContextMenuListener(null); + } + + /** + * This hook is called whenever an item in a context menu is selected. The + * default implementation simply returns false to have the normal processing + * happen (calling the item's Runnable or sending a message to its Handler + * as appropriate). You can use this method for any items for which you + * would like to do processing without those other facilities. + * <p> + * Use {@link MenuItem#getMenuInfo()} to get extra information set by the + * View that added this menu item. + * <p> + * Derived classes should call through to the base class for it to perform + * the default menu handling. + * + * @param item The context menu item that was selected. + * @return boolean Return false to allow normal context menu processing to + * proceed, true to consume it here. + */ + public boolean onContextItemSelected(MenuItem item) { + return false; + } + + void performStop() { + onStop(); + if (mStarted) { + mStarted = false; + if (mLoaderManager != null) { + if (mActivity == null || !mActivity.mChangingConfigurations) { + mLoaderManager.doStop(); + } else { + mLoaderManager.doRetain(); + } + } + } + } +} diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java new file mode 100644 index 0000000..4f3043c --- /dev/null +++ b/core/java/android/app/FragmentManager.java @@ -0,0 +1,1000 @@ +/* + * 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.app; + +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import java.util.ArrayList; + +final class FragmentManagerState implements Parcelable { + FragmentState[] mActive; + int[] mAdded; + BackStackState[] mBackStack; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArray(FragmentState.CREATOR); + mAdded = in.createIntArray(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedArray(mActive, flags); + dest.writeIntArray(mAdded); + dest.writeTypedArray(mBackStack, flags); + } + + public static final Parcelable.Creator<FragmentManagerState> CREATOR + = new Parcelable.Creator<FragmentManagerState>() { + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} + +/** + * @hide + * Container for fragments associated with an activity. + */ +public class FragmentManager { + static final boolean DEBUG = true; + static final String TAG = "FragmentManager"; + + ArrayList<Runnable> mPendingActions; + Runnable[] mTmpActions; + boolean mExecutingActions; + + ArrayList<Fragment> mActive; + ArrayList<Fragment> mAdded; + ArrayList<Integer> mAvailIndices; + ArrayList<BackStackEntry> mBackStack; + + int mCurState = Fragment.INITIALIZING; + Activity mActivity; + + boolean mNeedMenuInvalidate; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray<Parcelable> mStateArray = null; + + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + + Animation loadAnimation(Fragment fragment, int transit, boolean enter, + int transitionStyle) { + Animation animObj = fragment.onCreateAnimation(transitionStyle, enter, + fragment.mNextAnim); + if (animObj != null) { + return animObj; + } + + if (fragment.mNextAnim != 0) { + Animation anim = AnimationUtils.loadAnimation(mActivity, fragment.mNextAnim); + if (anim != null) { + return anim; + } + } + + if (transit == 0) { + return null; + } + + int styleIndex = transitToStyleIndex(transit, enter); + if (styleIndex < 0) { + return null; + } + + if (transitionStyle == 0 && mActivity.getWindow() != null) { + transitionStyle = mActivity.getWindow().getAttributes().windowAnimations; + } + if (transitionStyle == 0) { + return null; + } + + TypedArray attrs = mActivity.obtainStyledAttributes(transitionStyle, + com.android.internal.R.styleable.WindowAnimation); + int anim = attrs.getResourceId(styleIndex, 0); + attrs.recycle(); + + if (anim == 0) { + return null; + } + + return AnimationUtils.loadAnimation(mActivity, anim); + } + + void moveToState(Fragment f, int newState, int transit, int transitionStyle) { + // Fragments that are not currently added will sit in the onCreate() state. + if (!f.mAdded && newState > Fragment.CREATED) { + newState = Fragment.CREATED; + } + + if (f.mState < newState) { + switch (f.mState) { + case Fragment.INITIALIZING: + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + f.mActivity = mActivity; + f.mCalled = false; + f.onAttach(mActivity); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onAttach()"); + } + mActivity.onAttachFragment(f); + + if (!f.mRetaining) { + f.mCalled = false; + f.onCreate(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onCreate()"); + } + } + f.mRetaining = false; + if (f.mFromLayout) { + // For fragments that are part of the content view + // layout, we need to instantiate the view immediately + // and the inflater will take care of adding it. + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + null, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + f.restoreViewState(); + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + case Fragment.CREATED: + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto CONTENT: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + container = (ViewGroup)mActivity.findViewById(f.mContainerId); + if (container == null) { + throw new IllegalArgumentException("New view found for id 0x" + + Integer.toHexString(f.mContainerId) + + " for fragment " + f); + } + } + f.mContainer = container; + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + container, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + if (container != null) { + Animation anim = loadAnimation(f, transit, true, + transitionStyle); + if (anim != null) { + f.mView.setAnimation(anim); + } + container.addView(f.mView); + f.restoreViewState(); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + + f.mCalled = false; + f.onActivityCreated(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onReady()"); + } + f.mSavedFragmentState = null; + } + case Fragment.ACTIVITY_CREATED: + if (newState > Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.mCalled = false; + f.onStart(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStart()"); + } + } + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.mCalled = false; + f.mResumed = true; + f.onResume(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onResume()"); + } + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.mCalled = false; + f.onPause(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onPause()"); + } + f.mResumed = false; + } + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.mCalled = false; + f.performStop(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStop()"); + } + } + case Fragment.ACTIVITY_CREATED: + if (newState < Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "movefrom CONTENT: " + f); + if (f.mView != null) { + f.mCalled = false; + f.onDestroyView(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDestroyedView()"); + } + // Need to save the current view state if not + // done already. + if (!mActivity.isFinishing() && f.mSavedFragmentState == null) { + saveFragmentViewState(f); + } + if (f.mContainer != null) { + if (mCurState > Fragment.INITIALIZING) { + Animation anim = loadAnimation(f, transit, false, + transitionStyle); + if (anim != null) { + f.mView.setAnimation(anim); + } + } + f.mContainer.removeView(f.mView); + } + } + f.mContainer = null; + f.mView = null; + } + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + if (!f.mRetaining) { + f.mCalled = false; + f.onDestroy(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDestroy()"); + } + } + + f.mCalled = false; + f.onDetach(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDetach()"); + } + f.mActivity = null; + } + } + } + + f.mState = newState; + } + + void moveToState(int newState, boolean always) { + moveToState(newState, 0, 0, always); + } + + void moveToState(int newState, int transit, int transitStyle, boolean always) { + if (mActivity == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && mCurState == newState) { + return; + } + + mCurState = newState; + if (mActive != null) { + for (int i=0; i<mActive.size(); i++) { + Fragment f = mActive.get(i); + if (f != null) { + moveToState(f, newState, transit, transitStyle); + } + } + } + } + + void makeActive(Fragment f) { + if (f.mIndex >= 0) { + return; + } + + if (mAvailIndices == null || mAvailIndices.size() <= 0) { + if (mActive == null) { + mActive = new ArrayList<Fragment>(); + } + f.setIndex(mActive.size()); + mActive.add(f); + + } else { + f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1)); + mActive.set(f.mIndex, f); + } + } + + void makeInactive(Fragment f) { + if (f.mIndex < 0) { + return; + } + + mActive.set(f.mIndex, null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList<Integer>(); + } + mAvailIndices.add(f.mIndex); + mActivity.invalidateFragmentIndex(f.mIndex); + f.clearIndex(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (DEBUG) Log.v(TAG, "add: " + fragment); + if (mAdded == null) { + mAdded = new ArrayList<Fragment>(); + } + mAdded.add(fragment); + makeActive(fragment); + fragment.mAdded = true; + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment, mCurState, 0, 0); + } + } + + public void removeFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "remove: " + fragment); + mAdded.remove(fragment); + final boolean inactive = fragment.mBackStackNesting <= 0; + if (inactive) { + makeInactive(fragment); + } + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle); + } + + public void hideFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, false, + transitionStyle); + if (anim != null) { + fragment.mView.setAnimation(anim); + } + fragment.mView.setVisibility(View.GONE); + } + if (fragment.mAdded && fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(true); + } + } + + public void showFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, true, + transitionStyle); + if (anim != null) { + fragment.mView.setAnimation(anim); + } + fragment.mView.setVisibility(View.VISIBLE); + } + if (fragment.mAdded && fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(false); + } + } + + public Fragment findFragmentById(int id) { + if (mActive != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByTag(String tag) { + if (mActive != null && tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(String who) { + if (mActive != null && who != null) { + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && who.equals(f.mWho)) { + return f; + } + } + } + return null; + } + + public void enqueueAction(Runnable action) { + synchronized (this) { + if (mPendingActions == null) { + mPendingActions = new ArrayList<Runnable>(); + } + mPendingActions.add(action); + if (mPendingActions.size() == 1) { + mActivity.mHandler.removeCallbacks(mExecCommit); + mActivity.mHandler.post(mExecCommit); + } + } + } + + /** + * Only call from main thread! + */ + public void execPendingActions() { + if (mExecutingActions) { + throw new IllegalStateException("Recursive entry to execPendingActions"); + } + + while (true) { + int numActions; + + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + return; + } + + numActions = mPendingActions.size(); + if (mTmpActions == null || mTmpActions.length < numActions) { + mTmpActions = new Runnable[numActions]; + } + mPendingActions.toArray(mTmpActions); + mPendingActions.clear(); + mActivity.mHandler.removeCallbacks(mExecCommit); + } + + mExecutingActions = true; + for (int i=0; i<numActions; i++) { + mTmpActions[i].run(); + } + mExecutingActions = false; + } + } + + public void addBackStackState(BackStackEntry state) { + if (mBackStack == null) { + mBackStack = new ArrayList<BackStackEntry>(); + } + mBackStack.add(state); + } + + public boolean popBackStackState(Handler handler, String name) { + if (mBackStack == null) { + return false; + } + if (name == null) { + int last = mBackStack.size()-1; + if (last < 0) { + return false; + } + final BackStackEntry bss = mBackStack.remove(last); + enqueueAction(new Runnable() { + public void run() { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + bss); + bss.popFromBackStack(); + moveToState(mCurState, reverseTransit(bss.getTransition()), + bss.getTransitionStyle(), true); + } + }); + } else { + int index = mBackStack.size()-1; + while (index >= 0) { + BackStackEntry bss = mBackStack.get(index); + if (name.equals(bss.getName())) { + break; + } + } + if (index < 0 || index == mBackStack.size()-1) { + return false; + } + final ArrayList<BackStackEntry> states + = new ArrayList<BackStackEntry>(); + for (int i=mBackStack.size()-1; i>index; i--) { + states.add(mBackStack.remove(i)); + } + enqueueAction(new Runnable() { + public void run() { + for (int i=0; i<states.size(); i++) { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i)); + states.get(i).popFromBackStack(); + } + moveToState(mCurState, true); + } + }); + } + return true; + } + + ArrayList<Fragment> retainNonConfig() { + ArrayList<Fragment> fragments = null; + if (mActive != null) { + for (int i=0; i<mActive.size(); i++) { + Fragment f = mActive.get(i); + if (f != null && f.mRetainInstance) { + if (fragments == null) { + fragments = new ArrayList<Fragment>(); + } + fragments.add(f); + f.mRetaining = true; + } + } + } + return fragments; + } + + void saveFragmentViewState(Fragment f) { + if (f.mView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray<Parcelable>(); + } + f.mView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Parcelable saveAllState() { + if (mActive == null || mActive.size() <= 0) { + return null; + } + + // First collect all active fragments. + int N = mActive.size(); + FragmentState[] active = new FragmentState[N]; + boolean haveFragments = false; + for (int i=0; i<N; i++) { + Fragment f = mActive.get(i); + if (f != null) { + haveFragments = true; + + FragmentState fs = new FragmentState(f); + active[i] = fs; + + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.onSaveInstanceState(mStateBundle); + if (!mStateBundle.isEmpty()) { + fs.mSavedFragmentState = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + if (f.mSavedViewState != null) { + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + fs.mSavedFragmentState.putSparseParcelableArray( + FragmentState.VIEW_STATE_TAG, f.mSavedViewState); + } + } + + } + } + + if (!haveFragments) { + return null; + } + + int[] added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + N = mAdded.size(); + if (N > 0) { + added = new int[N]; + for (int i=0; i<N; i++) { + added[i] = mAdded.get(i).mIndex; + } + } + + // Now save back stack. + if (mBackStack != null) { + N = mBackStack.size(); + if (N > 0) { + backStack = new BackStackState[N]; + for (int i=0; i<N; i++) { + backStack[i] = new BackStackState(this, mBackStack.get(i)); + } + } + } + + FragmentManagerState fms = new FragmentManagerState(); + fms.mActive = active; + fms.mAdded = added; + fms.mBackStack = backStack; + return fms; + } + + void restoreAllState(Parcelable state, ArrayList<Fragment> nonConfig) { + // If there is no saved state at all, then there can not be + // any nonConfig fragments either, so that is that. + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + if (nonConfig != null) { + for (int i=0; i<nonConfig.size(); i++) { + Fragment f = nonConfig.get(i); + FragmentState fs = fms.mActive[f.mIndex]; + fs.mInstance = f; + f.mSavedViewState = null; + f.mBackStackNesting = 0; + f.mAdded = false; + if (fs.mSavedFragmentState != null) { + f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray( + FragmentState.VIEW_STATE_TAG); + } + } + } + + // Build the full list of active fragments, instantiating them from + // their saved state. + mActive = new ArrayList<Fragment>(fms.mActive.length); + if (mAvailIndices != null) { + mAvailIndices.clear(); + } + for (int i=0; i<fms.mActive.length; i++) { + FragmentState fs = fms.mActive[i]; + if (fs != null) { + mActive.add(fs.instantiate(mActivity)); + } else { + mActive.add(null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList<Integer>(); + } + mAvailIndices.add(i); + } + } + + // Build the list of currently added fragments. + if (fms.mAdded != null) { + mAdded = new ArrayList<Fragment>(fms.mAdded.length); + for (int i=0; i<fms.mAdded.length; i++) { + Fragment f = mActive.get(fms.mAdded[i]); + if (f == null) { + throw new IllegalStateException( + "No instantiated fragment for index #" + fms.mAdded[i]); + } + f.mAdded = true; + f.mImmediateActivity = mActivity; + mAdded.add(f); + } + } else { + mAdded = null; + } + + // Build the back stack. + if (fms.mBackStack != null) { + mBackStack = new ArrayList<BackStackEntry>(fms.mBackStack.length); + for (int i=0; i<fms.mBackStack.length; i++) { + BackStackEntry bse = fms.mBackStack[i].instantiate(this); + mBackStack.add(bse); + } + } else { + mBackStack = null; + } + } + + public void attachActivity(Activity activity) { + if (mActivity != null) throw new IllegalStateException(); + mActivity = activity; + } + + public void dispatchCreate() { + moveToState(Fragment.CREATED, false); + } + + public void dispatchActivityCreated() { + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchStart() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchResume() { + moveToState(Fragment.RESUMED, false); + } + + public void dispatchPause() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchStop() { + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchDestroy() { + moveToState(Fragment.INITIALIZING, false); + mActivity = null; + } + + public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) { + boolean show = false; + if (mActive != null) { + for (int i=0; i<mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu) { + show = true; + f.onCreateOptionsMenu(menu, inflater); + } + } + } + return show; + } + + public boolean dispatchPrepareOptionsMenu(Menu menu) { + boolean show = false; + if (mActive != null) { + for (int i=0; i<mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu) { + show = true; + f.onPrepareOptionsMenu(menu); + } + } + } + return show; + } + + public boolean dispatchOptionsItemSelected(MenuItem item) { + if (mActive != null) { + for (int i=0; i<mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu) { + if (f.onOptionsItemSelected(item)) { + return true; + } + } + } + } + return false; + } + + public boolean dispatchContextItemSelected(MenuItem item) { + if (mActive != null) { + for (int i=0; i<mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null && !f.mHidden) { + if (f.onContextItemSelected(item)) { + return true; + } + } + } + } + return false; + } + + public void dispatchOptionsMenuClosed(Menu menu) { + if (mActive != null) { + for (int i=0; i<mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu) { + f.onOptionsMenuClosed(menu); + } + } + } + } + + public static int reverseTransit(int transit) { + int rev = 0; + switch (transit) { + case FragmentTransaction.TRANSIT_ENTER: + rev = FragmentTransaction.TRANSIT_EXIT; + break; + case FragmentTransaction.TRANSIT_EXIT: + rev = FragmentTransaction.TRANSIT_ENTER; + break; + case FragmentTransaction.TRANSIT_SHOW: + rev = FragmentTransaction.TRANSIT_HIDE; + break; + case FragmentTransaction.TRANSIT_HIDE: + rev = FragmentTransaction.TRANSIT_SHOW; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_OPEN: + rev = FragmentTransaction.TRANSIT_ACTIVITY_CLOSE; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE: + rev = FragmentTransaction.TRANSIT_ACTIVITY_OPEN; + break; + case FragmentTransaction.TRANSIT_TASK_OPEN: + rev = FragmentTransaction.TRANSIT_TASK_CLOSE; + break; + case FragmentTransaction.TRANSIT_TASK_CLOSE: + rev = FragmentTransaction.TRANSIT_TASK_OPEN; + break; + case FragmentTransaction.TRANSIT_TASK_TO_FRONT: + rev = FragmentTransaction.TRANSIT_TASK_TO_BACK; + break; + case FragmentTransaction.TRANSIT_TASK_TO_BACK: + rev = FragmentTransaction.TRANSIT_TASK_TO_FRONT; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_OPEN: + rev = FragmentTransaction.TRANSIT_WALLPAPER_CLOSE; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE: + rev = FragmentTransaction.TRANSIT_WALLPAPER_OPEN; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN: + rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE: + rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN; + break; + } + return rev; + + } + + public static int transitToStyleIndex(int transit, boolean enter) { + int animAttr = -1; + switch (transit) { + case FragmentTransaction.TRANSIT_ENTER: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation; + break; + case FragmentTransaction.TRANSIT_EXIT: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation; + break; + case FragmentTransaction.TRANSIT_SHOW: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation; + break; + case FragmentTransaction.TRANSIT_HIDE: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_activityOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_activityOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_TO_FRONT: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskToFrontEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskToFrontExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_TO_BACK: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskToBackEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskToBackExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; + break; + } + return animAttr; + } +} diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java new file mode 100644 index 0000000..840f274 --- /dev/null +++ b/core/java/android/app/FragmentTransaction.java @@ -0,0 +1,151 @@ +package android.app; + +/** + * API for performing a set of Fragment operations. + */ +public interface FragmentTransaction { + /** + * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId. + */ + public FragmentTransaction add(Fragment fragment, String tag); + + /** + * Calls {@link #add(int, Fragment, String)} with a null tag. + */ + public FragmentTransaction add(int containerViewId, Fragment fragment); + + /** + * Add a fragment to the activity state. This fragment may optionally + * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView} + * returns non-null) into a container view of the activity. + * + * @param containerViewId Optional identifier of the container this fragment is + * to be placed in. If 0, it will not be placed in a container. + * @param fragment The fragment to be added. This fragment must not already + * be added to the activity. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link Activity#findFragmentByTag(String) + * Activity.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction add(int containerViewId, Fragment fragment, String tag); + + /** + * Calls {@link #replace(int, Fragment, String)} with a null tag. + */ + public FragmentTransaction replace(int containerViewId, Fragment fragment); + + /** + * Replace an existing fragment that was added to a container. This is + * essentially the same as calling {@link #remove(Fragment)} for all + * currently added fragments that were added with the same containerViewId + * and then {@link #add(int, Fragment, String)} with the same arguments + * given here. + * + * @param containerViewId Identifier of the container whose fragment(s) are + * to be replaced. + * @param fragment The new fragment to place in the container. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link Activity#findFragmentByTag(String) + * Activity.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag); + + /** + * Remove an existing fragment. If it was added to a container, its view + * is also removed from that container. + * + * @param fragment The fragment to be removed. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction remove(Fragment fragment); + + /** + * Hides an existing fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be hidden. + * + * @param fragment The fragment to be hidden. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction hide(Fragment fragment); + + /** + * Hides a previously hidden fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be shown. + * + * @param fragment The fragment to be shown. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction show(Fragment fragment); + + /** + * Bit mask that is set for all enter transitions. + */ + public final int TRANSIT_ENTER_MASK = 0x1000; + + /** + * Bit mask that is set for all exit transitions. + */ + public final int TRANSIT_EXIT_MASK = 0x2000; + + /** Not set up for a transition. */ + public final int TRANSIT_UNSET = -1; + /** No animation for transition. */ + public final int TRANSIT_NONE = 0; + /** Window has been added to the screen. */ + public final int TRANSIT_ENTER = 1 | TRANSIT_ENTER_MASK; + /** Window has been removed from the screen. */ + public final int TRANSIT_EXIT = 2 | TRANSIT_EXIT_MASK; + /** Window has been made visible. */ + public final int TRANSIT_SHOW = 3 | TRANSIT_ENTER_MASK; + /** Window has been made invisible. */ + public final int TRANSIT_HIDE = 4 | TRANSIT_EXIT_MASK; + /** The "application starting" preview window is no longer needed, and will + * animate away to show the real window. */ + public final int TRANSIT_PREVIEW_DONE = 5; + /** A window in a new activity is being opened on top of an existing one + * in the same task. */ + public final int TRANSIT_ACTIVITY_OPEN = 6 | TRANSIT_ENTER_MASK; + /** The window in the top-most activity is being closed to reveal the + * previous activity in the same task. */ + public final int TRANSIT_ACTIVITY_CLOSE = 7 | TRANSIT_EXIT_MASK; + /** A window in a new task is being opened on top of an existing one + * in another activity's task. */ + public final int TRANSIT_TASK_OPEN = 8 | TRANSIT_ENTER_MASK; + /** A window in the top-most activity is being closed to reveal the + * previous activity in a different task. */ + public final int TRANSIT_TASK_CLOSE = 9 | TRANSIT_EXIT_MASK; + /** A window in an existing task is being displayed on top of an existing one + * in another activity's task. */ + public final int TRANSIT_TASK_TO_FRONT = 10 | TRANSIT_ENTER_MASK; + /** A window in an existing task is being put below all other tasks. */ + public final int TRANSIT_TASK_TO_BACK = 11 | TRANSIT_EXIT_MASK; + /** A window in a new activity that doesn't have a wallpaper is being + * opened on top of one that does, effectively closing the wallpaper. */ + public final int TRANSIT_WALLPAPER_CLOSE = 12 | TRANSIT_EXIT_MASK; + /** A window in a new activity that does have a wallpaper is being + * opened on one that didn't, effectively opening the wallpaper. */ + public final int TRANSIT_WALLPAPER_OPEN = 13 | TRANSIT_ENTER_MASK; + /** A window in a new activity is being opened on top of an existing one, + * and both are on top of the wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_OPEN = 14 | TRANSIT_ENTER_MASK; + /** The window in the top-most activity is being closed to reveal the + * previous activity, and both are on top of he wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15 | TRANSIT_EXIT_MASK; + + public FragmentTransaction setCustomAnimations(int enter, int exit); + + public FragmentTransaction setTransition(int transit); + public FragmentTransaction setTransitionStyle(int styleRes); + + public FragmentTransaction addToBackStack(String name); + public void commit(); +} diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 20c9a80..8ea59a7 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -316,7 +316,11 @@ public interface IActivityManager extends IInterface { public void crashApplication(int uid, int initialPid, String packageName, String message) throws RemoteException; - + + // Cause the specified process to dump the specified heap. + public boolean dumpHeap(String process, boolean managed, String path, + ParcelFileDescriptor fd) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -533,4 +537,5 @@ public interface IActivityManager extends IInterface { int SET_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+111; int IS_TOP_ACTIVITY_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+112; int CRASH_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+113; + int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+114; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index c8ef17f..039bcb9 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -97,6 +97,8 @@ public interface IApplicationThread extends IInterface { void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException; void profilerControl(boolean start, String path, ParcelFileDescriptor fd) throws RemoteException; + void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) + throws RemoteException; void setSchedulingGroup(int group) throws RemoteException; void getMemoryInfo(Debug.MemoryInfo outInfo) throws RemoteException; static final int PACKAGE_REMOVED = 0; @@ -140,4 +142,5 @@ public interface IApplicationThread extends IInterface { int SCHEDULE_SUICIDE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32; int DISPATCH_PACKAGE_BROADCAST_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33; int SCHEDULE_CRASH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34; + int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35; } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index b8c3aa3..4d5f36a 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -997,8 +997,10 @@ public class Instrumentation { IllegalAccessException { Activity activity = (Activity)clazz.newInstance(); ActivityThread aThread = null; - activity.attach(context, aThread, this, token, application, intent, info, title, - parent, id, lastNonConfigurationInstance, new Configuration()); + activity.attach(context, aThread, this, token, application, intent, + info, title, parent, id, + (Activity.NonConfigurationInstances)lastNonConfigurationInstance, + new Configuration()); return activity; } @@ -1058,21 +1060,23 @@ public class Instrumentation { } public void callActivityOnDestroy(Activity activity) { - if (mWaitingActivities != null) { - synchronized (mSync) { - final int N = mWaitingActivities.size(); - for (int i=0; i<N; i++) { - final ActivityWaiter aw = mWaitingActivities.get(i); - final Intent intent = aw.intent; - if (intent.filterEquals(activity.getIntent())) { - aw.activity = activity; - mMessageQueue.addIdleHandler(new ActivityGoing(aw)); - } - } - } - } + // TODO: the following block causes intermittent hangs when using startActivity + // temporarily comment out until root cause is fixed (bug 2630683) +// if (mWaitingActivities != null) { +// synchronized (mSync) { +// final int N = mWaitingActivities.size(); +// for (int i=0; i<N; i++) { +// final ActivityWaiter aw = mWaitingActivities.get(i); +// final Intent intent = aw.intent; +// if (intent.filterEquals(activity.getIntent())) { +// aw.activity = activity; +// mMessageQueue.addIdleHandler(new ActivityGoing(aw)); +// } +// } +// } +// } - activity.onDestroy(); + activity.performDestroy(); if (mActivityMonitors != null) { synchronized (mSync) { @@ -1331,7 +1335,7 @@ public class Instrumentation { * is being started. * @param token Internal token identifying to the system who is starting * the activity; may be null. - * @param target Which activity is perform the start (and thus receiving + * @param target Which activity is performing the start (and thus receiving * any result); may be null if this call is not being made * from an activity. * @param intent The actual Intent to start. @@ -1381,6 +1385,64 @@ public class Instrumentation { return null; } + /** + * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, + * but for calls from a {#link Fragment}. + * + * @param who The Context from which the activity is being started. + * @param contextThread The main thread of the Context from which the activity + * is being started. + * @param token Internal token identifying to the system who is starting + * the activity; may be null. + * @param target Which fragment is performing the start (and thus receiving + * any result). + * @param intent The actual Intent to start. + * @param requestCode Identifier for this request's result; less than zero + * if the caller is not expecting a result. + * + * @return To force the return of a particular result, return an + * ActivityResult object containing the desired data; otherwise + * return null. The default implementation always returns null. + * + * @throws android.content.ActivityNotFoundException + * + * @see Activity#startActivity(Intent) + * @see Activity#startActivityForResult(Intent, int) + * @see Activity#startActivityFromChild + * + * {@hide} + */ + public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Fragment target, + Intent intent, int requestCode) { + IApplicationThread whoThread = (IApplicationThread) contextThread; + if (mActivityMonitors != null) { + synchronized (mSync) { + final int N = mActivityMonitors.size(); + for (int i=0; i<N; i++) { + final ActivityMonitor am = mActivityMonitors.get(i); + if (am.match(who, null, intent)) { + am.mHits++; + if (am.isBlocking()) { + return requestCode >= 0 ? am.getResult() : null; + } + break; + } + } + } + } + try { + int result = ActivityManagerNative.getDefault() + .startActivity(whoThread, intent, + intent.resolveTypeIfNeeded(who.getContentResolver()), + null, 0, token, target != null ? target.mWho : null, + requestCode, false, false); + checkStartActivityResult(result, intent); + } catch (RemoteException e) { + } + return null; + } + /*package*/ final void init(ActivityThread thread, Context instrContext, Context appContext, ComponentName component, IInstrumentationWatcher watcher) { diff --git a/core/java/android/app/ListActivity.java b/core/java/android/app/ListActivity.java index 4bf5518..d49968f 100644 --- a/core/java/android/app/ListActivity.java +++ b/core/java/android/app/ListActivity.java @@ -309,7 +309,7 @@ public class ListActivity extends Activity { if (mList != null) { return; } - setContentView(com.android.internal.R.layout.list_content); + setContentView(com.android.internal.R.layout.list_content_simple); } diff --git a/core/java/android/app/ListFragment.java b/core/java/android/app/ListFragment.java new file mode 100644 index 0000000..73ef869 --- /dev/null +++ b/core/java/android/app/ListFragment.java @@ -0,0 +1,406 @@ +/* + * 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.app; + +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +/** + * An fragment that displays a list of items by binding to a data source such as + * an array or Cursor, and exposes event handlers when the user selects an item. + * <p> + * ListActivity hosts a {@link android.widget.ListView ListView} object that can + * be bound to different data sources, typically either an array or a Cursor + * holding query results. Binding, screen layout, and row layout are discussed + * in the following sections. + * <p> + * <strong>Screen Layout</strong> + * </p> + * <p> + * ListActivity has a default layout that consists of a single list view. + * However, if you desire, you can customize the fragment layout by returning + * your own view hierarchy from {@link #onCreateView}. + * To do this, your view hierarchy MUST contain a ListView object with the + * id "@android:id/list" (or {@link android.R.id#list} if it's in code) + * <p> + * Optionally, your view hierarchy can contain another view object of any type to + * display when the list view is empty. This "empty list" notifier must have an + * id "android:empty". Note that when an empty view is present, the list view + * will be hidden when there is no data to display. + * <p> + * The following code demonstrates an (ugly) custom lisy layout. It has a list + * with a green background, and an alternate red "no data" message. + * </p> + * + * <pre> + * <?xml version="1.0" encoding="utf-8"?> + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + * android:orientation="vertical" + * android:layout_width="match_parent" + * android:layout_height="match_parent" + * android:paddingLeft="8dp" + * android:paddingRight="8dp"> + * + * <ListView android:id="@id/android:list" + * android:layout_width="match_parent" + * android:layout_height="match_parent" + * android:background="#00FF00" + * android:layout_weight="1" + * android:drawSelectorOnTop="false"/> + * + * <TextView android:id="@id/android:empty" + * android:layout_width="match_parent" + * android:layout_height="match_parent" + * android:background="#FF0000" + * android:text="No data"/> + * </LinearLayout> + * </pre> + * + * <p> + * <strong>Row Layout</strong> + * </p> + * <p> + * You can specify the layout of individual rows in the list. You do this by + * specifying a layout resource in the ListAdapter object hosted by the fragment + * (the ListAdapter binds the ListView to the data; more on this later). + * <p> + * A ListAdapter constructor takes a parameter that specifies a layout resource + * for each row. It also has two additional parameters that let you specify + * which data field to associate with which object in the row layout resource. + * These two parameters are typically parallel arrays. + * </p> + * <p> + * Android provides some standard row layout resources. These are in the + * {@link android.R.layout} class, and have names such as simple_list_item_1, + * simple_list_item_2, and two_line_list_item. The following layout XML is the + * source for the resource two_line_list_item, which displays two data + * fields,one above the other, for each list row. + * </p> + * + * <pre> + * <?xml version="1.0" encoding="utf-8"?> + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + * android:layout_width="match_parent" + * android:layout_height="wrap_content" + * android:orientation="vertical"> + * + * <TextView android:id="@+id/text1" + * android:textSize="16sp" + * android:textStyle="bold" + * android:layout_width="match_parent" + * android:layout_height="wrap_content"/> + * + * <TextView android:id="@+id/text2" + * android:textSize="16sp" + * android:layout_width="match_parent" + * android:layout_height="wrap_content"/> + * </LinearLayout> + * </pre> + * + * <p> + * You must identify the data bound to each TextView object in this layout. The + * syntax for this is discussed in the next section. + * </p> + * <p> + * <strong>Binding to Data</strong> + * </p> + * <p> + * You bind the ListFragment's ListView object to data using a class that + * implements the {@link android.widget.ListAdapter ListAdapter} interface. + * Android provides two standard list adapters: + * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps), + * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor + * query results. + * </p> + * + * @see #setListAdapter + * @see android.widget.ListView + */ +public class ListFragment extends Fragment { + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView<?> parent, View v, int position, long id) { + onListItemClick((ListView)parent, v, position, id); + } + }; + + ListAdapter mAdapter; + ListView mList; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mListContainer; + boolean mListShown; + + public ListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses + * can override to replace with their own layout. If doing so, the + * returned view hierarchy <em>must</em> have a ListView whose id + * is {@link android.R.id#list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id#empty android.R.id.empty} + * that is to be shown when the list is empty. + * + * <p>If you are overriding this method with your own custom content, + * consider including the standard layout {@link android.R.layout#list_content} + * in your layout file, so that you continue to retain all of the standard + * behavior of ListFragment. In particular, this is currently the only + * way to have the built-in indeterminant progress state be shown. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(com.android.internal.R.layout.list_content, + container, false); + } + + /** + * Attach to list view once Fragment is ready to run. + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. + * Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(ListView l, View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(ListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mList != null) { + mList.setAdapter(adapter); + if (!mListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, getView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified + * position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mList.getSelectedItemId(); + } + + /** + * Get the activity's list view widget. + */ + public ListView getListView() { + ensureList(); + return mList; + } + + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mList.setEmptyView(mStandardEmptyView); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * <p>Applications do not normally need to use this themselves. The default + * behavior of ListFragment is to start with the list not being shown, only + * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}. + * If the list at that point had not been shown, when it does get shown + * it will be do without the user ever seeing the hidden state. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when + * transitioning from the previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ListAdapter getListAdapter() { + return mAdapter; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById( + com.android.internal.R.id.internalEmpty); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } + mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer); + mListContainer = root.findViewById(com.android.internal.R.id.listContainer); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mList == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } + } + mListShown = true; + mList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + setListAdapter(mAdapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } +} diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java new file mode 100644 index 0000000..7600899 --- /dev/null +++ b/core/java/android/app/LoaderManager.java @@ -0,0 +1,306 @@ +/* + * 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.app; + +import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.os.Bundle; +import android.util.SparseArray; + +/** + * Object associated with an {@link Activity} or {@link Fragment} for managing + * one or more {@link android.content.Loader} instances associated with it. + */ +public class LoaderManager { + final SparseArray<LoaderInfo> mLoaders = new SparseArray<LoaderInfo>(); + final SparseArray<LoaderInfo> mInactiveLoaders = new SparseArray<LoaderInfo>(); + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + + /** + * Callback interface for a client to interact with the manager. + */ + public interface LoaderCallbacks<D> { + public Loader<D> onCreateLoader(int id, Bundle args); + public void onLoadFinished(Loader<D> loader, D data); + } + + final class LoaderInfo implements Loader.OnLoadCompleteListener<Object> { + final int mId; + final Bundle mArgs; + LoaderManager.LoaderCallbacks<Object> mCallbacks; + Loader<Object> mLoader; + Object mData; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + boolean mDestroyed; + boolean mListenerRegistered; + + public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks<Object> callbacks) { + mId = id; + mArgs = args; + mCallbacks = callbacks; + } + + void start() { + if (mRetaining && mRetainingStarted) { + // Our owner is started, but we were being retained from a + // previous instance in the started state... so there is really + // nothing to do here, since the loaders are still started. + mStarted = true; + return; + } + + if (mLoader == null && mCallbacks != null) { + mLoader = mCallbacks.onCreateLoader(mId, mArgs); + } + if (mLoader != null) { + mLoader.registerListener(mId, this); + mListenerRegistered = true; + mLoader.startLoading(); + mStarted = true; + } + } + + void retain() { + mRetaining = true; + mRetainingStarted = mStarted; + mStarted = false; + mCallbacks = null; + } + + void finishRetain() { + if (mRetaining) { + mRetaining = false; + if (mStarted != mRetainingStarted) { + if (!mStarted) { + // This loader was retained in a started state, but + // at the end of retaining everything our owner is + // no longer started... so make it stop. + stop(); + } + } + if (mStarted && mData != null && mCallbacks != null) { + // This loader was retained, and now at the point of + // finishing the retain we find we remain started, have + // our data, and the owner has a new callback... so + // let's deliver the data now. + mCallbacks.onLoadFinished(mLoader, mData); + } + } + } + + void stop() { + mStarted = false; + if (mLoader != null && mListenerRegistered) { + // Let the loader know we're done with it + mListenerRegistered = false; + mLoader.unregisterListener(this); + } + } + + void destroy() { + mDestroyed = true; + mCallbacks = null; + if (mLoader != null) { + if (mListenerRegistered) { + mListenerRegistered = false; + mLoader.unregisterListener(this); + } + mLoader.destroy(); + } + } + + @Override public void onLoadComplete(Loader<Object> loader, Object data) { + if (mDestroyed) { + return; + } + + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + mData = data; + if (mCallbacks != null) { + mCallbacks.onLoadFinished(loader, data); + } + + // Look for an inactive loader and destroy it if found + LoaderInfo info = mInactiveLoaders.get(mId); + if (info != null) { + Loader<Object> oldLoader = info.mLoader; + if (oldLoader != null) { + oldLoader.unregisterListener(info); + oldLoader.destroy(); + } + mInactiveLoaders.remove(mId); + } + } + } + + LoaderManager(boolean started) { + mStarted = started; + } + + private LoaderInfo createLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks<Object> callback) { + LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); + mLoaders.put(id, info); + Loader<Object> loader = callback.onCreateLoader(id, args); + info.mLoader = (Loader<Object>)loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), so only start them + // here if we're past that point of the activitiy's life cycle + loader.registerListener(id, info); + loader.startLoading(); + } + return info; + } + + /** + * Ensures a loader is initialized an active. If the loader doesn't + * already exist, one is created and started. Otherwise the last created + * loader is re-used. + * + * <p>In either case, the given callback is associated with the loader, and + * will be called as the loader state changes. If at the point of call + * the caller is in its started state, and the requested loader + * already exists and has generated its data, then + * callback. {@link LoaderCallbacks#onLoadFinished} will + * be called immediately (inside of this function), so you must be prepared + * for this to happen. + */ + @SuppressWarnings("unchecked") + public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) { + LoaderInfo info = mLoaders.get(id); + + if (info == null) { + // Loader doesn't already exist; create. + info = createLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); + } else { + info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback; + } + + if (info.mData != null && mStarted) { + // If the loader has already generated its data, report it now. + info.mCallbacks.onLoadFinished(info.mLoader, info.mData); + } + + return (Loader<D>)info.mLoader; + } + + /** + * Create a new loader in this manager, registers the callbacks to it, + * and starts it loading. If a loader with the same id has previously been + * started it will automatically be destroyed when the new loader completes + * its work. The callback will be delivered before the old loader + * is destroyed. + */ + @SuppressWarnings("unchecked") + public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) { + LoaderInfo info = mLoaders.get(id); + if (info != null) { + if (mInactiveLoaders.get(id) != null) { + // We already have an inactive loader for this ID that we are + // waiting for! Now we have three active loaders... let's just + // drop the one in the middle, since we are still waiting for + // its result but that result is already out of date. + info.destroy(); + } else { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + mInactiveLoaders.put(id, info); + } + } + + info = createLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); + return (Loader<D>)info.mLoader; + } + + /** + * Stops and removes the loader with the given ID. + */ + public void stopLoader(int id) { + int idx = mLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mLoaders.valueAt(idx); + mLoaders.removeAt(idx); + Loader<Object> loader = info.mLoader; + if (loader != null) { + loader.unregisterListener(info); + loader.destroy(); + } + } + } + + /** + * Return the Loader with the given id or null if no matching Loader + * is found. + */ + @SuppressWarnings("unchecked") + public <D> Loader<D> getLoader(int id) { + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return (Loader<D>)mLoaders.get(id).mLoader; + } + return null; + } + + void doStart() { + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).start(); + } + mStarted = true; + } + + void doStop() { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).stop(); + } + mStarted = false; + } + + void doRetain() { + mRetaining = true; + mStarted = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).retain(); + } + } + + void finishRetain() { + mRetaining = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).finishRetain(); + } + } + + void doDestroy() { + if (!mRetaining) { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).destroy(); + } + } + + for (int i = mInactiveLoaders.size()-1; i >= 0; i--) { + mInactiveLoaders.valueAt(i).destroy(); + } + mInactiveLoaders.clear(); + } +} diff --git a/core/java/android/app/LoaderManagingFragment.java b/core/java/android/app/LoaderManagingFragment.java new file mode 100644 index 0000000..5d417a0 --- /dev/null +++ b/core/java/android/app/LoaderManagingFragment.java @@ -0,0 +1,204 @@ +/* + * 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.app; + +import android.content.Loader; +import android.os.Bundle; + +import java.util.HashMap; + +/** + * A Fragment that has utility methods for managing {@link Loader}s. + * + * @param <D> The type of data returned by the Loader. If you're using multiple Loaders with + * different return types use Object and case the results. + */ +public abstract class LoaderManagingFragment<D> extends Fragment + implements Loader.OnLoadCompleteListener<D> { + private boolean mStarted = false; + + static final class LoaderInfo<D> { + public Bundle args; + public Loader<D> loader; + } + private HashMap<Integer, LoaderInfo<D>> mLoaders; + private HashMap<Integer, LoaderInfo<D>> mInactiveLoaders; + + /** + * Registers a loader with this activity, registers the callbacks on it, and starts it loading. + * If a loader with the same id has previously been started it will automatically be destroyed + * when the new loader completes it's work. The callback will be delivered before the old loader + * is destroyed. + */ + public Loader<D> startLoading(int id, Bundle args) { + LoaderInfo<D> info = mLoaders.get(id); + if (info != null) { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + mInactiveLoaders.put(id, info); + } + info = new LoaderInfo<D>(); + info.args = args; + mLoaders.put(id, info); + Loader<D> loader = onCreateLoader(id, args); + info.loader = loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), so only start them + // here if we're past that point of the activitiy's life cycle + loader.registerListener(id, this); + loader.startLoading(); + } + return loader; + } + + protected abstract Loader<D> onCreateLoader(int id, Bundle args); + protected abstract void onInitializeLoaders(); + protected abstract void onLoadFinished(Loader<D> loader, D data); + + public final void onLoadComplete(Loader<D> loader, D data) { + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + onLoadFinished(loader, data); + + // Look for an inactive loader and destroy it if found + int id = loader.getId(); + LoaderInfo<D> info = mInactiveLoaders.get(id); + if (info != null) { + Loader<D> oldLoader = info.loader; + if (oldLoader != null) { + oldLoader.destroy(); + } + mInactiveLoaders.remove(id); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + + if (mLoaders == null) { + // Look for a passed along loader and create a new one if it's not there +// TODO: uncomment once getLastNonConfigurationInstance method is available +// mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance(); + if (mLoaders == null) { + mLoaders = new HashMap<Integer, LoaderInfo<D>>(); + onInitializeLoaders(); + } + } + if (mInactiveLoaders == null) { + mInactiveLoaders = new HashMap<Integer, LoaderInfo<D>>(); + } + } + + @Override + public void onStart() { + super.onStart(); + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + int id = entry.getKey(); + if (loader == null) { + loader = onCreateLoader(id, info.args); + info.loader = loader; + } + loader.registerListener(id, this); + loader.startLoading(); + } + + mStarted = true; + } + + @Override + public void onStop() { + super.onStop(); + + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + if (loader == null) { + continue; + } + + // Let the loader know we're done with it + loader.unregisterListener(this); + + // The loader isn't getting passed along to the next instance so ask it to stop loading + if (!getActivity().isChangingConfigurations()) { + loader.stopLoading(); + } + } + + mStarted = false; + } + + /** TO DO: This needs to be turned into a retained fragment. + @Override + public Object onRetainNonConfigurationInstance() { + // Pass the loader along to the next guy + Object result = mLoaders; + mLoaders = null; + return result; + } + **/ + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mLoaders != null) { + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + if (loader == null) { + continue; + } + loader.destroy(); + } + } + } + + /** + * Stops and removes the loader with the given ID. + */ + public void stopLoading(int id) { + if (mLoaders != null) { + LoaderInfo<D> info = mLoaders.remove(id); + if (info != null) { + Loader<D> loader = info.loader; + if (loader != null) { + loader.unregisterListener(this); + loader.destroy(); + } + } + } + } + + /** + * @return the Loader with the given id or null if no matching Loader + * is found. + */ + public Loader<D> getLoader(int id) { + LoaderInfo<D> loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return mLoaders.get(id).loader; + } + return null; + } +} diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java index a24fcae..524de6f 100644 --- a/core/java/android/app/LocalActivityManager.java +++ b/core/java/android/app/LocalActivityManager.java @@ -20,13 +20,11 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Binder; import android.os.Bundle; -import android.util.Config; import android.util.Log; import android.view.Window; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; /** @@ -38,7 +36,7 @@ import java.util.Map; */ public class LocalActivityManager { private static final String TAG = "LocalActivityManager"; - private static final boolean localLOGV = false || Config.LOGV; + private static final boolean localLOGV = false; // Internal token for an Activity being managed by LocalActivityManager. private static class LocalActivityRecord extends Binder { @@ -112,11 +110,16 @@ public class LocalActivityManager { if (r.curState == INITIALIZING) { // Get the lastNonConfigurationInstance for the activity - HashMap<String,Object> lastNonConfigurationInstances = - mParent.getLastNonConfigurationChildInstances(); - Object instance = null; + HashMap<String, Object> lastNonConfigurationInstances = + mParent.getLastNonConfigurationChildInstances(); + Object instanceObj = null; if (lastNonConfigurationInstances != null) { - instance = lastNonConfigurationInstances.get(r.id); + instanceObj = lastNonConfigurationInstances.get(r.id); + } + Activity.NonConfigurationInstances instance = null; + if (instanceObj != null) { + instance = new Activity.NonConfigurationInstances(); + instance.activity = instanceObj; } // We need to have always created the activity. @@ -346,7 +349,7 @@ public class LocalActivityManager { } private Window performDestroy(LocalActivityRecord r, boolean finish) { - Window win = null; + Window win; win = r.window; if (r.curState == RESUMED && !finish) { performPause(r, finish); @@ -380,7 +383,8 @@ public class LocalActivityManager { if (r != null) { win = performDestroy(r, finish); if (finish) { - mActivities.remove(r); + mActivities.remove(id); + mActivityArray.remove(r); } } return win; @@ -441,10 +445,8 @@ public class LocalActivityManager { */ public void dispatchCreate(Bundle state) { if (state != null) { - final Iterator<String> i = state.keySet().iterator(); - while (i.hasNext()) { + for (String id : state.keySet()) { try { - final String id = i.next(); final Bundle astate = state.getBundle(id); LocalActivityRecord r = mActivities.get(id); if (r != null) { @@ -457,9 +459,7 @@ public class LocalActivityManager { } } catch (Exception e) { // Recover from -all- app errors. - Log.e(TAG, - "Exception thrown when restoring LocalActivityManager state", - e); + Log.e(TAG, "Exception thrown when restoring LocalActivityManager state", e); } } } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 296d70a4..3066f5c 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -46,7 +46,7 @@ public class DevicePolicyManager { private final Context mContext; private final IDevicePolicyManager mService; - + private final Handler mHandler; private DevicePolicyManager(Context context, Handler handler) { @@ -61,14 +61,14 @@ public class DevicePolicyManager { DevicePolicyManager me = new DevicePolicyManager(context, handler); return me.mService != null ? me : null; } - + /** * Activity action: ask the user to add a new device administrator to the system. * The desired policy is the ComponentName of the policy in the * {@link #EXTRA_DEVICE_ADMIN} extra field. This will invoke a UI to * bring the user through adding the device administrator to the system (or * allowing them to reject it). - * + * * <p>You can optionally include the {@link #EXTRA_ADD_EXPLANATION} * field to provide the user with additional explanation (in addition * to your component's description) about what is being added. @@ -76,7 +76,7 @@ public class DevicePolicyManager { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN"; - + /** * Activity action: send when any policy admin changes a policy. * This is generally used to find out when a new policy is in effect. @@ -92,7 +92,7 @@ public class DevicePolicyManager { * @see #ACTION_ADD_DEVICE_ADMIN */ public static final String EXTRA_DEVICE_ADMIN = "android.app.extra.DEVICE_ADMIN"; - + /** * An optional CharSequence providing additional explanation for why the * admin is being added. @@ -100,22 +100,21 @@ public class DevicePolicyManager { * @see #ACTION_ADD_DEVICE_ADMIN */ public static final String EXTRA_ADD_EXPLANATION = "android.app.extra.ADD_EXPLANATION"; - - /** - * Activity action: have the user enter a new password. This activity - * should be launched after using {@link #setPasswordQuality(ComponentName, int)} - * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the - * user enter a new password that meets the current requirements. You can - * use {@link #isActivePasswordSufficient()} to determine whether you need - * to have the user select a new password in order to meet the current - * constraints. Upon being resumed from this activity, - * you can check the new password characteristics to see if they are - * sufficient. + + /** + * Activity action: have the user enter a new password. This activity should + * be launched after using {@link #setPasswordQuality(ComponentName, int)}, + * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the user + * enter a new password that meets the current requirements. You can use + * {@link #isActivePasswordSufficient()} to determine whether you need to + * have the user select a new password in order to meet the current + * constraints. Upon being resumed from this activity, you can check the new + * password characteristics to see if they are sufficient. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD"; - + /** * Return true if the given administrator component is currently * active (enabled) in the system. @@ -130,7 +129,7 @@ public class DevicePolicyManager { } return false; } - + /** * Return a list of all currently active device administrator's component * names. Note that if there are no administrators than null may be @@ -146,7 +145,7 @@ public class DevicePolicyManager { } return null; } - + /** * @hide */ @@ -160,7 +159,7 @@ public class DevicePolicyManager { } return false; } - + /** * Remove a current administration component. This can only be called * by the application that owns the administration component; if you @@ -176,28 +175,28 @@ public class DevicePolicyManager { } } } - + /** * Constant for {@link #setPasswordQuality}: the policy has no requirements * for the password. Note that quality constants are ordered so that higher * values are more restrictive. */ public static final int PASSWORD_QUALITY_UNSPECIFIED = 0; - + /** * Constant for {@link #setPasswordQuality}: the policy requires some kind * of password, but doesn't care what it is. Note that quality constants * are ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_SOMETHING = 0x10000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least numeric characters. Note that quality * constants are ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_NUMERIC = 0x20000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least alphabetic (or other symbol) characters. @@ -205,7 +204,7 @@ public class DevicePolicyManager { * restrictive. */ public static final int PASSWORD_QUALITY_ALPHABETIC = 0x40000; - + /** * Constant for {@link #setPasswordQuality}: the user must have entered a * password containing at least <em>both></em> numeric <em>and</em> @@ -213,7 +212,19 @@ public class DevicePolicyManager { * ordered so that higher values are more restrictive. */ public static final int PASSWORD_QUALITY_ALPHANUMERIC = 0x50000; - + + /** + * Constant for {@link #setPasswordQuality}: the user must have entered a + * password containing at least a letter, a numerical digit and a special + * symbol, by default. With this password quality, passwords can be + * restricted to contain various sets of characters, like at least an + * uppercase letter, etc. These are specified using various methods, + * like {@link #setPasswordMinimumLowerCase(ComponentName, int)}. Note + * that quality constants are ordered so that higher values are more + * restrictive. + */ + public static final int PASSWORD_QUALITY_COMPLEX = 0x60000; + /** * Called by an application that is administering the device to set the * password restrictions it is imposing. After setting this, the user @@ -222,21 +233,21 @@ public class DevicePolicyManager { * will remain until the user has set a new one, so the change does not * take place immediately. To prompt the user for a new password, use * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. - * + * * <p>Quality constants are ordered so that higher values are more restrictive; * thus the highest requested quality constant (between the policy set here, * the user's preference, and any other considerations) is the one that * is in effect. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param quality The new desired quality. One of * {@link #PASSWORD_QUALITY_UNSPECIFIED}, {@link #PASSWORD_QUALITY_SOMETHING}, * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}, - * or {@link #PASSWORD_QUALITY_ALPHANUMERIC}. + * {@link #PASSWORD_QUALITY_ALPHANUMERIC} or {@link #PASSWORD_QUALITY_COMPLEX}. */ public void setPasswordQuality(ComponentName admin, int quality) { if (mService != null) { @@ -247,7 +258,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current minimum password quality for all admins * or a particular one. @@ -264,7 +275,7 @@ public class DevicePolicyManager { } return PASSWORD_QUALITY_UNSPECIFIED; } - + /** * Called by an application that is administering the device to set the * minimum allowed password length. After setting this, the user @@ -274,14 +285,14 @@ public class DevicePolicyManager { * take place immediately. To prompt the user for a new password, use * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This * constraint is only imposed if the administrator has also requested either - * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}, - * or {@link #PASSWORD_QUALITY_ALPHANUMERIC} + * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC} + * {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX} * with {@link #setPasswordQuality}. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param length The new desired minimum password length. A value of 0 * means there is no restriction. @@ -295,7 +306,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current minimum password length for all admins * or a particular one. @@ -312,7 +323,379 @@ public class DevicePolicyManager { } return 0; } - + + /** + * Called by an application that is administering the device to set the + * minimum number of upper case letters required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 0. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of upper case letters + * required in the password. A value of 0 means there is no + * restriction. + */ + public void setPasswordMinimumUpperCase(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumUpperCase(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of upper case letters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of upper case letters required in the + * password. + */ + public int getPasswordMinimumUpperCase(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumUpperCase(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of lower case letters required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 0. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of lower case letters + * required in the password. A value of 0 means there is no + * restriction. + */ + public void setPasswordMinimumLowerCase(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumLowerCase(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of lower case letters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of lower case letters required in the + * password. + */ + public int getPasswordMinimumLowerCase(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumLowerCase(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of letters required in the password. After setting this, + * the user will not be able to enter a new password that is not at least as + * restrictive as what has been set. Note that the current password will + * remain until the user has set a new one, so the change does not take + * place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of letters required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumLetters(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumLetters(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of letters required in the password for all + * admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumLetters(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of letters required in the password. + */ + public int getPasswordMinimumLetters(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumLetters(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of numerical digits required in the password. After + * setting this, the user will not be able to enter a new password that is + * not at least as restrictive as what has been set. Note that the current + * password will remain until the user has set a new one, so the change does + * not take place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of numerical digits required + * in the password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumNumeric(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumNumeric(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of numerical digits required in the password + * for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumNumeric(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of numerical digits required in the password. + */ + public int getPasswordMinimumNumeric(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumNumeric(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of symbols required in the password. After setting this, + * the user will not be able to enter a new password that is not at least as + * restrictive as what has been set. Note that the current password will + * remain until the user has set a new one, so the change does not take + * place immediately. To prompt the user for a new password, use + * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This + * constraint is only imposed if the administrator has also requested + * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The + * default value is 1. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of symbols required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumSymbols(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumSymbols(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of symbols required in the password for all + * admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumSymbols(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of symbols required in the password. + */ + public int getPasswordMinimumSymbols(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumSymbols(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the + * minimum number of non-letter characters (numerical digits or symbols) + * required in the password. After setting this, the user will not be able + * to enter a new password that is not at least as restrictive as what has + * been set. Note that the current password will remain until the user has + * set a new one, so the change does not take place immediately. To prompt + * the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} after + * setting this value. This constraint is only imposed if the administrator + * has also requested {@link #PASSWORD_QUALITY_COMPLEX} with + * {@link #setPasswordQuality}. The default value is 0. + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired minimum number of letters required in the + * password. A value of 0 means there is no restriction. + */ + public void setPasswordMinimumNonLetter(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordMinimumNonLetter(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current number of non-letter characters required in the + * password for all admins or a particular one. This is the same value as + * set by {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)} + * and only applies when the password quality is + * {@link #PASSWORD_QUALITY_COMPLEX}. + * + * @param admin The name of the admin component to check, or null to + * aggregate all admins. + * @return The minimum number of letters required in the password. + */ + public int getPasswordMinimumNonLetter(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordMinimumNonLetter(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by an application that is administering the device to set the length + * of the password history. After setting this, the user will not be able to + * enter a new password that is the same as any password in the history. Note + * that the current password will remain until the user has set a new one, so + * the change does not take place immediately. To prompt the user for a new + * password, use {@link #ACTION_SET_NEW_PASSWORD} after setting this value. + * This constraint is only imposed if the administrator has also requested + * either {@link #PASSWORD_QUALITY_NUMERIC}, + * {@link #PASSWORD_QUALITY_ALPHABETIC}, or + * {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}. + * + * <p> + * The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this + * method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param length The new desired length of password history. A value of 0 + * means there is no restriction. + */ + public void setPasswordHistoryLength(ComponentName admin, int length) { + if (mService != null) { + try { + mService.setPasswordHistoryLength(admin, length); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Retrieve the current password history length for all admins + * or a particular one. + * @param admin The name of the admin component to check, or null to aggregate + * all admins. + * @return The length of the password history + */ + public int getPasswordHistoryLength(ComponentName admin) { + if (mService != null) { + try { + return mService.getPasswordHistoryLength(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + /** * Return the maximum password length that the device supports for a * particular password quality. @@ -323,16 +706,16 @@ public class DevicePolicyManager { // Kind-of arbitrary. return 16; } - + /** * Determine whether the current password the user has set is sufficient * to meet the policy requirements (quality, minimum length) that have been * requested. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @return Returns true if the password meets the current requirements, * else false. */ @@ -346,11 +729,11 @@ public class DevicePolicyManager { } return false; } - + /** * Retrieve the number of times the user has failed at entering a * password since that last successful password entry. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to be able to call * this method; if it has not, a security exception will be thrown. @@ -373,14 +756,14 @@ public class DevicePolicyManager { * watching for failed passwords and wiping the device, and requires * that you request both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}}. - * + * * <p>To implement any other policy (e.g. wiping data for a particular * application only, erasing or revoking credentials, or reporting the * failure to a server), you should implement * {@link DeviceAdminReceiver#onPasswordFailed(Context, android.content.Intent)} * instead. Do not use this API, because if the maximum count is reached, * the device will be wiped immediately, and your callback will not be invoked. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param num The number of failed password attempts at which point the * device will wipe its data. @@ -394,7 +777,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current maximum number of login attempts that are allowed * before the device wipes itself, for all admins @@ -412,13 +795,13 @@ public class DevicePolicyManager { } return 0; } - + /** * Flag for {@link #resetPassword}: don't allow other admins to change * the password again until the user has entered it. */ public static final int RESET_PASSWORD_REQUIRE_ENTRY = 0x0001; - + /** * Force a new device unlock password (the password needed to access the * entire device, not for individual accounts) on the user. This takes @@ -431,11 +814,11 @@ public class DevicePolicyManager { * that the password may be a stronger quality (containing alphanumeric * characters when the requested quality is only numeric), in which case * the currently active quality will be increased to match. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param password The new password for the user. * @param flags May be 0 or {@link #RESET_PASSWORD_REQUIRE_ENTRY}. * @return Returns true if the password was applied, or false if it is @@ -451,16 +834,16 @@ public class DevicePolicyManager { } return false; } - + /** * Called by an application that is administering the device to set the * maximum time for user activity until the device will lock. This limits * the length that the user can set. It takes effect immediately. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param timeMs The new desired maximum time to lock in milliseconds. * A value of 0 means there is no restriction. @@ -474,7 +857,7 @@ public class DevicePolicyManager { } } } - + /** * Retrieve the current maximum time to unlock for all admins * or a particular one. @@ -491,11 +874,11 @@ public class DevicePolicyManager { } return 0; } - + /** * Make the device lock immediately, as if the lock screen timeout has * expired at the point of this call. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call * this method; if it has not, a security exception will be thrown. @@ -509,16 +892,16 @@ public class DevicePolicyManager { } } } - + /** * Ask the user date be wiped. This will cause the device to reboot, * erasing all user data while next booting up. External storage such * as SD cards will not be erased. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call * this method; if it has not, a security exception will be thrown. - * + * * @param flags Bit mask of additional options: currently must be 0. */ public void wipeData(int flags) { @@ -530,7 +913,7 @@ public class DevicePolicyManager { } } } - + /** * @hide */ @@ -543,7 +926,7 @@ public class DevicePolicyManager { } } } - + /** * @hide */ @@ -556,10 +939,10 @@ public class DevicePolicyManager { Log.w(TAG, "Unable to retrieve device policy " + cn, e); return null; } - + ResolveInfo ri = new ResolveInfo(); ri.activityInfo = ai; - + try { return new DeviceAdminInfo(mContext, ri); } catch (XmlPullParserException e) { @@ -570,7 +953,7 @@ public class DevicePolicyManager { return null; } } - + /** * @hide */ @@ -587,16 +970,18 @@ public class DevicePolicyManager { /** * @hide */ - public void setActivePasswordState(int quality, int length) { + public void setActivePasswordState(int quality, int length, int letters, int uppercase, + int lowercase, int numbers, int symbols, int nonletter) { if (mService != null) { try { - mService.setActivePasswordState(quality, length); + mService.setActivePasswordState(quality, length, letters, uppercase, lowercase, + numbers, symbols, nonletter); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } } } - + /** * @hide */ @@ -609,7 +994,7 @@ public class DevicePolicyManager { } } } - + /** * @hide */ diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 6fc4dc5..3ada95c 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -27,10 +27,31 @@ import android.os.RemoteCallback; interface IDevicePolicyManager { void setPasswordQuality(in ComponentName who, int quality); int getPasswordQuality(in ComponentName who); - + void setPasswordMinimumLength(in ComponentName who, int length); int getPasswordMinimumLength(in ComponentName who); + + void setPasswordMinimumUpperCase(in ComponentName who, int length); + int getPasswordMinimumUpperCase(in ComponentName who); + + void setPasswordMinimumLowerCase(in ComponentName who, int length); + int getPasswordMinimumLowerCase(in ComponentName who); + + void setPasswordMinimumLetters(in ComponentName who, int length); + int getPasswordMinimumLetters(in ComponentName who); + + void setPasswordMinimumNumeric(in ComponentName who, int length); + int getPasswordMinimumNumeric(in ComponentName who); + + void setPasswordMinimumSymbols(in ComponentName who, int length); + int getPasswordMinimumSymbols(in ComponentName who); + + void setPasswordMinimumNonLetter(in ComponentName who, int length); + int getPasswordMinimumNonLetter(in ComponentName who); + void setPasswordHistoryLength(in ComponentName who, int length); + int getPasswordHistoryLength(in ComponentName who); + boolean isActivePasswordSufficient(); int getCurrentFailedPasswordAttempts(); @@ -53,7 +74,8 @@ interface IDevicePolicyManager { void getRemoveWarning(in ComponentName policyReceiver, in RemoteCallback result); void removeActiveAdmin(in ComponentName policyReceiver); - void setActivePasswordState(int quality, int length); + void setActivePasswordState(int quality, int length, int letters, int uppercase, int lowercase, + int numbers, int symbols, int nonletter); void reportFailedPasswordAttempt(); void reportSuccessfulPasswordAttempt(); } diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index b33b097..3c19ea3 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -23,9 +23,9 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.os.SystemClock; -import android.os.Parcelable; import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -275,6 +275,7 @@ public class AppWidgetHostView extends FrameLayout { } } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (CROSSFADE) { int alpha; diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d2ab85e..3f12bf9 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -292,7 +292,15 @@ public class AppWidgetManager { */ public List<AppWidgetProviderInfo> getInstalledProviders() { try { - return sService.getInstalledProviders(); + List<AppWidgetProviderInfo> providers = sService.getInstalledProviders(); + for (AppWidgetProviderInfo info : providers) { + // Converting complex to dp. + info.minWidth = + TypedValue.complexToDimensionPixelSize(info.minWidth, mDisplayMetrics); + info.minHeight = + TypedValue.complexToDimensionPixelSize(info.minHeight, mDisplayMetrics); + } + return providers; } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java index cee2865..396e92d 100644 --- a/core/java/android/appwidget/AppWidgetProviderInfo.java +++ b/core/java/android/appwidget/AppWidgetProviderInfo.java @@ -110,6 +110,17 @@ public class AppWidgetProviderInfo implements Parcelable { * @hide Pending API approval */ public String oldName; + + /** + * A preview of what the AppWidget will look like after it's configured. + * If not supplied, the AppWidget's icon will be used. + * + * <p>This field corresponds to the <code>android:previewImage</code> attribute in + * the <code><receiver></code> element in the AndroidManifest.xml file. + * + * @hide Pending API approval + */ + public int previewImage; public AppWidgetProviderInfo() { } @@ -130,6 +141,7 @@ public class AppWidgetProviderInfo implements Parcelable { } this.label = in.readString(); this.icon = in.readInt(); + this.previewImage = in.readInt(); } @@ -152,6 +164,7 @@ public class AppWidgetProviderInfo implements Parcelable { } out.writeString(this.label); out.writeInt(this.icon); + out.writeInt(this.previewImage); } public int describeContents() { diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java index c7fea9e..0c9bab2 100644 --- a/core/java/android/bluetooth/BluetoothClass.java +++ b/core/java/android/bluetooth/BluetoothClass.java @@ -259,6 +259,8 @@ public final class BluetoothClass implements Parcelable { public static final int PROFILE_A2DP = 1; /** @hide */ public static final int PROFILE_OPP = 2; + /** @hide */ + public static final int PROFILE_HID = 3; /** * Check class bits for possible bluetooth profile support. @@ -324,6 +326,8 @@ public final class BluetoothClass implements Parcelable { default: return false; } + } else if (profile == PROFILE_HID) { + return (getDeviceClass() & Device.Major.PERIPHERAL) == Device.Major.PERIPHERAL; } else { return false; } diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java new file mode 100644 index 0000000..1793838 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -0,0 +1,241 @@ +/* + * 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.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Public API for controlling the Bluetooth HID (Input Device) Profile + * + * BluetoothInputDevice is a proxy object used to make calls to Bluetooth Service + * which handles the HID profile. + * + * Creating a BluetoothInputDevice object will initiate a binding with the + * Bluetooth service. Users of this object should call close() when they + * are finished, so that this proxy object can unbind from the service. + * + * Currently the Bluetooth service runs in the system server and this + * proxy object will be immediately bound to the service on construction. + * + * @hide + */ +public final class BluetoothInputDevice { + private static final String TAG = "BluetoothInputDevice"; + private static final boolean DBG = false; + + /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */ + public static final String EXTRA_INPUT_DEVICE_STATE = + "android.bluetooth.inputdevice.extra.INPUT_DEVICE_STATE"; + /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */ + public static final String EXTRA_PREVIOUS_INPUT_DEVICE_STATE = + "android.bluetooth.inputdevice.extra.PREVIOUS_INPUT_DEVICE_STATE"; + + /** Indicates the state of an input device has changed. + * This intent will always contain EXTRA_INPUT_DEVICE_STATE, + * EXTRA_PREVIOUS_INPUT_DEVICE_STATE and BluetoothDevice.EXTRA_DEVICE + * extras. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_INPUT_DEVICE_STATE_CHANGED = + "android.bluetooth.inputdevice.action.INPUT_DEVICE_STATE_CHANGED"; + + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + public static final int STATE_DISCONNECTING = 3; + + /** + * Auto connection, incoming and outgoing connection are allowed at this + * priority level. + */ + public static final int PRIORITY_AUTO_CONNECT = 1000; + /** + * Incoming and outgoing connection are allowed at this priority level + */ + public static final int PRIORITY_ON = 100; + /** + * Connections to the device are not allowed at this priority level. + */ + public static final int PRIORITY_OFF = 0; + /** + * Default priority level when the device is unpaired. + */ + public static final int PRIORITY_UNDEFINED = -1; + + private final IBluetooth mService; + private final Context mContext; + + /** + * Create a BluetoothInputDevice proxy object for interacting with the local + * Bluetooth Service which handle the HID profile. + * @param c Context + */ + public BluetoothInputDevice(Context c) { + mContext = c; + + IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE); + if (b != null) { + mService = IBluetooth.Stub.asInterface(b); + } else { + Log.w(TAG, "Bluetooth Service not available!"); + + // Instead of throwing an exception which prevents people from going + // into Wireless settings in the emulator. Let it crash later when it is actually used. + mService = null; + } + } + + /** Initiate a connection to an Input device. + * + * This function returns false on error and true if the connection + * attempt is being made. + * + * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when the + * connection is completed. + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean connectInputDevice(BluetoothDevice device) { + if (DBG) log("connectInputDevice(" + device + ")"); + try { + return mService.connectInputDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Initiate disconnect from an Input Device. + * This function return false on error and true if the disconnection + * attempt is being made. + * + * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when + * disconnect is completed. + * + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean disconnectInputDevice(BluetoothDevice device) { + if (DBG) log("disconnectInputDevice(" + device + ")"); + try { + return mService.disconnectInputDevice(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Check if a specified InputDevice is connected. + * + * @param device Remote BT device. + * @return True if connected , false otherwise and on error. + * @hide + */ + public boolean isInputDeviceConnected(BluetoothDevice device) { + if (DBG) log("isInputDeviceConnected(" + device + ")"); + int state = getInputDeviceState(device); + if (state == STATE_CONNECTED) return true; + return false; + } + + /** Check if any Input Device is connected. + * + * @return a unmodifiable set of connected Input Devices, or null on error. + * @hide + */ + public Set<BluetoothDevice> getConnectedInputDevices() { + if (DBG) log("getConnectedInputDevices()"); + try { + return Collections.unmodifiableSet( + new HashSet<BluetoothDevice>( + Arrays.asList(mService.getConnectedInputDevices()))); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return null; + } + } + + /** Get the state of an Input Device. + * + * @param device Remote BT device. + * @return The current state of the Input Device + * @hide + */ + public int getInputDeviceState(BluetoothDevice device) { + if (DBG) log("getInputDeviceState(" + device + ")"); + try { + return mService.getInputDeviceState(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return STATE_DISCONNECTED; + } + } + + /** + * Set priority of an input device. + * + * Priority is a non-negative integer. Priority can take the following + * values: + * {@link PRIORITY_ON}, {@link PRIORITY_OFF}, {@link PRIORITY_AUTO_CONNECT} + * + * @param device Paired device. + * @param priority Integer priority + * @return true if priority is set, false on error + */ + public boolean setInputDevicePriority(BluetoothDevice device, int priority) { + if (DBG) log("setInputDevicePriority(" + device + ", " + priority + ")"); + try { + return mService.setInputDevicePriority(device, priority); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** + * Get the priority associated with an Input Device. + * + * @param device Input Device + * @return non-negative priority, or negative error code on error. + */ + public int getInputDevicePriority(BluetoothDevice device) { + if (DBG) log("getInputDevicePriority(" + device + ")"); + try { + return mService.getInputDevicePriority(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return PRIORITY_OFF; + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java index 4164a3d..f1ee907 100644 --- a/core/java/android/bluetooth/BluetoothUuid.java +++ b/core/java/android/bluetooth/BluetoothUuid.java @@ -49,6 +49,8 @@ public final class BluetoothUuid { ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"); public static final ParcelUuid ObexObjectPush = ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb"); + public static final ParcelUuid Hid = + ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb"); public static final ParcelUuid[] RESERVED_UUIDS = { AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget, @@ -82,6 +84,10 @@ public final class BluetoothUuid { return uuid.equals(AvrcpTarget); } + public static boolean isInputDevice(ParcelUuid uuid) { + return uuid.equals(Hid); + } + /** * Returns true if ParcelUuid is present in uuidArray * diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index ea71034..75f093c 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -17,6 +17,7 @@ package android.bluetooth; import android.bluetooth.IBluetoothCallback; +import android.bluetooth.BluetoothDevice; import android.os.ParcelUuid; /** @@ -72,4 +73,12 @@ interface IBluetooth boolean connectHeadset(String address); boolean disconnectHeadset(String address); boolean notifyIncomingConnection(String address); + + // HID profile APIs + boolean connectInputDevice(in BluetoothDevice device); + boolean disconnectInputDevice(in BluetoothDevice device); + BluetoothDevice[] getConnectedInputDevices(); // change to Set<> once AIDL supports + int getInputDeviceState(in BluetoothDevice device); + boolean setInputDevicePriority(in BluetoothDevice device, int priority); + int getInputDevicePriority(in BluetoothDevice device); } diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java new file mode 100644 index 0000000..b19c072 --- /dev/null +++ b/core/java/android/content/AsyncTaskLoader.java @@ -0,0 +1,107 @@ +/* + * 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.content; + +import android.os.AsyncTask; + +/** + * Abstract Loader that provides an {@link AsyncTask} to do the work. + * + * @param <D> the data type to be loaded. + */ +public abstract class AsyncTaskLoader<D> extends Loader<D> { + final class LoadTask extends AsyncTask<Void, Void, D> { + + private D result; + + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + result = AsyncTaskLoader.this.loadInBackground(); + return result; + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + AsyncTaskLoader.this.dispatchOnLoadComplete(data); + } + + @Override + protected void onCancelled() { + AsyncTaskLoader.this.onCancelled(result); + } + } + + LoadTask mTask; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + @Override + public void forceLoad() { + cancelLoad(); + mTask = new LoadTask(); + mTask.execute((Void[]) null); + } + + /** + * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)} + * for more info. + * + * @return <tt>false</tt> if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called, and + * <tt>true</tt> otherwise + */ + public boolean cancelLoad() { + if (mTask != null) { + boolean cancelled = mTask.cancel(false); + mTask = null; + return cancelled; + } + return false; + } + + /** + * Called if the task was canceled before it was completed. Gives the class a chance + * to properly dispose of the result. + */ + public void onCancelled(D data) { + } + + void dispatchOnLoadComplete(D data) { + mTask = null; + deliverResult(data); + } + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * results directly, but should return them from this method, which will eventually end up + * calling deliverResult on the UI thread. If implementations need to process + * the results on the UI thread they may override deliverResult and do so + * there. + * + * @return the result of the load + */ + public abstract D loadInBackground(); +} diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 69f7611..2ea0df96 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -44,9 +44,9 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.ArrayList; /** @@ -401,8 +401,7 @@ public abstract class ContentResolver { /** * Open a raw file descriptor to access data under a "content:" URI. This * interacts with the underlying {@link ContentProvider#openAssetFile} - * ContentProvider.openAssetFile()} method of the provider associated with the - * given URI, to retrieve any file stored there. + * method of the provider associated with the given URI, to retrieve any file stored there. * * <h5>Accepts the following URI schemes:</h5> * <ul> @@ -1342,7 +1341,7 @@ public abstract class ContentResolver { } private final class CursorWrapperInner extends CursorWrapper { - private IContentProvider mContentProvider; + private final IContentProvider mContentProvider; public static final String TAG="CursorWrapperInner"; private boolean mCloseFlag = false; @@ -1371,7 +1370,7 @@ public abstract class ContentResolver { } private final class ParcelFileDescriptorInner extends ParcelFileDescriptor { - private IContentProvider mContentProvider; + private final IContentProvider mContentProvider; public static final String TAG="ParcelFileDescriptorInner"; private boolean mReleaseProviderFlag = false; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index a14bd8f..b49d801 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -21,6 +21,7 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; @@ -588,6 +589,32 @@ public abstract class Context { int mode, CursorFactory factory); /** + * Open a new private SQLiteDatabase associated with this Context's + * application package. Creates the database file if it doesn't exist. + * + * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.</p> + * + * @param name The name (unique in the application package) of the database. + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_WORLD_READABLE} + * and {@link #MODE_WORLD_WRITEABLE} to control permissions. + * @param factory An optional factory class that is called to instantiate a + * cursor when query is called. + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. if null, {@link android.database.DefaultDatabaseErrorHandler} is assumed. + * @return The contents of a newly created database with the given name. + * @throws android.database.sqlite.SQLiteException if the database file could not be opened. + * + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + * @see #deleteDatabase + */ + public abstract SQLiteDatabase openOrCreateDatabase(String name, + int mode, CursorFactory factory, DatabaseErrorHandler errorHandler); + + /** * Delete an existing private SQLiteDatabase associated with this Context's * application package. * @@ -1372,7 +1399,6 @@ public abstract class Context { public static final String SENSOR_SERVICE = "sensor"; /** - * @hide * Use with {@link #getSystemService} to retrieve a {@link * android.os.storage.StorageManager} for accesssing system storage * functions. diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index a447108..3f5d215 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -20,6 +20,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; @@ -204,6 +205,12 @@ public class ContextWrapper extends Context { } @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + return mBase.openOrCreateDatabase(name, mode, factory, errorHandler); + } + + @Override public boolean deleteDatabase(String name) { return mBase.deleteDatabase(name); } diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java new file mode 100644 index 0000000..e230394 --- /dev/null +++ b/core/java/android/content/CursorLoader.java @@ -0,0 +1,158 @@ +/* + * 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.content; + +import android.database.Cursor; +import android.net.Uri; + +/** + * A loader that queries the {@link ContentResolver} and returns a {@link Cursor}. + */ +public class CursorLoader extends AsyncTaskLoader<Cursor> { + Cursor mCursor; + ForceLoadContentObserver mObserver; + boolean mStopped; + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + // Ensure the cursor window is filled + if (cursor != null) { + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (mStopped) { + // An async query came in while the loader is stopped + cursor.close(); + return; + } + mCursor = cursor; + super.deliverResult(cursor); + } + + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + public void startLoading() { + mStopped = false; + + if (mCursor != null) { + deliverResult(mCursor); + } else { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + public void stopLoading() { + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + mCursor = null; + } + + // Attempt to cancel the current load task if possible. + cancelLoad(); + + // Make sure that any outstanding loads clean themselves up properly + mStopped = true; + } + + @Override + public void onCancelled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + public void destroy() { + // Ensure the loader is stopped + stopLoading(); + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } +} diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java new file mode 100644 index 0000000..db40e48 --- /dev/null +++ b/core/java/android/content/Loader.java @@ -0,0 +1,154 @@ +/* + * 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.content; + +import android.database.ContentObserver; +import android.os.Handler; + +/** + * An abstract class that performs asynchronous loading of data. While Loaders are active + * they should monitor the source of their data and deliver new results when the contents + * change. + * + * @param <D> The result returned when the load is complete + */ +public abstract class Loader<D> { + int mId; + OnLoadCompleteListener<D> mListener; + Context mContext; + + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + forceLoad(); + } + } + + public interface OnLoadCompleteListener<D> { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader<D> loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the UI thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. The callbacks will + * be called on the UI thread so it's safe to pass the results to widgets. + * + * Must be called from the UI thread + */ + public void registerListener(int id, OnLoadCompleteListener<D> listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Must be called from the UI thread + */ + public void unregisterListener(OnLoadCompleteListener<D> listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. Calling + * {@link #stopLoading} will stop the delivery of callbacks. + * + * Must be called from the UI thread + */ + public abstract void startLoading(); + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + public abstract void forceLoad(); + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called + * + * Must be called from the UI thread + */ + public abstract void stopLoading(); + + /** + * Destroys the loader and frees its resources, making it unusable. + * + * Must be called from the UI thread + */ + public abstract void destroy(); +}
\ No newline at end of file diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index a15e29e..5847216 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -17,6 +17,7 @@ package android.content; import java.util.Map; +import java.util.Set; /** * Interface for accessing and modifying preference data returned by {@link @@ -69,6 +70,17 @@ public interface SharedPreferences { Editor putString(String key, String value); /** + * Set a set of String values in the preferences editor, to be written + * back once {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param values The new values for the preference. + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putStringSet(String key, Set<String> values); + + /** * Set an int value in the preferences editor, to be written back once * {@link #commit} is called. * @@ -186,6 +198,20 @@ public interface SharedPreferences { String getString(String key, String defValue); /** + * Retrieve a set of String values from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValues Values to return if this preference does not exist. + * + * @return Returns the preference values if they exist, or defValues. + * Throws ClassCastException if there is a preference with this name + * that is not a Set. + * + * @throws ClassCastException + */ + Set<String> getStringSet(String key, Set<String> defValues); + + /** * Retrieve an int value from the preferences. * * @param key The name of the preference to retrieve. diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index d0b67cc..7f749bb 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -16,6 +16,8 @@ package android.content; +import com.google.android.collect.Maps; + import com.android.internal.R; import com.android.internal.util.ArrayUtils; @@ -55,6 +57,7 @@ import android.util.Pair; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -126,14 +129,13 @@ public class SyncManager implements OnAccountsUpdateListener { private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000; - private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock"; + private static final String SYNC_WAKE_LOCK_PREFIX = "SyncWakeLock"; private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY; - volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; @@ -195,6 +197,8 @@ public class SyncManager implements OnAccountsUpdateListener { private static final Account[] INITIAL_ACCOUNTS_ARRAY = new Account[0]; + private final PowerManager mPowerManager; + public void onAccountsUpdated(Account[] accounts) { // remember if this was the first time this was called after an update final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY; @@ -356,15 +360,13 @@ public class SyncManager implements OnAccountsUpdateListener { } else { mNotificationMgr = null; } - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK); - mSyncWakeLock.setReferenceCounted(false); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); // This WakeLock is used to ensure that we stay awake between the time that we receive // a sync alarm notification and when we finish processing it. We need to do this // because we don't do the work in the alarm handler, rather we do it in a message // handler. - mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, HANDLE_SYNC_ALARM_WAKE_LOCK); mHandleAlarmWakeLock.setReferenceCounted(false); @@ -1302,6 +1304,9 @@ public class SyncManager implements OnAccountsUpdateListener { public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); + private PowerManager.WakeLock mSyncWakeLock; + private final HashMap<Pair<String, String>, PowerManager.WakeLock> mWakeLocks = + Maps.newHashMap(); // used to track if we have installed the error notification so that we don't reinstall // it if sync is still failing @@ -1315,6 +1320,18 @@ public class SyncManager implements OnAccountsUpdateListener { } } + private PowerManager.WakeLock getSyncWakeLock(String accountType, String authority) { + final Pair<String, String> wakeLockKey = Pair.create(accountType, authority); + PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey); + if (wakeLock == null) { + final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + accountType; + wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wakeLock.setReferenceCounted(false); + mWakeLocks.put(wakeLockKey, wakeLock); + } + return wakeLock; + } + private void waitUntilReadyToRun() { CountDownLatch latch = mReadyToRunLatch; if (latch != null) { @@ -1477,8 +1494,9 @@ public class SyncManager implements OnAccountsUpdateListener { } } finally { final boolean isSyncInProgress = mActiveSyncContext != null; - if (!isSyncInProgress) { + if (!isSyncInProgress && mSyncWakeLock != null) { mSyncWakeLock.release(); + mSyncWakeLock = null; } manageSyncNotification(); manageErrorNotification(); @@ -1704,7 +1722,26 @@ public class SyncManager implements OnAccountsUpdateListener { return; } - mSyncWakeLock.acquire(); + // Find the wakelock for this account and authority and store it in mSyncWakeLock. + // Be sure to release the previous wakelock so that we don't end up with it being + // held until it is used again. + // There are a couple tricky things about this code: + // - make sure that we acquire the new wakelock before releasing the old one, + // otherwise the device might go to sleep as soon as we release it. + // - since we use non-reference counted wakelocks we have to be sure not to do + // the release if the wakelock didn't change. Othewise we would do an + // acquire followed by a release on the same lock, resulting in no lock + // being held. + PowerManager.WakeLock oldWakeLock = mSyncWakeLock; + try { + mSyncWakeLock = getSyncWakeLock(op.account.type, op.authority); + mSyncWakeLock.acquire(); + } finally { + if (oldWakeLock != null && oldWakeLock != mSyncWakeLock) { + oldWakeLock.release(); + } + } + // no need to schedule an alarm, as that will be done by our caller. // the next step will occur when we get either a timeout or a diff --git a/core/java/android/content/XmlDocumentProvider.java b/core/java/android/content/XmlDocumentProvider.java new file mode 100644 index 0000000..153ad38 --- /dev/null +++ b/core/java/android/content/XmlDocumentProvider.java @@ -0,0 +1,436 @@ +/* + * 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.content; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ContentResolver.OpenResourceIdResult; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.net.http.AndroidHttpClient; +import android.util.Log; +import android.widget.CursorAdapter; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.BitSet; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * A read-only content provider which extracts data out of an XML document. + * + * <p>A XPath-like selection pattern is used to select some nodes in the XML document. Each such + * node will create a row in the {@link Cursor} result.</p> + * + * Each row is then populated with columns that are also defined as XPath-like projections. These + * projections fetch attributes values or text in the matching row node or its children. + * + * <p>To add this provider in your application, you should add its declaration to your application + * manifest: + * <pre class="prettyprint"> + * <provider android:name="android.content.XmlDocumentProvider" android:authorities="xmldocument" /> + * </pre> + * </p> + * + * <h2>Node selection syntax</h2> + * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of + * <code>/node_name</code> node selection patterns. + * + * <p>The <code>/root/child1/child2</code> pattern will for instance match all nodes named + * <code>child2</code> which are children of a node named <code>child1</code> which are themselves + * children of a root node named <code>root</code>.</p> + * + * Any <code>/</code> separator in the previous expression can be replaced by a <code>//</code> + * separator instead, which indicated a <i>descendant</i> instead of a child. + * + * <p>The <code>//node1//node2</code> pattern will for instance match all nodes named + * <code>node2</code> which are descendant of a node named <code>node1</code> located anywhere in + * the document hierarchy.</p> + * + * Node names can contain namespaces in the form <code>namespace:node</code>. + * + * <h2>Projection syntax</h2> + * For every selected node, the projection will then extract actual data from this node and its + * descendant. + * + * <p>Use a syntax similar to the selection syntax described above to select the text associated + * with a child of the selected node. The implicit root of this projection pattern is the selected + * node. <code>/</code> will hence refer to the text of the selected node, while + * <code>/child1</code> will fetch the text of its child named <code>child1</code> and + * <code>//child1</code> will match any <i>descendant</i> named <code>child1</code>. If several + * nodes match the projection pattern, their texts are appended as a result.</p> + * + * A projection can also fetch any node attribute by appending a <code>@attribute_name</code> + * pattern to the previously described syntax. <code>//child1@price</code> will for instance match + * the attribute <code>price</code> of any <code>child1</code> descendant. + * + * <p>If a projection does not match any node/attribute, its associated value will be an empty + * string.</p> + * + * <h2>Example</h2> + * Using the following XML document: + * <pre class="prettyprint"> + * <library> + * <book id="EH94"> + * <title>The Old Man and the Sea</title> + * <author>Ernest Hemingway</author> + * </book> + * <book id="XX10"> + * <title>The Arabian Nights: Tales of 1,001 Nights</title> + * </book> + * <no-id> + * <book> + * <title>Animal Farm</title> + * <author>George Orwell</author> + * </book> + * </no-id> + * </library> + * </pre> + * A selection pattern of <code>/library//book</code> will match the three book entries (while + * <code>/library/book</code> will only match the first two ones). + * + * <p>Defining the projections as <code>/title</code>, <code>/author</code> and <code>@id</code> + * will retrieve the associated data. Note that the author of the second book as well as the id of + * the third are empty strings. + */ +public class XmlDocumentProvider extends ContentProvider { + /* + * Ideas for improvement: + * - Expand XPath-like syntax to allow for [nb] child number selector + * - Address the starting . bug in AbstractCursor which prevents a true XPath syntax. + * - Provide an alternative to concatenation when several node match (list-like). + * - Support namespaces in attribute names. + * - Incremental Cursor creation, pagination + */ + private static final String LOG_TAG = "XmlDocumentProvider"; + private AndroidHttpClient mHttpClient; + + @Override + public boolean onCreate() { + return true; + } + + /** + * Query data from the XML document referenced in the URI. + * + * <p>The XML document can be a local resource or a file that will be downloaded from the + * Internet. In the latter case, your application needs to request the INTERNET permission in + * its manifest.</p> + * + * The URI will be of the form <code>content://xmldocument/?resource=R.xml.myFile</code> for a + * local resource. <code>xmldocument</code> should match the authority declared for this + * provider in your manifest. Internet documents are referenced using + * <code>content://xmldocument/?url=</code> followed by an encoded version of the URL of your + * document (see {@link Uri#encode(String)}). + * + * <p>The number of columns of the resulting Cursor is equal to the size of the projection + * array plus one, named <code>_id</code> which will contain a unique row id (allowing the + * Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection + * patterns.</p> + * + * @param uri The URI of your local resource or Internet document. + * @param projection A set of patterns that will be used to extract data from each selected + * node. See class documentation for pattern syntax. + * @param selection A selection pattern which will select the nodes that will create the + * Cursor's rows. See class documentation for pattern syntax. + * @param selectionArgs This parameter is ignored. + * @param sortOrder The row order in the resulting cursor is determined from the node order in + * the XML document. This parameter is ignored. + * @return A Cursor or null in case of error. + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + + XmlPullParser parser = null; + mHttpClient = null; + + final String url = uri.getQueryParameter("url"); + if (url != null) { + parser = getUriXmlPullParser(url); + } else { + final String resource = uri.getQueryParameter("resource"); + if (resource != null) { + Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + getContext().getPackageName() + "/" + resource); + parser = getResourceXmlPullParser(resourceUri); + } + } + + if (parser != null) { + XMLCursor xmlCursor = new XMLCursor(selection, projection); + try { + xmlCursor.parseWith(parser); + return xmlCursor; + } catch (IOException e) { + Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e); + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Error while parsing XML " + uri, e); + } finally { + if (mHttpClient != null) { + mHttpClient.close(); + } + } + } + + return null; + } + + /** + * Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser. + * @param url The URL of the XML document that is to be parsed. + * @return An XmlPullParser on this document. + */ + protected XmlPullParser getUriXmlPullParser(String url) { + XmlPullParser parser = null; + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + parser = factory.newPullParser(); + } catch (XmlPullParserException e) { + Log.e(LOG_TAG, "Unable to create XmlPullParser", e); + return null; + } + + InputStream inputStream = null; + try { + final HttpGet get = new HttpGet(url); + mHttpClient = AndroidHttpClient.newInstance("Android"); + HttpResponse response = mHttpClient.execute(get); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final HttpEntity entity = response.getEntity(); + if (entity != null) { + inputStream = entity.getContent(); + } + } + } catch (IOException e) { + Log.w(LOG_TAG, "Error while retrieving XML file " + url, e); + return null; + } + + try { + parser.setInput(inputStream, null); + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Error while reading XML file from " + url, e); + return null; + } + + return parser; + } + + /** + * Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your + * own parser. + * @param resourceUri A fully qualified resource name referencing a local XML resource. + * @return An XmlPullParser on this resource. + */ + protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) { + OpenResourceIdResult resourceId; + try { + resourceId = getContext().getContentResolver().getResourceId(resourceUri); + return resourceId.r.getXml(resourceId.id); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e); + return null; + } + } + + /** + * Returns "vnd.android.cursor.dir/xmldoc". + */ + @Override + public String getType(Uri uri) { + return "vnd.android.cursor.dir/xmldoc"; + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + /** + * This ContentProvider is read-only. This method throws an UnsupportedOperationException. + **/ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + private static class XMLCursor extends MatrixCursor { + private final Pattern mSelectionPattern; + private Pattern[] mProjectionPatterns; + private String[] mAttributeNames; + private String[] mCurrentValues; + private BitSet[] mActiveTextDepthMask; + private final int mNumberOfProjections; + + public XMLCursor(String selection, String[] projections) { + super(projections); + // The first column in projections is used for the _ID + mNumberOfProjections = projections.length - 1; + mSelectionPattern = createPattern(selection); + createProjectionPattern(projections); + } + + private Pattern createPattern(String input) { + String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$"; + return Pattern.compile(pattern); + } + + private void createProjectionPattern(String[] projections) { + mProjectionPatterns = new Pattern[mNumberOfProjections]; + mAttributeNames = new String[mNumberOfProjections]; + mActiveTextDepthMask = new BitSet[mNumberOfProjections]; + // Add a column to store _ID + mCurrentValues = new String[mNumberOfProjections + 1]; + + for (int i=0; i<mNumberOfProjections; i++) { + mActiveTextDepthMask[i] = new BitSet(); + String projection = projections[i + 1]; // +1 to skip the _ID column + int atIndex = projection.lastIndexOf('@', projection.length()); + if (atIndex >= 0) { + mAttributeNames[i] = projection.substring(atIndex+1); + projection = projection.substring(0, atIndex); + } else { + mAttributeNames[i] = null; + } + + // Conforms to XPath standard: reference to local context starts with a . + if (projection.charAt(0) == '.') { + projection = projection.substring(1); + } + mProjectionPatterns[i] = createPattern(projection); + } + } + + public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException { + StringBuilder path = new StringBuilder(); + Stack<Integer> pathLengthStack = new Stack<Integer>(); + + // There are two parsing mode: in root mode, rootPath is updated and nodes matching + // selectionPattern are searched for and currentNodeDepth is negative. + // When a node matching selectionPattern is found, currentNodeDepth is set to 0 and + // updated as children are parsed and projectionPatterns are searched in nodePath. + int currentNodeDepth = -1; + + // Index where local selected node path starts from in path + int currentNodePathStartIndex = 0; + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + + if (eventType == XmlPullParser.START_TAG) { + // Update path + pathLengthStack.push(path.length()); + path.append('/'); + String prefix = null; + try { + // getPrefix is not supported by local Xml resource parser + prefix = parser.getPrefix(); + } catch (RuntimeException e) { + prefix = null; + } + if (prefix != null) { + path.append(prefix); + path.append(':'); + } + path.append(parser.getName()); + + if (currentNodeDepth >= 0) { + currentNodeDepth++; + } else { + // A node matching selection is found: initialize child parsing mode + if (mSelectionPattern.matcher(path.toString()).matches()) { + currentNodeDepth = 0; + currentNodePathStartIndex = path.length(); + mCurrentValues[0] = Integer.toString(getCount()); // _ID + for (int i = 0; i < mNumberOfProjections; i++) { + // Reset values to default (empty string) + mCurrentValues[i + 1] = ""; + mActiveTextDepthMask[i].clear(); + } + } + } + + // This test has to be separated from the previous one as currentNodeDepth can + // be modified above (when a node matching selection is found). + if (currentNodeDepth >= 0) { + final String localNodePath = path.substring(currentNodePathStartIndex); + for (int i = 0; i < mNumberOfProjections; i++) { + if (mProjectionPatterns[i].matcher(localNodePath).matches()) { + String attribute = mAttributeNames[i]; + if (attribute != null) { + mCurrentValues[i + 1] = + parser.getAttributeValue(null, attribute); + } else { + mActiveTextDepthMask[i].set(currentNodeDepth, true); + } + } + } + } + + } else if (eventType == XmlPullParser.END_TAG) { + // Pop last node from path + final int length = pathLengthStack.pop(); + path.setLength(length); + + if (currentNodeDepth >= 0) { + if (currentNodeDepth == 0) { + // Leaving a selection matching node: add a new row with results + addRow(mCurrentValues); + } else { + for (int i = 0; i < mNumberOfProjections; i++) { + mActiveTextDepthMask[i].set(currentNodeDepth, false); + } + } + currentNodeDepth--; + } + + } else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) { + for (int i = 0; i < mNumberOfProjections; i++) { + if ((currentNodeDepth >= 0) && + (mActiveTextDepthMask[i].get(currentNodeDepth))) { + mCurrentValues[i + 1] += parser.getText(); + } + } + } + + eventType = parser.next(); + } + } + } +} diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 7901b155..35f22dc 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -284,6 +284,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public static final int FLAG_HEAVY_WEIGHT = 1<<20; /** + * Value for {@link #flags}: true when the application's rendering should + * be hardware accelerated. + */ + public static final int FLAG_HARDWARE_ACCELERATED = 1<<21; + + /** * Value for {@link #flags}: this is true if the application has set * its android:neverEncrypt to true, false otherwise. It is used to specify * that this package specifically "opts-out" of a secured file system solution, diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 5dc41d2..6098617 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1536,6 +1536,12 @@ public class PackageParser { } if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated, + false)) { + ai.flags |= ApplicationInfo.FLAG_HARDWARE_ACCELERATED; + } + + if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hasCode, true)) { ai.flags |= ApplicationInfo.FLAG_HAS_CODE; diff --git a/core/java/android/content/res/PluralRules.java b/core/java/android/content/res/PluralRules.java deleted file mode 100644 index 2dce3c1..0000000 --- a/core/java/android/content/res/PluralRules.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2007 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.res; - -import java.util.Locale; - -/* - * Yuck-o. This is not the right way to implement this. When the ICU PluralRules - * object has been integrated to android, we should switch to that. For now, yuck-o. - */ - -abstract class PluralRules { - - static final int QUANTITY_OTHER = 0x0000; - static final int QUANTITY_ZERO = 0x0001; - static final int QUANTITY_ONE = 0x0002; - static final int QUANTITY_TWO = 0x0004; - static final int QUANTITY_FEW = 0x0008; - static final int QUANTITY_MANY = 0x0010; - - static final int ID_OTHER = 0x01000004; - - abstract int quantityForNumber(int n); - - final int attrForNumber(int n) { - return PluralRules.attrForQuantity(quantityForNumber(n)); - } - - static final int attrForQuantity(int quantity) { - // see include/utils/ResourceTypes.h - switch (quantity) { - case QUANTITY_ZERO: return 0x01000005; - case QUANTITY_ONE: return 0x01000006; - case QUANTITY_TWO: return 0x01000007; - case QUANTITY_FEW: return 0x01000008; - case QUANTITY_MANY: return 0x01000009; - default: return ID_OTHER; - } - } - - static final String stringForQuantity(int quantity) { - switch (quantity) { - case QUANTITY_ZERO: - return "zero"; - case QUANTITY_ONE: - return "one"; - case QUANTITY_TWO: - return "two"; - case QUANTITY_FEW: - return "few"; - case QUANTITY_MANY: - return "many"; - default: - return "other"; - } - } - - static final PluralRules ruleForLocale(Locale locale) { - String lang = locale.getLanguage(); - if ("cs".equals(lang)) { - if (cs == null) cs = new cs(); - return cs; - } - else { - if (en == null) en = new en(); - return en; - } - } - - private static PluralRules cs; - private static class cs extends PluralRules { - int quantityForNumber(int n) { - if (n == 1) { - return QUANTITY_ONE; - } - else if (n >= 2 && n <= 4) { - return QUANTITY_FEW; - } - else { - return QUANTITY_OTHER; - } - } - } - - private static PluralRules en; - private static class en extends PluralRules { - int quantityForNumber(int n) { - if (n == 1) { - return QUANTITY_ONE; - } - else { - return QUANTITY_OTHER; - } - } - } -} - diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 0608cc0..5ac55c4 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -16,7 +16,6 @@ package android.content.res; - import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -41,6 +40,8 @@ import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.Locale; +import libcore.icu.NativePluralRules; + /** * Class for accessing an application's resources. This sits on top of the * asset manager of the application (accessible through getAssets()) and @@ -52,6 +53,8 @@ public class Resources { private static final boolean DEBUG_CONFIG = false; private static final boolean TRACE_FOR_PRELOAD = false; + private static final int ID_OTHER = 0x01000004; + // Use the current SDK version code. If we are a development build, // also allow the previous SDK version + 1. private static final int sSdkVersion = Build.VERSION.SDK_INT @@ -86,7 +89,7 @@ public class Resources { /*package*/ final AssetManager mAssets; private final Configuration mConfiguration = new Configuration(); /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); - PluralRules mPluralRule; + private NativePluralRules mPluralRule; private CompatibilityInfo mCompatibilityInfo; private Display mDefaultDisplay; @@ -203,9 +206,17 @@ public class Resources { } /** + * Return the character sequence associated with a particular resource ID for a particular + * numerical quantity. + * + * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String + * Resources</a> for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules. * * @throws NotFoundException Throws NotFoundException if the given ID does not exist. * @@ -213,29 +224,52 @@ public class Resources { * possibly styled text information. */ public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { - PluralRules rule = getPluralRule(); - CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity)); + NativePluralRules rule = getPluralRule(); + CharSequence res = mAssets.getResourceBagText(id, + attrForQuantityCode(rule.quantityForInt(quantity))); if (res != null) { return res; } - res = mAssets.getResourceBagText(id, PluralRules.ID_OTHER); + res = mAssets.getResourceBagText(id, ID_OTHER); if (res != null) { return res; } throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id) + " quantity=" + quantity - + " item=" + PluralRules.stringForQuantity(rule.quantityForNumber(quantity))); + + " item=" + stringForQuantityCode(rule.quantityForInt(quantity))); } - private PluralRules getPluralRule() { + private NativePluralRules getPluralRule() { synchronized (mSync) { if (mPluralRule == null) { - mPluralRule = PluralRules.ruleForLocale(mConfiguration.locale); + mPluralRule = NativePluralRules.forLocale(mConfiguration.locale); } return mPluralRule; } } + private static int attrForQuantityCode(int quantityCode) { + switch (quantityCode) { + case NativePluralRules.ZERO: return 0x01000005; + case NativePluralRules.ONE: return 0x01000006; + case NativePluralRules.TWO: return 0x01000007; + case NativePluralRules.FEW: return 0x01000008; + case NativePluralRules.MANY: return 0x01000009; + default: return ID_OTHER; + } + } + + private static String stringForQuantityCode(int quantityCode) { + switch (quantityCode) { + case NativePluralRules.ZERO: return "zero"; + case NativePluralRules.ONE: return "one"; + case NativePluralRules.TWO: return "two"; + case NativePluralRules.FEW: return "few"; + case NativePluralRules.MANY: return "many"; + default: return "other"; + } + } + /** * Return the string value associated with a particular resource ID. It * will be stripped of any styled text information. @@ -290,6 +324,9 @@ public class Resources { * stripped of any styled text information. * {@more} * + * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String + * Resources</a> for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. @@ -312,6 +349,9 @@ public class Resources { * Return the string value associated with a particular resource ID for a particular * numerical quantity. * + * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String + * Resources</a> for more on quantity strings. + * * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. @@ -1334,7 +1374,7 @@ public class Resources { } synchronized (mSync) { if (mPluralRule != null) { - mPluralRule = PluralRules.ruleForLocale(config.locale); + mPluralRule = NativePluralRules.forLocale(config.locale); } } } diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index a5e5e46..9b14998 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -18,16 +18,11 @@ package android.database; import android.content.ContentResolver; import android.net.Uri; +import android.os.Bundle; import android.util.Config; import android.util.Log; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; import java.lang.ref.WeakReference; -import java.lang.UnsupportedOperationException; import java.util.HashMap; import java.util.Map; @@ -56,6 +51,10 @@ public abstract class AbstractCursor implements CrossProcessCursor { abstract public double getDouble(int column); abstract public boolean isNull(int column); + public int getType(int column) { + throw new UnsupportedOperationException(); + } + // TODO implement getBlob in all cursor types public byte[] getBlob(int column) { throw new UnsupportedOperationException("getBlob is not supported"); @@ -88,7 +87,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { } mDataSetObservable.notifyInvalidated(); } - + public boolean requery() { if (mSelfObserver != null && mSelfObserverRegistered == false) { mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); @@ -109,22 +108,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * @hide - * @deprecated - */ - public boolean commitUpdates(Map<? extends Long,? extends Map<String,Object>> values) { - return false; - } - - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return false; - } - - /** * This function is called every time the cursor is successfully scrolled * to a new position, giving the subclass a chance to update any state it * may have. If it returns false the move function will also do so and the @@ -320,137 +303,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { return getColumnNames()[columnIndex]; } - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return update(columnIndex, Short.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return update(columnIndex, Integer.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return update(columnIndex, Long.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return update(columnIndex, Float.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return update(columnIndex, Double.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return update(columnIndex, null); - } - - /** - * @hide - * @deprecated - */ - public boolean update(int columnIndex, Object obj) { - if (!supportsUpdates()) { - return false; - } - - // Long.valueOf() returns null sometimes! -// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex)); - Long rowid = new Long(getLong(mRowIdColumnIndex)); - if (rowid == null) { - throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex); - } - - synchronized(mUpdatedRows) { - Map<String, Object> row = mUpdatedRows.get(rowid); - if (row == null) { - row = new HashMap<String, Object>(); - mUpdatedRows.put(rowid, row); - } - row.put(getColumnNames()[columnIndex], obj); - } - - return true; - } - - /** - * Returns <code>true</code> if there are pending updates that have not yet been committed. - * - * @return <code>true</code> if there are pending updates that have not yet been committed. - * @hide - * @deprecated - */ - public boolean hasUpdates() { - synchronized(mUpdatedRows) { - return mUpdatedRows.size() > 0; - } - } - - /** - * @hide - * @deprecated - */ - public void abortUpdates() { - synchronized(mUpdatedRows) { - mUpdatedRows.clear(); - } - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return commitUpdates(null); - } - - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mRowIdColumnIndex != -1; - } - public void registerContentObserver(ContentObserver observer) { mContentObservable.registerObserver(observer); } @@ -478,9 +330,9 @@ public abstract class AbstractCursor implements CrossProcessCursor { return mDataSetObservable; } + public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); - } public void unregisterDataSetObserver(DataSetObserver observer) { @@ -535,36 +387,19 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * This function returns true if the field has been updated and is - * used in conjunction with {@link #getUpdatedField} to allow subclasses to - * support reading uncommitted updates. NOTE: This function and - * {@link #getUpdatedField} should be called together inside of a - * block synchronized on mUpdatedRows. - * - * @param columnIndex the column index of the field to check - * @return true if the field has been updated, false otherwise + * @deprecated Always returns false since Cursors do not support updating rows */ + @Deprecated protected boolean isFieldUpdated(int columnIndex) { - if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) { - Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID); - if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) { - return true; - } - } return false; } /** - * This function returns the uncommitted updated value for the field - * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should - * be called together inside of a block synchronized on mUpdatedRows. - * - * @param columnIndex the column index of the field to retrieve - * @return the updated value + * @deprecated Always returns null since Cursors do not support updating rows */ + @Deprecated protected Object getUpdatedField(int columnIndex) { - Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID); - return updates.get(getColumnNames()[columnIndex]); + return null; } /** @@ -614,11 +449,9 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * This HashMap contains a mapping from Long rowIDs to another Map - * that maps from String column names to new values. A NULL value means to - * remove an existing value, and all numeric values are in their class - * forms, i.e. Integer, Long, Float, etc. + * @deprecated This is never updated by this class and should not be used */ + @Deprecated protected HashMap<Long, Map<String, Object>> mUpdatedRows; /** @@ -628,6 +461,11 @@ public abstract class AbstractCursor implements CrossProcessCursor { protected int mRowIdColumnIndex; protected int mPos; + /** + * If {@link #mRowIdColumnIndex} is not -1 this contains contains the value of + * the column at {@link #mRowIdColumnIndex} for the current row this cursor is + * pointing at. + */ protected Long mCurrentRowID; protected ContentResolver mContentResolver; protected boolean mClosed = false; diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index 27a02e2..8addaa8 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -19,202 +19,105 @@ package android.database; /** * A base class for Cursors that store their data in {@link CursorWindow}s. */ -public abstract class AbstractWindowedCursor extends AbstractCursor -{ +public abstract class AbstractWindowedCursor extends AbstractCursor { @Override - public byte[] getBlob(int columnIndex) - { + public byte[] getBlob(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return (byte[])getUpdatedField(columnIndex); - } - } - return mWindow.getBlob(mPos, columnIndex); } @Override - public String getString(int columnIndex) - { + public String getString(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return (String)getUpdatedField(columnIndex); - } - } - return mWindow.getString(mPos, columnIndex); } - + @Override - public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) - { + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - super.copyStringToBuffer(columnIndex, buffer); - } - } - mWindow.copyStringToBuffer(mPos, columnIndex, buffer); } @Override - public short getShort(int columnIndex) - { + public short getShort(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.shortValue(); - } - } - return mWindow.getShort(mPos, columnIndex); } @Override - public int getInt(int columnIndex) - { + public int getInt(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.intValue(); - } - } - return mWindow.getInt(mPos, columnIndex); } @Override - public long getLong(int columnIndex) - { + public long getLong(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.longValue(); - } - } - return mWindow.getLong(mPos, columnIndex); } @Override - public float getFloat(int columnIndex) - { + public float getFloat(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.floatValue(); - } - } - return mWindow.getFloat(mPos, columnIndex); } @Override - public double getDouble(int columnIndex) - { + public double getDouble(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Number value = (Number)getUpdatedField(columnIndex); - return value.doubleValue(); - } - } - return mWindow.getDouble(mPos, columnIndex); } @Override - public boolean isNull(int columnIndex) - { + public boolean isNull(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - return getUpdatedField(columnIndex) == null; - } - } - - return mWindow.isNull(mPos, columnIndex); + return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL; } - public boolean isBlob(int columnIndex) - { - checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object == null || object instanceof byte[]; - } - } - - return mWindow.isBlob(mPos, columnIndex); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isBlob(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_BLOB; } - public boolean isString(int columnIndex) - { - checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object == null || object instanceof String; - } - } - - return mWindow.isString(mPos, columnIndex); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isString(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_STRING; } - public boolean isLong(int columnIndex) - { - checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object != null && (object instanceof Integer || object instanceof Long); - } - } + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isLong(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER; + } - return mWindow.isLong(mPos, columnIndex); + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isFloat(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_FLOAT; } - public boolean isFloat(int columnIndex) - { + @Override + public int getType(int columnIndex) { checkPosition(); - - synchronized(mUpdatedRows) { - if (isFieldUpdated(columnIndex)) { - Object object = getUpdatedField(columnIndex); - return object != null && (object instanceof Float || object instanceof Double); - } - } - - return mWindow.isFloat(mPos, columnIndex); + return mWindow.getType(mPos, columnIndex); } @Override - protected void checkPosition() - { + protected void checkPosition() { super.checkPosition(); if (mWindow == null) { - throw new StaleDataException("Access closed cursor"); + throw new StaleDataException("Attempting to access a closed cursor"); } } diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index baa94d8..fa62d69 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -17,13 +17,10 @@ package android.database; import android.os.Binder; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; -import android.os.Bundle; - -import java.util.HashMap; -import java.util.Map; +import android.os.RemoteException; /** * Native implementation of the bulk cursor. This is only for use in implementing @@ -120,26 +117,6 @@ public abstract class BulkCursorNative extends Binder implements IBulkCursor return true; } - case UPDATE_ROWS_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - // TODO - what ClassLoader should be passed to readHashMap? - // TODO - switch to Bundle - HashMap<Long, Map<String, Object>> values = data.readHashMap(null); - boolean result = updateRows(values); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - - case DELETE_ROW_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - int position = data.readInt(); - boolean result = deleteRow(position); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - case ON_MOVE_TRANSACTION: { data.enforceInterface(IBulkCursor.descriptor); int position = data.readInt(); @@ -343,48 +320,6 @@ final class BulkCursorProxy implements IBulkCursor { return count; } - public boolean updateRows(Map values) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeMap(values); - - mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - - public boolean deleteRow(int position) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeInt(position); - - mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - public boolean getWantsAllOnMoveCalls() throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index 1469ea2..2cb2aec 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -16,12 +16,10 @@ package android.database; -import android.os.RemoteException; import android.os.Bundle; +import android.os.RemoteException; import android.util.Log; -import java.util.Map; - /** * Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local * process. @@ -174,38 +172,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - try { - boolean result = mBulkCursor.deleteRow(mPos); - if (result != false) { - // The window contains the old value, discard it - mWindow = null; - - // Fix up the position - mCount = mBulkCursor.count(); - if (mPos < mCount) { - int oldPos = mPos; - mPos = -1; - moveToPosition(oldPos); - } else { - mPos = mCount; - } - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to delete row because the remote process is dead"); - return false; - } - } - @Override public String[] getColumnNames() { if (mColumns == null) { @@ -219,44 +185,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map<? extends Long, - ? extends Map<String,Object>> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?"); - return false; - } - - synchronized(mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() <= 0) { - return false; - } - - try { - boolean result = mBulkCursor.updateRows(mUpdatedRows); - - if (result == true) { - mUpdatedRows.clear(); - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to commit updates because the remote process is dead"); - return false; - } - } - } - @Override public Bundle getExtras() { try { diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index 6539156..c03c586 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -30,6 +30,25 @@ import java.util.Map; * threads should perform its own synchronization when using the Cursor. */ public interface Cursor { + /* + * Values returned by {@link #getType(int)}. + * These should be consistent with the corresponding types defined in CursorWindow.h + */ + /** Value returned by {@link #getType(int)} if the specified column is null */ + static final int FIELD_TYPE_NULL = 0; + + /** Value returned by {@link #getType(int)} if the specified column type is integer */ + static final int FIELD_TYPE_INTEGER = 1; + + /** Value returned by {@link #getType(int)} if the specified column type is float */ + static final int FIELD_TYPE_FLOAT = 2; + + /** Value returned by {@link #getType(int)} if the specified column type is string */ + static final int FIELD_TYPE_STRING = 3; + + /** Value returned by {@link #getType(int)} if the specified column type is blob */ + static final int FIELD_TYPE_BLOB = 4; + /** * Returns the numbers of rows in the cursor. * @@ -146,22 +165,6 @@ public interface Cursor { boolean isAfterLast(); /** - * Removes the row at the current cursor position from the underlying data - * store. After this method returns the cursor will be pointing to the row - * after the row that is deleted. This has the side effect of decrementing - * the result of count() by one. - * <p> - * The query must have the row ID column in its selection, otherwise this - * call will fail. - * - * @hide - * @return whether the record was successfully deleted. - * @deprecated use {@link ContentResolver#delete(Uri, String, String[])} - */ - @Deprecated - boolean deleteRow(); - - /** * Returns the zero-based index for the given column name, or -1 if the column doesn't exist. * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which * will make the error more clear. @@ -295,194 +298,33 @@ public interface Cursor { double getDouble(int columnIndex); /** - * Returns <code>true</code> if the value in the indicated column is null. - * - * @param columnIndex the zero-based index of the target column. - * @return whether the column value is null. - */ - boolean isNull(int columnIndex); - - /** - * Returns <code>true</code> if the cursor supports updates. - * - * @return whether the cursor supports updates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean supportsUpdates(); - - /** - * Returns <code>true</code> if there are pending updates that have not yet been committed. - * - * @return <code>true</code> if there are pending updates that have not yet been committed. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean hasUpdates(); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateBlob(int columnIndex, byte[] value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateString(int columnIndex, String value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. + * Returns data type of the given column's value. + * The preferred type of the column is returned but the data may be converted to other types + * as documented in the get-type methods such as {@link #getInt(int)}, {@link #getFloat(int)} + * etc. + *<p> + * Returned column types are + * <ul> + * <li>{@link #FIELD_TYPE_NULL}</li> + * <li>{@link #FIELD_TYPE_INTEGER}</li> + * <li>{@link #FIELD_TYPE_FLOAT}</li> + * <li>{@link #FIELD_TYPE_STRING}</li> + * <li>{@link #FIELD_TYPE_BLOB}</li> + *</ul> + *</p> * * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods + * @return column value type */ - @Deprecated - boolean updateShort(int columnIndex, short value); + int getType(int columnIndex); /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateInt(int columnIndex, int value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateLong(int columnIndex, long value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateFloat(int columnIndex, float value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateDouble(int columnIndex, double value); - - /** - * Removes the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. + * Returns <code>true</code> if the value in the indicated column is null. * * @param columnIndex the zero-based index of the target column. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateToNull(int columnIndex); - - /** - * Atomically commits all updates to the backing store. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(); - - /** - * Atomically commits all updates to the backing store, as well as the - * updates included in values. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @param values A map from row IDs to Maps associating column names with - * updated values. A null value indicates the field should be - removed. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(Map<? extends Long, - ? extends Map<String,Object>> values); - - /** - * Reverts all updates made to the cursor since the last call to - * commitUpdates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods + * @return whether the column value is null. */ - @Deprecated - void abortUpdates(); + boolean isNull(int columnIndex); /** * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. @@ -496,6 +338,10 @@ public interface Cursor { * contents. This may be done at any time, including after a call to {@link * #deactivate}. * + * Since this method could execute a query on the database and potentially take + * a while, it could cause ANR if it is called on Main (UI) thread. + * A warning is printed if this method is being executed on Main thread. + * * @return true if the requery succeeded, false if not, in which case the * cursor becomes invalid. */ diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 748eb99..8bc7de2 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -16,16 +16,12 @@ package android.database; -import android.database.sqlite.SQLiteMisuseException; -import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Config; import android.util.Log; -import java.util.Map; - /** * Wraps a BulkCursor around an existing Cursor making it remotable. @@ -38,7 +34,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative private final CrossProcessCursor mCursor; private CursorWindow mWindow; private final String mProviderName; - private final boolean mReadOnly; private ContentObserverProxy mObserver; private static final class ContentObserverProxy extends ContentObserver @@ -98,7 +93,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative "Only CrossProcessCursor cursors are supported across process for now", e); } mProviderName = providerName; - mReadOnly = !allowWrite; createAndRegisterObserverProxy(observer); } @@ -197,31 +191,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } } - public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - return mCursor.commitUpdates(values); - } - - public boolean deleteRow(int position) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - if (mCursor.moveToPosition(position) == false) { - return false; - } - return mCursor.deleteRow(); - } - public Bundle getExtras() { return mCursor.getExtras(); } diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index c756825..599431f 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -217,18 +217,13 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return {@code true} if given field is {@code NULL} + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isNull(int row, int col) { - acquireReference(); - try { - return isNull_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_NULL; } - private native boolean isNull_native(int row, int col); - /** * Returns a byte array for the given field. * @@ -248,35 +243,56 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { private native byte[] getBlob_native(int row, int col); /** - * Checks if a field contains either a blob or is null. + * Returns data type of the given column's value. + *<p> + * Returned column types are + * <ul> + * <li>{@link Cursor#FIELD_TYPE_NULL}</li> + * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li> + * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li> + * <li>{@link Cursor#FIELD_TYPE_STRING}</li> + * <li>{@link Cursor#FIELD_TYPE_BLOB}</li> + *</ul> + *</p> * * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from - * @return {@code true} if given field is {@code NULL} or a blob + * @return the value type */ - public boolean isBlob(int row, int col) { + public int getType(int row, int col) { acquireReference(); try { - return isBlob_native(row - mStartPos, col); + return getType_native(row - mStartPos, col); } finally { releaseReference(); } } /** + * Checks if a field contains either a blob or is null. + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is {@code NULL} or a blob + * @deprecated use {@link #getType(int, int)} instead + */ + @Deprecated + public boolean isBlob(int row, int col) { + int type = getType(row, col); + return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL; + } + + /** * Checks if a field contains a long * * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return {@code true} if given field is a long + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isLong(int row, int col) { - acquireReference(); - try { - return isInteger_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_INTEGER; } /** @@ -285,14 +301,11 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return {@code true} if given field is a float + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isFloat(int row, int col) { - acquireReference(); - try { - return isFloat_native(row - mStartPos, col); - } finally { - releaseReference(); - } + return getType(row, col) == Cursor.FIELD_TYPE_FLOAT; } /** @@ -301,20 +314,15 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return {@code true} if given field is {@code NULL} or a String + * @deprecated use {@link #getType(int, int)} instead */ + @Deprecated public boolean isString(int row, int col) { - acquireReference(); - try { - return isString_native(row - mStartPos, col); - } finally { - releaseReference(); - } + int type = getType(row, col); + return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL; } - private native boolean isBlob_native(int row, int col); - private native boolean isString_native(int row, int col); - private native boolean isInteger_native(int row, int col); - private native boolean isFloat_native(int row, int col); + private native int getType_native(int row, int col); /** * Returns a String for the given field. diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java index f0aa7d7..3c3bd43 100644 --- a/core/java/android/database/CursorWrapper.java +++ b/core/java/android/database/CursorWrapper.java @@ -17,28 +17,26 @@ package android.database; import android.content.ContentResolver; -import android.database.CharArrayBuffer; import android.net.Uri; import android.os.Bundle; -import java.util.Map; - /** - * Wrapper class for Cursor that delegates all calls to the actual cursor object + * Wrapper class for Cursor that delegates all calls to the actual cursor object. The primary + * use for this class is to extend a cursor while overriding only a subset of its methods. */ - public class CursorWrapper implements Cursor { + private final Cursor mCursor; + public CursorWrapper(Cursor cursor) { mCursor = cursor; } - + /** - * @hide - * @deprecated + * @return the wrapped cursor */ - public void abortUpdates() { - mCursor.abortUpdates(); + public Cursor getWrappedCursor() { + return mCursor; } public void close() { @@ -49,23 +47,6 @@ public class CursorWrapper implements Cursor { return mCursor.isClosed(); } - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return mCursor.commitUpdates(); - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates( - Map<? extends Long, ? extends Map<String, Object>> values) { - return mCursor.commitUpdates(values); - } - public int getCount() { return mCursor.getCount(); } @@ -74,14 +55,6 @@ public class CursorWrapper implements Cursor { mCursor.deactivate(); } - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return mCursor.deleteRow(); - } - public boolean moveToFirst() { return mCursor.moveToFirst(); } @@ -147,14 +120,6 @@ public class CursorWrapper implements Cursor { return mCursor.getWantsAllOnMoveCalls(); } - /** - * @hide - * @deprecated - */ - public boolean hasUpdates() { - return mCursor.hasUpdates(); - } - public boolean isAfterLast() { return mCursor.isAfterLast(); } @@ -171,6 +136,10 @@ public class CursorWrapper implements Cursor { return mCursor.isLast(); } + public int getType(int columnIndex) { + return mCursor.getType(columnIndex); + } + public boolean isNull(int columnIndex) { return mCursor.isNull(columnIndex); } @@ -219,14 +188,6 @@ public class CursorWrapper implements Cursor { mCursor.setNotificationUri(cr, uri); } - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mCursor.supportsUpdates(); - } - public void unregisterContentObserver(ContentObserver observer) { mCursor.unregisterContentObserver(observer); } @@ -234,72 +195,5 @@ public class CursorWrapper implements Cursor { public void unregisterDataSetObserver(DataSetObserver observer) { mCursor.unregisterDataSetObserver(observer); } - - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return mCursor.updateDouble(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return mCursor.updateFloat(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return mCursor.updateInt(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return mCursor.updateLong(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return mCursor.updateShort(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return mCursor.updateString(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return mCursor.updateBlob(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return mCursor.updateToNull(columnIndex); - } - - private Cursor mCursor; - } diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java index 9200e81..51c72c1 100644 --- a/core/java/android/database/DataSetObservable.java +++ b/core/java/android/database/DataSetObservable.java @@ -27,8 +27,12 @@ public class DataSetObservable extends Observable<DataSetObserver> { */ public void notifyChanged() { synchronized(mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onChanged(); + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); } } } @@ -39,8 +43,8 @@ public class DataSetObservable extends Observable<DataSetObserver> { */ public void notifyInvalidated() { synchronized (mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onInvalidated(); + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onInvalidated(); } } } diff --git a/core/java/android/pim/vcard/exception/VCardException.java b/core/java/android/database/DatabaseErrorHandler.java index e557219..f0c5452 100644 --- a/core/java/android/pim/vcard/exception/VCardException.java +++ b/core/java/android/database/DatabaseErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * 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. @@ -13,23 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.pim.vcard.exception; -public class VCardException extends java.lang.Exception { - /** - * Constructs a VCardException object - */ - public VCardException() { - super(); - } +package android.database; + +import android.database.sqlite.SQLiteDatabase; + +/** + * An interface to let the apps define the actions to take when the following errors are detected + * database corruption + */ +public interface DatabaseErrorHandler { /** - * Constructs a VCardException object - * - * @param message the error message + * defines the method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. */ - public VCardException(String message) { - super(message); - } - + void onCorruption(SQLiteDatabase dbObj); } diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 9bfbb74..af93eee 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -193,6 +193,37 @@ public class DatabaseUtils { } /** + * Returns data type of the given object's value. + *<p> + * Returned values are + * <ul> + * <li>{@link Cursor#FIELD_TYPE_NULL}</li> + * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li> + * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li> + * <li>{@link Cursor#FIELD_TYPE_STRING}</li> + * <li>{@link Cursor#FIELD_TYPE_BLOB}</li> + *</ul> + *</p> + * + * @param obj the object whose value type is to be returned + * @return object value type + * @hide + */ + public static int getTypeOfObject(Object obj) { + if (obj == null) { + return Cursor.FIELD_TYPE_NULL; + } else if (obj instanceof byte[]) { + return Cursor.FIELD_TYPE_BLOB; + } else if (obj instanceof Float || obj instanceof Double) { + return Cursor.FIELD_TYPE_FLOAT; + } else if (obj instanceof Long || obj instanceof Integer) { + return Cursor.FIELD_TYPE_INTEGER; + } else { + return Cursor.FIELD_TYPE_STRING; + } + } + + /** * Appends an SQL string to the given StringBuilder, including the opening * and closing single quotes. Any single quotes internal to sqlString will * be escaped. diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java new file mode 100644 index 0000000..3619e48 --- /dev/null +++ b/core/java/android/database/DefaultDatabaseErrorHandler.java @@ -0,0 +1,94 @@ +/* + * 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.database; + +import java.io.File; +import java.util.ArrayList; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +/** + * Default class used defining the actions to take when the following errors are detected + * database corruption + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(dbObj.getPath()); + return; + } + + ArrayList<Pair<String, String>> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = dbObj.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair<String, String> p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(dbObj.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.e(TAG, "deleting the database file: " + fileName); + try { + new File(fileName).delete(); + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: " + e.getMessage()); + } + } +} diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 46790a3..244c88f 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -16,16 +16,14 @@ package android.database; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; -import android.os.Bundle; - -import java.util.Map; +import android.os.RemoteException; /** * This interface provides a low-level way to pass bulk cursor data across - * both process and language boundries. Application code should use the Cursor + * both process and language boundaries. Application code should use the Cursor * interface directly. * * {@hide} @@ -54,10 +52,6 @@ public interface IBulkCursor extends IInterface { */ public String[] getColumnNames() throws RemoteException; - public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) throws RemoteException; - - public boolean deleteRow(int position) throws RemoteException; - public void deactivate() throws RemoteException; public void close() throws RemoteException; @@ -76,8 +70,6 @@ public interface IBulkCursor extends IInterface { static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; static final int COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; - static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; - static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5; static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6; static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7; diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java index d5c3a32..5c1b968 100644 --- a/core/java/android/database/MatrixCursor.java +++ b/core/java/android/database/MatrixCursor.java @@ -272,6 +272,11 @@ public class MatrixCursor extends AbstractCursor { } @Override + public int getType(int column) { + return DatabaseUtils.getTypeOfObject(get(column)); + } + + @Override public boolean isNull(int column) { return get(column) == null; } diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java index 722d707..2c25db7 100644 --- a/core/java/android/database/MergeCursor.java +++ b/core/java/android/database/MergeCursor.java @@ -92,32 +92,6 @@ public class MergeCursor extends AbstractCursor return false; } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() - { - return mCursor.deleteRow(); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates() { - int length = mCursors.length; - for (int i = 0 ; i < length ; i++) { - if (mCursors[i] != null) { - mCursors[i].commitUpdates(); - } - } - onChange(true); - return true; - } - @Override public String getString(int column) { @@ -155,6 +129,11 @@ public class MergeCursor extends AbstractCursor } @Override + public int getType(int column) { + return mCursor.getType(column); + } + + @Override public boolean isNull(int column) { return mCursor.isNull(column); diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/database/RequeryOnUiThreadException.java index 9fe8b7f..97a50d8 100644 --- a/core/java/android/pim/vcard/exception/VCardVersionException.java +++ b/core/java/android/database/RequeryOnUiThreadException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * 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. @@ -14,16 +14,16 @@ * limitations under the License. */ -package android.pim.vcard.exception; +package android.database; /** - * VCardException used only when the version of the vCard is different. + * An exception that indicates invoking {@link Cursor#requery()} on Main thread could cause ANR. + * This exception should encourage apps to invoke {@link Cursor#requery()} in a background thread. + * @hide */ -public class VCardVersionException extends VCardException { - public VCardVersionException() { - super(); - } - public VCardVersionException(String message) { - super(message); +public class RequeryOnUiThreadException extends RuntimeException { + public RequeryOnUiThreadException(String packageName) { + super("In " + packageName + " Requery is executing on main (UI) thread. could cause ANR. " + + "do it in background thread."); } } diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java new file mode 100644 index 0000000..50b2919 --- /dev/null +++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java @@ -0,0 +1,361 @@ +/* + * 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.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. It is set based on the amount of memory the device has. + * TODO: set this with 'a system call' which returns the amount of memory the device has + */ + private static final int DEFAULT_CONNECTION_POOL_SIZE = 1; + + /** the pool size set for this {@link SQLiteDatabase} */ + private volatile int mMaxPoolSize = DEFAULT_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 */ void close() { + synchronized(mParentDbObj) { + 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 */ SQLiteDatabase get(String sql) { + SQLiteDatabase db = null; + PoolObj poolObj = null; + synchronized(mParentDbObj) { + 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.isSqlInStatementCache(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.isSqlInStatementCache(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 */ void release(SQLiteDatabase db) { + PoolObj poolObj; + synchronized(mParentDbObj) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert db.mConnectionNum > 0; + doAsserts(); + assert mPool.get(db.mConnectionNum - 1).mDb == db; + } + + 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 */ ArrayList<SQLiteDatabase> getConnectionList() { + ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>(); + synchronized(mParentDbObj) { + 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); + } + } + + /* package */ void setMaxPoolSize(int size) { + synchronized(mParentDbObj) { + mMaxPoolSize = size; + } + } + + /* package */ int getMaxPoolSize() { + synchronized(mParentDbObj) { + 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/DatabaseObjectNotClosedException.java b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java index 8ac4c0f..f28c70f 100644 --- a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java +++ b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java @@ -21,13 +21,11 @@ package android.database.sqlite; * that is not explicitly closed * @hide */ -public class DatabaseObjectNotClosedException extends RuntimeException -{ +public class DatabaseObjectNotClosedException extends RuntimeException { private static final String s = "Application did not close the cursor or database object " + "that was opened here"; - public DatabaseObjectNotClosedException() - { + public DatabaseObjectNotClosedException() { super(s); } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java index 25aa9b3..9889a21 100644 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -78,20 +78,13 @@ import android.util.Log; * existing compiled SQL program already around */ private void compile(String sql, boolean forceCompilation) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } + mDatabase.verifyLockOwner(); // Only compile if we don't have a valid statement already or the caller has // explicitly requested a recompile. if (forceCompilation) { - mDatabase.lock(); - try { - // Note that the native_compile() takes care of destroying any previously - // existing programs before it compiles. - native_compile(sql); - } finally { - mDatabase.unlock(); - } + // Note that the native_compile() takes care of destroying any previously + // existing programs before it compiles. + native_compile(sql); } } @@ -102,13 +95,8 @@ import android.util.Log; if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.v(TAG, "closed and deallocated DbObj (id#" + nStatement +")"); } - try { - mDatabase.lock(); - native_finalize(); - nStatement = 0; - } finally { - mDatabase.unlock(); - } + mDatabase.finalizeStatementLater(nStatement); + nStatement = 0; } } @@ -134,6 +122,10 @@ import android.util.Log; mInUse = false; } + /* package */ synchronized boolean isInUse() { + return mInUse; + } + /** * Make sure that the native resource is cleaned up. */ @@ -162,5 +154,4 @@ import android.util.Log; * @param sql The SQL to compile. */ private final native void native_compile(String sql); - private final native void native_finalize(); } diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c7e58fa..eecd01e 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -16,20 +16,19 @@ package android.database.sqlite; +import android.app.ActivityThread; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; import android.database.DataSetObserver; -import android.database.SQLException; - +import android.database.RequeryOnUiThreadException; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.Process; -import android.text.TextUtils; import android.util.Config; import android.util.Log; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; @@ -77,6 +76,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { private int mCursorState = 0; private ReentrantLock mLock = null; private boolean mPendingData = false; + + /** + * Used by {@link #requery()} to remember for which database we've already shown the warning. + */ + private static final HashMap<String, Boolean> sAlreadyWarned = new HashMap<String, Boolean>(); /** * support for a cursor variant that doesn't always read all results @@ -321,166 +325,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - checkPosition(); - - // Only allow deletes if there is an ID column, and the ID has been read from it - if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { - Log.e(TAG, - "Could not delete row because either the row ID column is not available or it" + - "has not been read."); - return false; - } - - boolean success; - - /* - * Ensure we don't change the state of the database when another - * thread is holding the database lock. requery() and moveTo() are also - * synchronized here to make sure they get the state of the database - * immediately following the DELETE. - */ - mDatabase.lock(); - try { - try { - mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", - new String[] {mCurrentRowID.toString()}); - success = true; - } catch (SQLException e) { - success = false; - } - - int pos = mPos; - requery(); - - /* - * Ensure proper cursor state. Note that mCurrentRowID changes - * in this call. - */ - moveToPosition(pos); - } finally { - mDatabase.unlock(); - } - - if (success) { - onChange(true); - return true; - } else { - return false; - } - } - @Override public String[] getColumnNames() { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean supportsUpdates() { - return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map<? extends Long, - ? extends Map<String, Object>> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you " - + "include the _id column?"); - return false; - } - - /* - * Prevent other threads from changing the updated rows while they're - * being processed here. - */ - synchronized (mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() == 0) { - return true; - } - - /* - * Prevent other threads from changing the database state while - * we process the updated rows, and prevents us from changing the - * database behind the back of another thread. - */ - mDatabase.beginTransaction(); - try { - StringBuilder sql = new StringBuilder(128); - - // For each row that has been updated - for (Map.Entry<Long, Map<String, Object>> rowEntry : - mUpdatedRows.entrySet()) { - Map<String, Object> values = rowEntry.getValue(); - Long rowIdObj = rowEntry.getKey(); - - if (rowIdObj == null || values == null) { - throw new IllegalStateException("null rowId or values found! rowId = " - + rowIdObj + ", values = " + values); - } - - if (values.size() == 0) { - continue; - } - - long rowId = rowIdObj.longValue(); - - Iterator<Map.Entry<String, Object>> valuesIter = - values.entrySet().iterator(); - - sql.setLength(0); - sql.append("UPDATE " + mEditTable + " SET "); - - // For each column value that has been updated - Object[] bindings = new Object[values.size()]; - int i = 0; - while (valuesIter.hasNext()) { - Map.Entry<String, Object> entry = valuesIter.next(); - sql.append(entry.getKey()); - sql.append("=?"); - bindings[i] = entry.getValue(); - if (valuesIter.hasNext()) { - sql.append(", "); - } - i++; - } - - sql.append(" WHERE " + mColumns[mRowIdColumnIndex] - + '=' + rowId); - sql.append(';'); - mDatabase.execSQL(sql.toString(), bindings); - mDatabase.rowUpdated(mEditTable, rowId); - } - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - - mUpdatedRows.clear(); - } - - // Let any change observers know about the update - onChange(true); - - return true; - } - private void deactivateCommon() { if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); mCursorState = 0; @@ -506,11 +355,30 @@ public class SQLiteCursor extends AbstractWindowedCursor { mDriver.cursorClosed(); } + /** + * Show a warning against the use of requery() if called on the main thread. + * This warning is shown per database per process. + */ + private void warnIfUiThread() { + if (Looper.getMainLooper() == Looper.myLooper()) { + String databasePath = mDatabase.getPath(); + // We show the warning once per database in order not to spam logcat. + if (!sAlreadyWarned.containsKey(databasePath)) { + sAlreadyWarned.put(databasePath, true); + String packageName = ActivityThread.currentPackageName(); + Log.w(TAG, "should not attempt requery on main (UI) thread: app = " + + packageName == null ? "'unknown'" : packageName, + new RequeryOnUiThreadException(packageName)); + } + } + } + @Override public boolean requery() { if (isClosed()) { return false; } + warnIfUiThread(); long timeStart = 0; if (Config.LOGV) { timeStart = System.currentTimeMillis(); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index cdc9bbb..441370a 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,16 +16,16 @@ package android.database.sqlite; -import com.google.android.collect.Maps; - -import android.app.ActivityThread; import android.app.AppGlobals; import android.content.ContentValues; import android.database.Cursor; +import android.database.DatabaseErrorHandler; 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.text.TextUtils; @@ -38,11 +38,11 @@ import dalvik.system.BlockGuard; import java.io.File; import java.lang.ref.WeakReference; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Random; @@ -233,48 +233,81 @@ public class SQLiteDatabase extends SQLiteClosable { // lock acquistions of the database. /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; - /** Used by native code, do not rename */ - /* package */ int mNativeHandle = 0; + /** Used by native code, do not rename. make it volatile, so it is thread-safe. */ + /* package */ volatile int mNativeHandle = 0; /** Used to make temp table names unique */ /* package */ int mTempTableSequence = 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 String mPath; + 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 int mFlags; + private final int mFlags; /** The optional factory to use when creating new Cursors */ - private CursorFactory mFactory; + private final CursorFactory mFactory; - private WeakHashMap<SQLiteClosable, Object> mPrograms; + private final WeakHashMap<SQLiteClosable, Object> mPrograms; /** - * for each instance of this class, a cache is maintained to store + * 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 + * 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. + * 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. * * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method - * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because - * most of the apps don't use "?" syntax in their sql, caching is not useful for them. - */ - /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap(); + * (@link setMaxSqlCacheSize(int)}). + */ + // default statement-cache size per database connection ( = instance of this class) + private int mMaxSqlCacheSize = 25; + /* package */ final Map<String, SQLiteCompiledSql> mCompiledQueries = + new LinkedHashMap<String, SQLiteCompiledSql>(mMaxSqlCacheSize + 1, 0.75f, true) { + @Override + public boolean removeEldestEntry(Map.Entry<String, SQLiteCompiledSql> eldest) { + // eldest = least-recently used entry + // if it needs to be removed to accommodate a new entry, + // close {@link SQLiteCompiledSql} represented by this entry, if not in use + // and then let it be removed from the Map. + // when this is called, the caller must be trying to add a just-compiled stmt + // to cache; i.e., caller should already have acquired database lock AND + // the lock on mCompiledQueries. do as assert of these two 2 facts. + verifyLockOwner(); + if (this.size() <= mMaxSqlCacheSize) { + // cache is not full. nothing needs to be removed + return false; + } + // cache is full. eldest will be removed. + SQLiteCompiledSql entry = eldest.getValue(); + if (!entry.isInUse()) { + // this {@link SQLiteCompiledSql} is not in use. release it. + entry.releaseSqlStatement(); + } + // return true, so that this entry is removed automatically by the caller. + return true; + } + }; /** - * @hide + * 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. */ - public static final int MAX_SQL_CACHE_SIZE = 250; - private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance + public static final int MAX_SQL_CACHE_SIZE = 100; private int mCacheFullWarnings; private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; @@ -282,45 +315,49 @@ public class SQLiteDatabase extends SQLiteClosable { private int mNumCacheHits; private int mNumCacheMisses; - /** the following 2 members maintain the time when a database is opened and closed */ - private String mTimeOpened = null; - private String mTimeClosed = null; - /** Used to find out where this object was created in case it never got closed. */ - private Throwable mStackTrace = null; + private final Throwable mStackTrace; // System property that enables logging of slow queries. Specify the threshold in ms. private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; private final int mSlowQueryThreshold; - /** - * @param closable + /** 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. */ - void addSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.put(closable, null); - } finally { - unlock(); - } + /* 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; + + private static final String MEMORY_DB_PATH = ":memory:"; + + 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); } - void removeSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.remove(closable); - } finally { - unlock(); - } + synchronized void removeSQLiteClosable(SQLiteClosable closable) { + mPrograms.remove(closable); } @Override protected void onAllReferencesReleased() { if (isOpen()) { - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - dbclose(); + // close the database which will close all pending statements to be finalized also + close(); } } @@ -350,19 +387,8 @@ public class SQLiteDatabase extends SQLiteClosable { private boolean mLockingEnabled = true; /* package */ void onCorruption() { - Log.e(TAG, "Removing corrupt database: " + mPath); EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - try { - // Close the database (if we can), which will cause subsequent operations to fail. - close(); - } finally { - // Delete the corrupt file. Don't re-create it now -- that would just confuse people - // -- but the next time someone tries to open it, they can set it up from scratch. - if (!mPath.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(mPath).delete(); - } - } + mErrorHandler.onCorruption(this); } /** @@ -460,11 +486,14 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * Begins a transaction in EXCLUSIVE mode. + * <p> + * Transactions can be nested. + * When the outer transaction is ended all of * the work done in that transaction and all of the nested transactions will be committed or * rolled back. The changes will be rolled back if any transaction is ended without being * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. - * + * </p> * <p>Here is the standard idiom for transactions: * * <pre> @@ -478,15 +507,42 @@ public class SQLiteDatabase extends SQLiteClosable { * </pre> */ public void beginTransaction() { - beginTransactionWithListener(null /* transactionStatusCallback */); + beginTransaction(null /* transactionStatusCallback */, true); } /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When + * the outer transaction is ended all of the work done in that transaction + * and all of the nested transactions will be committed or rolled back. The + * changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they + * will be committed. + * <p> + * Here is the standard idiom for transactions: + * + * <pre> + * db.beginTransactionNonExclusive(); + * try { + * ... + * db.setTransactionSuccessful(); + * } finally { + * db.endTransaction(); + * } + * </pre> + */ + public void beginTransactionNonExclusive() { + beginTransaction(null /* transactionStatusCallback */, false); + } + + /** + * Begins a transaction in EXCLUSIVE mode. + * <p> + * Transactions can be nested. + * When the outer transaction is ended all of * the work done in that transaction and all of the nested transactions will be committed or * rolled back. The changes will be rolled back if any transaction is ended without being * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. - * + * </p> * <p>Here is the standard idiom for transactions: * * <pre> @@ -498,15 +554,48 @@ public class SQLiteDatabase extends SQLiteClosable { * db.endTransaction(); * } * </pre> + * * @param transactionListener listener that should be notified when the transaction begins, * commits, or is rolled back, either explicitly or by a call to * {@link #yieldIfContendedSafely}. */ public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { + beginTransaction(transactionListener, true); + } + + /** + * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When + * the outer transaction is ended all of the work done in that transaction + * and all of the nested transactions will be committed or rolled back. The + * changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they + * will be committed. + * <p> + * Here is the standard idiom for transactions: + * + * <pre> + * db.beginTransactionWithListenerNonExclusive(listener); + * try { + * ... + * db.setTransactionSuccessful(); + * } finally { + * db.endTransaction(); + * } + * </pre> + * + * @param transactionListener listener that should be notified when the + * transaction begins, commits, or is rolled back, either + * explicitly or by a call to {@link #yieldIfContendedSafely}. + */ + public void beginTransactionWithListenerNonExclusive( + SQLiteTransactionListener transactionListener) { + beginTransaction(transactionListener, false); + } + + private void beginTransaction(SQLiteTransactionListener transactionListener, + boolean exclusive) { + verifyDbIsOpen(); lockForced(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } boolean ok = false; try { // If this thread already had the lock then get out @@ -524,7 +613,14 @@ public class SQLiteDatabase extends SQLiteClosable { // This thread didn't already have the lock, so begin a database // transaction now. - execSQL("BEGIN EXCLUSIVE;"); + // STOPSHIP - uncomment the following 1 line + // if (exclusive) { + // STOPSHIP - remove the following 1 line + if (exclusive && mConnectionPool == null) { + execSQL("BEGIN EXCLUSIVE;"); + } else { + execSQL("BEGIN IMMEDIATE;"); + } mTransactionListener = transactionListener; mTransactionIsSuccessful = true; mInnerTransactionIsSuccessful = false; @@ -551,12 +647,7 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } + verifyLockOwner(); try { if (mInnerTransactionIsSuccessful) { mInnerTransactionIsSuccessful = false; @@ -581,6 +672,18 @@ public class SQLiteDatabase extends SQLiteClosable { } 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"); + } + } } else { try { execSQL("ROLLBACK;"); @@ -614,9 +717,7 @@ public class SQLiteDatabase extends SQLiteClosable { * transaction is already marked as successful. */ public void setTransactionSuccessful() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } @@ -814,30 +915,72 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLiteException if the database cannot be opened */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { - SQLiteDatabase sqliteDatabase = null; + return openDatabase(path, factory, flags, new DefaultDatabaseErrorHandler()); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + * <p>Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.</p> + * + * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.</p> + * + * @param path to database file to open and/or create + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode + * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption + * when sqlite reports database corruption + * @return the newly opened database + * @throws SQLiteException if the database cannot be opened + */ + 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); + //STOPSHIP - uncomment the following line + //sqliteDatabase.setJournalMode(path, "TRUNCATE"); + // STOPSHIP remove the following lines + sqliteDatabase.enableWriteAheadLogging(); + + // add this database to the list of databases opened in this process + ActiveDatabases.addActiveDatabase(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 { // Open the database. - sqliteDatabase = new SQLiteDatabase(path, factory, flags); + db.dbopen(path, flags); + db.setLocale(Locale.getDefault()); if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - sqliteDatabase.enableSqlTracing(path); + db.enableSqlTracing(path, connectionNum); } if (SQLiteDebug.DEBUG_SQL_TIME) { - sqliteDatabase.enableSqlProfiling(path); + db.enableSqlProfiling(path, connectionNum); } + return db; } catch (SQLiteDatabaseCorruptException e) { - // Try to recover from this, if we can. - // TODO: should we do this for other open failures? - Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); - EventLog.writeEvent(EVENT_DB_CORRUPT, path); - if (!path.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(path).delete(); - } - sqliteDatabase = new SQLiteDatabase(path, factory, flags); + 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; } - ActiveDatabases.getInstance().mActiveDatabases.add( - new WeakReference<SQLiteDatabase>(sqliteDatabase)); - return sqliteDatabase; } /** @@ -855,6 +998,25 @@ public class SQLiteDatabase extends SQLiteClosable { } /** + * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + 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 + if (!dbPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + 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); + } + } + } + + /** * Create a memory backed SQLite database. Its contents will be destroyed * when the database is closed. * @@ -867,7 +1029,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase create(CursorFactory factory) { // This is a magic string with special meaning for SQLite. - return openDatabase(":memory:", factory, CREATE_IF_NECESSARY); + return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY); } /** @@ -877,18 +1039,26 @@ public class SQLiteDatabase extends SQLiteClosable { if (!isOpen()) { return; // already closed } + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum); + } lock(); try { closeClosable(); + // finalize ALL statements queued up so far + closePendingStatements(); // close this database instance - regardless of its reference count value - onAllReferencesReleased(); + dbclose(); + if (mConnectionPool != null) { + mConnectionPool.close(); + } } finally { unlock(); } } private void closeClosable() { - /* deallocate all compiled sql statement objects from mCompiledQueries cache. + /* 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 @@ -918,19 +1088,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the database version */ public int getVersion() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, "PRAGMA user_version;"); - long version = prog.simpleQueryForLong(); - return (int) version; - } finally { - if (prog != null) prog.close(); - unlock(); - } + return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue(); } /** @@ -948,20 +1106,8 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the new maximum database size */ public long getMaximumSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA max_page_count;"); - long pageCount = prog.simpleQueryForLong(); - return pageCount * getPageSize(); - } finally { - if (prog != null) prog.close(); - unlock(); - } + long pageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count;", null); + return pageCount * getPageSize(); } /** @@ -972,26 +1118,15 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the new maximum database size */ public long setMaximumSize(long numBytes) { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - long pageSize = getPageSize(); - long numPages = numBytes / pageSize; - // If numBytes isn't a multiple of pageSize, bump up a page - if ((numBytes % pageSize) != 0) { - numPages++; - } - prog = new SQLiteStatement(this, - "PRAGMA max_page_count = " + numPages); - long newPageCount = prog.simpleQueryForLong(); - return newPageCount * pageSize; - } finally { - if (prog != null) prog.close(); - unlock(); + long pageSize = getPageSize(); + long numPages = numBytes / pageSize; + // If numBytes isn't a multiple of pageSize, bump up a page + if ((numBytes % pageSize) != 0) { + numPages++; } + long newPageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count = " + numPages, + null); + return newPageCount * pageSize; } /** @@ -1000,20 +1135,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the database page size, in bytes */ public long getPageSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA page_size;"); - long size = prog.simpleQueryForLong(); - return size; - } finally { - if (prog != null) prog.close(); - unlock(); - } + return DatabaseUtils.longForQuery(this, "PRAGMA page_size;", null); } /** @@ -1101,7 +1223,7 @@ public class SQLiteDatabase extends SQLiteClosable { if (info != null) { execSQL("UPDATE " + info.masterTable + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey - + " FROM " + table + " WHERE _id=" + rowId + ")"); + + " FROM " + table + " WHERE _id=?)", new String[] {String.valueOf(rowId)}); } } @@ -1134,6 +1256,8 @@ public class SQLiteDatabase extends SQLiteClosable { * statement and fill in those values with {@link SQLiteProgram#bindString} * and {@link SQLiteProgram#bindLong} each time you want to run the * statement. Statements may not return result sets larger than 1x1. + *<p> + * No two threads should be using the same {@link SQLiteStatement} at the same time. * * @param sql The raw SQL statement, may contain ? for unknown values to be * bound later. @@ -1141,15 +1265,8 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. */ public SQLiteStatement compileStatement(String sql) throws SQLException { - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - return new SQLiteStatement(this, sql); - } finally { - unlock(); - } + verifyDbIsOpen(); + return new SQLiteStatement(this, sql); } /** @@ -1226,9 +1343,7 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); @@ -1339,9 +1454,7 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } + verifyDbIsOpen(); BlockGuard.getThreadPolicy().onReadFromDisk(); long timeStart = 0; @@ -1349,7 +1462,8 @@ public class SQLiteDatabase extends SQLiteClosable { timeStart = System.currentTimeMillis(); } - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + SQLiteDatabase db = getDbConnection(sql); + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); Cursor cursor = null; try { @@ -1375,6 +1489,7 @@ public class SQLiteDatabase extends SQLiteClosable { : "<null>") + ", count is " + count); } } + releaseDbConnection(db); } return cursor; } @@ -1501,10 +1616,8 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) { + verifyDbIsOpen(); BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } // Measurements show most sql lengths <= 152 StringBuilder sql = new StringBuilder(152); @@ -1593,11 +1706,9 @@ public class SQLiteDatabase extends SQLiteClosable { * whereClause. */ public int delete(String table, String whereClause, String[] whereArgs) { + verifyDbIsOpen(); BlockGuard.getThreadPolicy().onWriteToDisk(); lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } SQLiteStatement statement = null; try { statement = compileStatement("DELETE FROM " + table @@ -1677,10 +1788,8 @@ public class SQLiteDatabase extends SQLiteClosable { sql.append(whereClause); } + verifyDbIsOpen(); lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } SQLiteStatement statement = null; try { statement = compileStatement(sql.toString()); @@ -1725,21 +1834,39 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock + * Execute a single SQL statement that is NOT a SELECT + * or any other SQL statement that returns data. + * <p> + * Use of this method is discouraged as it doesn't perform well when issuing the same SQL + * statement repeatedly (see {@link #compileStatement(String)} to prepare statements for + * repeated use), and it has no means to return any data (such as the number of affected rows). + * Instead, you're encouraged to use {@link #insert(String, String, ContentValues)}, + * {@link #update(String, ContentValues, String, String[])}, et al, when possible. + * </p> + * <p> + * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'<value>" statement if your app is using + * {@link #enableWriteAheadLogging()} + * </p> * + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { + sql = sql.trim(); + String prefix = sql.substring(0, 6); + if (prefix.equalsIgnoreCase("ATTACH")) { + disableWriteAheadLogging(); + } + verifyDbIsOpen(); BlockGuard.getThreadPolicy().onWriteToDisk(); long timeStart = SystemClock.uptimeMillis(); lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); try { + closePendingStatements(); native_execSQL(sql); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); @@ -1759,11 +1886,45 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock, + * Execute a single SQL statement that is NOT a SELECT/INSERT/UPDATE/DELETE. + * <p> + * For INSERT statements, use any of the following instead. + * <ul> + * <li>{@link #insert(String, String, ContentValues)}</li> + * <li>{@link #insertOrThrow(String, String, ContentValues)}</li> + * <li>{@link #insertWithOnConflict(String, String, ContentValues, int)}</li> + * </ul> + * <p> + * For UPDATE statements, use any of the following instead. + * <ul> + * <li>{@link #update(String, ContentValues, String, String[])}</li> + * <li>{@link #updateWithOnConflict(String, ContentValues, String, String[], int)}</li> + * </ul> + * <p> + * For DELETE statements, use any of the following instead. + * <ul> + * <li>{@link #delete(String, String, String[])}</li> + * </ul> + * <p> + * For example, the following are good candidates for using this method: + * <ul> + * <li>ALTER TABLE</li> + * <li>CREATE or DROP table / trigger / view / index / virtual table</li> + * <li>REINDEX</li> + * <li>RELEASE</li> + * <li>SAVEPOINT</li> + * <li>PRAGMA that returns no data</li> + * </ul> + * </p> + * <p> + * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'<value>" statement if your app is using + * {@link #enableWriteAheadLogging()} + * </p> * - * @param sql + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. * @throws SQLException If the SQL string is invalid for some reason */ @@ -1772,11 +1933,9 @@ public class SQLiteDatabase extends SQLiteClosable { if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } + verifyDbIsOpen(); long timeStart = SystemClock.uptimeMillis(); lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } SQLiteStatement statement = null; try { statement = compileStatement(sql); @@ -1810,14 +1969,19 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Private constructor. See {@link #create} and {@link #openDatabase}. + * Private constructor. * * @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. */ - private SQLiteDatabase(String path, CursorFactory factory, int flags) { + private SQLiteDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler, short connectionNum) { if (path == null) { throw new IllegalArgumentException("path should not be null"); } @@ -1826,25 +1990,11 @@ public class SQLiteDatabase extends SQLiteClosable { mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; - dbopen(mPath, mFlags); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeOpened = getTime(); - } mPrograms = new WeakHashMap<SQLiteClosable,Object>(); - try { - setLocale(Locale.getDefault()); - } catch (RuntimeException e) { - Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); - dbclose(); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - throw e; - } - } - - private String getTime() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); + // 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; } /** @@ -1970,6 +2120,20 @@ public class SQLiteDatabase extends SQLiteClosable { } } + /* package */ void verifyDbIsOpen() { + if (!isOpen()) { + throw new IllegalStateException("database " + getPath() + " (conn# " + + mConnectionNum + ") already closed"); + } + } + + /* package */ void verifyLockOwner() { + verifyDbIsOpen(); + if (mLockingEnabled && !isDbLockedByCurrentThread()) { + throw new IllegalStateException("Don't have database lock!"); + } + } + /* * ============================================================================ * @@ -1977,22 +2141,14 @@ public class SQLiteDatabase extends SQLiteClosable { * ============================================================================ */ /** - * adds the given sql and its compiled-statement-id-returned-by-sqlite to the + * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the * cache of compiledQueries attached to 'this'. - * - * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, + * <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 */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); - } - return; - } - SQLiteCompiledSql compiledSql = null; synchronized(mCompiledQueries) { // don't insert the new mapping if a mapping already exists @@ -2000,35 +2156,30 @@ public class SQLiteDatabase extends SQLiteClosable { if (compiledSql != null) { return; } - // add this <sql, compiledStatement> to the cache + if (mCompiledQueries.size() == mMaxSqlCacheSize) { /* * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. - * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times - * chances are it is NOT using ? for bindargs - so caching is useless. - * TODO: either let the callers set max cchesize for their app, or intelligently - * figure out what should be cached for a given app. + * log a warning. + * chances are it is NOT using ? for bindargs - or cachesize is too small. */ if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + "; i.e., NO space for this sql statement in cache: " + - sql + ". Please change your sql statements to use '?' for " + - "bindargs, instead of using actual values"); - } - // don't add this entry to cache - } else { - // cache is NOT full. add this to cache. - mCompiledQueries.put(sql, compiledStatement); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + - mCompiledQueries.size() + "|" + sql); + getPath() + ". Consider increasing cachesize."); } + } + /* add the given SQLiteCompiledSql compiledStatement to cache. + * no need to worry about the cache size - because {@link #mCompiledQueries} + * self-limits its size to {@link #mMaxSqlCacheSize}. + */ + mCompiledQueries.put(sql, compiledStatement); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + + mCompiledQueries.size() + "|" + sql); } } - return; } - private void deallocCachedSqlStatements() { synchronized (mCompiledQueries) { for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { @@ -2039,20 +2190,13 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * from the compiledQueries cache, returns the compiled-statement-id for the given sql. - * returns null, if not found in the cache. + * From the compiledQueries cache, returns the compiled-statement-id for the given SQL. + * Returns null, if not found in the cache. */ /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { SQLiteCompiledSql compiledStatement = null; boolean cacheHit; synchronized(mCompiledQueries) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache NOT found|" + getPath()); - } - return null; - } cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; } if (cacheHit) { @@ -2065,80 +2209,248 @@ public class SQLiteDatabase extends SQLiteClosable { Log.v(TAG, "|cache_stats|" + getPath() + "|" + mCompiledQueries.size() + "|" + mNumCacheHits + "|" + mNumCacheMisses + - "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); + "|" + cacheHit + "|" + sql); } return compiledStatement; } /** - * returns true if the given sql is cached in compiled-sql cache. - * @hide + * 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). + *<p> + * Maximum cache size can ONLY be increased from its current size (default = 10). + * If this method is called with smaller size than the current maximum value, + * then IllegalStateException is thrown. + *<p> + * 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. */ - public boolean isInCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { + public synchronized void setMaxSqlCacheSize(int cacheSize) { + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); + } else if (cacheSize < mMaxSqlCacheSize) { + throw new IllegalStateException("cannot set cacheSize to a value less than the value " + + "set with previous setMaxSqlCacheSize() call."); + } + mMaxSqlCacheSize = cacheSize; + } + + /* package */ boolean isSqlInStatementCache(String sql) { + synchronized (mCompiledQueries) { return mCompiledQueries.containsKey(sql); } } + /* 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); + } + } + /** - * purges the given sql from the compiled-sql cache. + * public visibility only for testing. otherwise, package visibility is sufficient * @hide */ - public void purgeFromCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - mCompiledQueries.remove(sql); + public 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(); + } + // finalize all the statements from the copied list + int size = list.size(); + for (int i = 0; i < size; i++) { + native_finalize(list.get(i)); } } /** - * remove everything from the compiled sql cache + * for testing only * @hide */ - public void resetCompiledSqlCache() { - synchronized(mCompiledQueries) { - mCompiledQueries.clear(); + public ArrayList<Integer> getQueuedUpStmtList() { + return mClosedStatementIds; + } + + /** + * This method enables parallel execution of queries from multiple threads on the same database. + * It does this by opening multiple handles to the database and using a different + * database handle for each query. + * <p> + * If a transaction is in progress on one connection handle and say, a table is updated in the + * transaction, then query on the same table on another connection handle will block for the + * transaction to complete. But this method enables such queries to execute by having them + * return old version of the data from the table. Most often it is the data that existed in the + * table prior to the above transaction updates on that table. + * <p> + * Maximum number of simultaneous handles used to execute queries in parallel is + * dependent upon the device memory and possibly other properties. + * <p> + * After calling this method, execution of queries in parallel is enabled as long as this + * database handle is open. To disable execution of queries in parallel, database should + * be closed and reopened. + * <p> + * If a query is part of a transaction, then it is executed on the same database handle the + * transaction was begun. + * <p> + * If the database has any attached databases, then execution of queries in paralel is NOT + * possible. In such cases, a message is printed to logcat and false is returned. + * <p> + * This feature is not available for :memory: databases. In such cases, + * a message is printed to logcat and false is returned. + * <p> + * A typical way to use this method is the following: + * <pre> + * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory, + * CREATE_IF_NECESSARY, myDatabaseErrorHandler); + * db.enableWriteAheadLogging(); + * </pre> + * <p> + * Writers should use {@link #beginTransactionNonExclusive()} or + * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)} + * to start a trsnsaction. + * Non-exclusive mode allows database file to be in readable by threads executing queries. + * </p> + * + * @return true if write-ahead-logging is set. false otherwise + */ + public synchronized boolean enableWriteAheadLogging() { + if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + 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 (getAttachedDbs().size() > 1) { + Log.i(TAG, "this database: " + mPath + " has attached databases. can't enable WAL."); + return false; + } + if (mConnectionPool == null) { + mConnectionPool = new DatabaseConnectionPool(this); + setJournalMode(mPath, "WAL"); + } + return true; } /** - * return the current maxCacheSqlCacheSize - * @hide + * package visibility only for testing purposes */ - public synchronized int getMaxSqlCacheSize() { - return mMaxSqlCacheSize; + /* package */ synchronized void disableWriteAheadLogging() { + if (mConnectionPool == null) { + return; + } + mConnectionPool.close(); + mConnectionPool = null; } /** - * set the max size of the compiled sql cache for this database after purging the cache. - * (size of the cache = number of compiled-sql-statements stored in the cache). - * - * max cache size can ONLY be increased from its current size (default = 0). - * if this method is called with smaller size than the current value of mMaxSqlCacheSize, - * then IllegalStateException is thrown + * Sets the database connection handle pool size to the given value. + * Database connection handle pool is enabled when the app calls + * {@link #enableWriteAheadLogging()}. + * <p> + * The default connection handle pool is set by the system by taking into account various + * aspects of the device, such as memory, number of cores etc. It is recommended that + * applications use the default pool size set by the system. * - * synchronized because we don't want t threads to change cache size at the same time. - * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) - * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or - * < the value set with previous setMaxSqlCacheSize() call. - * - * @hide + * @param size the value the connection handle pool size should be set to. */ - public synchronized void setMaxSqlCacheSize(int cacheSize) { - if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); - } else if (cacheSize < mMaxSqlCacheSize) { - throw new IllegalStateException("cannot set cacheSize to a value less than the value " + - "set with previous setMaxSqlCacheSize() call."); + public synchronized void setConnectionPoolSize(int size) { + if (mConnectionPool == null) { + throw new IllegalStateException("connection pool not enabled"); } - mMaxSqlCacheSize = cacheSize; + int i = mConnectionPool.getMaxPoolSize(); + if (size < i) { + throw new IllegalArgumentException( + "cannot set max pool size to a value less than the current max value(=" + + i + ")"); + } + mConnectionPool.setMaxPoolSize(size); + } + + /* package */ SQLiteDatabase createPoolConnection(short connectionNum) { + return openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum); + } + + private boolean isPooledConnection() { + return this.mConnectionNum > 0; + } + + /* package */ SQLiteDatabase getDbConnection(String sql) { + verifyDbIsOpen(); + + // use the current connection handle if + // 1. this is a pooled connection handle + // 2. OR, if this thread is in a transaction + // 3. OR, if there is NO connection handle pool setup + SQLiteDatabase db = null; + if (isPooledConnection() || + (inTransaction() && mLock.isHeldByCurrentThread()) || + (this.mConnectionPool == null)) { + db = this; + } else { + // get a connection handle from the pool + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert mConnectionPool != null; + } + db = mConnectionPool.get(sql); + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "getDbConnection threadid = " + Thread.currentThread().getId() + + ", request on # " + mConnectionNum + + ", assigned # " + db.mConnectionNum + ", " + getPath()); + } + return db; + } + + 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()); + } + mConnectionPool.release(db); } static class ActiveDatabases { private static final ActiveDatabases activeDatabases = new ActiveDatabases(); private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases = - new HashSet<WeakReference<SQLiteDatabase>>(); + new HashSet<WeakReference<SQLiteDatabase>>(); private ActiveDatabases() {} // disable instantiation of this class - static ActiveDatabases getInstance() {return activeDatabases;} + static ActiveDatabases getInstance() { + return activeDatabases; + } + private static void addActiveDatabase(SQLiteDatabase sqliteDatabase) { + activeDatabases.mActiveDatabases.add(new WeakReference<SQLiteDatabase>(sqliteDatabase)); + } } /** @@ -2152,83 +2464,131 @@ public class SQLiteDatabase extends SQLiteClosable { if (db == null || !db.isOpen()) { continue; } - // 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); + try { + // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db + int lookasideUsed = db.native_getDbLookaside(); - // get list of attached dbs and for each db, get its size and pagesize - ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db); - if (attachedDbs == null) { - continue; - } - for (int i = 0; i < attachedDbs.size(); i++) { - Pair<String, String> p = attachedDbs.get(i); - long pageCount = getPragmaVal(db, p.first + ".page_count;"); - - // 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); + // 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 + ArrayList<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.mNumCacheHits, db.mNumCacheMisses, + db.mCompiledQueries.size())); } } - if (pageCount > 0) { - dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed)); + // if there are pooled connections, return the cache stats for them also. + if (db.mConnectionPool != null) { + for (SQLiteDatabase pDb : db.mConnectionPool.getConnectionList()) { + dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") " + + lastnode, 0, 0, 0, pDb.mNumCacheHits, pDb.mNumCacheMisses, + pDb.mCompiledQueries.size())); + } } + } catch (SQLiteException e) { + // ignore. we don't care about exceptions when we are taking adb + // bugreport! } } return dbStatsList; } /** - * get the specified pragma value from sqlite for the specified database. - * only handles pragma's that return int/long. - * NO JAVA locks are held in this method. - * TODO: use this to do all pragma's in this class + * Returns list of full pathnames of all attached databases including the main database + * by executing 'pragma database_list' on the database. + * + * @return ArrayList of pairs of (database name, database file path) or null if the database + * is not open. */ - private static long getPragmaVal(SQLiteDatabase db, String pragma) { - if (!db.isOpen()) { - return 0; + public ArrayList<Pair<String, String>> getAttachedDbs() { + if (!isOpen()) { + return null; } - SQLiteStatement prog = null; + ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); + Cursor c = null; try { - prog = new SQLiteStatement(db, "PRAGMA " + pragma); - long val = prog.simpleQueryForLong(); - return val; + c = rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + // sqlite returns a row for each database in the returned list of databases. + // in each row, + // 1st column is the database name such as main, or the database + // name specified on the "ATTACH" command + // 2nd column is the database file path. + attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); + } } finally { - if (prog != null) prog.close(); + if (c != null) { + c.close(); + } } + return attachedDbs; } /** - * returns list of full pathnames of all attached databases - * including the main database - * TODO: move this to {@link DatabaseUtils} - */ - private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) { - if (!dbObj.isOpen()) { - return null; - } - ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); - Cursor c = dbObj.rawQuery("pragma database_list;", null); - while (c.moveToNext()) { - attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); + * Runs 'pragma integrity_check' on the given database (and all the attached databases) + * and returns true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + *<p> + * If the result is false, then this method logs the errors reported by the integrity_check + * command execution. + *<p> + * Note that 'pragma integrity_check' on a database can take a long time. + * + * @return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + */ + public boolean isDatabaseIntegrityOk() { + verifyDbIsOpen(); + ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(); + if (attachedDbs == null) { + throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " + + "be retrieved. probably because the database is closed"); + } + boolean isDatabaseCorrupt = false; + for (int i = 0; i < attachedDbs.size(); i++) { + Pair<String, String> p = attachedDbs.get(i); + SQLiteStatement prog = null; + try { + prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);"); + String rslt = prog.simpleQueryForString(); + if (!rslt.equalsIgnoreCase("ok")) { + // integrity_checker failed on main or attached databases + isDatabaseCorrupt = true; + Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + } + } finally { + if (prog != null) prog.close(); + } } - c.close(); - return attachedDbs; + return isDatabaseCorrupt; } /** @@ -2239,21 +2599,27 @@ public class SQLiteDatabase extends SQLiteClosable { private native void dbopen(String path, int flags); /** - * Native call to setup tracing of all sql statements + * Native call to setup tracing of all SQL statements * * @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. */ - private native void enableSqlTracing(String path); + private native void enableSqlTracing(String path, short connectionNum); /** - * Native call to setup profiling of all sql statements. + * 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 + * (wall-clock time) of each of the SQL statements, as they * are executed. * * @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. */ - private native void enableSqlProfiling(String path); + private native void enableSqlProfiling(String path, short connectionNum); /** * Native call to execute a raw SQL statement. {@link #lock} must be held @@ -2291,4 +2657,11 @@ public class SQLiteDatabase extends SQLiteClosable { * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED */ private native int native_getDbLookaside(); + + /** + * finalizes the given statement id. + * + * @param statementId statement to be finzlied by sqlite + */ + private final native void native_finalize(int statementId); } diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index 89c3f96..9496079 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -132,11 +132,16 @@ public final class SQLiteDebug { /** documented here http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */ public int lookaside; - public DbStats(String dbName, long pageCount, long pageSize, int lookaside) { + /** statement cache stats: hits/misses/cachesize */ + public String cache; + + public DbStats(String dbName, long pageCount, long pageSize, int lookaside, + int hits, int misses, int cachesize) { this.dbName = dbName; - this.pageSize = pageSize; + this.pageSize = pageSize / 1024; dbSize = (pageCount * pageSize) / 1024; this.lookaside = lookaside; + this.cache = hits + "/" + misses + "/" + cachesize; } } diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index 2144fc3..be49257 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -39,9 +39,12 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { public Cursor query(CursorFactory factory, String[] selectionArgs) { // Compile the query - SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); + SQLiteQuery query = null; try { + mDatabase.lock(); + mDatabase.closePendingStatements(); + query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); // Arg binding int numArgs = selectionArgs == null ? 0 : selectionArgs.length; for (int i = 0; i < numArgs; i++) { @@ -61,6 +64,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { } finally { // Make sure this object is cleaned up if something happens if (query != null) query.close(); + mDatabase.unlock(); } } diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index aefbabc..0f2e872 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -17,6 +17,8 @@ package android.database.sqlite; import android.content.Context; +import android.database.DatabaseErrorHandler; +import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.util.Log; @@ -45,6 +47,7 @@ public abstract class SQLiteOpenHelper { private SQLiteDatabase mDatabase = null; private boolean mIsInitializing = false; + private final DatabaseErrorHandler mErrorHandler; /** * Create a helper object to create, open, and/or manage a database. @@ -58,12 +61,37 @@ public abstract class SQLiteOpenHelper { * {@link #onUpgrade} will be used to upgrade the database */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) { + this(context, name, factory, version, new DefaultDatabaseErrorHandler()); + } + + /** + * Create a helper object to create, open, and/or manage a database. + * The database is not actually created or opened until one of + * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called. + * + * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.</p> + * + * @param context to use to open or create the database + * @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 + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption. + */ + public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, + DatabaseErrorHandler errorHandler) { if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); + if (errorHandler == null) { + throw new IllegalArgumentException("DatabaseErrorHandler param value can't be null."); + } mContext = context; mName = name; mFactory = factory; mNewVersion = version; + mErrorHandler = errorHandler; } /** @@ -101,10 +129,14 @@ public abstract class SQLiteOpenHelper { if (mName == null) { db = SQLiteDatabase.create(null); } else { - db = mContext.openOrCreateDatabase(mName, 0, mFactory); + db = mContext.openOrCreateDatabase(mName, 0, mFactory, mErrorHandler); } int version = db.getVersion(); + if (version > mNewVersion) { + throw new IllegalStateException("Database " + mName + + " cannot be downgraded. instead, please uninstall new version first."); + } if (version != mNewVersion) { db.beginTransaction(); try { @@ -175,7 +207,8 @@ public abstract class SQLiteOpenHelper { try { mIsInitializing = true; String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY); + db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY, + mErrorHandler); if (db.getVersion() != mNewVersion) { throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to " + mNewVersion + ": " + path); diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 4d96f12..017b65f 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -17,10 +17,13 @@ package android.database.sqlite; import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; /** * 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. */ @@ -28,6 +31,11 @@ public abstract class SQLiteProgram extends SQLiteClosable { private static final String TAG = "SQLiteProgram"; + /** the type of sql statement being processed by this object */ + /* package */ static final int SELECT_STMT = 1; + private static final int UPDATE_STMT = 2; + private static final int OTHER_STMT = 3; + /** The database this program is compiled against. * @deprecated do not use this */ @@ -58,38 +66,67 @@ public abstract class SQLiteProgram extends SQLiteClosable { @Deprecated protected int nStatement = 0; + /** + * 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 + */ + private ArrayList<Pair<Integer, Object>> bindArgs = null; + /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { - mDatabase = db; + this(db, sql, true); + } + + /* package */ SQLiteProgram(SQLiteDatabase db, String sql, boolean compileFlag) { mSql = sql.trim(); db.acquireReference(); db.addSQLiteClosable(this); - this.nHandle = db.mNativeHandle; + mDatabase = db; + nHandle = db.mNativeHandle; + if (compileFlag) { + compileSql(); + } + } + private void compileSql() { // only cache CRUD statements - String prefixSql = mSql.substring(0, 6); - if (!prefixSql.equalsIgnoreCase("INSERT") && !prefixSql.equalsIgnoreCase("UPDATE") && - !prefixSql.equalsIgnoreCase("REPLAC") && - !prefixSql.equalsIgnoreCase("DELETE") && !prefixSql.equalsIgnoreCase("SELECT")) { - mCompiledSql = new SQLiteCompiledSql(db, sql); + if (getSqlStatementType(mSql) == OTHER_STMT) { + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); nStatement = mCompiledSql.nStatement; // since it is not in the cache, no need to acquire() it. return; } - // it is not pragma - mCompiledSql = db.getCompiledStatementForSql(sql); + mCompiledSql = mDatabase.getCompiledStatementForSql(mSql); if (mCompiledSql == null) { // create a new compiled-sql obj - mCompiledSql = new SQLiteCompiledSql(db, sql); + 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(); - db.addToCompiledQueries(sql, mCompiledSql); + mDatabase.addToCompiledQueries(mSql, mCompiledSql); if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.v(TAG, "Created DbObj (id#" + mCompiledSql.nStatement + - ") for sql: " + sql); + ") for sql: " + mSql); } } else { // it is already in compiled-sql cache. @@ -100,12 +137,12 @@ public abstract class SQLiteProgram extends SQLiteClosable { // 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(db, sql); + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.v(TAG, "** possible bug ** Created NEW DbObj (id#" + mCompiledSql.nStatement + ") because the previously created DbObj (id#" + last + - ") was not released for sql:" + sql); + ") was not released for sql:" + mSql); } // since it is not in the cache, no need to acquire() it. } @@ -113,11 +150,27 @@ public abstract class SQLiteProgram extends SQLiteClosable { nStatement = mCompiledSql.nStatement; } + /* package */ int getSqlStatementType(String sql) { + if (mSql.length() < 6) { + return OTHER_STMT; + } + String prefixSql = mSql.substring(0, 6); + if (prefixSql.equalsIgnoreCase("SELECT")) { + return SELECT_STMT; + } else if (prefixSql.equalsIgnoreCase("INSERT") || + prefixSql.equalsIgnoreCase("UPDATE") || + prefixSql.equalsIgnoreCase("REPLAC") || + prefixSql.equalsIgnoreCase("DELETE")) { + return UPDATE_STMT; + } + return OTHER_STMT; + } + @Override protected void onAllReferencesReleased() { releaseCompiledSqlIfNotInCache(); - mDatabase.releaseReference(); mDatabase.removeSQLiteClosable(this); + mDatabase.releaseReference(); } @Override @@ -126,7 +179,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { mDatabase.releaseReference(); } - private void releaseCompiledSqlIfNotInCache() { + /* package */ synchronized void releaseCompiledSqlIfNotInCache() { if (mCompiledSql == null) { return; } @@ -135,22 +188,34 @@ public abstract class SQLiteProgram extends SQLiteClosable { // it is NOT in compiled-sql cache. i.e., responsibility of // releasing this statement is on me. mCompiledSql.releaseSqlStatement(); - mCompiledSql = null; - nStatement = 0; } else { // it is in compiled-sql cache. reset its CompiledSql#mInUse flag mCompiledSql.release(); } - } + } + mCompiledSql = null; + nStatement = 0; } /** * 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 nStatement; + return -1; + } + + /** + * used only for testing purposes + */ + /* package */ int getSqlStatementId() { + synchronized(this) { + return (mCompiledSql == null) ? 0 : nStatement; + } } /* package */ String getSqlString() { @@ -176,14 +241,20 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param index The 1-based index to the parameter to bind null to */ public void bindNull(int index) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_null(index); - } finally { - releaseReference(); + mDatabase.verifyDbIsOpen(); + synchronized (this) { + acquireReference(); + try { + if (this.nStatement == 0) { + // since the SQL statement is not compiled, don't do the binding yet. + // can be done before executing the SQL statement + addToBindArgs(index, null); + } else { + native_bind_null(index); + } + } finally { + releaseReference(); + } } } @@ -195,14 +266,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindLong(int index, long value) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_long(index, value); - } finally { - releaseReference(); + mDatabase.verifyDbIsOpen(); + synchronized (this) { + acquireReference(); + try { + if (this.nStatement == 0) { + addToBindArgs(index, value); + } else { + native_bind_long(index, value); + } + } finally { + releaseReference(); + } } } @@ -214,14 +289,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindDouble(int index, double value) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_double(index, value); - } finally { - releaseReference(); + mDatabase.verifyDbIsOpen(); + synchronized (this) { + acquireReference(); + try { + if (this.nStatement == 0) { + addToBindArgs(index, value); + } else { + native_bind_double(index, value); + } + } finally { + releaseReference(); + } } } @@ -236,14 +315,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_string(index, value); - } finally { - releaseReference(); + mDatabase.verifyDbIsOpen(); + synchronized (this) { + acquireReference(); + try { + if (this.nStatement == 0) { + addToBindArgs(index, value); + } else { + native_bind_string(index, value); + } + } finally { + releaseReference(); + } } } @@ -258,14 +341,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_bind_blob(index, value); - } finally { - releaseReference(); + mDatabase.verifyDbIsOpen(); + synchronized (this) { + acquireReference(); + try { + if (this.nStatement == 0) { + addToBindArgs(index, value); + } else { + native_bind_blob(index, value); + } + } finally { + releaseReference(); + } } } @@ -273,14 +360,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Clears all existing bindings. Unset bindings are treated as NULL. */ public void clearBindings() { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - acquireReference(); - try { - native_clear_bindings(); - } finally { - releaseReference(); + synchronized (this) { + bindArgs = null; + if (this.nStatement == 0) { + return; + } + mDatabase.verifyDbIsOpen(); + acquireReference(); + try { + native_clear_bindings(); + } finally { + releaseReference(); + } } } @@ -288,14 +379,40 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Release this program's resources, making it invalid. */ public void close() { - if (!mDatabase.isOpen()) { + synchronized (this) { + bindArgs = null; + if (nHandle == 0 || !mDatabase.isOpen()) { + return; + } + releaseReference(); + } + } + + private synchronized void addToBindArgs(int index, Object value) { + if (bindArgs == null) { + bindArgs = new ArrayList<Pair<Integer, Object>>(); + } + bindArgs.add(new Pair<Integer, Object>(index, value)); + } + + /* package */ synchronized void compileAndbindAllArgs() { + assert nStatement == 0; + compileSql(); + if (bindArgs == null) { return; } - mDatabase.lock(); - try { - releaseReference(); - } finally { - mDatabase.unlock(); + for (Pair<Integer, Object> p : bindArgs) { + if (p.second == null) { + native_bind_null(p.first); + } else if (p.second instanceof Long) { + native_bind_long(p.first, (Long)p.second); + } else if (p.second instanceof Double) { + native_bind_double(p.first, (Double)p.second); + } else if (p.second instanceof byte[]) { + native_bind_blob(p.first, (byte[])p.second); + } else { + native_bind_string(p.first, (String)p.second); + } } } @@ -320,6 +437,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { 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(); + /* package */ final native void native_clear_bindings(); } diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 905b66b..e6011ee 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -72,11 +72,6 @@ public class SQLiteQuery extends SQLiteProgram { // is not safe in this situation. the native code will ignore maxRead int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex, maxRead, lastPos); - - // Logging - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.d(TAG, "fillWindow(): " + mSql); - } mDatabase.logTimeStat(mSql, timeStart); return numRows; } catch (IllegalStateException e){ diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index 9e425c3..b902803 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -25,12 +25,18 @@ import dalvik.system.BlockGuard; * 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. */ +@SuppressWarnings("deprecation") public class SQLiteStatement extends SQLiteProgram { + private static final boolean READ = true; + private static final boolean WRITE = false; + + private SQLiteDatabase mOrigDb; + /** * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} @@ -38,7 +44,7 @@ public class SQLiteStatement extends SQLiteProgram * @param sql */ /* package */ SQLiteStatement(SQLiteDatabase db, String sql) { - super(db, sql); + super(db, sql, false /* don't compile sql statement */); } /** @@ -49,20 +55,14 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public void execute() { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - - acquireReference(); - try { - native_execute(); - mDatabase.logTimeStat(mSql, timeStart); - } finally { - releaseReference(); - mDatabase.unlock(); + synchronized(this) { + long timeStart = acquireAndLock(WRITE); + try { + native_execute(); + mDatabase.logTimeStat(mSql, timeStart); + } finally { + releaseAndUnlock(); + } } } @@ -76,21 +76,15 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { - BlockGuard.getThreadPolicy().onWriteToDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - - acquireReference(); - try { - native_execute(); - mDatabase.logTimeStat(mSql, timeStart); - return (mDatabase.lastChangeCount() > 0) ? mDatabase.lastInsertRow() : -1; - } finally { - releaseReference(); - mDatabase.unlock(); + synchronized(this) { + long timeStart = acquireAndLock(WRITE); + try { + native_execute(); + mDatabase.logTimeStat(mSql, timeStart); + return (mDatabase.lastChangeCount() > 0) ? mDatabase.lastInsertRow() : -1; + } finally { + releaseAndUnlock(); + } } } @@ -103,21 +97,15 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { - BlockGuard.getThreadPolicy().onReadFromDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - - acquireReference(); - try { - long retValue = native_1x1_long(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } finally { - releaseReference(); - mDatabase.unlock(); + synchronized(this) { + long timeStart = acquireAndLock(READ); + try { + long retValue = native_1x1_long(); + mDatabase.logTimeStat(mSql, timeStart); + return retValue; + } finally { + releaseAndUnlock(); + } } } @@ -130,22 +118,68 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { - BlockGuard.getThreadPolicy().onReadFromDisk(); - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); + synchronized(this) { + long timeStart = acquireAndLock(READ); + try { + String retValue = native_1x1_string(); + mDatabase.logTimeStat(mSql, timeStart); + return retValue; + } finally { + releaseAndUnlock(); + } } - long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); + } - acquireReference(); - try { - String retValue = native_1x1_string(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } finally { - releaseReference(); - mDatabase.unlock(); + /** + * 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>get lock on the database</li> + * <li>acquire reference on this object</li> + * <li>and then return the current time _before_ the database lock was acquired</li> + * </ul> + * <p> + * This method removes the duplcate code from the other public + * methods in this class. + */ + private long acquireAndLock(boolean rwFlag) { + // use pooled database connection handles for SELECT SQL statements + mDatabase.verifyDbIsOpen(); + SQLiteDatabase db = (getSqlStatementType(mSql) != SELECT_STMT) ? mDatabase + : mDatabase.getDbConnection(mSql); + // use the database connection obtained above + mOrigDb = mDatabase; + mDatabase = db; + nHandle = mDatabase.mNativeHandle; + if (rwFlag == WRITE) { + BlockGuard.getThreadPolicy().onWriteToDisk(); + } else { + BlockGuard.getThreadPolicy().onReadFromDisk(); } + long startTime = SystemClock.uptimeMillis(); + mDatabase.lock(); + acquireReference(); + mDatabase.closePendingStatements(); + compileAndbindAllArgs(); + return startTime; + } + + /** + * this method releases locks and references acquired in {@link #acquireAndLock(boolean)}. + */ + private void releaseAndUnlock() { + releaseReference(); + mDatabase.unlock(); + 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. + releaseCompiledSqlIfNotInCache(); + // restore the database connection handle to the original value + mDatabase = mOrigDb; + nHandle = mDatabase.mNativeHandle; } private final native void native_execute(); diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java index 70519ff..aaf3898 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -84,14 +84,14 @@ public class SensorEvent { * sensor itself (<b>Fs</b>) using the relation: * </p> * - * <b><center>Ad = - ·Fs / mass</center></b> + * <b><center>Ad = - ∑Fs / mass</center></b> * * <p> * In particular, the force of gravity is always influencing the measured * acceleration: * </p> * - * <b><center>Ad = -g - ·F / mass</center></b> + * <b><center>Ad = -g - ∑F / mass</center></b> * * <p> * For this reason, when the device is sitting on a table (and obviously not diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 280ded6..6335296 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -524,5 +524,20 @@ public class ConnectivityManager } catch (RemoteException e) { return TETHER_ERROR_SERVICE_UNAVAIL; } - } + } + + /** + * Ensure the device stays awake until we connect with the next network + * @param forWhome The name of the network going down for logging purposes + * @return {@code true} on success, {@code false} on failure + * {@hide} + */ + public boolean requestNetworkTransitionWakelock(String forWhom) { + try { + mService.requestNetworkTransitionWakelock(forWhom); + return true; + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/net/Downloads.java b/core/java/android/net/Downloads.java index fd33781..ddde5c1 100644 --- a/core/java/android/net/Downloads.java +++ b/core/java/android/net/Downloads.java @@ -430,11 +430,10 @@ public final class Downloads { ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query( - downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */, - null /* sort order */); + Cursor c = cr.query(downloadUri, DOWNLOADS_PROJECTION, null /* selection */, + null /* selection args */, null /* sort order */); try { - if (!c.moveToNext()) { + if (c == null || !c.moveToNext()) { return result; } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index b05c2ed..5a14cc9 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -72,4 +72,6 @@ interface IConnectivityManager String[] getTetherableUsbRegexs(); String[] getTetherableWifiRegexs(); + + void requestNetworkTransitionWakelock(in String forWhom); } diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index 214510d..e74db67 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -22,12 +22,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.RemoteException; import android.os.Handler; +import android.os.Message; import android.os.ServiceManager; -import android.os.SystemProperties; import com.android.internal.telephony.ITelephony; import com.android.internal.telephony.Phone; import com.android.internal.telephony.TelephonyIntents; import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo; +import android.net.NetworkProperties; import android.telephony.TelephonyManager; import android.util.Log; import android.text.TextUtils; @@ -39,7 +41,7 @@ import android.text.TextUtils; * * {@hide} */ -public class MobileDataStateTracker extends NetworkStateTracker { +public class MobileDataStateTracker implements NetworkStateTracker { private static final String TAG = "MobileDataStateTracker"; private static final boolean DBG = true; @@ -48,10 +50,16 @@ public class MobileDataStateTracker extends NetworkStateTracker { private ITelephony mPhoneService; private String mApnType; - private String mApnTypeToWatchFor; - private String mApnName; - private boolean mEnabled; private BroadcastReceiver mStateReceiver; + private static String[] sDnsPropNames; + private NetworkInfo mNetworkInfo; + private boolean mTeardownRequested = false; + private Handler mTarget; + private Context mContext; + private NetworkProperties mNetworkProperties; + private boolean mPrivateDnsRouteSet = false; + private int mDefaultGatewayAddr = 0; + private boolean mDefaultRouteSet = false; /** * Create a new MobileDataStateTracker @@ -62,24 +70,16 @@ public class MobileDataStateTracker extends NetworkStateTracker { * @param tag the name of this network */ public MobileDataStateTracker(Context context, Handler target, int netType, String tag) { - super(context, target, netType, + mTarget = target; + mContext = context; + mNetworkInfo = new NetworkInfo(netType, TelephonyManager.getDefault().getNetworkType(), tag, TelephonyManager.getDefault().getNetworkTypeName()); mApnType = networkTypeToApnType(netType); - if (TextUtils.equals(mApnType, Phone.APN_TYPE_HIPRI)) { - mApnTypeToWatchFor = Phone.APN_TYPE_DEFAULT; - } else { - mApnTypeToWatchFor = mApnType; - } mPhoneService = null; - if(netType == ConnectivityManager.TYPE_MOBILE) { - mEnabled = true; - } else { - mEnabled = false; - } - mDnsPropNames = new String[] { + sDnsPropNames = new String[] { "net.rmnet0.dns1", "net.rmnet0.dns2", "net.eth0.dns1", @@ -94,6 +94,45 @@ public class MobileDataStateTracker extends NetworkStateTracker { } /** + * Return the IP addresses of the DNS servers available for the mobile data + * network interface. + * @return a list of DNS addresses, with no holes. + */ + public String[] getDnsPropNames() { + return sDnsPropNames; + } + + public boolean isPrivateDnsRouteSet() { + return mPrivateDnsRouteSet; + } + + public void privateDnsRouteSet(boolean enabled) { + mPrivateDnsRouteSet = enabled; + } + + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + public int getDefaultGatewayAddr() { + return mDefaultGatewayAddr; + } + + public boolean isDefaultRouteSet() { + return mDefaultRouteSet; + } + + public void defaultRouteSet(boolean enabled) { + mDefaultRouteSet = enabled; + } + + /** + * This is not implemented. + */ + public void releaseWakeLock() { + } + + /** * Begin monitoring mobile data connectivity. */ public void startMonitoring() { @@ -103,38 +142,34 @@ public class MobileDataStateTracker extends NetworkStateTracker { filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); mStateReceiver = new MobileDataStateReceiver(); - Intent intent = mContext.registerReceiver(mStateReceiver, filter); - if (intent != null) - mMobileDataState = getMobileDataState(intent); - else - mMobileDataState = Phone.DataState.DISCONNECTED; - } - - private Phone.DataState getMobileDataState(Intent intent) { - String str = intent.getStringExtra(Phone.STATE_KEY); - if (str != null) { - String apnTypeList = - intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); - if (isApnTypeIncluded(apnTypeList)) { - return Enum.valueOf(Phone.DataState.class, str); - } - } - return Phone.DataState.DISCONNECTED; + mContext.registerReceiver(mStateReceiver, filter); + mMobileDataState = Phone.DataState.DISCONNECTED; } - private boolean isApnTypeIncluded(String typeList) { - /* comma seperated list - split and check */ - if (typeList == null) - return false; + /** + * Record the roaming status of the device, and if it is a change from the previous + * status, send a notification to any listeners. + * @param isRoaming {@code true} if the device is now roaming, {@code false} + * if it is no longer roaming. + */ + private void setRoamingStatus(boolean isRoaming) { + if (isRoaming != mNetworkInfo.isRoaming()) { + mNetworkInfo.setRoaming(isRoaming); + Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo); + msg.sendToTarget(); + } + } - String[] list = typeList.split(","); - for(int i=0; i< list.length; i++) { - if (TextUtils.equals(list[i], mApnTypeToWatchFor) || - TextUtils.equals(list[i], Phone.APN_TYPE_ALL)) { - return true; + private void setSubtype(int subtype, String subtypeName) { + if (mNetworkInfo.isConnected()) { + int oldSubtype = mNetworkInfo.getSubtype(); + if (subtype != oldSubtype) { + mNetworkInfo.setSubtype(subtype, subtypeName); + Message msg = mTarget.obtainMessage( + EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo); + msg.sendToTarget(); } } - return false; } private class MobileDataStateReceiver extends BroadcastReceiver { @@ -142,57 +177,38 @@ public class MobileDataStateTracker extends NetworkStateTracker { synchronized(this) { if (intent.getAction().equals(TelephonyIntents. ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { - Phone.DataState state = getMobileDataState(intent); + String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY); + + if (!TextUtils.equals(apnType, mApnType)) { + return; + } + Phone.DataState state = Enum.valueOf(Phone.DataState.class, + intent.getStringExtra(Phone.STATE_KEY)); String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY); String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); - String apnTypeList = intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); - mApnName = apnName; boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, false); - - // set this regardless of the apnTypeList. It's all the same radio/network - // underneath mNetworkInfo.setIsAvailable(!unavailable); - if (isApnTypeIncluded(apnTypeList)) { - if (mEnabled == false) { - // if we're not enabled but the APN Type is supported by this connection - // we should record the interface name if one's provided. If the user - // turns on this network we will need the interfacename but won't get - // a fresh connected message - TODO fix this when we get per-APN - // notifications - if (state == Phone.DataState.CONNECTED) { - if (DBG) Log.d(TAG, "replacing old mInterfaceName (" + - mInterfaceName + ") with " + - intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY) + - " for " + mApnType); - mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); - } - return; - } - } else { - return; - } - if (DBG) Log.d(TAG, mApnType + " Received state= " + state + ", old= " + mMobileDataState + ", reason= " + - (reason == null ? "(unspecified)" : reason) + - ", apnTypeList= " + apnTypeList); + (reason == null ? "(unspecified)" : reason)); if (mMobileDataState != state) { mMobileDataState = state; switch (state) { case DISCONNECTED: if(isTeardownRequested()) { - mEnabled = false; setTeardownRequested(false); } setDetailedState(DetailedState.DISCONNECTED, reason, apnName); - if (mInterfaceName != null) { - NetworkUtils.resetConnections(mInterfaceName); + if (mNetworkProperties != null) { + NetworkUtils.resetConnections(mNetworkProperties.getInterface(). + getName()); } + // TODO - check this // can't do this here - ConnectivityService needs it to clear stuff // it's ok though - just leave it to be refreshed next time // we connect. @@ -208,9 +224,11 @@ public class MobileDataStateTracker extends NetworkStateTracker { setDetailedState(DetailedState.SUSPENDED, reason, apnName); break; case CONNECTED: - mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); - if (mInterfaceName == null) { - Log.d(TAG, "CONNECTED event did not supply interface name."); + mNetworkProperties = intent.getParcelableExtra( + Phone.DATA_NETWORK_PROPERTIES_KEY); + if (mNetworkProperties == null) { + Log.d(TAG, + "CONNECTED event did not supply network properties."); } setDetailedState(DetailedState.CONNECTED, reason, apnName); break; @@ -218,11 +236,14 @@ public class MobileDataStateTracker extends NetworkStateTracker { } } else if (intent.getAction(). equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) { - mEnabled = false; + String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY); + if (!TextUtils.equals(apnType, mApnType)) { + return; + } String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY); String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); - if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" + - reason == null ? "" : "(" + reason + ")"); + if (DBG) Log.d(TAG, mApnType + "Received " + intent.getAction() + + " broadcast" + reason == null ? "" : "(" + reason + ")"); setDetailedState(DetailedState.FAILED, reason, apnName); } TelephonyManager tm = TelephonyManager.getDefault(); @@ -320,70 +341,88 @@ public class MobileDataStateTracker extends NetworkStateTracker { /** * Tear down mobile data connectivity, i.e., disable the ability to create * mobile data connections. + * TODO - make async and return nothing? */ - @Override public boolean teardown() { - // since we won't get a notification currently (TODO - per APN notifications) - // we won't get a disconnect message until all APN's on the current connection's - // APN list are disabled. That means privateRoutes for DNS and such will remain on - - // not a problem since that's all shared with whatever other APN is still on, but - // ugly. setTeardownRequested(true); return (setEnableApn(mApnType, false) != Phone.APN_REQUEST_FAILED); } /** + * Record the detailed state of a network, and if it is a + * change from the previous state, send a notification to + * any listeners. + * @param state the new @{code DetailedState} + */ + private void setDetailedState(NetworkInfo.DetailedState state) { + setDetailedState(state, null, null); + } + + /** + * Record the detailed state of a network, and if it is a + * change from the previous state, send a notification to + * any listeners. + * @param state the new @{code DetailedState} + * @param reason a {@code String} indicating a reason for the state change, + * if one was supplied. May be {@code null}. + * @param extraInfo optional {@code String} providing extra information about the state change + */ + private void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) { + if (DBG) Log.d(TAG, "setDetailed state, old =" + + mNetworkInfo.getDetailedState() + " and new state=" + state); + if (state != mNetworkInfo.getDetailedState()) { + boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING); + String lastReason = mNetworkInfo.getReason(); + /* + * If a reason was supplied when the CONNECTING state was entered, and no + * reason was supplied for entering the CONNECTED state, then retain the + * reason that was supplied when going to CONNECTING. + */ + if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null + && lastReason != null) + reason = lastReason; + mNetworkInfo.setDetailedState(state, reason, extraInfo); + Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); + msg.sendToTarget(); + } + } + + private void setDetailedStateInternal(NetworkInfo.DetailedState state) { + mNetworkInfo.setDetailedState(state, null, null); + } + + public void setTeardownRequested(boolean isRequested) { + mTeardownRequested = isRequested; + } + + public boolean isTeardownRequested() { + return mTeardownRequested; + } + + /** * Re-enable mobile data connectivity after a {@link #teardown()}. + * TODO - make async and always get a notification? */ public boolean reconnect() { + boolean retValue = false; //connected or expect to be? setTeardownRequested(false); switch (setEnableApn(mApnType, true)) { case Phone.APN_ALREADY_ACTIVE: - // TODO - remove this when we get per-apn notifications - mEnabled = true; // need to set self to CONNECTING so the below message is handled. - mMobileDataState = Phone.DataState.CONNECTING; - setDetailedState(DetailedState.CONNECTING, Phone.REASON_APN_CHANGED, null); - //send out a connected message - Intent intent = new Intent(TelephonyIntents. - ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); - intent.putExtra(Phone.STATE_KEY, Phone.DataState.CONNECTED.toString()); - intent.putExtra(Phone.STATE_CHANGE_REASON_KEY, Phone.REASON_APN_CHANGED); - intent.putExtra(Phone.DATA_APN_TYPES_KEY, mApnTypeToWatchFor); - intent.putExtra(Phone.DATA_APN_KEY, mApnName); - intent.putExtra(Phone.DATA_IFACE_NAME_KEY, mInterfaceName); - intent.putExtra(Phone.NETWORK_UNAVAILABLE_KEY, false); - if (mStateReceiver != null) mStateReceiver.onReceive(mContext, intent); + retValue = true; break; case Phone.APN_REQUEST_STARTED: - mEnabled = true; // no need to do anything - we're already due some status update intents + retValue = true; break; case Phone.APN_REQUEST_FAILED: - if (mPhoneService == null && mApnType == Phone.APN_TYPE_DEFAULT) { - // on startup we may try to talk to the phone before it's ready - // since the phone will come up enabled, go with that. - // TODO - this also comes up on telephony crash: if we think mobile data is - // off and the telephony stuff crashes and has to restart it will come up - // enabled (making a data connection). We will then be out of sync. - // A possible solution is a broadcast when telephony restarts. - mEnabled = true; - return false; - } - // else fall through case Phone.APN_TYPE_NOT_AVAILABLE: - // Default is always available, but may be off due to - // AirplaneMode or E-Call or whatever.. - if (mApnType != Phone.APN_TYPE_DEFAULT) { - mEnabled = false; - } break; default: Log.e(TAG, "Error in reconnect - unexpected response."); - mEnabled = false; break; } - return mEnabled; + return retValue; } /** @@ -457,23 +496,9 @@ public class MobileDataStateTracker extends NetworkStateTracker { } /** - * Ensure that a network route exists to deliver traffic to the specified - * host via the mobile data network. - * @param hostAddress the IP address of the host to which the route is desired, - * in network byte order. - * @return {@code true} on success, {@code false} on failure + * This is not supported. */ - @Override - public boolean requestRouteToHost(int hostAddress) { - if (DBG) { - Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress) + - " for " + mApnType + "(" + mInterfaceName + ")"); - } - if (mInterfaceName != null && hostAddress != -1) { - return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0; - } else { - return false; - } + public void interpretScanResultsAvailable() { } @Override @@ -537,4 +562,8 @@ public class MobileDataStateTracker extends NetworkStateTracker { return null; } } + + public NetworkProperties getNetworkProperties() { + return mNetworkProperties; + } } diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 649cb8c..21f711c 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -121,7 +121,10 @@ public class NetworkInfo implements Parcelable { */ public NetworkInfo(int type) {} - NetworkInfo(int type, int subtype, String typeName, String subtypeName) { + /** + * @hide + */ + public NetworkInfo(int type, int subtype, String typeName, String subtypeName) { if (!ConnectivityManager.isNetworkTypeValid(type)) { throw new IllegalArgumentException("Invalid network type: " + type); } @@ -281,8 +284,9 @@ public class NetworkInfo implements Parcelable { * if one was supplied. May be {@code null}. * @param extraInfo an optional {@code String} providing addditional network state * information passed up from the lower networking layers. + * @hide */ - void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { + public void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { this.mDetailedState = detailedState; this.mState = stateMap.get(detailedState); this.mReason = reason; diff --git a/core/java/android/net/NetworkProperties.aidl b/core/java/android/net/NetworkProperties.aidl new file mode 100644 index 0000000..07aac6e --- /dev/null +++ b/core/java/android/net/NetworkProperties.aidl @@ -0,0 +1,22 @@ +/* +** +** Copyright (C) 2009 Qualcomm Innovation Center, Inc. All Rights Reserved. +** 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.net; + +parcelable NetworkProperties; + diff --git a/core/java/android/net/NetworkProperties.java b/core/java/android/net/NetworkProperties.java new file mode 100644 index 0000000..56e1f1a --- /dev/null +++ b/core/java/android/net/NetworkProperties.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2008 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.net; + +import android.os.Parcelable; +import android.os.Parcel; +import android.util.Log; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Describes the properties of a network interface or single address + * of an interface. + * TODO - consider adding optional fields like Apn and ApnType + * @hide + */ +public class NetworkProperties implements Parcelable { + + private NetworkInterface mIface; + private Collection<InetAddress> mAddresses; + private Collection<InetAddress> mDnses; + private InetAddress mGateway; + private ProxyProperties mHttpProxy; + + public NetworkProperties() { + clear(); + } + + public synchronized void setInterface(NetworkInterface iface) { + mIface = iface; + } + public synchronized NetworkInterface getInterface() { + return mIface; + } + public synchronized String getInterfaceName() { + return (mIface == null ? null : mIface.getName()); + } + + public synchronized void addAddress(InetAddress address) { + mAddresses.add(address); + } + public synchronized Collection<InetAddress> getAddresses() { + return mAddresses; + } + + public synchronized void addDns(InetAddress dns) { + mDnses.add(dns); + } + public synchronized Collection<InetAddress> getDnses() { + return mDnses; + } + + public synchronized void setGateway(InetAddress gateway) { + mGateway = gateway; + } + public synchronized InetAddress getGateway() { + return mGateway; + } + + public synchronized void setHttpProxy(ProxyProperties proxy) { + mHttpProxy = proxy; + } + public synchronized ProxyProperties getHttpProxy() { + return mHttpProxy; + } + + public synchronized void clear() { + mIface = null; + mAddresses = new ArrayList<InetAddress>(); + mDnses = new ArrayList<InetAddress>(); + mGateway = null; + mHttpProxy = null; + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + public synchronized String toString() { + String ifaceName = (mIface == null ? "" : "InterfaceName: " + mIface.getName() + " "); + + String ip = "IpAddresses: ["; + for (InetAddress addr : mAddresses) ip += addr.toString() + ","; + ip += "] "; + + String dns = "DnsAddresses: ["; + for (InetAddress addr : mDnses) dns += addr.toString() + ","; + dns += "] "; + + String proxy = (mHttpProxy == null ? "" : "HttpProxy: " + mHttpProxy.toString() + " "); + String gateway = (mGateway == null ? "" : "Gateway: " + mGateway.toString() + " "); + + return ifaceName + ip + gateway + dns + proxy; + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public synchronized void writeToParcel(Parcel dest, int flags) { + dest.writeString(getInterfaceName()); + dest.writeInt(mAddresses.size()); + for(InetAddress a : mAddresses) { + dest.writeString(a.getHostName()); + dest.writeByteArray(a.getAddress()); + } + dest.writeInt(mDnses.size()); + for(InetAddress d : mDnses) { + dest.writeString(d.getHostName()); + dest.writeByteArray(d.getAddress()); + } + if (mGateway != null) { + dest.writeByte((byte)1); + dest.writeString(mGateway.getHostName()); + dest.writeByteArray(mGateway.getAddress()); + } else { + dest.writeByte((byte)0); + } + if (mHttpProxy != null) { + dest.writeByte((byte)1); + dest.writeParcelable(mHttpProxy, flags); + } else { + dest.writeByte((byte)0); + } + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public static final Creator<NetworkProperties> CREATOR = + new Creator<NetworkProperties>() { + public NetworkProperties createFromParcel(Parcel in) { + NetworkProperties netProp = new NetworkProperties(); + String iface = in.readString(); + if (iface != null) { + try { + netProp.setInterface(NetworkInterface.getByName(iface)); + } catch (Exception e) { + return null; + } + } + int addressCount = in.readInt(); + for (int i=0; i<addressCount; i++) { + try { + netProp.addAddress(InetAddress.getByAddress(in.readString(), + in.createByteArray())); + } catch (UnknownHostException e) { } + } + addressCount = in.readInt(); + for (int i=0; i<addressCount; i++) { + try { + netProp.addDns(InetAddress.getByAddress(in.readString(), + in.createByteArray())); + } catch (UnknownHostException e) { } + } + if (in.readByte() == 1) { + try { + netProp.setGateway(InetAddress.getByAddress(in.readString(), + in.createByteArray())); + } catch (UnknownHostException e) {} + } + if (in.readByte() == 1) { + netProp.setHttpProxy((ProxyProperties)in.readParcelable(null)); + } + return netProp; + } + + public NetworkProperties[] newArray(int size) { + return new NetworkProperties[size]; + } + }; +} diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 1fb0144..44215e7 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -16,40 +16,15 @@ package android.net; -import java.io.FileWriter; -import java.io.IOException; - -import android.os.Handler; -import android.os.Message; -import android.os.SystemProperties; -import android.content.Context; -import android.text.TextUtils; -import android.util.Config; -import android.util.Log; - - /** - * Each subclass of this class keeps track of the state of connectivity - * of a network interface. All state information for a network should - * be kept in a Tracker class. This superclass manages the - * network-type-independent aspects of network state. + * Interface for connectivity service to act on a network interface. + * All state information for a network should be kept in a Tracker class. + * This interface defines network-type-independent functions that should + * be implemented by the Tracker class. * * {@hide} */ -public abstract class NetworkStateTracker extends Handler { - - protected NetworkInfo mNetworkInfo; - protected Context mContext; - protected Handler mTarget; - protected String mInterfaceName; - protected String[] mDnsPropNames; - private boolean mPrivateDnsRouteSet; - protected int mDefaultGatewayAddr; - private boolean mDefaultRouteSet; - private boolean mTeardownRequested; - - private static boolean DBG = true; - private static final String TAG = "NetworkStateTracker"; +public interface NetworkStateTracker { public static final int EVENT_STATE_CHANGED = 1; public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2; @@ -63,306 +38,86 @@ public abstract class NetworkStateTracker extends Handler { public static final int EVENT_ROAMING_CHANGED = 5; public static final int EVENT_NETWORK_SUBTYPE_CHANGED = 6; public static final int EVENT_RESTORE_DEFAULT_NETWORK = 7; - - public NetworkStateTracker(Context context, - Handler target, - int networkType, - int subType, - String typeName, - String subtypeName) { - super(); - mContext = context; - mTarget = target; - mTeardownRequested = false; - - this.mNetworkInfo = new NetworkInfo(networkType, subType, typeName, subtypeName); - } - - public NetworkInfo getNetworkInfo() { - return mNetworkInfo; - } + public static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8; /** - * Return the system properties name associated with the tcp buffer sizes - * for this network. + * Fetch NetworkInfo for the network */ - public abstract String getTcpBufferSizesPropName(); + public NetworkInfo getNetworkInfo(); /** - * Return the IP addresses of the DNS servers available for the mobile data - * network interface. - * @return a list of DNS addresses, with no holes. + * Fetch NetworkProperties for the network */ - public String[] getNameServers() { - return getNameServerList(mDnsPropNames); - } - - /** - * Return the IP addresses of the DNS servers available for this - * network interface. - * @param propertyNames the names of the system properties whose values - * give the IP addresses. Properties with no values are skipped. - * @return an array of {@code String}s containing the IP addresses - * of the DNS servers, in dot-notation. This may have fewer - * non-null entries than the list of names passed in, since - * some of the passed-in names may have empty values. - */ - static protected String[] getNameServerList(String[] propertyNames) { - String[] dnsAddresses = new String[propertyNames.length]; - int i, j; - - for (i = 0, j = 0; i < propertyNames.length; i++) { - String value = SystemProperties.get(propertyNames[i]); - // The GSM layer sometimes sets a bogus DNS server address of - // 0.0.0.0 - if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) { - dnsAddresses[j++] = value; - } - } - return dnsAddresses; - } - - public void addPrivateDnsRoutes() { - if (DBG) { - Log.d(TAG, "addPrivateDnsRoutes for " + this + - "(" + mInterfaceName + ") - mPrivateDnsRouteSet = "+mPrivateDnsRouteSet); - } - if (mInterfaceName != null && !mPrivateDnsRouteSet) { - for (String addrString : getNameServers()) { - int addr = NetworkUtils.lookupHost(addrString); - if (addr != -1 && addr != 0) { - if (DBG) Log.d(TAG, " adding "+addrString+" ("+addr+")"); - NetworkUtils.addHostRoute(mInterfaceName, addr); - } - } - mPrivateDnsRouteSet = true; - } - } - - public void removePrivateDnsRoutes() { - // TODO - we should do this explicitly but the NetUtils api doesnt - // support this yet - must remove all. No worse than before - if (mInterfaceName != null && mPrivateDnsRouteSet) { - if (DBG) { - Log.d(TAG, "removePrivateDnsRoutes for " + mNetworkInfo.getTypeName() + - " (" + mInterfaceName + ")"); - } - NetworkUtils.removeHostRoutes(mInterfaceName); - mPrivateDnsRouteSet = false; - } - } - - public void addDefaultRoute() { - if ((mInterfaceName != null) && (mDefaultGatewayAddr != 0) && - mDefaultRouteSet == false) { - if (DBG) { - Log.d(TAG, "addDefaultRoute for " + mNetworkInfo.getTypeName() + - " (" + mInterfaceName + "), GatewayAddr=" + mDefaultGatewayAddr); - } - NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr); - mDefaultRouteSet = true; - } - } - - public void removeDefaultRoute() { - if (mInterfaceName != null && mDefaultRouteSet == true) { - if (DBG) { - Log.d(TAG, "removeDefaultRoute for " + mNetworkInfo.getTypeName() + " (" + - mInterfaceName + ")"); - } - NetworkUtils.removeDefaultRoute(mInterfaceName); - mDefaultRouteSet = false; - } - } + public NetworkProperties getNetworkProperties(); /** - * Reads the network specific TCP buffer sizes from SystemProperties - * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system - * wide use + * Return the system properties name associated with the tcp buffer sizes + * for this network. */ - public void updateNetworkSettings() { - String key = getTcpBufferSizesPropName(); - String bufferSizes = SystemProperties.get(key); - - if (bufferSizes.length() == 0) { - Log.e(TAG, key + " not found in system properties. Using defaults"); - - // Setting to default values so we won't be stuck to previous values - key = "net.tcp.buffersize.default"; - bufferSizes = SystemProperties.get(key); - } - - // Set values in kernel - if (bufferSizes.length() != 0) { - if (DBG) { - Log.v(TAG, "Setting TCP values: [" + bufferSizes - + "] which comes from [" + key + "]"); - } - setBufferSize(bufferSizes); - } - } + public String getTcpBufferSizesPropName(); /** - * Release the wakelock, if any, that may be held while handling a - * disconnect operation. + * Check if private DNS route is set for the network */ - public void releaseWakeLock() { - } + public boolean isPrivateDnsRouteSet(); /** - * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max] - * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem - * - * @param bufferSizes in the format of "readMin, readInitial, readMax, - * writeMin, writeInitial, writeMax" + * Set a flag indicating private DNS route is set */ - private void setBufferSize(String bufferSizes) { - try { - String[] values = bufferSizes.split(","); - - if (values.length == 6) { - final String prefix = "/sys/kernel/ipv4/tcp_"; - stringToFile(prefix + "rmem_min", values[0]); - stringToFile(prefix + "rmem_def", values[1]); - stringToFile(prefix + "rmem_max", values[2]); - stringToFile(prefix + "wmem_min", values[3]); - stringToFile(prefix + "wmem_def", values[4]); - stringToFile(prefix + "wmem_max", values[5]); - } else { - Log.e(TAG, "Invalid buffersize string: " + bufferSizes); - } - } catch (IOException e) { - Log.e(TAG, "Can't set tcp buffer sizes:" + e); - } - } + public void privateDnsRouteSet(boolean enabled); /** - * Writes string to file. Basically same as "echo -n $string > $filename" - * - * @param filename - * @param string - * @throws IOException + * Fetch default gateway address for the network */ - private void stringToFile(String filename, String string) throws IOException { - FileWriter out = new FileWriter(filename); - try { - out.write(string); - } finally { - out.close(); - } - } + public int getDefaultGatewayAddr(); /** - * Record the detailed state of a network, and if it is a - * change from the previous state, send a notification to - * any listeners. - * @param state the new @{code DetailedState} + * Check if default route is set */ - public void setDetailedState(NetworkInfo.DetailedState state) { - setDetailedState(state, null, null); - } + public boolean isDefaultRouteSet(); /** - * Record the detailed state of a network, and if it is a - * change from the previous state, send a notification to - * any listeners. - * @param state the new @{code DetailedState} - * @param reason a {@code String} indicating a reason for the state change, - * if one was supplied. May be {@code null}. - * @param extraInfo optional {@code String} providing extra information about the state change + * Set a flag indicating default route is set for the network */ - public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) { - if (DBG) Log.d(TAG, "setDetailed state, old ="+mNetworkInfo.getDetailedState()+" and new state="+state); - if (state != mNetworkInfo.getDetailedState()) { - boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING); - String lastReason = mNetworkInfo.getReason(); - /* - * If a reason was supplied when the CONNECTING state was entered, and no - * reason was supplied for entering the CONNECTED state, then retain the - * reason that was supplied when going to CONNECTING. - */ - if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null - && lastReason != null) - reason = lastReason; - mNetworkInfo.setDetailedState(state, reason, extraInfo); - Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - } - - protected void setDetailedStateInternal(NetworkInfo.DetailedState state) { - mNetworkInfo.setDetailedState(state, null, null); - } + public void defaultRouteSet(boolean enabled); - public void setTeardownRequested(boolean isRequested) { - mTeardownRequested = isRequested; - } - - public boolean isTeardownRequested() { - return mTeardownRequested; - } - /** - * Send a notification that the results of a scan for network access - * points has completed, and results are available. + * Indicate tear down requested from connectivity */ - protected void sendScanResultsAvailable() { - Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo); - msg.sendToTarget(); - } + public void setTeardownRequested(boolean isRequested); /** - * Record the roaming status of the device, and if it is a change from the previous - * status, send a notification to any listeners. - * @param isRoaming {@code true} if the device is now roaming, {@code false} - * if it is no longer roaming. + * Check if tear down was requested */ - protected void setRoamingStatus(boolean isRoaming) { - if (isRoaming != mNetworkInfo.isRoaming()) { - mNetworkInfo.setRoaming(isRoaming); - Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - } - - protected void setSubtype(int subtype, String subtypeName) { - if (mNetworkInfo.isConnected()) { - int oldSubtype = mNetworkInfo.getSubtype(); - if (subtype != oldSubtype) { - mNetworkInfo.setSubtype(subtype, subtypeName); - Message msg = mTarget.obtainMessage( - EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo); - msg.sendToTarget(); - } - } - } + public boolean isTeardownRequested(); - public abstract void startMonitoring(); + public void startMonitoring(); /** * Disable connectivity to a network * @return {@code true} if a teardown occurred, {@code false} if the * teardown did not occur. */ - public abstract boolean teardown(); + public boolean teardown(); /** * Reenable connectivity to a network after a {@link #teardown()}. + * @return {@code true} if we're connected or expect to be connected */ - public abstract boolean reconnect(); + public boolean reconnect(); /** * Turn the wireless radio off for a network. * @param turnOn {@code true} to turn the radio on, {@code false} */ - public abstract boolean setRadio(boolean turnOn); + public boolean setRadio(boolean turnOn); /** * Returns an indication of whether this network is available for * connections. A value of {@code false} means that some quasi-permanent * condition prevents connectivity to this network. */ - public abstract boolean isAvailable(); + public boolean isAvailable(); /** * Tells the underlying networking system that the caller wants to @@ -376,7 +131,7 @@ public abstract class NetworkStateTracker extends Handler { * implementation+feature combination, except that the value {@code -1} * always indicates failure. */ - public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid); + public int startUsingNetworkFeature(String feature, int callingPid, int callingUid); /** * Tells the underlying networking system that the caller is finished @@ -390,23 +145,12 @@ public abstract class NetworkStateTracker extends Handler { * implementation+feature combination, except that the value {@code -1} * always indicates failure. */ - public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid); - - /** - * Ensure that a network route exists to deliver traffic to the specified - * host via this network interface. - * @param hostAddress the IP address of the host to which the route is desired - * @return {@code true} on success, {@code false} on failure - */ - public boolean requestRouteToHost(int hostAddress) { - return false; - } + public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid); /** * Interprets scan results. This will be called at a safe time for * processing, and from a safe thread. */ - public void interpretScanResultsAvailable() { - } + public void interpretScanResultsAvailable(); } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index a3ae01b..564bc1f 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -32,13 +32,37 @@ public class NetworkUtils { public native static int disableInterface(String interfaceName); /** Add a route to the specified host via the named interface. */ - public native static int addHostRoute(String interfaceName, int hostaddr); + public static int addHostRoute(String interfaceName, InetAddress hostaddr) { + int v4Int = v4StringToInt(hostaddr.getHostAddress()); + if (v4Int != 0) { + return addHostRouteNative(interfaceName, v4Int); + } else { + return -1; + } + } + private native static int addHostRouteNative(String interfaceName, int hostaddr); /** Add a default route for the named interface. */ - public native static int setDefaultRoute(String interfaceName, int gwayAddr); + public static int setDefaultRoute(String interfaceName, InetAddress gwayAddr) { + int v4Int = v4StringToInt(gwayAddr.getHostAddress()); + if (v4Int != 0) { + return setDefaultRouteNative(interfaceName, v4Int); + } else { + return -1; + } + } + private native static int setDefaultRouteNative(String interfaceName, int hostaddr); /** Return the gateway address for the default route for the named interface. */ - public native static int getDefaultRoute(String interfaceName); + public static InetAddress getDefaultRoute(String interfaceName) { + int addr = getDefaultRouteNative(interfaceName); + try { + return InetAddress.getByAddress(v4IntToArray(addr)); + } catch (UnknownHostException e) { + return null; + } + } + private native static int getDefaultRouteNative(String interfaceName); /** Remove host routes that uses the named interface. */ public native static int removeHostRoutes(String interfaceName); @@ -105,27 +129,30 @@ public class NetworkUtils { private native static boolean configureNative( String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2); - /** - * Look up a host name and return the result as an int. Works if the argument - * is an IP address in dot notation. Obviously, this can only be used for IPv4 - * addresses. - * @param hostname the name of the host (or the IP address) - * @return the IP address as an {@code int} in network byte order - */ - public static int lookupHost(String hostname) { - InetAddress inetAddress; + // The following two functions are glue to tie the old int-based address scheme + // to the new InetAddress scheme. They should go away when we go fully to InetAddress + // TODO - remove when we switch fully to InetAddress + public static byte[] v4IntToArray(int addr) { + byte[] addrBytes = new byte[4]; + addrBytes[0] = (byte)(addr & 0xff); + addrBytes[1] = (byte)((addr >> 8) & 0xff); + addrBytes[2] = (byte)((addr >> 16) & 0xff); + addrBytes[3] = (byte)((addr >> 24) & 0xff); + return addrBytes; + } + + public static int v4StringToInt(String str) { + int result = 0; + String[] array = str.split("\\."); + if (array.length != 4) return 0; try { - inetAddress = InetAddress.getByName(hostname); - } catch (UnknownHostException e) { - return -1; + result = Integer.parseInt(array[3]); + result = (result << 8) + Integer.parseInt(array[2]); + result = (result << 8) + Integer.parseInt(array[1]); + result = (result << 8) + Integer.parseInt(array[0]); + } catch (NumberFormatException e) { + return 0; } - byte[] addrBytes; - int addr; - addrBytes = inetAddress.getAddress(); - addr = ((addrBytes[3] & 0xff) << 24) - | ((addrBytes[2] & 0xff) << 16) - | ((addrBytes[1] & 0xff) << 8) - | (addrBytes[0] & 0xff); - return addr; + return result; } } diff --git a/core/java/android/net/ProxyProperties.java b/core/java/android/net/ProxyProperties.java new file mode 100644 index 0000000..6828dd4 --- /dev/null +++ b/core/java/android/net/ProxyProperties.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2007 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.net; + + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * A container class for the http proxy info + * @hide + */ +public class ProxyProperties implements Parcelable { + + private InetAddress mProxy; + private int mPort; + private String mExclusionList; + + public ProxyProperties() { + } + + public synchronized InetAddress getAddress() { + return mProxy; + } + public synchronized void setAddress(InetAddress proxy) { + mProxy = proxy; + } + + public synchronized int getPort() { + return mPort; + } + public synchronized void setPort(int port) { + mPort = port; + } + + public synchronized String getExclusionList() { + return mExclusionList; + } + public synchronized void setExclusionList(String exclusionList) { + mExclusionList = exclusionList; + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public synchronized void writeToParcel(Parcel dest, int flags) { + if (mProxy != null) { + dest.writeByte((byte)1); + dest.writeString(mProxy.getHostName()); + dest.writeByteArray(mProxy.getAddress()); + } else { + dest.writeByte((byte)0); + } + dest.writeInt(mPort); + dest.writeString(mExclusionList); + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public static final Creator<ProxyProperties> CREATOR = + new Creator<ProxyProperties>() { + public ProxyProperties createFromParcel(Parcel in) { + ProxyProperties proxyProperties = new ProxyProperties(); + if (in.readByte() == 1) { + try { + proxyProperties.setAddress(InetAddress.getByAddress(in.readString(), + in.createByteArray())); + } catch (UnknownHostException e) {} + } + proxyProperties.setPort(in.readInt()); + proxyProperties.setExclusionList(in.readString()); + return proxyProperties; + } + + public ProxyProperties[] newArray(int size) { + return new ProxyProperties[size]; + } + }; + +}; diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index e07ee59..915e342 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -61,6 +61,7 @@ import android.content.ContentResolver; import android.net.SSLCertificateSocketFactory; import android.net.SSLSessionCache; import android.os.Looper; +import android.util.Base64; import android.util.Log; /** @@ -81,6 +82,11 @@ public final class AndroidHttpClient implements HttpClient { private static final String TAG = "AndroidHttpClient"; + private static String[] textContentTypes = new String[] { + "text/", + "application/xml", + "application/json" + }; /** Interceptor throws an exception if the executing thread is blocked */ private static final HttpRequestInterceptor sThreadCheckInterceptor = @@ -358,7 +364,7 @@ public final class AndroidHttpClient implements HttpClient { } if (level < Log.VERBOSE || level > Log.ASSERT) { throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); + + Log.VERBOSE + ".." + Log.ASSERT + "]"); } curlConfiguration = new LoggingConfiguration(name, level); @@ -431,12 +437,17 @@ public final class AndroidHttpClient implements HttpClient { if (entity.getContentLength() < 1024) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); entity.writeTo(stream); - String entityString = stream.toString(); - // TODO: Check the content type, too. - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); + if (isBinaryContent(request)) { + String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP); + builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; "); + builder.append(" --data-binary @/tmp/$$.bin"); + } else { + String entityString = stream.toString(); + builder.append(" --data-ascii \"") + .append(entityString) + .append("\""); + } } else { builder.append(" [TOO MUCH DATA TO INCLUDE]"); } @@ -446,6 +457,30 @@ public final class AndroidHttpClient implements HttpClient { return builder.toString(); } + private static boolean isBinaryContent(HttpUriRequest request) { + Header[] headers; + headers = request.getHeaders(Headers.CONTENT_ENCODING); + if (headers != null) { + for (Header header : headers) { + if ("gzip".equalsIgnoreCase(header.getValue())) { + return true; + } + } + } + + headers = request.getHeaders(Headers.CONTENT_TYPE); + if (headers != null) { + for (Header header : headers) { + for (String contentType : textContentTypes) { + if (header.getValue().startsWith(contentType)) { + return false; + } + } + } + } + return true; + } + /** * Returns the date of the given HTTP date string. This method can identify * and parse the date formats emitted by common HTTP servers, such as diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index c527fe4..c36ad38 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -80,14 +80,10 @@ class CertificateChainValidator { throws IOException { X509Certificate[] serverCertificates = null; - // start handshake, close the socket if we fail - try { - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - } catch (IOException e) { - closeSocketThrowException( - sslSocket, e.getMessage(), - "failed to perform SSL handshake"); + // get a valid SSLSession, close the socket if we fail + SSLSession sslSession = sslSession = sslSocket.getSession(); + if (!sslSession.isValid()) { + closeSocketThrowException(sslSocket, "failed to perform SSL handshake"); } // retrieve the chain of the server peer certificates diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index d28148c..832ce84 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -16,16 +16,16 @@ package android.os; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.Callable; -import java.util.concurrent.FutureTask; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -36,8 +36,8 @@ import java.util.concurrent.atomic.AtomicInteger; * <p>An asynchronous task is defined by a computation that runs on a background thread and * whose result is published on the UI thread. An asynchronous task is defined by 3 generic * types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>, - * and 4 steps, called <code>begin</code>, <code>doInBackground</code>, - * <code>processProgress</code> and <code>end</code>.</p> + * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>, + * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p> * * <h2>Usage</h2> * <p>AsyncTask must be subclassed to be used. The subclass will override at least @@ -187,6 +187,17 @@ public abstract class AsyncTask<Params, Progress, Result> { }; mFuture = new FutureTask<Result>(mWorker) { + + @Override + protected void set(Result v) { + super.set(v); + if (isCancelled()) { + Message message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, + new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null)); + message.sendToTarget(); + } + } + @Override protected void done() { Message message; @@ -402,14 +413,19 @@ public abstract class AsyncTask<Params, Progress, Result> { * still running. Each call to this method will trigger the execution of * {@link #onProgressUpdate} on the UI thread. * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * * @param values The progress values to update the UI with. * * @see #onProgressUpdate * @see #doInBackground */ protected final void publishProgress(Progress... values) { - sHandler.obtainMessage(MESSAGE_POST_PROGRESS, - new AsyncTaskResult<Progress>(this, values)).sendToTarget(); + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult<Progress>(this, values)).sendToTarget(); + } } private void finish(Result result) { diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2e14667..d23b161 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -730,7 +730,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Dump "hprof" data to the specified file. This will cause a GC. + * Dump "hprof" data to the specified file. This may cause a GC. * * @param fileName Full pathname of output file (e.g. "/sdcard/dump.hprof"). * @throws UnsupportedOperationException if the VM was built without @@ -742,11 +742,24 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** - * Collect "hprof" and send it to DDMS. This will cause a GC. + * Like dumpHprofData(String), but takes an already-opened + * FileDescriptor to which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Primarily for use by the "am" shell command. + * + * @hide + */ + public static void dumpHprofData(String fileName, FileDescriptor fd) + throws IOException { + VMDebug.dumpHprofData(fileName, fd); + } + + /** + * Collect "hprof" and send it to DDMS. This may cause a GC. * * @throws UnsupportedOperationException if the VM was built without * HPROF support. - * * @hide */ public static void dumpHprofDataDdms() { @@ -754,6 +767,13 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** + * Writes native heap data to the specified file descriptor. + * + * @hide + */ + public static native void dumpNativeHeap(FileDescriptor fd); + + /** * Returns the number of sent transactions from this process. * @return The number of sent transactions or -1 if it could not read t. */ diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java index 7b883a7..d3d39d6 100644 --- a/core/java/android/os/storage/StorageEventListener.java +++ b/core/java/android/os/storage/StorageEventListener.java @@ -18,7 +18,6 @@ package android.os.storage; /** * Used for receiving notifications from the StorageManager - * @hide */ public abstract class StorageEventListener { /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index a12603c..b49979c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -45,8 +45,6 @@ import java.util.List; * {@link android.content.Context#getSystemService(java.lang.String)} with an argument * of {@link android.content.Context#STORAGE_SERVICE}. * - * @hide - * */ public class StorageManager diff --git a/core/java/android/os/storage/StorageResultCode.java b/core/java/android/os/storage/StorageResultCode.java index 075f47f..07d95df 100644 --- a/core/java/android/os/storage/StorageResultCode.java +++ b/core/java/android/os/storage/StorageResultCode.java @@ -19,8 +19,6 @@ package android.os.storage; /** * Class that provides access to constants returned from StorageManager * and lower level MountService APIs. - * - * @hide */ public class StorageResultCode { diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 635323e..282417d 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -181,7 +181,9 @@ public class RecurrenceSet { boolean inUtc = start.parse(dtstart); boolean allDay = start.allDay; - if (inUtc) { + // We force TimeZone to UTC for "all day recurring events" as the server is sending no + // TimeZone in DTSTART for them + if (inUtc || allDay) { tzid = Time.TIMEZONE_UTC; } @@ -204,10 +206,7 @@ public class RecurrenceSet { } if (allDay) { - // TODO: also change tzid to be UTC? that would be consistent, but - // that would not reflect the original timezone value back to the - // server. - start.timezone = Time.TIMEZONE_UTC; + start.timezone = Time.TIMEZONE_UTC; } long millis = start.toMillis(false /* use isDst */); values.put(Calendar.Events.DTSTART, millis); diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java deleted file mode 100644 index 875c29e..0000000 --- a/core/java/android/pim/vcard/JapaneseUtils.java +++ /dev/null @@ -1,380 +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.pim.vcard; - -import java.util.HashMap; -import java.util.Map; - -/** - * TextUtils especially for Japanese. - */ -/* package */ class JapaneseUtils { - static private final Map<Character, String> sHalfWidthMap = - new HashMap<Character, String>(); - - static { - // There's no logical mapping rule in Unicode. Sigh. - sHalfWidthMap.put('\u3001', "\uFF64"); - sHalfWidthMap.put('\u3002', "\uFF61"); - sHalfWidthMap.put('\u300C', "\uFF62"); - sHalfWidthMap.put('\u300D', "\uFF63"); - sHalfWidthMap.put('\u301C', "~"); - sHalfWidthMap.put('\u3041', "\uFF67"); - sHalfWidthMap.put('\u3042', "\uFF71"); - sHalfWidthMap.put('\u3043', "\uFF68"); - sHalfWidthMap.put('\u3044', "\uFF72"); - sHalfWidthMap.put('\u3045', "\uFF69"); - sHalfWidthMap.put('\u3046', "\uFF73"); - sHalfWidthMap.put('\u3047', "\uFF6A"); - sHalfWidthMap.put('\u3048', "\uFF74"); - sHalfWidthMap.put('\u3049', "\uFF6B"); - sHalfWidthMap.put('\u304A', "\uFF75"); - sHalfWidthMap.put('\u304B', "\uFF76"); - sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); - sHalfWidthMap.put('\u304D', "\uFF77"); - sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); - sHalfWidthMap.put('\u304F', "\uFF78"); - sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); - sHalfWidthMap.put('\u3051', "\uFF79"); - sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); - sHalfWidthMap.put('\u3053', "\uFF7A"); - sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); - sHalfWidthMap.put('\u3055', "\uFF7B"); - sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); - sHalfWidthMap.put('\u3057', "\uFF7C"); - sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); - sHalfWidthMap.put('\u3059', "\uFF7D"); - sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); - sHalfWidthMap.put('\u305B', "\uFF7E"); - sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); - sHalfWidthMap.put('\u305D', "\uFF7F"); - sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); - sHalfWidthMap.put('\u305F', "\uFF80"); - sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); - sHalfWidthMap.put('\u3061', "\uFF81"); - sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); - sHalfWidthMap.put('\u3063', "\uFF6F"); - sHalfWidthMap.put('\u3064', "\uFF82"); - sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); - sHalfWidthMap.put('\u3066', "\uFF83"); - sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); - sHalfWidthMap.put('\u3068', "\uFF84"); - sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); - sHalfWidthMap.put('\u306A', "\uFF85"); - sHalfWidthMap.put('\u306B', "\uFF86"); - sHalfWidthMap.put('\u306C', "\uFF87"); - sHalfWidthMap.put('\u306D', "\uFF88"); - sHalfWidthMap.put('\u306E', "\uFF89"); - sHalfWidthMap.put('\u306F', "\uFF8A"); - sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); - sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); - sHalfWidthMap.put('\u3072', "\uFF8B"); - sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); - sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); - sHalfWidthMap.put('\u3075', "\uFF8C"); - sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); - sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); - sHalfWidthMap.put('\u3078', "\uFF8D"); - sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); - sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); - sHalfWidthMap.put('\u307B', "\uFF8E"); - sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); - sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); - sHalfWidthMap.put('\u307E', "\uFF8F"); - sHalfWidthMap.put('\u307F', "\uFF90"); - sHalfWidthMap.put('\u3080', "\uFF91"); - sHalfWidthMap.put('\u3081', "\uFF92"); - sHalfWidthMap.put('\u3082', "\uFF93"); - sHalfWidthMap.put('\u3083', "\uFF6C"); - sHalfWidthMap.put('\u3084', "\uFF94"); - sHalfWidthMap.put('\u3085', "\uFF6D"); - sHalfWidthMap.put('\u3086', "\uFF95"); - sHalfWidthMap.put('\u3087', "\uFF6E"); - sHalfWidthMap.put('\u3088', "\uFF96"); - sHalfWidthMap.put('\u3089', "\uFF97"); - sHalfWidthMap.put('\u308A', "\uFF98"); - sHalfWidthMap.put('\u308B', "\uFF99"); - sHalfWidthMap.put('\u308C', "\uFF9A"); - sHalfWidthMap.put('\u308D', "\uFF9B"); - sHalfWidthMap.put('\u308E', "\uFF9C"); - sHalfWidthMap.put('\u308F', "\uFF9C"); - sHalfWidthMap.put('\u3090', "\uFF72"); - sHalfWidthMap.put('\u3091', "\uFF74"); - sHalfWidthMap.put('\u3092', "\uFF66"); - sHalfWidthMap.put('\u3093', "\uFF9D"); - sHalfWidthMap.put('\u309B', "\uFF9E"); - sHalfWidthMap.put('\u309C', "\uFF9F"); - sHalfWidthMap.put('\u30A1', "\uFF67"); - sHalfWidthMap.put('\u30A2', "\uFF71"); - sHalfWidthMap.put('\u30A3', "\uFF68"); - sHalfWidthMap.put('\u30A4', "\uFF72"); - sHalfWidthMap.put('\u30A5', "\uFF69"); - sHalfWidthMap.put('\u30A6', "\uFF73"); - sHalfWidthMap.put('\u30A7', "\uFF6A"); - sHalfWidthMap.put('\u30A8', "\uFF74"); - sHalfWidthMap.put('\u30A9', "\uFF6B"); - sHalfWidthMap.put('\u30AA', "\uFF75"); - sHalfWidthMap.put('\u30AB', "\uFF76"); - sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); - sHalfWidthMap.put('\u30AD', "\uFF77"); - sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); - sHalfWidthMap.put('\u30AF', "\uFF78"); - sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); - sHalfWidthMap.put('\u30B1', "\uFF79"); - sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); - sHalfWidthMap.put('\u30B3', "\uFF7A"); - sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); - sHalfWidthMap.put('\u30B5', "\uFF7B"); - sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); - sHalfWidthMap.put('\u30B7', "\uFF7C"); - sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); - sHalfWidthMap.put('\u30B9', "\uFF7D"); - sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); - sHalfWidthMap.put('\u30BB', "\uFF7E"); - sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); - sHalfWidthMap.put('\u30BD', "\uFF7F"); - sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); - sHalfWidthMap.put('\u30BF', "\uFF80"); - sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); - sHalfWidthMap.put('\u30C1', "\uFF81"); - sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); - sHalfWidthMap.put('\u30C3', "\uFF6F"); - sHalfWidthMap.put('\u30C4', "\uFF82"); - sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); - sHalfWidthMap.put('\u30C6', "\uFF83"); - sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); - sHalfWidthMap.put('\u30C8', "\uFF84"); - sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); - sHalfWidthMap.put('\u30CA', "\uFF85"); - sHalfWidthMap.put('\u30CB', "\uFF86"); - sHalfWidthMap.put('\u30CC', "\uFF87"); - sHalfWidthMap.put('\u30CD', "\uFF88"); - sHalfWidthMap.put('\u30CE', "\uFF89"); - sHalfWidthMap.put('\u30CF', "\uFF8A"); - sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); - sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); - sHalfWidthMap.put('\u30D2', "\uFF8B"); - sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); - sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); - sHalfWidthMap.put('\u30D5', "\uFF8C"); - sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); - sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); - sHalfWidthMap.put('\u30D8', "\uFF8D"); - sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); - sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); - sHalfWidthMap.put('\u30DB', "\uFF8E"); - sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); - sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); - sHalfWidthMap.put('\u30DE', "\uFF8F"); - sHalfWidthMap.put('\u30DF', "\uFF90"); - sHalfWidthMap.put('\u30E0', "\uFF91"); - sHalfWidthMap.put('\u30E1', "\uFF92"); - sHalfWidthMap.put('\u30E2', "\uFF93"); - sHalfWidthMap.put('\u30E3', "\uFF6C"); - sHalfWidthMap.put('\u30E4', "\uFF94"); - sHalfWidthMap.put('\u30E5', "\uFF6D"); - sHalfWidthMap.put('\u30E6', "\uFF95"); - sHalfWidthMap.put('\u30E7', "\uFF6E"); - sHalfWidthMap.put('\u30E8', "\uFF96"); - sHalfWidthMap.put('\u30E9', "\uFF97"); - sHalfWidthMap.put('\u30EA', "\uFF98"); - sHalfWidthMap.put('\u30EB', "\uFF99"); - sHalfWidthMap.put('\u30EC', "\uFF9A"); - sHalfWidthMap.put('\u30ED', "\uFF9B"); - sHalfWidthMap.put('\u30EE', "\uFF9C"); - sHalfWidthMap.put('\u30EF', "\uFF9C"); - sHalfWidthMap.put('\u30F0', "\uFF72"); - sHalfWidthMap.put('\u30F1', "\uFF74"); - sHalfWidthMap.put('\u30F2', "\uFF66"); - sHalfWidthMap.put('\u30F3', "\uFF9D"); - sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); - sHalfWidthMap.put('\u30F5', "\uFF76"); - sHalfWidthMap.put('\u30F6', "\uFF79"); - sHalfWidthMap.put('\u30FB', "\uFF65"); - sHalfWidthMap.put('\u30FC', "\uFF70"); - sHalfWidthMap.put('\uFF01', "!"); - sHalfWidthMap.put('\uFF02', "\""); - sHalfWidthMap.put('\uFF03', "#"); - sHalfWidthMap.put('\uFF04', "$"); - sHalfWidthMap.put('\uFF05', "%"); - sHalfWidthMap.put('\uFF06', "&"); - sHalfWidthMap.put('\uFF07', "'"); - sHalfWidthMap.put('\uFF08', "("); - sHalfWidthMap.put('\uFF09', ")"); - sHalfWidthMap.put('\uFF0A', "*"); - sHalfWidthMap.put('\uFF0B', "+"); - sHalfWidthMap.put('\uFF0C', ","); - sHalfWidthMap.put('\uFF0D', "-"); - sHalfWidthMap.put('\uFF0E', "."); - sHalfWidthMap.put('\uFF0F', "/"); - sHalfWidthMap.put('\uFF10', "0"); - sHalfWidthMap.put('\uFF11', "1"); - sHalfWidthMap.put('\uFF12', "2"); - sHalfWidthMap.put('\uFF13', "3"); - sHalfWidthMap.put('\uFF14', "4"); - sHalfWidthMap.put('\uFF15', "5"); - sHalfWidthMap.put('\uFF16', "6"); - sHalfWidthMap.put('\uFF17', "7"); - sHalfWidthMap.put('\uFF18', "8"); - sHalfWidthMap.put('\uFF19', "9"); - sHalfWidthMap.put('\uFF1A', ":"); - sHalfWidthMap.put('\uFF1B', ";"); - sHalfWidthMap.put('\uFF1C', "<"); - sHalfWidthMap.put('\uFF1D', "="); - sHalfWidthMap.put('\uFF1E', ">"); - sHalfWidthMap.put('\uFF1F', "?"); - sHalfWidthMap.put('\uFF20', "@"); - sHalfWidthMap.put('\uFF21', "A"); - sHalfWidthMap.put('\uFF22', "B"); - sHalfWidthMap.put('\uFF23', "C"); - sHalfWidthMap.put('\uFF24', "D"); - sHalfWidthMap.put('\uFF25', "E"); - sHalfWidthMap.put('\uFF26', "F"); - sHalfWidthMap.put('\uFF27', "G"); - sHalfWidthMap.put('\uFF28', "H"); - sHalfWidthMap.put('\uFF29', "I"); - sHalfWidthMap.put('\uFF2A', "J"); - sHalfWidthMap.put('\uFF2B', "K"); - sHalfWidthMap.put('\uFF2C', "L"); - sHalfWidthMap.put('\uFF2D', "M"); - sHalfWidthMap.put('\uFF2E', "N"); - sHalfWidthMap.put('\uFF2F', "O"); - sHalfWidthMap.put('\uFF30', "P"); - sHalfWidthMap.put('\uFF31', "Q"); - sHalfWidthMap.put('\uFF32', "R"); - sHalfWidthMap.put('\uFF33', "S"); - sHalfWidthMap.put('\uFF34', "T"); - sHalfWidthMap.put('\uFF35', "U"); - sHalfWidthMap.put('\uFF36', "V"); - sHalfWidthMap.put('\uFF37', "W"); - sHalfWidthMap.put('\uFF38', "X"); - sHalfWidthMap.put('\uFF39', "Y"); - sHalfWidthMap.put('\uFF3A', "Z"); - sHalfWidthMap.put('\uFF3B', "["); - sHalfWidthMap.put('\uFF3C', "\\"); - sHalfWidthMap.put('\uFF3D', "]"); - sHalfWidthMap.put('\uFF3E', "^"); - sHalfWidthMap.put('\uFF3F', "_"); - sHalfWidthMap.put('\uFF41', "a"); - sHalfWidthMap.put('\uFF42', "b"); - sHalfWidthMap.put('\uFF43', "c"); - sHalfWidthMap.put('\uFF44', "d"); - sHalfWidthMap.put('\uFF45', "e"); - sHalfWidthMap.put('\uFF46', "f"); - sHalfWidthMap.put('\uFF47', "g"); - sHalfWidthMap.put('\uFF48', "h"); - sHalfWidthMap.put('\uFF49', "i"); - sHalfWidthMap.put('\uFF4A', "j"); - sHalfWidthMap.put('\uFF4B', "k"); - sHalfWidthMap.put('\uFF4C', "l"); - sHalfWidthMap.put('\uFF4D', "m"); - sHalfWidthMap.put('\uFF4E', "n"); - sHalfWidthMap.put('\uFF4F', "o"); - sHalfWidthMap.put('\uFF50', "p"); - sHalfWidthMap.put('\uFF51', "q"); - sHalfWidthMap.put('\uFF52', "r"); - sHalfWidthMap.put('\uFF53', "s"); - sHalfWidthMap.put('\uFF54', "t"); - sHalfWidthMap.put('\uFF55', "u"); - sHalfWidthMap.put('\uFF56', "v"); - sHalfWidthMap.put('\uFF57', "w"); - sHalfWidthMap.put('\uFF58', "x"); - sHalfWidthMap.put('\uFF59', "y"); - sHalfWidthMap.put('\uFF5A', "z"); - sHalfWidthMap.put('\uFF5B', "{"); - sHalfWidthMap.put('\uFF5C', "|"); - sHalfWidthMap.put('\uFF5D', "}"); - sHalfWidthMap.put('\uFF5E', "~"); - sHalfWidthMap.put('\uFF61', "\uFF61"); - sHalfWidthMap.put('\uFF62', "\uFF62"); - sHalfWidthMap.put('\uFF63', "\uFF63"); - sHalfWidthMap.put('\uFF64', "\uFF64"); - sHalfWidthMap.put('\uFF65', "\uFF65"); - sHalfWidthMap.put('\uFF66', "\uFF66"); - sHalfWidthMap.put('\uFF67', "\uFF67"); - sHalfWidthMap.put('\uFF68', "\uFF68"); - sHalfWidthMap.put('\uFF69', "\uFF69"); - sHalfWidthMap.put('\uFF6A', "\uFF6A"); - sHalfWidthMap.put('\uFF6B', "\uFF6B"); - sHalfWidthMap.put('\uFF6C', "\uFF6C"); - sHalfWidthMap.put('\uFF6D', "\uFF6D"); - sHalfWidthMap.put('\uFF6E', "\uFF6E"); - sHalfWidthMap.put('\uFF6F', "\uFF6F"); - sHalfWidthMap.put('\uFF70', "\uFF70"); - sHalfWidthMap.put('\uFF71', "\uFF71"); - sHalfWidthMap.put('\uFF72', "\uFF72"); - sHalfWidthMap.put('\uFF73', "\uFF73"); - sHalfWidthMap.put('\uFF74', "\uFF74"); - sHalfWidthMap.put('\uFF75', "\uFF75"); - sHalfWidthMap.put('\uFF76', "\uFF76"); - sHalfWidthMap.put('\uFF77', "\uFF77"); - sHalfWidthMap.put('\uFF78', "\uFF78"); - sHalfWidthMap.put('\uFF79', "\uFF79"); - sHalfWidthMap.put('\uFF7A', "\uFF7A"); - sHalfWidthMap.put('\uFF7B', "\uFF7B"); - sHalfWidthMap.put('\uFF7C', "\uFF7C"); - sHalfWidthMap.put('\uFF7D', "\uFF7D"); - sHalfWidthMap.put('\uFF7E', "\uFF7E"); - sHalfWidthMap.put('\uFF7F', "\uFF7F"); - sHalfWidthMap.put('\uFF80', "\uFF80"); - sHalfWidthMap.put('\uFF81', "\uFF81"); - sHalfWidthMap.put('\uFF82', "\uFF82"); - sHalfWidthMap.put('\uFF83', "\uFF83"); - sHalfWidthMap.put('\uFF84', "\uFF84"); - sHalfWidthMap.put('\uFF85', "\uFF85"); - sHalfWidthMap.put('\uFF86', "\uFF86"); - sHalfWidthMap.put('\uFF87', "\uFF87"); - sHalfWidthMap.put('\uFF88', "\uFF88"); - sHalfWidthMap.put('\uFF89', "\uFF89"); - sHalfWidthMap.put('\uFF8A', "\uFF8A"); - sHalfWidthMap.put('\uFF8B', "\uFF8B"); - sHalfWidthMap.put('\uFF8C', "\uFF8C"); - sHalfWidthMap.put('\uFF8D', "\uFF8D"); - sHalfWidthMap.put('\uFF8E', "\uFF8E"); - sHalfWidthMap.put('\uFF8F', "\uFF8F"); - sHalfWidthMap.put('\uFF90', "\uFF90"); - sHalfWidthMap.put('\uFF91', "\uFF91"); - sHalfWidthMap.put('\uFF92', "\uFF92"); - sHalfWidthMap.put('\uFF93', "\uFF93"); - sHalfWidthMap.put('\uFF94', "\uFF94"); - sHalfWidthMap.put('\uFF95', "\uFF95"); - sHalfWidthMap.put('\uFF96', "\uFF96"); - sHalfWidthMap.put('\uFF97', "\uFF97"); - sHalfWidthMap.put('\uFF98', "\uFF98"); - sHalfWidthMap.put('\uFF99', "\uFF99"); - sHalfWidthMap.put('\uFF9A', "\uFF9A"); - sHalfWidthMap.put('\uFF9B', "\uFF9B"); - sHalfWidthMap.put('\uFF9C', "\uFF9C"); - sHalfWidthMap.put('\uFF9D', "\uFF9D"); - sHalfWidthMap.put('\uFF9E', "\uFF9E"); - sHalfWidthMap.put('\uFF9F', "\uFF9F"); - sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); - } - - /** - * Return half-width version of that character if possible. Return null if not possible - * @param ch input character - * @return CharSequence object if the mapping for ch exists. Return null otherwise. - */ - public static String tryGetHalfWidthText(char ch) { - if (sHalfWidthMap.containsKey(ch)) { - return sHalfWidthMap.get(ch); - } else { - return null; - } - } -} diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java deleted file mode 100644 index 1da6d7a..0000000 --- a/core/java/android/pim/vcard/VCardBuilder.java +++ /dev/null @@ -1,1932 +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.pim.vcard; - -import android.content.ContentValues; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Event; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Nickname; -import android.provider.ContactsContract.CommonDataKinds.Note; -import android.provider.ContactsContract.CommonDataKinds.Organization; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.CommonDataKinds.Relation; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.CommonDataKinds.Website; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.util.CharsetUtils; -import android.util.Log; - -import org.apache.commons.codec.binary.Base64; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.UnsupportedCharsetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * The class which lets users create their own vCard String. - */ -public class VCardBuilder { - private static final String LOG_TAG = "VCardBuilder"; - - // If you add the other element, please check all the columns are able to be - // converted to String. - // - // e.g. BLOB is not what we can handle here now. - private static final Set<String> sAllowedAndroidPropertySet = - Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( - Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, - Relation.CONTENT_ITEM_TYPE))); - - public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; - public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; - public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; - - private static final String VCARD_DATA_VCARD = "VCARD"; - private static final String VCARD_DATA_PUBLIC = "PUBLIC"; - - private static final String VCARD_PARAM_SEPARATOR = ";"; - private static final String VCARD_END_OF_LINE = "\r\n"; - private static final String VCARD_DATA_SEPARATOR = ":"; - private static final String VCARD_ITEM_SEPARATOR = ";"; - private static final String VCARD_WS = " "; - private static final String VCARD_PARAM_EQUAL = "="; - - private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; - - private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; - private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; - - private static final String SHIFT_JIS = "SHIFT_JIS"; - private static final String UTF_8 = "UTF-8"; - - private final int mVCardType; - - private final boolean mIsV30; - private final boolean mIsJapaneseMobilePhone; - private final boolean mOnlyOneNoteFieldIsAvailable; - private final boolean mIsDoCoMo; - private final boolean mShouldUseQuotedPrintable; - private final boolean mUsesAndroidProperty; - private final boolean mUsesDefactProperty; - private final boolean mUsesUtf8; - private final boolean mUsesShiftJis; - private final boolean mAppendTypeParamName; - private final boolean mRefrainsQPToNameProperties; - private final boolean mNeedsToConvertPhoneticString; - - private final boolean mShouldAppendCharsetParam; - - private final String mCharsetString; - private final String mVCardCharsetParameter; - - private StringBuilder mBuilder; - private boolean mEndAppended; - - public VCardBuilder(final int vcardType) { - mVCardType = vcardType; - - mIsV30 = VCardConfig.isV30(vcardType); - mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType); - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); - mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); - mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); - mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); - mUsesUtf8 = VCardConfig.usesUtf8(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); - mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); - mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); - mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); - - mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); - - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but - // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in - // Android, not shown to the public). - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; - } else { - mCharsetString = UTF_8; - mVCardCharsetParameter = "CHARSET=" + UTF_8; - } - clear(); - } - - public void clear() { - mBuilder = new StringBuilder(); - mEndAppended = false; - appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (mIsV30) { - appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); - } else { - appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); - } - } - - private boolean containsNonEmptyName(final ContentValues contentValues) { - final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); - final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); - final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); - final String prefix = contentValues.getAsString(StructuredName.PREFIX); - final String suffix = contentValues.getAsString(StructuredName.SUFFIX); - final String phoneticFamilyName = - contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String phoneticMiddleName = - contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final String phoneticGivenName = - contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); - final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); - return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && - TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && - TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) && - TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && - TextUtils.isEmpty(displayName)); - } - - private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) { - ContentValues primaryContentValues = null; - ContentValues subprimaryContentValues = null; - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null){ - continue; - } - Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); - if (isSuperPrimary != null && isSuperPrimary > 0) { - // We choose "super primary" ContentValues. - primaryContentValues = contentValues; - break; - } else if (primaryContentValues == null) { - // We choose the first "primary" ContentValues - // if "super primary" ContentValues does not exist. - final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); - if (isPrimary != null && isPrimary > 0 && - containsNonEmptyName(contentValues)) { - primaryContentValues = contentValues; - // Do not break, since there may be ContentValues with "super primary" - // afterword. - } else if (subprimaryContentValues == null && - containsNonEmptyName(contentValues)) { - subprimaryContentValues = contentValues; - } - } - } - - if (primaryContentValues == null) { - if (subprimaryContentValues != null) { - // We choose the first ContentValues if any "primary" ContentValues does not exist. - primaryContentValues = subprimaryContentValues; - } else { - Log.e(LOG_TAG, "All ContentValues given from database is empty."); - primaryContentValues = new ContentValues(); - } - } - - return primaryContentValues; - } - - /** - * For safety, we'll emit just one value around StructuredName, as external importers - * may get confused with multiple "N", "FN", etc. properties, though it is valid in - * vCard spec. - */ - public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) { - if (contentValuesList == null || contentValuesList.isEmpty()) { - if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_N, ""); - } else if (mIsV30) { - // vCard 3.0 requires "N" and "FN" properties. - appendLine(VCardConstants.PROPERTY_N, ""); - appendLine(VCardConstants.PROPERTY_FN, ""); - } - return this; - } - - final ContentValues contentValues = getPrimaryContentValue(contentValuesList); - final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); - final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); - final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); - final String prefix = contentValues.getAsString(StructuredName.PREFIX); - final String suffix = contentValues.getAsString(StructuredName.SUFFIX); - final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); - - if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { - final boolean reallyAppendCharsetParameterToName = - shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix); - final boolean reallyUseQuotedPrintableToName = - (!mRefrainsQPToNameProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); - - final String formattedName; - if (!TextUtils.isEmpty(displayName)) { - formattedName = displayName; - } else { - formattedName = VCardUtils.constructNameFromElements( - VCardConfig.getNameOrderType(mVCardType), - familyName, middleName, givenName, prefix, suffix); - } - final boolean reallyAppendCharsetParameterToFN = - shouldAppendCharsetParam(formattedName); - final boolean reallyUseQuotedPrintableToFN = - !mRefrainsQPToNameProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); - - final String encodedFamily; - final String encodedGiven; - final String encodedMiddle; - final String encodedPrefix; - final String encodedSuffix; - if (reallyUseQuotedPrintableToName) { - encodedFamily = encodeQuotedPrintable(familyName); - encodedGiven = encodeQuotedPrintable(givenName); - encodedMiddle = encodeQuotedPrintable(middleName); - encodedPrefix = encodeQuotedPrintable(prefix); - encodedSuffix = encodeQuotedPrintable(suffix); - } else { - encodedFamily = escapeCharacters(familyName); - encodedGiven = escapeCharacters(givenName); - encodedMiddle = escapeCharacters(middleName); - encodedPrefix = escapeCharacters(prefix); - encodedSuffix = escapeCharacters(suffix); - } - - final String encodedFormattedname = - (reallyUseQuotedPrintableToFN ? - encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); - - mBuilder.append(VCardConstants.PROPERTY_N); - if (mIsDoCoMo) { - if (reallyAppendCharsetParameterToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - // DoCoMo phones require that all the elements in the "family name" field. - mBuilder.append(formattedName); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - } else { - if (reallyAppendCharsetParameterToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedFamily); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedGiven); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedMiddle); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedPrefix); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedSuffix); - } - mBuilder.append(VCARD_END_OF_LINE); - - // FN property - mBuilder.append(VCardConstants.PROPERTY_FN); - if (reallyAppendCharsetParameterToFN) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToFN) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedFormattedname); - mBuilder.append(VCARD_END_OF_LINE); - } else if (!TextUtils.isEmpty(displayName)) { - final boolean reallyUseQuotedPrintableToDisplayName = - (!mRefrainsQPToNameProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); - final String encodedDisplayName = - reallyUseQuotedPrintableToDisplayName ? - encodeQuotedPrintable(displayName) : - escapeCharacters(displayName); - - mBuilder.append(VCardConstants.PROPERTY_N); - if (shouldAppendCharsetParam(displayName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintableToDisplayName) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedDisplayName); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - mBuilder.append(VCardConstants.PROPERTY_FN); - - // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it - // when it would be useful for external importers, assuming no external - // importer allows this vioration. - if (shouldAppendCharsetParam(displayName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedDisplayName); - mBuilder.append(VCARD_END_OF_LINE); - } else if (mIsV30) { - // vCard 3.0 specification requires these fields. - appendLine(VCardConstants.PROPERTY_N, ""); - appendLine(VCardConstants.PROPERTY_FN, ""); - } else if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_N, ""); - } - - appendPhoneticNameFields(contentValues); - return this; - } - - private void appendPhoneticNameFields(final ContentValues contentValues) { - final String phoneticFamilyName; - final String phoneticMiddleName; - final String phoneticGivenName; - { - final String tmpPhoneticFamilyName = - contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String tmpPhoneticMiddleName = - contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final String tmpPhoneticGivenName = - contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); - if (mNeedsToConvertPhoneticString) { - phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); - phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); - phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); - } else { - phoneticFamilyName = tmpPhoneticFamilyName; - phoneticMiddleName = tmpPhoneticMiddleName; - phoneticGivenName = tmpPhoneticGivenName; - } - } - - if (TextUtils.isEmpty(phoneticFamilyName) - && TextUtils.isEmpty(phoneticMiddleName) - && TextUtils.isEmpty(phoneticGivenName)) { - if (mIsDoCoMo) { - mBuilder.append(VCardConstants.PROPERTY_SOUND); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - } - return; - } - - // Try to emit the field(s) related to phonetic name. - if (mIsV30) { - final String sortString = VCardUtils - .constructNameFromElements(mVCardType, - phoneticFamilyName, phoneticMiddleName, phoneticGivenName); - mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); - if (shouldAppendCharsetParam(sortString)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(escapeCharacters(sortString)); - mBuilder.append(VCARD_END_OF_LINE); - } else if (mIsJapaneseMobilePhone) { - // Note: There is no appropriate property for expressing - // phonetic name in vCard 2.1, while there is in - // vCard 3.0 (SORT-STRING). - // We chose to use DoCoMo's way when the device is Japanese one - // since it is supported by - // a lot of Japanese mobile phones. This is "X-" property, so - // any parser hopefully would not get confused with this. - // - // Also, DoCoMo's specification requires vCard composer to use just the first - // column. - // i.e. - // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; - // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; - mBuilder.append(VCardConstants.PROPERTY_SOUND); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); - - boolean reallyUseQuotedPrintable = - (!mRefrainsQPToNameProperties - && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticFamilyName) - && VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticMiddleName) - && VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticGivenName))); - - final String encodedPhoneticFamilyName; - final String encodedPhoneticMiddleName; - final String encodedPhoneticGivenName; - if (reallyUseQuotedPrintable) { - encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); - encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); - encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); - } else { - encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); - encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); - encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); - } - - if (shouldAppendCharsetParam(encodedPhoneticFamilyName, - encodedPhoneticMiddleName, encodedPhoneticGivenName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - { - boolean first = true; - if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { - mBuilder.append(encodedPhoneticFamilyName); - first = false; - } - if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { - if (first) { - first = false; - } else { - mBuilder.append(' '); - } - mBuilder.append(encodedPhoneticMiddleName); - } - if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { - if (!first) { - mBuilder.append(' '); - } - mBuilder.append(encodedPhoneticGivenName); - } - } - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - } - - if (mUsesDefactProperty) { - if (!TextUtils.isEmpty(phoneticGivenName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); - final String encodedPhoneticGivenName; - if (reallyUseQuotedPrintable) { - encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); - } else { - encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME); - if (shouldAppendCharsetParam(phoneticGivenName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticGivenName); - mBuilder.append(VCARD_END_OF_LINE); - } - if (!TextUtils.isEmpty(phoneticMiddleName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); - final String encodedPhoneticMiddleName; - if (reallyUseQuotedPrintable) { - encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); - } else { - encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME); - if (shouldAppendCharsetParam(phoneticMiddleName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticMiddleName); - mBuilder.append(VCARD_END_OF_LINE); - } - if (!TextUtils.isEmpty(phoneticFamilyName)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); - final String encodedPhoneticFamilyName; - if (reallyUseQuotedPrintable) { - encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); - } else { - encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); - } - mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME); - if (shouldAppendCharsetParam(phoneticFamilyName)) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedPhoneticFamilyName); - mBuilder.append(VCARD_END_OF_LINE); - } - } - } - - public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) { - final boolean useAndroidProperty; - if (mIsV30) { - useAndroidProperty = false; - } else if (mUsesAndroidProperty) { - useAndroidProperty = true; - } else { - // There's no way to add this field. - return this; - } - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - final String nickname = contentValues.getAsString(Nickname.NAME); - if (TextUtils.isEmpty(nickname)) { - continue; - } - if (useAndroidProperty) { - appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues); - } else { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname); - } - } - } - return this; - } - - public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) { - boolean phoneLineExists = false; - if (contentValuesList != null) { - Set<String> phoneSet = new HashSet<String>(); - for (ContentValues contentValues : contentValuesList) { - final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); - final String label = contentValues.getAsString(Phone.LABEL); - final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - String phoneNumber = contentValues.getAsString(Phone.NUMBER); - if (phoneNumber != null) { - phoneNumber = phoneNumber.trim(); - } - if (TextUtils.isEmpty(phoneNumber)) { - continue; - } - - // PAGER number needs unformatted "phone number". - final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); - if (type == Phone.TYPE_PAGER || - VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { - phoneLineExists = true; - if (!phoneSet.contains(phoneNumber)) { - phoneSet.add(phoneNumber); - appendTelLine(type, label, phoneNumber, isPrimary); - } - } else { - final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber); - if (phoneNumberList.isEmpty()) { - continue; - } - phoneLineExists = true; - for (String actualPhoneNumber : phoneNumberList) { - if (!phoneSet.contains(actualPhoneNumber)) { - final int format = VCardUtils.getPhoneNumberFormat(mVCardType); - final String formattedPhoneNumber = - PhoneNumberUtils.formatNumber(actualPhoneNumber, format); - phoneSet.add(actualPhoneNumber); - appendTelLine(type, label, formattedPhoneNumber, isPrimary); - } - } // for (String actualPhoneNumber : phoneNumberList) { - } - } - } - - if (!phoneLineExists && mIsDoCoMo) { - appendTelLine(Phone.TYPE_HOME, "", "", false); - } - - return this; - } - - /** - * <p> - * Splits a given string expressing phone numbers into several strings, and remove - * unnecessary characters inside them. The size of a returned list becomes 1 when - * no split is needed. - * </p> - * <p> - * The given number "may" have several phone numbers when the contact entry is corrupted - * because of its original source. - * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)" - * </p> - * <p> - * This kind of "phone numbers" will not be created with Android vCard implementation, - * but we may encounter them if the source of the input data has already corrupted - * implementation. - * </p> - * <p> - * To handle this case, this method first splits its input into multiple parts - * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and - * removes unnecessary strings like "(Miami)". - * </p> - * <p> - * Do not call this method when trimming is inappropriate for its receivers. - * </p> - */ - private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) { - final List<String> phoneList = new ArrayList<String>(); - - StringBuilder builder = new StringBuilder(); - final int length = phoneNumber.length(); - for (int i = 0; i < length; i++) { - final char ch = phoneNumber.charAt(i); - if (Character.isDigit(ch) || ch == '+') { - builder.append(ch); - } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { - phoneList.add(builder.toString()); - builder = new StringBuilder(); - } - } - if (builder.length() > 0) { - phoneList.add(builder.toString()); - } - - return phoneList; - } - - public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) { - boolean emailAddressExists = false; - if (contentValuesList != null) { - final Set<String> addressSet = new HashSet<String>(); - for (ContentValues contentValues : contentValuesList) { - String emailAddress = contentValues.getAsString(Email.DATA); - if (emailAddress != null) { - emailAddress = emailAddress.trim(); - } - if (TextUtils.isEmpty(emailAddress)) { - continue; - } - Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); - final int type = (typeAsObject != null ? - typeAsObject : DEFAULT_EMAIL_TYPE); - final String label = contentValues.getAsString(Email.LABEL); - Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - emailAddressExists = true; - if (!addressSet.contains(emailAddress)) { - addressSet.add(emailAddress); - appendEmailLine(type, label, emailAddress, isPrimary); - } - } - } - - if (!emailAddressExists && mIsDoCoMo) { - appendEmailLine(Email.TYPE_HOME, "", "", false); - } - - return this; - } - - public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) { - if (contentValuesList == null || contentValuesList.isEmpty()) { - if (mIsDoCoMo) { - mBuilder.append(VCardConstants.PROPERTY_ADR); - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCardConstants.PARAM_TYPE_HOME); - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(VCARD_END_OF_LINE); - } - } else { - if (mIsDoCoMo) { - appendPostalsForDoCoMo(contentValuesList); - } else { - appendPostalsForGeneric(contentValuesList); - } - } - - return this; - } - - private static final Map<Integer, Integer> sPostalTypePriorityMap; - - static { - sPostalTypePriorityMap = new HashMap<Integer, Integer>(); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); - } - - /** - * Tries to append just one line. If there's no appropriate address - * information, append an empty line. - */ - private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) { - int currentPriority = Integer.MAX_VALUE; - int currentType = Integer.MAX_VALUE; - ContentValues currentContentValues = null; - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); - final int priority = - (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); - if (priority < currentPriority) { - currentPriority = priority; - currentType = typeAsInteger; - currentContentValues = contentValues; - if (priority == 0) { - break; - } - } - } - - if (currentContentValues == null) { - Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); - return; - } - - final String label = currentContentValues.getAsString(StructuredPostal.LABEL); - appendPostalLine(currentType, label, currentContentValues, false, true); - } - - private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) { - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final int type = (typeAsInteger != null ? - typeAsInteger : DEFAULT_POSTAL_TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - final Integer isPrimaryAsInteger = - contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - appendPostalLine(type, label, contentValues, isPrimary, false); - } - } - - private static class PostalStruct { - final boolean reallyUseQuotedPrintable; - final boolean appendCharset; - final String addressData; - public PostalStruct(final boolean reallyUseQuotedPrintable, - final boolean appendCharset, final String addressData) { - this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; - this.appendCharset = appendCharset; - this.addressData = addressData; - } - } - - /** - * @return null when there's no information available to construct the data. - */ - private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name - final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX); - final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); - final String rawStreet = contentValues.getAsString(StructuredPostal.STREET); - final String rawLocality = contentValues.getAsString(StructuredPostal.CITY); - final String rawRegion = contentValues.getAsString(StructuredPostal.REGION); - final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE); - final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY); - final String[] rawAddressArray = new String[]{ - rawPoBox, rawNeighborhood, rawStreet, rawLocality, - rawRegion, rawPostalCode, rawCountry}; - if (!VCardUtils.areAllEmpty(rawAddressArray)) { - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray)); - final boolean appendCharset = - !VCardUtils.containsOnlyPrintableAscii(rawAddressArray); - final String encodedPoBox; - final String encodedStreet; - final String encodedLocality; - final String encodedRegion; - final String encodedPostalCode; - final String encodedCountry; - final String encodedNeighborhood; - - final String rawLocality2; - // This looks inefficient since we encode rawLocality and rawNeighborhood twice, - // but this is intentional. - // - // QP encoding may add line feeds when needed and the result of - // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood) - // may be different from - // - encodedLocality + " " + encodedNeighborhood. - // - // We use safer way. - if (TextUtils.isEmpty(rawLocality)) { - if (TextUtils.isEmpty(rawNeighborhood)) { - rawLocality2 = ""; - } else { - rawLocality2 = rawNeighborhood; - } - } else { - if (TextUtils.isEmpty(rawNeighborhood)) { - rawLocality2 = rawLocality; - } else { - rawLocality2 = rawLocality + " " + rawNeighborhood; - } - } - if (reallyUseQuotedPrintable) { - encodedPoBox = encodeQuotedPrintable(rawPoBox); - encodedStreet = encodeQuotedPrintable(rawStreet); - encodedLocality = encodeQuotedPrintable(rawLocality2); - encodedRegion = encodeQuotedPrintable(rawRegion); - encodedPostalCode = encodeQuotedPrintable(rawPostalCode); - encodedCountry = encodeQuotedPrintable(rawCountry); - } else { - encodedPoBox = escapeCharacters(rawPoBox); - encodedStreet = escapeCharacters(rawStreet); - encodedLocality = escapeCharacters(rawLocality2); - encodedRegion = escapeCharacters(rawRegion); - encodedPostalCode = escapeCharacters(rawPostalCode); - encodedCountry = escapeCharacters(rawCountry); - encodedNeighborhood = escapeCharacters(rawNeighborhood); - } - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(encodedPoBox); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedStreet); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedLocality); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedRegion); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedPostalCode); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedCountry); - return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); - } else { // VCardUtils.areAllEmpty(rawAddressArray) == true - // Try to use FORMATTED_ADDRESS instead. - final String rawFormattedAddress = - contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); - if (TextUtils.isEmpty(rawFormattedAddress)) { - return null; - } - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress)); - final boolean appendCharset = - !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress); - final String encodedFormattedAddress; - if (reallyUseQuotedPrintable) { - encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress); - } else { - encodedFormattedAddress = escapeCharacters(rawFormattedAddress); - } - - // We use the second value ("Extended Address") just because Japanese mobile phones - // do so. If the other importer expects the value be in the other field, some flag may - // be needed. - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedFormattedAddress); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); - } - } - - public VCardBuilder appendIms(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); - if (protocolAsObject == null) { - continue; - } - final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); - if (propertyName == null) { - continue; - } - String data = contentValues.getAsString(Im.DATA); - if (data != null) { - data = data.trim(); - } - if (TextUtils.isEmpty(data)) { - continue; - } - final String typeAsString; - { - final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); - switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { - case Im.TYPE_HOME: { - typeAsString = VCardConstants.PARAM_TYPE_HOME; - break; - } - case Im.TYPE_WORK: { - typeAsString = VCardConstants.PARAM_TYPE_WORK; - break; - } - case Im.TYPE_CUSTOM: { - final String label = contentValues.getAsString(Im.LABEL); - typeAsString = (label != null ? "X-" + label : null); - break; - } - case Im.TYPE_OTHER: // Ignore - default: { - typeAsString = null; - break; - } - } - } - - final List<String> parameterList = new ArrayList<String>(); - if (!TextUtils.isEmpty(typeAsString)) { - parameterList.add(typeAsString); - } - final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - - appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); - } - } - return this; - } - - public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String website = contentValues.getAsString(Website.URL); - if (website != null) { - website = website.trim(); - } - - // Note: vCard 3.0 does not allow any parameter addition toward "URL" - // property, while there's no document in vCard 2.1. - if (!TextUtils.isEmpty(website)) { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website); - } - } - } - return this; - } - - public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String company = contentValues.getAsString(Organization.COMPANY); - if (company != null) { - company = company.trim(); - } - String department = contentValues.getAsString(Organization.DEPARTMENT); - if (department != null) { - department = department.trim(); - } - String title = contentValues.getAsString(Organization.TITLE); - if (title != null) { - title = title.trim(); - } - - StringBuilder orgBuilder = new StringBuilder(); - if (!TextUtils.isEmpty(company)) { - orgBuilder.append(company); - } - if (!TextUtils.isEmpty(department)) { - if (orgBuilder.length() > 0) { - orgBuilder.append(';'); - } - orgBuilder.append(department); - } - final String orgline = orgBuilder.toString(); - appendLine(VCardConstants.PROPERTY_ORG, orgline, - !VCardUtils.containsOnlyPrintableAscii(orgline), - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); - - if (!TextUtils.isEmpty(title)) { - appendLine(VCardConstants.PROPERTY_TITLE, title, - !VCardUtils.containsOnlyPrintableAscii(title), - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); - } - } - } - return this; - } - - public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - byte[] data = contentValues.getAsByteArray(Photo.PHOTO); - if (data == null) { - continue; - } - final String photoType = VCardUtils.guessImageType(data); - if (photoType == null) { - Log.d(LOG_TAG, "Unknown photo type. Ignored."); - continue; - } - final String photoString = new String(Base64.encodeBase64(data)); - if (!TextUtils.isEmpty(photoString)) { - appendPhotoLine(photoString, photoType); - } - } - } - return this; - } - - public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - if (mOnlyOneNoteFieldIsAvailable) { - final StringBuilder noteBuilder = new StringBuilder(); - boolean first = true; - for (final ContentValues contentValues : contentValuesList) { - String note = contentValues.getAsString(Note.NOTE); - if (note == null) { - note = ""; - } - if (note.length() > 0) { - if (first) { - first = false; - } else { - noteBuilder.append('\n'); - } - noteBuilder.append(note); - } - } - final String noteStr = noteBuilder.toString(); - // This means we scan noteStr completely twice, which is redundant. - // But for now, we assume this is not so time-consuming.. - final boolean shouldAppendCharsetInfo = - !VCardUtils.containsOnlyPrintableAscii(noteStr); - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendLine(VCardConstants.PROPERTY_NOTE, noteStr, - shouldAppendCharsetInfo, reallyUseQuotedPrintable); - } else { - for (ContentValues contentValues : contentValuesList) { - final String noteStr = contentValues.getAsString(Note.NOTE); - if (!TextUtils.isEmpty(noteStr)) { - final boolean shouldAppendCharsetInfo = - !VCardUtils.containsOnlyPrintableAscii(noteStr); - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendLine(VCardConstants.PROPERTY_NOTE, noteStr, - shouldAppendCharsetInfo, reallyUseQuotedPrintable); - } - } - } - } - return this; - } - - public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { - if (contentValuesList != null) { - String primaryBirthday = null; - String secondaryBirthday = null; - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); - final int eventType; - if (eventTypeAsInteger != null) { - eventType = eventTypeAsInteger; - } else { - eventType = Event.TYPE_OTHER; - } - if (eventType == Event.TYPE_BIRTHDAY) { - final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); - if (birthdayCandidate == null) { - continue; - } - final Integer isSuperPrimaryAsInteger = - contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); - final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? - (isSuperPrimaryAsInteger > 0) : false); - if (isSuperPrimary) { - // "super primary" birthday should the prefered one. - primaryBirthday = birthdayCandidate; - break; - } - final Integer isPrimaryAsInteger = - contentValues.getAsInteger(Event.IS_PRIMARY); - final boolean isPrimary = (isPrimaryAsInteger != null ? - (isPrimaryAsInteger > 0) : false); - if (isPrimary) { - // We don't break here since "super primary" birthday may exist later. - primaryBirthday = birthdayCandidate; - } else if (secondaryBirthday == null) { - // First entry is set to the "secondary" candidate. - secondaryBirthday = birthdayCandidate; - } - } else if (mUsesAndroidProperty) { - // Event types other than Birthday is not supported by vCard. - appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); - } - } - if (primaryBirthday != null) { - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, - primaryBirthday.trim()); - } else if (secondaryBirthday != null){ - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, - secondaryBirthday.trim()); - } - } - return this; - } - - public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) { - if (mUsesAndroidProperty && contentValuesList != null) { - for (final ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); - } - } - return this; - } - - public void appendPostalLine(final int type, final String label, - final ContentValues contentValues, - final boolean isPrimary, final boolean emitLineEveryTime) { - final boolean reallyUseQuotedPrintable; - final boolean appendCharset; - final String addressValue; - { - PostalStruct postalStruct = tryConstructPostalStruct(contentValues); - if (postalStruct == null) { - if (emitLineEveryTime) { - reallyUseQuotedPrintable = false; - appendCharset = false; - addressValue = ""; - } else { - return; - } - } else { - reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; - appendCharset = postalStruct.appendCharset; - addressValue = postalStruct.addressData; - } - } - - List<String> parameterList = new ArrayList<String>(); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - switch (type) { - case StructuredPostal.TYPE_HOME: { - parameterList.add(VCardConstants.PARAM_TYPE_HOME); - break; - } - case StructuredPostal.TYPE_WORK: { - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - break; - } - case StructuredPostal.TYPE_CUSTOM: { - if (!TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - // We're not sure whether the label is valid in the spec - // ("IANA-token" in the vCard 3.0 is unclear...) - // Just for safety, we add "X-" at the beggining of each label. - // Also checks the label obeys with vCard 3.0 spec. - parameterList.add("X-" + label); - } - break; - } - case StructuredPostal.TYPE_OTHER: { - break; - } - default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); - break; - } - } - - mBuilder.append(VCardConstants.PROPERTY_ADR); - if (!parameterList.isEmpty()) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (appendCharset) { - // Strictly, vCard 3.0 does not allow exporters to emit charset information, - // but we will add it since the information should be useful for importers, - // - // Assume no parser does not emit error with this parameter in vCard 3.0. - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(addressValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendEmailLine(final int type, final String label, - final String rawValue, final boolean isPrimary) { - final String typeAsString; - switch (type) { - case Email.TYPE_CUSTOM: { - if (VCardUtils.isMobilePhoneLabel(label)) { - typeAsString = VCardConstants.PARAM_TYPE_CELL; - } else if (!TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - typeAsString = "X-" + label; - } else { - typeAsString = null; - } - break; - } - case Email.TYPE_HOME: { - typeAsString = VCardConstants.PARAM_TYPE_HOME; - break; - } - case Email.TYPE_WORK: { - typeAsString = VCardConstants.PARAM_TYPE_WORK; - break; - } - case Email.TYPE_OTHER: { - typeAsString = null; - break; - } - case Email.TYPE_MOBILE: { - typeAsString = VCardConstants.PARAM_TYPE_CELL; - break; - } - default: { - Log.e(LOG_TAG, "Unknown Email type: " + type); - typeAsString = null; - break; - } - } - - final List<String> parameterList = new ArrayList<String>(); - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - if (!TextUtils.isEmpty(typeAsString)) { - parameterList.add(typeAsString); - } - - appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, - rawValue); - } - - public void appendTelLine(final Integer typeAsInteger, final String label, - final String encodedValue, boolean isPrimary) { - mBuilder.append(VCardConstants.PROPERTY_TEL); - mBuilder.append(VCARD_PARAM_SEPARATOR); - - final int type; - if (typeAsInteger == null) { - type = Phone.TYPE_OTHER; - } else { - type = typeAsInteger; - } - - ArrayList<String> parameterList = new ArrayList<String>(); - switch (type) { - case Phone.TYPE_HOME: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); - break; - } - case Phone.TYPE_WORK: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); - break; - } - case Phone.TYPE_FAX_HOME: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); - break; - } - case Phone.TYPE_FAX_WORK: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); - break; - } - case Phone.TYPE_MOBILE: { - parameterList.add(VCardConstants.PARAM_TYPE_CELL); - break; - } - case Phone.TYPE_PAGER: { - if (mIsDoCoMo) { - // Not sure about the reason, but previous implementation had - // used "VOICE" instead of "PAGER" - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else { - parameterList.add(VCardConstants.PARAM_TYPE_PAGER); - } - break; - } - case Phone.TYPE_OTHER: { - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - break; - } - case Phone.TYPE_CAR: { - parameterList.add(VCardConstants.PARAM_TYPE_CAR); - break; - } - case Phone.TYPE_COMPANY_MAIN: { - // There's no relevant field in vCard (at least 2.1). - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - isPrimary = true; - break; - } - case Phone.TYPE_ISDN: { - parameterList.add(VCardConstants.PARAM_TYPE_ISDN); - break; - } - case Phone.TYPE_MAIN: { - isPrimary = true; - break; - } - case Phone.TYPE_OTHER_FAX: { - parameterList.add(VCardConstants.PARAM_TYPE_FAX); - break; - } - case Phone.TYPE_TELEX: { - parameterList.add(VCardConstants.PARAM_TYPE_TLX); - break; - } - case Phone.TYPE_WORK_MOBILE: { - parameterList.addAll( - Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); - break; - } - case Phone.TYPE_WORK_PAGER: { - parameterList.add(VCardConstants.PARAM_TYPE_WORK); - // See above. - if (mIsDoCoMo) { - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else { - parameterList.add(VCardConstants.PARAM_TYPE_PAGER); - } - break; - } - case Phone.TYPE_MMS: { - parameterList.add(VCardConstants.PARAM_TYPE_MSG); - break; - } - case Phone.TYPE_CUSTOM: { - if (TextUtils.isEmpty(label)) { - // Just ignore the custom type. - parameterList.add(VCardConstants.PARAM_TYPE_VOICE); - } else if (VCardUtils.isMobilePhoneLabel(label)) { - parameterList.add(VCardConstants.PARAM_TYPE_CELL); - } else { - final String upperLabel = label.toUpperCase(); - if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { - parameterList.add(upperLabel); - } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - // Note: Strictly, vCard 2.1 does not allow "X-" parameter without - // "TYPE=" string. - parameterList.add("X-" + label); - } - } - break; - } - case Phone.TYPE_RADIO: - case Phone.TYPE_TTY_TDD: - default: { - break; - } - } - - if (isPrimary) { - parameterList.add(VCardConstants.PARAM_TYPE_PREF); - } - - if (parameterList.isEmpty()) { - appendUncommonPhoneType(mBuilder, type); - } else { - appendTypeParameters(parameterList); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - /** - * Appends phone type string which may not be available in some devices. - */ - private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { - if (mIsDoCoMo) { - // The previous implementation for DoCoMo had been conservative - // about miscellaneous types. - builder.append(VCardConstants.PARAM_TYPE_VOICE); - } else { - String phoneType = VCardUtils.getPhoneTypeString(type); - if (phoneType != null) { - appendTypeParameter(phoneType); - } else { - Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); - } - } - } - - /** - * @param encodedValue Must be encoded by BASE64 - * @param photoType - */ - public void appendPhotoLine(final String encodedValue, final String photoType) { - StringBuilder tmpBuilder = new StringBuilder(); - tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); - tmpBuilder.append(VCARD_PARAM_SEPARATOR); - if (mIsV30) { - tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); - } else { - tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); - } - tmpBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameter(tmpBuilder, photoType); - tmpBuilder.append(VCARD_DATA_SEPARATOR); - tmpBuilder.append(encodedValue); - - final String tmpStr = tmpBuilder.toString(); - tmpBuilder = new StringBuilder(); - int lineCount = 0; - final int length = tmpStr.length(); - final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 - - VCARD_END_OF_LINE.length(); - final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); - int maxNum = maxNumForFirstLine; - for (int i = 0; i < length; i++) { - tmpBuilder.append(tmpStr.charAt(i)); - lineCount++; - if (lineCount > maxNum) { - tmpBuilder.append(VCARD_END_OF_LINE); - tmpBuilder.append(VCARD_WS); - maxNum = maxNumInGeneral; - lineCount = 0; - } - } - mBuilder.append(tmpBuilder.toString()); - mBuilder.append(VCARD_END_OF_LINE); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { - if (!sAllowedAndroidPropertySet.contains(mimeType)) { - return; - } - final List<String> rawValueList = new ArrayList<String>(); - for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { - String value = contentValues.getAsString("data" + i); - if (value == null) { - value = ""; - } - rawValueList.add(value); - } - - boolean needCharset = - (mShouldAppendCharsetParam && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM); - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(mimeType); // Should not be encoded. - for (String rawValue : rawValueList) { - final String encodedValue; - if (reallyUseQuotedPrintable) { - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard 3.0 - // (which says "When generating a content line, lines longer than - // 75 characters SHOULD be folded"), though several - // (even well-known) applications do not care this. - encodedValue = escapeCharacters(rawValue); - } - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(encodedValue); - } - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final String rawValue) { - appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); - } - - public void appendLineWithCharsetAndQPDetection( - final String propertyName, final List<String> rawValueList) { - appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final List<String> parameterList, final String rawValue) { - final boolean needCharset = - !VCardUtils.containsOnlyPrintableAscii(rawValue); - final boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)); - appendLine(propertyName, parameterList, - rawValue, needCharset, reallyUseQuotedPrintable); - } - - public void appendLineWithCharsetAndQPDetection(final String propertyName, - final List<String> parameterList, final List<String> rawValueList) { - boolean needCharset = - (mShouldAppendCharsetParam && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - boolean reallyUseQuotedPrintable = - (mShouldUseQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); - appendLine(propertyName, parameterList, rawValueList, - needCharset, reallyUseQuotedPrintable); - } - - /** - * Appends one line with a given property name and value. - */ - public void appendLine(final String propertyName, final String rawValue) { - appendLine(propertyName, rawValue, false, false); - } - - public void appendLine(final String propertyName, final List<String> rawValueList) { - appendLine(propertyName, rawValueList, false, false); - } - - public void appendLine(final String propertyName, - final String rawValue, final boolean needCharset, - boolean reallyUseQuotedPrintable) { - appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable); - } - - public void appendLine(final String propertyName, final List<String> parameterList, - final String rawValue) { - appendLine(propertyName, parameterList, rawValue, false, false); - } - - public void appendLine(final String propertyName, final List<String> parameterList, - final String rawValue, final boolean needCharset, - boolean reallyUseQuotedPrintable) { - mBuilder.append(propertyName); - if (parameterList != null && parameterList.size() > 0) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - - final String encodedValue; - if (reallyUseQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard spec, though - // several (even well-known) applications do not care this. - encodedValue = escapeCharacters(rawValue); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - mBuilder.append(encodedValue); - mBuilder.append(VCARD_END_OF_LINE); - } - - public void appendLine(final String propertyName, final List<String> rawValueList, - final boolean needCharset, boolean needQuotedPrintable) { - appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); - } - - public void appendLine(final String propertyName, final List<String> parameterList, - final List<String> rawValueList, final boolean needCharset, - final boolean needQuotedPrintable) { - mBuilder.append(propertyName); - if (parameterList != null && parameterList.size() > 0) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(parameterList); - } - if (needCharset) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(mVCardCharsetParameter); - } - if (needQuotedPrintable) { - mBuilder.append(VCARD_PARAM_SEPARATOR); - mBuilder.append(VCARD_PARAM_ENCODING_QP); - } - - mBuilder.append(VCARD_DATA_SEPARATOR); - boolean first = true; - for (String rawValue : rawValueList) { - final String encodedValue; - if (needQuotedPrintable) { - encodedValue = encodeQuotedPrintable(rawValue); - } else { - // TODO: one line may be too huge, which may be invalid in vCard 3.0 - // (which says "When generating a content line, lines longer than - // 75 characters SHOULD be folded"), though several - // (even well-known) applications do not care this. - encodedValue = escapeCharacters(rawValue); - } - - if (first) { - first = false; - } else { - mBuilder.append(VCARD_ITEM_SEPARATOR); - } - mBuilder.append(encodedValue); - } - mBuilder.append(VCARD_END_OF_LINE); - } - - /** - * VCARD_PARAM_SEPARATOR must be appended before this method being called. - */ - private void appendTypeParameters(final List<String> types) { - // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, - // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. - boolean first = true; - for (final String typeValue : types) { - // Note: vCard 3.0 specifies the different type of acceptable type Strings, but - // we don't emit that kind of vCard 3.0 specific type since there should be - // high probabilyty in which external importers cannot understand them. - // - // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they - // are quoted.) - if (!VCardUtils.isV21Word(typeValue)) { - continue; - } - if (first) { - first = false; - } else { - mBuilder.append(VCARD_PARAM_SEPARATOR); - } - appendTypeParameter(typeValue); - } - } - - /** - * VCARD_PARAM_SEPARATOR must be appended before this method being called. - */ - private void appendTypeParameter(final String type) { - appendTypeParameter(mBuilder, type); - } - - private void appendTypeParameter(final StringBuilder builder, final String type) { - // Refrain from using appendType() so that "TYPE=" is not be appended when the - // device is DoCoMo's (just for safety). - // - // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" - if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { - builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); - } - builder.append(type); - } - - /** - * Returns true when the property line should contain charset parameter - * information. This method may return true even when vCard version is 3.0. - * - * Strictly, adding charset information is invalid in VCard 3.0. - * However we'll add the info only when charset we use is not UTF-8 - * in vCard 3.0 format, since parser side may be able to use the charset - * via this field, though we may encounter another problem by adding it. - * - * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 - * recommends UTF-8. By adding this field, parsers may be able - * to know this text is NOT UTF-8 but Shift_Jis. - */ - private boolean shouldAppendCharsetParam(String...propertyValueList) { - if (!mShouldAppendCharsetParam) { - return false; - } - for (String propertyValue : propertyValueList) { - if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { - return true; - } - } - return false; - } - - private String encodeQuotedPrintable(final String str) { - if (TextUtils.isEmpty(str)) { - return ""; - } - - final StringBuilder builder = new StringBuilder(); - int index = 0; - int lineCount = 0; - byte[] strArray = null; - - try { - strArray = str.getBytes(mCharsetString); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " - + "Try default charset"); - strArray = str.getBytes(); - } - while (index < strArray.length) { - builder.append(String.format("=%02X", strArray[index])); - index += 1; - lineCount += 3; - - if (lineCount >= 67) { - // Specification requires CRLF must be inserted before the - // length of the line - // becomes more than 76. - // Assuming that the next character is a multi-byte character, - // it will become - // 6 bytes. - // 76 - 6 - 3 = 67 - builder.append("=\r\n"); - lineCount = 0; - } - } - - return builder.toString(); - } - - /** - * Append '\' to the characters which should be escaped. The character set is different - * not only between vCard 2.1 and vCard 3.0 but also among each device. - * - * Note that Quoted-Printable string must not be input here. - */ - @SuppressWarnings("fallthrough") - private String escapeCharacters(final String unescaped) { - if (TextUtils.isEmpty(unescaped)) { - return ""; - } - - final StringBuilder tmpBuilder = new StringBuilder(); - final int length = unescaped.length(); - for (int i = 0; i < length; i++) { - final char ch = unescaped.charAt(i); - switch (ch) { - case ';': { - tmpBuilder.append('\\'); - tmpBuilder.append(';'); - break; - } - case '\r': { - if (i + 1 < length) { - char nextChar = unescaped.charAt(i); - if (nextChar == '\n') { - break; - } else { - // fall through - } - } else { - // fall through - } - } - case '\n': { - // In vCard 2.1, there's no specification about this, while - // vCard 3.0 explicitly requires this should be encoded to "\n". - tmpBuilder.append("\\n"); - break; - } - case '\\': { - if (mIsV30) { - tmpBuilder.append("\\\\"); - break; - } else { - // fall through - } - } - case '<': - case '>': { - if (mIsDoCoMo) { - tmpBuilder.append('\\'); - tmpBuilder.append(ch); - } else { - tmpBuilder.append(ch); - } - break; - } - case ',': { - if (mIsV30) { - tmpBuilder.append("\\,"); - } else { - tmpBuilder.append(ch); - } - break; - } - default: { - tmpBuilder.append(ch); - break; - } - } - } - return tmpBuilder.toString(); - } - - @Override - public String toString() { - if (!mEndAppended) { - if (mIsDoCoMo) { - appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); - appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); - appendLine(VCardConstants.PROPERTY_X_NO, ""); - appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); - } - appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); - mEndAppended = true; - } - return mBuilder.toString(); - } -} diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java deleted file mode 100644 index 0e8b665..0000000 --- a/core/java/android/pim/vcard/VCardComposer.java +++ /dev/null @@ -1,592 +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.pim.vcard; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Entity; -import android.content.EntityIterator; -import android.content.Entity.NamedContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; -import android.net.Uri; -import android.pim.vcard.exception.VCardException; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.RawContactsEntity; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Event; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Nickname; -import android.provider.ContactsContract.CommonDataKinds.Note; -import android.provider.ContactsContract.CommonDataKinds.Organization; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.CommonDataKinds.Relation; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.CommonDataKinds.Website; -import android.util.CharsetUtils; -import android.util.Log; - -import java.io.BufferedWriter; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.UnsupportedCharsetException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * <p> - * The class for composing VCard from Contacts information. Note that this is - * completely differnt implementation from - * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. - * </p> - * - * <p> - * Usually, this class should be used like this. - * </p> - * - * <pre class="prettyprint">VCardComposer composer = null; - * try { - * composer = new VCardComposer(context); - * composer.addHandler( - * composer.new HandlerForOutputStream(outputStream)); - * if (!composer.init()) { - * // Do something handling the situation. - * return; - * } - * while (!composer.isAfterLast()) { - * if (mCanceled) { - * // Assume a user may cancel this operation during the export. - * return; - * } - * if (!composer.createOneEntry()) { - * // Do something handling the error situation. - * return; - * } - * } - * } finally { - * if (composer != null) { - * composer.terminate(); - * } - * } </pre> - */ -public class VCardComposer { - private static final String LOG_TAG = "VCardComposer"; - - public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; - public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; - public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; - - public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = - "Failed to get database information"; - - public static final String FAILURE_REASON_NO_ENTRY = - "There's no exportable in the database"; - - public static final String FAILURE_REASON_NOT_INITIALIZED = - "The vCard composer object is not correctly initialized"; - - /** Should be visible only from developers... (no need to translate, hopefully) */ - public static final String FAILURE_REASON_UNSUPPORTED_URI = - "The Uri vCard composer received is not supported by the composer."; - - public static final String NO_ERROR = "No error"; - - public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; - - private static final String SHIFT_JIS = "SHIFT_JIS"; - private static final String UTF_8 = "UTF-8"; - - /** - * Special URI for testing. - */ - public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; - public static final Uri VCARD_TEST_AUTHORITY_URI = - Uri.parse("content://" + VCARD_TEST_AUTHORITY); - public static final Uri CONTACTS_TEST_CONTENT_URI = - Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); - - private static final Map<Integer, String> sImMap; - - static { - sImMap = new HashMap<Integer, String>(); - sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); - sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); - sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); - sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); - sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); - sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - // Google talk is a special case. - } - - public static interface OneEntryHandler { - public boolean onInit(Context context); - public boolean onEntryCreated(String vcard); - public void onTerminate(); - } - - /** - * <p> - * An useful example handler, which emits VCard String to outputstream one by one. - * </p> - * <p> - * The input OutputStream object is closed() on {@link #onTerminate()}. - * Must not close the stream outside. - * </p> - */ - public class HandlerForOutputStream implements OneEntryHandler { - @SuppressWarnings("hiding") - private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; - - final private OutputStream mOutputStream; // mWriter will close this. - private Writer mWriter; - - private boolean mOnTerminateIsCalled = false; - - /** - * Input stream will be closed on the detruction of this object. - */ - public HandlerForOutputStream(OutputStream outputStream) { - mOutputStream = outputStream; - } - - public boolean onInit(Context context) { - try { - mWriter = new BufferedWriter(new OutputStreamWriter( - mOutputStream, mCharsetString)); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); - mErrorReason = "Encoding is not supported (usually this does not happen!): " - + mCharsetString; - return false; - } - - if (mIsDoCoMo) { - try { - // Create one empty entry. - mWriter.write(createOneEntryInternal("-1", null)); - } catch (VCardException e) { - Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + - e.getMessage()); - return false; - } catch (IOException e) { - Log.e(LOG_TAG, - "IOException occurred during exportOneContactData: " - + e.getMessage()); - mErrorReason = "IOException occurred: " + e.getMessage(); - return false; - } - } - return true; - } - - public boolean onEntryCreated(String vcard) { - try { - mWriter.write(vcard); - } catch (IOException e) { - Log.e(LOG_TAG, - "IOException occurred during exportOneContactData: " - + e.getMessage()); - mErrorReason = "IOException occurred: " + e.getMessage(); - return false; - } - return true; - } - - public void onTerminate() { - mOnTerminateIsCalled = true; - if (mWriter != null) { - try { - // Flush and sync the data so that a user is able to pull - // the SDCard just after - // the export. - mWriter.flush(); - if (mOutputStream != null - && mOutputStream instanceof FileOutputStream) { - ((FileOutputStream) mOutputStream).getFD().sync(); - } - } catch (IOException e) { - Log.d(LOG_TAG, - "IOException during closing the output stream: " - + e.getMessage()); - } finally { - try { - mWriter.close(); - } catch (IOException e) { - } - } - } - } - - @Override - public void finalize() { - if (!mOnTerminateIsCalled) { - onTerminate(); - } - } - } - - private final Context mContext; - private final int mVCardType; - private final boolean mCareHandlerErrors; - private final ContentResolver mContentResolver; - - private final boolean mIsDoCoMo; - private final boolean mUsesShiftJis; - private Cursor mCursor; - private int mIdColumn; - - private final String mCharsetString; - private boolean mTerminateIsCalled; - private final List<OneEntryHandler> mHandlerList; - - private String mErrorReason = NO_ERROR; - - private static final String[] sContactsProjection = new String[] { - Contacts._ID, - }; - - public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); - } - - public VCardComposer(Context context, int vcardType) { - this(context, vcardType, true); - } - - public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { - this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); - } - - /** - * Construct for supporting call log entry vCard composing. - */ - public VCardComposer(final Context context, final int vcardType, - final boolean careHandlerErrors) { - mContext = context; - mVCardType = vcardType; - mCareHandlerErrors = careHandlerErrors; - mContentResolver = context.getContentResolver(); - - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); - mHandlerList = new ArrayList<OneEntryHandler>(); - - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - } else { - mCharsetString = UTF_8; - } - } - - /** - * Must be called before {@link #init()}. - */ - public void addHandler(OneEntryHandler handler) { - if (handler != null) { - mHandlerList.add(handler); - } - } - - /** - * @return Returns true when initialization is successful and all the other - * methods are available. Returns false otherwise. - */ - public boolean init() { - return init(null, null); - } - - public boolean init(final String selection, final String[] selectionArgs) { - return init(Contacts.CONTENT_URI, selection, selectionArgs, null); - } - - /** - * Note that this is unstable interface, may be deleted in the future. - */ - public boolean init(final Uri contentUri, final String selection, - final String[] selectionArgs, final String sortOrder) { - if (contentUri == null) { - return false; - } - - if (mCareHandlerErrors) { - List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( - mHandlerList.size()); - for (OneEntryHandler handler : mHandlerList) { - if (!handler.onInit(mContext)) { - for (OneEntryHandler finished : finishedList) { - finished.onTerminate(); - } - return false; - } - } - } else { - // Just ignore the false returned from onInit(). - for (OneEntryHandler handler : mHandlerList) { - handler.onInit(mContext); - } - } - - final String[] projection; - if (Contacts.CONTENT_URI.equals(contentUri) || - CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { - projection = sContactsProjection; - } else { - mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; - return false; - } - mCursor = mContentResolver.query( - contentUri, projection, selection, selectionArgs, sortOrder); - - if (mCursor == null) { - mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; - return false; - } - - if (getCount() == 0 || !mCursor.moveToFirst()) { - try { - mCursor.close(); - } catch (SQLiteException e) { - Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); - } finally { - mCursor = null; - mErrorReason = FAILURE_REASON_NO_ENTRY; - } - return false; - } - - mIdColumn = mCursor.getColumnIndex(Contacts._ID); - - return true; - } - - public boolean createOneEntry() { - return createOneEntry(null); - } - - /** - * @param getEntityIteratorMethod For Dependency Injection. - * @hide just for testing. - */ - public boolean createOneEntry(Method getEntityIteratorMethod) { - if (mCursor == null || mCursor.isAfterLast()) { - mErrorReason = FAILURE_REASON_NOT_INITIALIZED; - return false; - } - String vcard; - try { - if (mIdColumn >= 0) { - vcard = createOneEntryInternal(mCursor.getString(mIdColumn), - getEntityIteratorMethod); - } else { - Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); - return true; - } - } catch (VCardException e) { - Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); - return false; - } catch (OutOfMemoryError error) { - // Maybe some data (e.g. photo) is too big to have in memory. But it - // should be rare. - Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry."); - System.gc(); - // TODO: should tell users what happened? - return true; - } finally { - mCursor.moveToNext(); - } - - // This function does not care the OutOfMemoryError on the handler side - // :-P - if (mCareHandlerErrors) { - List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( - mHandlerList.size()); - for (OneEntryHandler handler : mHandlerList) { - if (!handler.onEntryCreated(vcard)) { - return false; - } - } - } else { - for (OneEntryHandler handler : mHandlerList) { - handler.onEntryCreated(vcard); - } - } - - return true; - } - - private String createOneEntryInternal(final String contactId, - Method getEntityIteratorMethod) throws VCardException { - final Map<String, List<ContentValues>> contentValuesListMap = - new HashMap<String, List<ContentValues>>(); - // The resolver may return the entity iterator with no data. It is possible. - // e.g. If all the data in the contact of the given contact id are not exportable ones, - // they are hidden from the view of this method, though contact id itself exists. - EntityIterator entityIterator = null; - try { - final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() - .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") - .build(); - final String selection = Data.CONTACT_ID + "=?"; - final String[] selectionArgs = new String[] {contactId}; - if (getEntityIteratorMethod != null) { - // Please note that this branch is executed by some tests only - try { - entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, - mContentResolver, uri, selection, selectionArgs, null); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + - e.getMessage()); - } catch (IllegalAccessException e) { - Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + - e.getMessage()); - } catch (InvocationTargetException e) { - Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); - StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); - for (StackTraceElement element : stackTraceElements) { - Log.e(LOG_TAG, " at " + element.toString()); - } - throw new VCardException("InvocationTargetException has been thrown: " + - e.getCause().getMessage()); - } - } else { - entityIterator = RawContacts.newEntityIterator(mContentResolver.query( - uri, null, selection, selectionArgs, null)); - } - - if (entityIterator == null) { - Log.e(LOG_TAG, "EntityIterator is null"); - return ""; - } - - if (!entityIterator.hasNext()) { - Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); - return ""; - } - - while (entityIterator.hasNext()) { - Entity entity = entityIterator.next(); - for (NamedContentValues namedContentValues : entity.getSubValues()) { - ContentValues contentValues = namedContentValues.values; - String key = contentValues.getAsString(Data.MIMETYPE); - if (key != null) { - List<ContentValues> contentValuesList = - contentValuesListMap.get(key); - if (contentValuesList == null) { - contentValuesList = new ArrayList<ContentValues>(); - contentValuesListMap.put(key, contentValuesList); - } - contentValuesList.add(contentValues); - } - } - } - } finally { - if (entityIterator != null) { - entityIterator.close(); - } - } - - final VCardBuilder builder = new VCardBuilder(mVCardType); - builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) - .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) - .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) - .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) - .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) - .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) - .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); - if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { - builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); - } - builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) - .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) - .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) - .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); - return builder.toString(); - } - - public void terminate() { - for (OneEntryHandler handler : mHandlerList) { - handler.onTerminate(); - } - - if (mCursor != null) { - try { - mCursor.close(); - } catch (SQLiteException e) { - Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); - } - mCursor = null; - } - - mTerminateIsCalled = true; - } - - @Override - public void finalize() { - if (!mTerminateIsCalled) { - terminate(); - } - } - - public int getCount() { - if (mCursor == null) { - return 0; - } - return mCursor.getCount(); - } - - public boolean isAfterLast() { - if (mCursor == null) { - return false; - } - return mCursor.isAfterLast(); - } - - /** - * @return Return the error reason if possible. - */ - public String getErrorReason() { - return mErrorReason; - } -} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java deleted file mode 100644 index 8219840..0000000 --- a/core/java/android/pim/vcard/VCardConfig.java +++ /dev/null @@ -1,477 +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.pim.vcard; - -import android.telephony.PhoneNumberUtils; -import android.util.Log; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * The class representing VCard related configurations. Useful static methods are not in this class - * but in VCardUtils. - */ -public class VCardConfig { - private static final String LOG_TAG = "VCardConfig"; - - /* package */ static final int LOG_LEVEL_NONE = 0; - /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; - /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2; - /* package */ static final int LOG_LEVEL_VERBOSE = - LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING; - - /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; - - /* package */ static final int PARSE_TYPE_UNKNOWN = 0; - /* package */ static final int PARSE_TYPE_APPLE = 1; - /* package */ static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; // For Japanese mobile phones. - /* package */ static final int PARSE_TYPE_FOMA = 3; // For Japanese FOMA mobile phones. - /* package */ static final int PARSE_TYPE_WINDOWS_MOBILE_JP = 4; - - // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and - // decode the unicode to the original charset. If not, this setting will cause some bug. - public static final String DEFAULT_CHARSET = "iso-8859-1"; - - public static final int FLAG_V21 = 0; - public static final int FLAG_V30 = 1; - - // 0x2 is reserved for the future use ... - - public static final int NAME_ORDER_DEFAULT = 0; - public static final int NAME_ORDER_EUROPE = 0x4; - public static final int NAME_ORDER_JAPANESE = 0x8; - private static final int NAME_ORDER_MASK = 0xC; - - // 0x10 is reserved for safety - - private static final int FLAG_CHARSET_UTF8 = 0; - private static final int FLAG_CHARSET_SHIFT_JIS = 0x100; - private static final int FLAG_CHARSET_MASK = 0xF00; - - /** - * The flag indicating the vCard composer will add some "X-" properties used only in Android - * when the formal vCard specification does not have appropriate fields for that data. - * - * For example, Android accepts nickname information while vCard 2.1 does not. - * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") - * instead of just dropping it. - * - * vCard parser code automatically parses the field emitted even when this flag is off. - * - * Note that this flag does not assure all the information must be hold in the emitted vCard. - */ - private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; - - /** - * The flag indicating the vCard composer will add some "X-" properties seen in the - * vCard data emitted by the other softwares/devices when the formal vCard specification - * does not have appropriate field(s) for that data. - * - * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are - * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other - * non-Android devices/softwares. We chose to enable the vCard composer to use those - * defact properties since they are also useful for Android devices. - * - * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 - * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens - * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. - */ - private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; - - /** - * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese - * mobile careers) should be used. This flag does not include any other information like - * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's - * dialect but the name order should be European", but it is not recommended. - */ - private static final int FLAG_DOCOMO = 0x20000000; - - /** - * <P> - * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" - * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). - * </P> - * <P> - * We actually cannot define what is the "primary" property. Note that this is NOT defined - * in vCard specification either. Also be aware that it is NOT related to "primary" notion - * used in {@link android.provider.ContactsContract}. - * This notion is just for vCard composition in Android. - * </P> - * <P> - * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 - * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc. - * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the - * other properties like "ADR", "ORG", etc. - * <P> - * We are afraid of the case where some vCard importer also forget handling QP presuming QP is - * not used in such fields. - * </P> - * <P> - * This flag is useful when some target importer you are going to focus on does not accept - * such properties with Quoted-Printable encoding. - * </P> - * <P> - * Again, we should not use this flag at all for complying vCard 2.1 spec. - * </P> - * <P> - * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this - * kind of problem (hopefully). - * </P> - */ - public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; - - /** - * <P> - * The flag indicating that phonetic name related fields must be converted to - * appropriate form. Note that "appropriate" is not defined in any vCard specification. - * This is Android-specific. - * </P> - * <P> - * One typical (and currently sole) example where we need this flag is the time when - * we need to emit Japanese phonetic names into vCard entries. The property values - * should be encoded into half-width katakana when the target importer is Japanese mobile - * phones', which are probably not able to parse full-width hiragana/katakana for - * historical reasons, while the vCard importers embedded to softwares for PC should be - * able to parse them as we expect. - * </P> - */ - public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; - - /** - * <P> - * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params - * every time possible. The default behavior does not emit it and is valid in the spec. - * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification. - * </P> - * <P> - * Detail: - * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0. - * </p> - * <P> - * e.g.<BR /> - * 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."<BR /> - * 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."<BR /> - * 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."<BR /> - * </P> - * <P> - * 2) had been the default of VCard exporter/importer in Android, but it is found that - * some external exporter is not able to parse the type format like 2) but only 3). - * </P> - * <P> - * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" - * strings (which should be rare though), please use this flag. - * </P> - * <P> - * Example usage: int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); - * </P> - */ - public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; - - /** - * <P> - * The flag asking exporter to refrain image export. - * </P> - * @hide will be deleted in the near future. - */ - public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x02000000; - - /** - * <P> - * The flag indicating the vCard composer does touch nothing toward phone number Strings - * but leave it as is. - * </P> - * <P> - * The vCard specifications mention nothing toward phone numbers, while some devices - * do (wrongly, but with innevitable reasons). - * For example, there's a possibility Japanese mobile phones are expected to have - * just numbers, hypens, plus, etc. but not usual alphabets, while US mobile phones - * should get such characters. To make exported vCard simple for external parsers, - * we have used {@link PhoneNumberUtils#formatNumber(String)} during export, and - * removed unnecessary characters inside the number (e.g. "111-222-3333 (Miami)" - * becomes "111-222-3333"). - * Unfortunate side effect of that use was some control characters used in the other - * areas may be badly affected by the formatting. - * </P> - * <P> - * This flag disables that formatting, affecting both importer and exporter. - * If the user is aware of some side effects due to the implicit formatting, use this flag. - * </P> - */ - public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000; - - //// The followings are VCard types available from importer/exporter. //// - - /** - * <P> - * Generic vCard format with the vCard 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used toward formatting - * some values. - * </P> - * <P> - * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", - * while it should be "Prefix Family Middle Given Suffix" in Japan for example. - * </P> - */ - public static final int VCARD_TYPE_V21_GENERIC_UTF8 = - (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; - - /** - * <P> - * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * </P> - * <P> - * Not fully ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_GENERIC_UTF8 = - (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; - - /** - * <P> - * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. - * Currently, only name order is considered ("Prefix Middle Given Family Suffix") - * </P> - */ - public static final int VCARD_TYPE_V21_EUROPE_UTF8 = - (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; - - /** - * <P> - * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_EUROPE_UTF8 = - (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; - - /** - * <P> - * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; - - /** - * <P> - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V21_JAPANESE_SJIS = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; - - /** - * <P> - * vCard format for miscellaneous Japanese devices, using Shift_Jis for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_JAPANESE_SJIS = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; - - /** - * <P> - * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; - - /** - * <P> - * The vCard 2.1 based format which (partially) considers the convention in Japanese - * mobile phones, where phonetic names are translated to half-width katakana if - * possible, etc. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_CONVERT_PHONETIC_NAME_STRINGS | - FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; - - /** - * <P> - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. - * </p> - * <P> - * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. - * No Android-specific property nor defact property is included. The "Primary" properties - * are NOT encoded to Quoted-Printable. - * </P> - */ - public static final int VCARD_TYPE_DOCOMO = - (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); - - /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; - - private static final Map<String, Integer> sVCardTypeMap; - private static final Set<Integer> sJapaneseMobileTypeSet; - - static { - sVCardTypeMap = new HashMap<String, Integer>(); - sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); - sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); - - sJapaneseMobileTypeSet = new HashSet<Integer>(); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); - sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); - } - - public static int getVCardTypeFromString(final String vcardTypeString) { - final String loweredKey = vcardTypeString.toLowerCase(); - if (sVCardTypeMap.containsKey(loweredKey)) { - return sVCardTypeMap.get(loweredKey); - } else if ("default".equalsIgnoreCase(vcardTypeString)) { - return VCARD_TYPE_DEFAULT; - } else { - Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\""); - return VCARD_TYPE_DEFAULT; - } - } - - public static boolean isV30(final int vcardType) { - return ((vcardType & FLAG_V30) != 0); - } - - public static boolean shouldUseQuotedPrintable(final int vcardType) { - return !isV30(vcardType); - } - - public static boolean usesUtf8(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8); - } - - public static boolean usesShiftJis(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS); - } - - public static int getNameOrderType(final int vcardType) { - return vcardType & NAME_ORDER_MASK; - } - - public static boolean usesAndroidSpecificProperty(final int vcardType) { - return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); - } - - public static boolean usesDefactProperty(final int vcardType) { - return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); - } - - public static boolean showPerformanceLog() { - return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; - } - - public static boolean shouldRefrainQPToNameProperties(final int vcardType) { - return (!shouldUseQuotedPrintable(vcardType) || - ((vcardType & FLAG_REFRAIN_QP_TO_NAME_PROPERTIES) != 0)); - } - - public static boolean appendTypeParamName(final int vcardType) { - return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); - } - - /** - * @return true if the device is Japanese and some Japanese convension is - * applied to creating "formatted" something like FORMATTED_ADDRESS. - */ - public static boolean isJapaneseDevice(final int vcardType) { - // TODO: Some mask will be required so that this method wrongly interpret - // Japanese"-like" vCard type. - // e.g. VCARD_TYPE_V21_JAPANESE_SJIS | FLAG_APPEND_TYPE_PARAMS - return sJapaneseMobileTypeSet.contains(vcardType); - } - - /* package */ static boolean refrainPhoneNumberFormatting(final int vcardType) { - return ((vcardType & FLAG_REFRAIN_PHONE_NUMBER_FORMATTING) != 0); - } - - public static boolean needsToConvertPhoneticString(final int vcardType) { - return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0); - } - - public static boolean onlyOneNoteFieldIsAvailable(final int vcardType) { - return vcardType == VCARD_TYPE_DOCOMO; - } - - public static boolean isDoCoMo(final int vcardType) { - return ((vcardType & FLAG_DOCOMO) != 0); - } - - private VCardConfig() { - } -} diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java deleted file mode 100644 index 8c07126..0000000 --- a/core/java/android/pim/vcard/VCardConstants.java +++ /dev/null @@ -1,152 +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.pim.vcard; - -/** - * Constants used in both exporter and importer code. - */ -public class VCardConstants { - public static final String VERSION_V21 = "2.1"; - public static final String VERSION_V30 = "3.0"; - - // The property names valid both in vCard 2.1 and 3.0. - public static final String PROPERTY_BEGIN = "BEGIN"; - public static final String PROPERTY_VERSION = "VERSION"; - public static final String PROPERTY_N = "N"; - public static final String PROPERTY_FN = "FN"; - public static final String PROPERTY_ADR = "ADR"; - public static final String PROPERTY_EMAIL = "EMAIL"; - public static final String PROPERTY_NOTE = "NOTE"; - public static final String PROPERTY_ORG = "ORG"; - public static final String PROPERTY_SOUND = "SOUND"; // Not fully supported. - public static final String PROPERTY_TEL = "TEL"; - public static final String PROPERTY_TITLE = "TITLE"; - public static final String PROPERTY_ROLE = "ROLE"; - public static final String PROPERTY_PHOTO = "PHOTO"; - public static final String PROPERTY_LOGO = "LOGO"; - public static final String PROPERTY_URL = "URL"; - public static final String PROPERTY_BDAY = "BDAY"; // Birthday - public static final String PROPERTY_END = "END"; - - // Valid property names not supported (not appropriately handled) by our vCard importer now. - public static final String PROPERTY_REV = "REV"; - public static final String PROPERTY_AGENT = "AGENT"; - - // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. - public static final String PROPERTY_NAME = "NAME"; - public static final String PROPERTY_NICKNAME = "NICKNAME"; - public static final String PROPERTY_SORT_STRING = "SORT-STRING"; - - // De-fact property values expressing phonetic names. - public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; - public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; - public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - - // Properties both ContactsStruct in Eclair and de-fact vCard extensions - // shown in http://en.wikipedia.org/wiki/VCard support are defined here. - public static final String PROPERTY_X_AIM = "X-AIM"; - public static final String PROPERTY_X_MSN = "X-MSN"; - public static final String PROPERTY_X_YAHOO = "X-YAHOO"; - public static final String PROPERTY_X_ICQ = "X-ICQ"; - public static final String PROPERTY_X_JABBER = "X-JABBER"; - public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; - public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; - // Properties only ContactsStruct has. We alse use this. - public static final String PROPERTY_X_QQ = "X-QQ"; - public static final String PROPERTY_X_NETMEETING = "X-NETMEETING"; - - // Phone number for Skype, available as usual phone. - public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; - - // Property for Android-specific fields. - public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM"; - - // Properties for DoCoMo vCard. - public static final String PROPERTY_X_CLASS = "X-CLASS"; - public static final String PROPERTY_X_REDUCTION = "X-REDUCTION"; - public static final String PROPERTY_X_NO = "X-NO"; - public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; - - public static final String PARAM_TYPE = "TYPE"; - - public static final String PARAM_TYPE_HOME = "HOME"; - public static final String PARAM_TYPE_WORK = "WORK"; - public static final String PARAM_TYPE_FAX = "FAX"; - public static final String PARAM_TYPE_CELL = "CELL"; - public static final String PARAM_TYPE_VOICE = "VOICE"; - public static final String PARAM_TYPE_INTERNET = "INTERNET"; - - // Abbreviation of "prefered" according to vCard 2.1 specification. - // We interpret this value as "primary" property during import/export. - // - // Note: Both vCard specs does not mention anything about the requirement for this parameter, - // but there may be some vCard importer which will get confused with more than - // one "PREF"s in one property name, while Android accepts them. - public static final String PARAM_TYPE_PREF = "PREF"; - - // Phone type parameters valid in vCard and known to ContactsContract, but not so common. - public static final String PARAM_TYPE_CAR = "CAR"; - public static final String PARAM_TYPE_ISDN = "ISDN"; - public static final String PARAM_TYPE_PAGER = "PAGER"; - public static final String PARAM_TYPE_TLX = "TLX"; // Telex - - // Phone types existing in vCard 2.1 but not known to ContactsContract. - public static final String PARAM_TYPE_MODEM = "MODEM"; - public static final String PARAM_TYPE_MSG = "MSG"; - public static final String PARAM_TYPE_BBS = "BBS"; - public static final String PARAM_TYPE_VIDEO = "VIDEO"; - - // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). - // These types are basically encoded to "X-" parameters when composing vCard. - // Parser passes these when "X-" is added to the parameter or not. - public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK"; - public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO"; - public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD"; - public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT"; - // vCard composer translates this type to "WORK" + "PREF". Just for parsing. - public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; - // vCard composer translates this type to "VOICE" Just for parsing. - public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER"; - - // TYPE parameters for postal addresses. - public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL"; - public static final String PARAM_ADR_TYPE_DOM = "DOM"; - public static final String PARAM_ADR_TYPE_INTL = "INTL"; - - // TYPE parameters not officially valid but used in some vCard exporter. - // Do not use in composer side. - public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; - - // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in - // vCard 3.0. - public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; - - public interface ImportOnly { - public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; - // Some device emits this "X-" parameter for expressing Google Talk, - // which is specifically invalid but should be always properly accepted, and emitted - // in some special case (for that device/application). - public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; - } - - /* package */ static final int MAX_DATA_COLUMN = 15; - - /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; - static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75; - - private VCardConstants() { - } -}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java deleted file mode 100644 index 7c7e9b8..0000000 --- a/core/java/android/pim/vcard/VCardEntry.java +++ /dev/null @@ -1,1447 +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.pim.vcard; - -import android.accounts.Account; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentResolver; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.Groups; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Event; -import android.provider.ContactsContract.CommonDataKinds.GroupMembership; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Nickname; -import android.provider.ContactsContract.CommonDataKinds.Note; -import android.provider.ContactsContract.CommonDataKinds.Organization; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.CommonDataKinds.Website; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -/** - * This class bridges between data structure of Contact app and VCard data. - */ -public class VCardEntry { - private static final String LOG_TAG = "VCardEntry"; - - private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; - - private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; - private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; - - private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); - - static { - sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); - sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); - sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); - sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); - sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); - sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); - sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); - sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, - Im.PROTOCOL_GOOGLE_TALK); - } - - static public class PhoneData { - public final int type; - public final String data; - public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. - public boolean isPrimary; - public PhoneData(int type, String data, String label, boolean isPrimary) { - this.type = type; - this.data = data; - this.label = label; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PhoneData)) { - return false; - } - PhoneData phoneData = (PhoneData)obj; - return (type == phoneData.type && data.equals(phoneData.data) && - label.equals(phoneData.label) && isPrimary == phoneData.isPrimary); - } - - @Override - public String toString() { - return String.format("type: %d, data: %s, label: %s, isPrimary: %s", - type, data, label, isPrimary); - } - } - - static public class EmailData { - public final int type; - public final String data; - // Used only when TYPE is TYPE_CUSTOM. - public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. - public boolean isPrimary; - public EmailData(int type, String data, String label, boolean isPrimary) { - this.type = type; - this.data = data; - this.label = label; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof EmailData)) { - return false; - } - EmailData emailData = (EmailData)obj; - return (type == emailData.type && data.equals(emailData.data) && - label.equals(emailData.label) && isPrimary == emailData.isPrimary); - } - - @Override - public String toString() { - return String.format("type: %d, data: %s, label: %s, isPrimary: %s", - type, data, label, isPrimary); - } - } - - static public class PostalData { - // Determined by vCard spec. - // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name - public static final int ADDR_MAX_DATA_SIZE = 7; - private final String[] dataArray; - public final String pobox; - public final String extendedAddress; - public final String street; - public final String localty; - public final String region; - public final String postalCode; - public final String country; - public final int type; - public final String label; - public boolean isPrimary; - - public PostalData(final int type, final List<String> propValueList, - final String label, boolean isPrimary) { - this.type = type; - dataArray = new String[ADDR_MAX_DATA_SIZE]; - - int size = propValueList.size(); - if (size > ADDR_MAX_DATA_SIZE) { - size = ADDR_MAX_DATA_SIZE; - } - - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name - // - // Use Iterator assuming List may be LinkedList, though actually it is - // always ArrayList in the current implementation. - int i = 0; - for (String addressElement : propValueList) { - dataArray[i] = addressElement; - if (++i >= size) { - break; - } - } - while (i < ADDR_MAX_DATA_SIZE) { - dataArray[i++] = null; - } - - this.pobox = dataArray[0]; - this.extendedAddress = dataArray[1]; - this.street = dataArray[2]; - this.localty = dataArray[3]; - this.region = dataArray[4]; - this.postalCode = dataArray[5]; - this.country = dataArray[6]; - this.label = label; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PostalData)) { - return false; - } - final PostalData postalData = (PostalData)obj; - return (Arrays.equals(dataArray, postalData.dataArray) && - (type == postalData.type && - (type == StructuredPostal.TYPE_CUSTOM ? - (label == postalData.label) : true)) && - (isPrimary == postalData.isPrimary)); - } - - public String getFormattedAddress(final int vcardType) { - StringBuilder builder = new StringBuilder(); - boolean empty = true; - if (VCardConfig.isJapaneseDevice(vcardType)) { - // In Japan, the order is reversed. - for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { - String addressPart = dataArray[i]; - if (!TextUtils.isEmpty(addressPart)) { - if (!empty) { - builder.append(' '); - } else { - empty = false; - } - builder.append(addressPart); - } - } - } else { - for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { - String addressPart = dataArray[i]; - if (!TextUtils.isEmpty(addressPart)) { - if (!empty) { - builder.append(' '); - } else { - empty = false; - } - builder.append(addressPart); - } - } - } - - return builder.toString().trim(); - } - - @Override - public String toString() { - return String.format("type: %d, label: %s, isPrimary: %s", - type, label, isPrimary); - } - } - - static public class OrganizationData { - public final int type; - // non-final is Intentional: we may change the values since this info is separated into - // two parts in vCard: "ORG" + "TITLE". - public String companyName; - public String departmentName; - public String titleName; - public boolean isPrimary; - - public OrganizationData(int type, - String companyName, - String departmentName, - String titleName, - boolean isPrimary) { - this.type = type; - this.companyName = companyName; - this.departmentName = departmentName; - this.titleName = titleName; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof OrganizationData)) { - return false; - } - OrganizationData organization = (OrganizationData)obj; - return (type == organization.type && - TextUtils.equals(companyName, organization.companyName) && - TextUtils.equals(departmentName, organization.departmentName) && - TextUtils.equals(titleName, organization.titleName) && - isPrimary == organization.isPrimary); - } - - public String getFormattedString() { - final StringBuilder builder = new StringBuilder(); - if (!TextUtils.isEmpty(companyName)) { - builder.append(companyName); - } - - if (!TextUtils.isEmpty(departmentName)) { - if (builder.length() > 0) { - builder.append(", "); - } - builder.append(departmentName); - } - - if (!TextUtils.isEmpty(titleName)) { - if (builder.length() > 0) { - builder.append(", "); - } - builder.append(titleName); - } - - return builder.toString(); - } - - @Override - public String toString() { - return String.format( - "type: %d, company: %s, department: %s, title: %s, isPrimary: %s", - type, companyName, departmentName, titleName, isPrimary); - } - } - - static public class ImData { - public final int protocol; - public final String customProtocol; - public final int type; - public final String data; - public final boolean isPrimary; - - public ImData(final int protocol, final String customProtocol, final int type, - final String data, final boolean isPrimary) { - this.protocol = protocol; - this.customProtocol = customProtocol; - this.type = type; - this.data = data; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ImData)) { - return false; - } - ImData imData = (ImData)obj; - return (type == imData.type && protocol == imData.protocol - && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : - (imData.customProtocol == null)) - && (data != null ? data.equals(imData.data) : (imData.data == null)) - && isPrimary == imData.isPrimary); - } - - @Override - public String toString() { - return String.format( - "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", - type, protocol, customProtocol, data, isPrimary); - } - } - - public static class PhotoData { - public static final String FORMAT_FLASH = "SWF"; - public final int type; - public final String formatName; // used when type is not defined in ContactsContract. - public final byte[] photoBytes; - public final boolean isPrimary; - - public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) { - this.type = type; - this.formatName = formatName; - this.photoBytes = photoBytes; - this.isPrimary = isPrimary; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PhotoData)) { - return false; - } - PhotoData photoData = (PhotoData)obj; - return (type == photoData.type && - (formatName == null ? (photoData.formatName == null) : - formatName.equals(photoData.formatName)) && - (Arrays.equals(photoBytes, photoData.photoBytes)) && - (isPrimary == photoData.isPrimary)); - } - - @Override - public String toString() { - return String.format("type: %d, format: %s: size: %d, isPrimary: %s", - type, formatName, photoBytes.length, isPrimary); - } - } - - /* package */ static class Property { - private String mPropertyName; - private Map<String, Collection<String>> mParameterMap = - new HashMap<String, Collection<String>>(); - private List<String> mPropertyValueList = new ArrayList<String>(); - private byte[] mPropertyBytes; - - public void setPropertyName(final String propertyName) { - mPropertyName = propertyName; - } - - public void addParameter(final String paramName, final String paramValue) { - Collection<String> values; - if (!mParameterMap.containsKey(paramName)) { - if (paramName.equals("TYPE")) { - values = new HashSet<String>(); - } else { - values = new ArrayList<String>(); - } - mParameterMap.put(paramName, values); - } else { - values = mParameterMap.get(paramName); - } - values.add(paramValue); - } - - public void addToPropertyValueList(final String propertyValue) { - mPropertyValueList.add(propertyValue); - } - - public void setPropertyBytes(final byte[] propertyBytes) { - mPropertyBytes = propertyBytes; - } - - public final Collection<String> getParameters(String type) { - return mParameterMap.get(type); - } - - public final List<String> getPropertyValueList() { - return mPropertyValueList; - } - - public void clear() { - mPropertyName = null; - mParameterMap.clear(); - mPropertyValueList.clear(); - mPropertyBytes = null; - } - } - - private String mFamilyName; - private String mGivenName; - private String mMiddleName; - private String mPrefix; - private String mSuffix; - - // Used only when no family nor given name is found. - private String mFullName; - - private String mPhoneticFamilyName; - private String mPhoneticGivenName; - private String mPhoneticMiddleName; - - private String mPhoneticFullName; - - private List<String> mNickNameList; - - private String mDisplayName; - - private String mBirthday; - - private List<String> mNoteList; - private List<PhoneData> mPhoneList; - private List<EmailData> mEmailList; - private List<PostalData> mPostalList; - private List<OrganizationData> mOrganizationList; - private List<ImData> mImList; - private List<PhotoData> mPhotoList; - private List<String> mWebsiteList; - private List<List<String>> mAndroidCustomPropertyList; - - private final int mVCardType; - private final Account mAccount; - - public VCardEntry() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); - } - - public VCardEntry(int vcardType) { - this(vcardType, null); - } - - public VCardEntry(int vcardType, Account account) { - mVCardType = vcardType; - mAccount = account; - } - - private void addPhone(int type, String data, String label, boolean isPrimary) { - if (mPhoneList == null) { - mPhoneList = new ArrayList<PhoneData>(); - } - final StringBuilder builder = new StringBuilder(); - final String trimed = data.trim(); - final String formattedNumber; - if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { - formattedNumber = trimed; - } else { - final int length = trimed.length(); - for (int i = 0; i < length; i++) { - char ch = trimed.charAt(i); - if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { - builder.append(ch); - } - } - - // Use NANP in default when there's no information about locale. - final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); - formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); - } - PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary); - mPhoneList.add(phoneData); - } - - private void addNickName(final String nickName) { - if (mNickNameList == null) { - mNickNameList = new ArrayList<String>(); - } - mNickNameList.add(nickName); - } - - private void addEmail(int type, String data, String label, boolean isPrimary){ - if (mEmailList == null) { - mEmailList = new ArrayList<EmailData>(); - } - mEmailList.add(new EmailData(type, data, label, isPrimary)); - } - - private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ - if (mPostalList == null) { - mPostalList = new ArrayList<PostalData>(0); - } - mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); - } - - /** - * Should be called via {@link #handleOrgValue(int, List, boolean)} or - * {@link #handleTitleValue(String)}. - */ - private void addNewOrganization(int type, final String companyName, - final String departmentName, - final String titleName, boolean isPrimary) { - if (mOrganizationList == null) { - mOrganizationList = new ArrayList<OrganizationData>(); - } - mOrganizationList.add(new OrganizationData(type, companyName, - departmentName, titleName, isPrimary)); - } - - private static final List<String> sEmptyList = - Collections.unmodifiableList(new ArrayList<String>(0)); - - /** - * Set "ORG" related values to the appropriate data. If there's more than one - * {@link OrganizationData} objects, this input data are attached to the last one which - * does not have valid values (not including empty but only null). If there's no - * {@link OrganizationData} object, a new {@link OrganizationData} is created, - * whose title is set to null. - */ - private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) { - if (orgList == null) { - orgList = sEmptyList; - } - final String companyName; - final String departmentName; - final int size = orgList.size(); - switch (size) { - case 0: { - companyName = ""; - departmentName = null; - break; - } - case 1: { - companyName = orgList.get(0); - departmentName = null; - break; - } - default: { // More than 1. - companyName = orgList.get(0); - // We're not sure which is the correct string for department. - // In order to keep all the data, concatinate the rest of elements. - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < size; i++) { - if (i > 1) { - builder.append(' '); - } - builder.append(orgList.get(i)); - } - departmentName = builder.toString(); - } - } - if (mOrganizationList == null) { - // Create new first organization entry, with "null" title which may be - // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, isPrimary); - return; - } - for (OrganizationData organizationData : mOrganizationList) { - // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. - // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. - if (organizationData.companyName == null && - organizationData.departmentName == null) { - // Probably the "TITLE" property comes before the "ORG" property via - // handleTitleLine(). - organizationData.companyName = companyName; - organizationData.departmentName = departmentName; - organizationData.isPrimary = isPrimary; - return; - } - } - // No OrganizatioData is available. Create another one, with "null" title, which may be - // added via handleTitleValue(). - addNewOrganization(type, companyName, departmentName, null, isPrimary); - } - - /** - * Set "title" value to the appropriate data. If there's more than one - * OrganizationData objects, this input is attached to the last one which does not - * have valid title value (not including empty but only null). If there's no - * OrganizationData object, a new OrganizationData is created, whose company name is - * set to null. - */ - private void handleTitleValue(final String title) { - if (mOrganizationList == null) { - // Create new first organization entry, with "null" other info, which may be - // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); - return; - } - for (OrganizationData organizationData : mOrganizationList) { - if (organizationData.titleName == null) { - organizationData.titleName = title; - return; - } - } - // No Organization is available. Create another one, with "null" other info, which may be - // added via handleOrgValue(). - addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); - } - - private void addIm(int protocol, String customProtocol, int type, - String propValue, boolean isPrimary) { - if (mImList == null) { - mImList = new ArrayList<ImData>(); - } - mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); - } - - private void addNote(final String note) { - if (mNoteList == null) { - mNoteList = new ArrayList<String>(1); - } - mNoteList.add(note); - } - - private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { - if (mPhotoList == null) { - mPhotoList = new ArrayList<PhotoData>(1); - } - final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); - mPhotoList.add(photoData); - } - - @SuppressWarnings("fallthrough") - private void handleNProperty(List<String> elems) { - // Family, Given, Middle, Prefix, Suffix. (1 - 5) - int size; - if (elems == null || (size = elems.size()) < 1) { - return; - } - if (size > 5) { - size = 5; - } - - switch (size) { - // fallthrough - case 5: mSuffix = elems.get(4); - case 4: mPrefix = elems.get(3); - case 3: mMiddleName = elems.get(2); - case 2: mGivenName = elems.get(1); - default: mFamilyName = elems.get(0); - } - } - - /** - * Note: Some Japanese mobile phones use this field for phonetic name, - * since vCard 2.1 does not have "SORT-STRING" type. - * Also, in some cases, the field has some ';'s in it. - * Assume the ';' means the same meaning in N property - */ - @SuppressWarnings("fallthrough") - private void handlePhoneticNameFromSound(List<String> elems) { - if (!(TextUtils.isEmpty(mPhoneticFamilyName) && - TextUtils.isEmpty(mPhoneticMiddleName) && - TextUtils.isEmpty(mPhoneticGivenName))) { - // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. - // Ignore "SOUND;X-IRMC-N". - return; - } - - int size; - if (elems == null || (size = elems.size()) < 1) { - return; - } - - // Assume that the order is "Family, Given, Middle". - // This is not from specification but mere assumption. Some Japanese phones use this order. - if (size > 3) { - size = 3; - } - - if (elems.get(0).length() > 0) { - boolean onlyFirstElemIsNonEmpty = true; - for (int i = 1; i < size; i++) { - if (elems.get(i).length() > 0) { - onlyFirstElemIsNonEmpty = false; - break; - } - } - if (onlyFirstElemIsNonEmpty) { - final String[] namesArray = elems.get(0).split(" "); - final int nameArrayLength = namesArray.length; - if (nameArrayLength == 3) { - // Assume the string is "Family Middle Given". - mPhoneticFamilyName = namesArray[0]; - mPhoneticMiddleName = namesArray[1]; - mPhoneticGivenName = namesArray[2]; - } else if (nameArrayLength == 2) { - // Assume the string is "Family Given" based on the Japanese mobile - // phones' preference. - mPhoneticFamilyName = namesArray[0]; - mPhoneticGivenName = namesArray[1]; - } else { - mPhoneticFullName = elems.get(0); - } - return; - } - } - - switch (size) { - // fallthrough - case 3: mPhoneticMiddleName = elems.get(2); - case 2: mPhoneticGivenName = elems.get(1); - default: mPhoneticFamilyName = elems.get(0); - } - } - - public void addProperty(final Property property) { - final String propName = property.mPropertyName; - final Map<String, Collection<String>> paramMap = property.mParameterMap; - final List<String> propValueList = property.mPropertyValueList; - byte[] propBytes = property.mPropertyBytes; - - if (propValueList.size() == 0) { - return; - } - final String propValue = listToString(propValueList).trim(); - - if (propName.equals(VCardConstants.PROPERTY_VERSION)) { - // vCard version. Ignore this. - } else if (propName.equals(VCardConstants.PROPERTY_FN)) { - mFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) { - // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not - // actually exist in the real vCard data, does not exist. - mFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_N)) { - handleNProperty(propValueList); - } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { - mPhoneticFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || - propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { - addNickName(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) { - Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null - && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { - // As of 2009-10-08, Parser side does not split a property value into separated - // values using ';' (in other words, propValueList.size() == 1), - // which is correct behavior from the view of vCard 2.1. - // But we want it to be separated, so do the separation here. - final List<String> phoneticNameList = - VCardUtils.constructListFromValue(propValue, - VCardConfig.isV30(mVCardType)); - handlePhoneticNameFromSound(phoneticNameList); - } else { - // Ignore this field since Android cannot understand what it is. - } - } else if (propName.equals(VCardConstants.PROPERTY_ADR)) { - boolean valuesAreAllEmpty = true; - for (String value : propValueList) { - if (value.length() > 0) { - valuesAreAllEmpty = false; - break; - } - } - if (valuesAreAllEmpty) { - return; - } - - int type = -1; - String label = ""; - boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { - type = StructuredPostal.TYPE_HOME; - label = ""; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) || - typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { - // "COMPANY" seems emitted by Windows Mobile, which is not - // specifically supported by vCard 2.1. We assume this is same - // as "WORK". - type = StructuredPostal.TYPE_WORK; - label = ""; - } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) || - typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) || - typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { - // We do not have any appropriate way to store this information. - } else { - if (typeString.startsWith("X-") && type < 0) { - typeString = typeString.substring(2); - } - // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters - // emit non-standard types. We do not handle their values now. - type = StructuredPostal.TYPE_CUSTOM; - label = typeString; - } - } - } - // We use "HOME" as default - if (type < 0) { - type = StructuredPostal.TYPE_HOME; - } - - addPostal(type, propValueList, label, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) { - int type = -1; - String label = null; - boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { - type = Email.TYPE_HOME; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) { - type = Email.TYPE_WORK; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) { - type = Email.TYPE_MOBILE; - } else { - if (typeString.startsWith("X-") && type < 0) { - typeString = typeString.substring(2); - } - // vCard 3.0 allows iana-token. - // We may have INTERNET (specified in vCard spec), - // SCHOOL, etc. - type = Email.TYPE_CUSTOM; - label = typeString; - } - } - } - if (type < 0) { - type = Email.TYPE_OTHER; - } - addEmail(type, propValue, label, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_ORG)) { - // vCard specification does not specify other types. - final int type = Organization.TYPE_WORK; - boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } - } - } - handleOrgValue(type, propValueList, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { - handleTitleValue(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { - // This conflicts with TITLE. Ignore for now... - // handleTitleValue(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) || - propName.equals(VCardConstants.PROPERTY_LOGO)) { - Collection<String> paramMapValue = paramMap.get("VALUE"); - if (paramMapValue != null && paramMapValue.contains("URL")) { - // Currently we do not have appropriate example for testing this case. - } else { - final Collection<String> typeCollection = paramMap.get("TYPE"); - String formatName = null; - boolean isPrimary = false; - if (typeCollection != null) { - for (String typeValue : typeCollection) { - if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { - isPrimary = true; - } else if (formatName == null){ - formatName = typeValue; - } - } - } - addPhotoBytes(formatName, propBytes, isPrimary); - } - } else if (propName.equals(VCardConstants.PROPERTY_TEL)) { - final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - final Object typeObject = - VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); - final int type; - final String label; - if (typeObject instanceof Integer) { - type = (Integer)typeObject; - label = null; - } else { - type = Phone.TYPE_CUSTOM; - label = typeObject.toString(); - } - - final boolean isPrimary; - if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else { - isPrimary = false; - } - addPhone(type, propValue, label, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { - // The phone number available via Skype. - Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - final int type = Phone.TYPE_OTHER; - final boolean isPrimary; - if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else { - isPrimary = false; - } - addPhone(type, propValue, null, isPrimary); - } else if (sImMap.containsKey(propName)) { - final int protocol = sImMap.get(propName); - boolean isPrimary = false; - int type = -1; - final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); - if (typeCollection != null) { - for (String typeString : typeCollection) { - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - isPrimary = true; - } else if (type < 0) { - if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { - type = Im.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { - type = Im.TYPE_WORK; - } - } - } - } - if (type < 0) { - type = Phone.TYPE_HOME; - } - addIm(protocol, null, type, propValue, isPrimary); - } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) { - addNote(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_URL)) { - if (mWebsiteList == null) { - mWebsiteList = new ArrayList<String>(1); - } - mWebsiteList.add(propValue); - } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { - mBirthday = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { - mPhoneticGivenName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { - mPhoneticMiddleName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { - mPhoneticFamilyName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { - final List<String> customPropertyList = - VCardUtils.constructListFromValue(propValue, - VCardConfig.isV30(mVCardType)); - handleAndroidCustomProperty(customPropertyList); - /*} else if (propName.equals("REV")) { - // Revision of this VCard entry. I think we can ignore this. - } else if (propName.equals("UID")) { - } else if (propName.equals("KEY")) { - // Type is X509 or PGP? I don't know how to handle this... - } else if (propName.equals("MAILER")) { - } else if (propName.equals("TZ")) { - } else if (propName.equals("GEO")) { - } else if (propName.equals("CLASS")) { - // vCard 3.0 only. - // e.g. CLASS:CONFIDENTIAL - } else if (propName.equals("PROFILE")) { - // VCard 3.0 only. Must be "VCARD". I think we can ignore this. - } else if (propName.equals("CATEGORIES")) { - // VCard 3.0 only. - // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY - } else if (propName.equals("SOURCE")) { - // VCard 3.0 only. - } else if (propName.equals("PRODID")) { - // VCard 3.0 only. - // To specify the identifier for the product that created - // the vCard object.*/ - } else { - // Unknown X- words and IANA token. - } - } - - private void handleAndroidCustomProperty(final List<String> customPropertyList) { - if (mAndroidCustomPropertyList == null) { - mAndroidCustomPropertyList = new ArrayList<List<String>>(); - } - mAndroidCustomPropertyList.add(customPropertyList); - } - - /** - * Construct the display name. The constructed data must not be null. - */ - private void constructDisplayName() { - // FullName (created via "FN" or "NAME" field) is prefered. - if (!TextUtils.isEmpty(mFullName)) { - mDisplayName = mFullName; - } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { - mDisplayName = VCardUtils.constructNameFromElements(mVCardType, - mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); - } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && - TextUtils.isEmpty(mPhoneticGivenName))) { - mDisplayName = VCardUtils.constructNameFromElements(mVCardType, - mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName); - } else if (mEmailList != null && mEmailList.size() > 0) { - mDisplayName = mEmailList.get(0).data; - } else if (mPhoneList != null && mPhoneList.size() > 0) { - mDisplayName = mPhoneList.get(0).data; - } else if (mPostalList != null && mPostalList.size() > 0) { - mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); - } else if (mOrganizationList != null && mOrganizationList.size() > 0) { - mDisplayName = mOrganizationList.get(0).getFormattedString(); - } - - if (mDisplayName == null) { - mDisplayName = ""; - } - } - - /** - * Consolidate several fielsds (like mName) using name candidates, - */ - public void consolidateFields() { - constructDisplayName(); - - if (mPhoneticFullName != null) { - mPhoneticFullName = mPhoneticFullName.trim(); - } - } - - public Uri pushIntoContentResolver(ContentResolver resolver) { - ArrayList<ContentProviderOperation> operationList = - new ArrayList<ContentProviderOperation>(); - // After applying the batch the first result's Uri is returned so it is important that - // the RawContact is the first operation that gets inserted into the list - ContentProviderOperation.Builder builder = - ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); - String myGroupsId = null; - if (mAccount != null) { - builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); - builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); - - // Assume that caller side creates this group if it does not exist. - if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { - final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { - Groups.SOURCE_ID }, - Groups.TITLE + "=?", new String[] { - GOOGLE_MY_CONTACTS_GROUP }, null); - try { - if (cursor != null && cursor.moveToFirst()) { - myGroupsId = cursor.getString(0); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - } else { - builder.withValue(RawContacts.ACCOUNT_NAME, null); - builder.withValue(RawContacts.ACCOUNT_TYPE, null); - } - operationList.add(builder.build()); - - if (!nameFieldsAreEmpty()) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); - - builder.withValue(StructuredName.GIVEN_NAME, mGivenName); - builder.withValue(StructuredName.FAMILY_NAME, mFamilyName); - builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName); - builder.withValue(StructuredName.PREFIX, mPrefix); - builder.withValue(StructuredName.SUFFIX, mSuffix); - - if (!(TextUtils.isEmpty(mPhoneticGivenName) - && TextUtils.isEmpty(mPhoneticFamilyName) - && TextUtils.isEmpty(mPhoneticMiddleName))) { - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); - builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); - builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); - } else if (!TextUtils.isEmpty(mPhoneticFullName)) { - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName); - } - - builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); - operationList.add(builder.build()); - } - - if (mNickNameList != null && mNickNameList.size() > 0) { - for (String nickName : mNickNameList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); - builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); - builder.withValue(Nickname.NAME, nickName); - operationList.add(builder.build()); - } - } - - if (mPhoneList != null) { - for (PhoneData phoneData : mPhoneList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); - - builder.withValue(Phone.TYPE, phoneData.type); - if (phoneData.type == Phone.TYPE_CUSTOM) { - builder.withValue(Phone.LABEL, phoneData.label); - } - builder.withValue(Phone.NUMBER, phoneData.data); - if (phoneData.isPrimary) { - builder.withValue(Phone.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - if (mOrganizationList != null) { - for (OrganizationData organizationData : mOrganizationList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); - builder.withValue(Organization.TYPE, organizationData.type); - if (organizationData.companyName != null) { - builder.withValue(Organization.COMPANY, organizationData.companyName); - } - if (organizationData.departmentName != null) { - builder.withValue(Organization.DEPARTMENT, organizationData.departmentName); - } - if (organizationData.titleName != null) { - builder.withValue(Organization.TITLE, organizationData.titleName); - } - if (organizationData.isPrimary) { - builder.withValue(Organization.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - if (mEmailList != null) { - for (EmailData emailData : mEmailList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Email.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); - - builder.withValue(Email.TYPE, emailData.type); - if (emailData.type == Email.TYPE_CUSTOM) { - builder.withValue(Email.LABEL, emailData.label); - } - builder.withValue(Email.DATA, emailData.data); - if (emailData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - if (mPostalList != null) { - for (PostalData postalData : mPostalList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - VCardUtils.insertStructuredPostalDataUsingContactsStruct( - mVCardType, builder, postalData); - operationList.add(builder.build()); - } - } - - if (mImList != null) { - for (ImData imData : mImList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); - builder.withValue(Im.TYPE, imData.type); - builder.withValue(Im.PROTOCOL, imData.protocol); - if (imData.protocol == Im.PROTOCOL_CUSTOM) { - builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); - } - if (imData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); - } - } - } - - if (mNoteList != null) { - for (String note : mNoteList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); - builder.withValue(Note.NOTE, note); - operationList.add(builder.build()); - } - } - - if (mPhotoList != null) { - for (PhotoData photoData : mPhotoList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); - builder.withValue(Photo.PHOTO, photoData.photoBytes); - if (photoData.isPrimary) { - builder.withValue(Photo.IS_PRIMARY, 1); - } - operationList.add(builder.build()); - } - } - - if (mWebsiteList != null) { - for (String website : mWebsiteList) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Website.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); - builder.withValue(Website.URL, website); - // There's no information about the type of URL in vCard. - // We use TYPE_HOMEPAGE for safety. - builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); - operationList.add(builder.build()); - } - } - - if (!TextUtils.isEmpty(mBirthday)) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); - builder.withValue(Event.START_DATE, mBirthday); - builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); - operationList.add(builder.build()); - } - - if (mAndroidCustomPropertyList != null) { - for (List<String> customPropertyList : mAndroidCustomPropertyList) { - int size = customPropertyList.size(); - if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { - continue; - } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) { - size = VCardConstants.MAX_DATA_COLUMN + 1; - customPropertyList = - customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2); - } - - int i = 0; - for (final String customPropertyValue : customPropertyList) { - if (i == 0) { - final String mimeType = customPropertyValue; - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, mimeType); - } else { // 1 <= i && i <= MAX_DATA_COLUMNS - if (!TextUtils.isEmpty(customPropertyValue)) { - builder.withValue("data" + i, customPropertyValue); - } - } - - i++; - } - operationList.add(builder.build()); - } - } - - if (myGroupsId != null) { - builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); - builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); - builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId); - operationList.add(builder.build()); - } - - try { - ContentProviderResult[] results = resolver.applyBatch( - ContactsContract.AUTHORITY, operationList); - // the first result is always the raw_contact. return it's uri so - // that it can be found later. do null checking for badly behaving - // ContentResolvers - return (results == null || results.length == 0 || results[0] == null) - ? null - : results[0].uri; - } catch (RemoteException e) { - Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } catch (OperationApplicationException e) { - Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); - return null; - } - } - - public static VCardEntry buildFromResolver(ContentResolver resolver) { - return buildFromResolver(resolver, Contacts.CONTENT_URI); - } - - public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { - - return null; - } - - private boolean nameFieldsAreEmpty() { - return (TextUtils.isEmpty(mFamilyName) - && TextUtils.isEmpty(mMiddleName) - && TextUtils.isEmpty(mGivenName) - && TextUtils.isEmpty(mPrefix) - && TextUtils.isEmpty(mSuffix) - && TextUtils.isEmpty(mFullName) - && TextUtils.isEmpty(mPhoneticFamilyName) - && TextUtils.isEmpty(mPhoneticMiddleName) - && TextUtils.isEmpty(mPhoneticGivenName) - && TextUtils.isEmpty(mPhoneticFullName)); - } - - public boolean isIgnorable() { - return getDisplayName().length() == 0; - } - - private String listToString(List<String> list){ - final int size = list.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - int i = 0; - for (String type : list) { - builder.append(type); - if (i < size - 1) { - builder.append(";"); - } - } - return builder.toString(); - } else if (size == 1) { - return list.get(0); - } else { - return ""; - } - } - - // All getter methods should be used carefully, since they may change - // in the future as of 2009-10-05, on which I cannot be sure this structure - // is completely consolidated. - // - // Also note that these getter methods should be used only after - // all properties being pushed into this object. If not, incorrect - // value will "be stored in the local cache and" be returned to you. - - public String getFamilyName() { - return mFamilyName; - } - - public String getGivenName() { - return mGivenName; - } - - public String getMiddleName() { - return mMiddleName; - } - - public String getPrefix() { - return mPrefix; - } - - public String getSuffix() { - return mSuffix; - } - - public String getFullName() { - return mFullName; - } - - public String getPhoneticFamilyName() { - return mPhoneticFamilyName; - } - - public String getPhoneticGivenName() { - return mPhoneticGivenName; - } - - public String getPhoneticMiddleName() { - return mPhoneticMiddleName; - } - - public String getPhoneticFullName() { - return mPhoneticFullName; - } - - public final List<String> getNickNameList() { - return mNickNameList; - } - - public String getBirthday() { - return mBirthday; - } - - public final List<String> getNotes() { - return mNoteList; - } - - public final List<PhoneData> getPhoneList() { - return mPhoneList; - } - - public final List<EmailData> getEmailList() { - return mEmailList; - } - - public final List<PostalData> getPostalList() { - return mPostalList; - } - - public final List<OrganizationData> getOrganizationList() { - return mOrganizationList; - } - - public final List<ImData> getImList() { - return mImList; - } - - public final List<PhotoData> getPhotoList() { - return mPhotoList; - } - - public final List<String> getWebsiteList() { - return mWebsiteList; - } - - public String getDisplayName() { - if (mDisplayName == null) { - constructDisplayName(); - } - return mDisplayName; - } -} diff --git a/core/java/android/pim/vcard/VCardEntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java deleted file mode 100644 index 59a2baf..0000000 --- a/core/java/android/pim/vcard/VCardEntryCommitter.java +++ /dev/null @@ -1,68 +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.pim.vcard; - -import android.content.ContentResolver; -import android.net.Uri; -import android.util.Log; - -import java.util.ArrayList; - -/** - * <P> - * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver. - * </P> - * <P> - * Note:<BR /> - * Each vCard may contain big photo images encoded by BASE64, - * If we store all vCard entries in memory, OutOfMemoryError may be thrown. - * Thus, this class push each VCard entry into ContentResolver immediately. - * </P> - */ -public class VCardEntryCommitter implements VCardEntryHandler { - public static String LOG_TAG = "VCardEntryComitter"; - - private final ContentResolver mContentResolver; - private long mTimeToCommit; - private ArrayList<Uri> mCreatedUris = new ArrayList<Uri>(); - - public VCardEntryCommitter(ContentResolver resolver) { - mContentResolver = resolver; - } - - public void onStart() { - } - - public void onEnd() { - if (VCardConfig.showPerformanceLog()) { - Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); - } - } - - public void onEntryCreated(final VCardEntry contactStruct) { - long start = System.currentTimeMillis(); - mCreatedUris.add(contactStruct.pushIntoContentResolver(mContentResolver)); - mTimeToCommit += System.currentTimeMillis() - start; - } - - /** - * Returns the list of created Uris. This list should not be modified by the caller as it is - * not a clone. - */ - public ArrayList<Uri> getCreatedUris() { - return mCreatedUris; - } -}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java deleted file mode 100644 index 290ca2b..0000000 --- a/core/java/android/pim/vcard/VCardEntryConstructor.java +++ /dev/null @@ -1,305 +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.pim.vcard; - -import android.accounts.Account; -import android.util.CharsetUtils; -import android.util.Log; - -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.net.QuotedPrintableCodec; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class VCardEntryConstructor implements VCardInterpreter { - private static String LOG_TAG = "VCardEntryConstructor"; - - /** - * If there's no other information available, this class uses this charset for encoding - * byte arrays to String. - */ - /* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8"; - - private VCardEntry.Property mCurrentProperty = new VCardEntry.Property(); - private VCardEntry mCurrentContactStruct; - private String mParamType; - - /** - * The charset using which {@link VCardInterpreter} parses the text. - */ - private String mInputCharset; - - /** - * The charset with which byte array is encoded to String. - */ - final private String mCharsetForDecodedBytes; - final private boolean mStrictLineBreakParsing; - final private int mVCardType; - final private Account mAccount; - - /** For measuring performance. */ - private long mTimePushIntoContentResolver; - - final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); - - public VCardEntryConstructor() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); - } - - public VCardEntryConstructor(final int vcardType) { - this(null, null, false, vcardType, null); - } - - public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing, - final int vcardType, final Account account) { - this(null, charset, strictLineBreakParsing, vcardType, account); - } - - public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes, - final boolean strictLineBreakParsing, final int vcardType, - final Account account) { - if (inputCharset != null) { - mInputCharset = inputCharset; - } else { - mInputCharset = VCardConfig.DEFAULT_CHARSET; - } - if (charsetForDetodedBytes != null) { - mCharsetForDecodedBytes = charsetForDetodedBytes; - } else { - mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES; - } - mStrictLineBreakParsing = strictLineBreakParsing; - mVCardType = vcardType; - mAccount = account; - } - - public void addEntryHandler(VCardEntryHandler entryHandler) { - mEntryHandlers.add(entryHandler); - } - - public void start() { - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onStart(); - } - } - - public void end() { - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEnd(); - } - } - - /** - * Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}. - */ - public void clear() { - mCurrentContactStruct = null; - mCurrentProperty = new VCardEntry.Property(); - } - - /** - * Assume that VCard is not nested. In other words, this code does not accept - */ - public void startEntry() { - if (mCurrentContactStruct != null) { - Log.e(LOG_TAG, "Nested VCard code is not supported now."); - } - mCurrentContactStruct = new VCardEntry(mVCardType, mAccount); - } - - public void endEntry() { - mCurrentContactStruct.consolidateFields(); - for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEntryCreated(mCurrentContactStruct); - } - mCurrentContactStruct = null; - } - - public void startProperty() { - mCurrentProperty.clear(); - } - - public void endProperty() { - mCurrentContactStruct.addProperty(mCurrentProperty); - } - - public void propertyName(String name) { - mCurrentProperty.setPropertyName(name); - } - - public void propertyGroup(String group) { - } - - public void propertyParamType(String type) { - if (mParamType != null) { - Log.e(LOG_TAG, "propertyParamType() is called more than once " + - "before propertyParamValue() is called"); - } - mParamType = type; - } - - public void propertyParamValue(String value) { - if (mParamType == null) { - // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. - mParamType = "TYPE"; - } - mCurrentProperty.addParameter(mParamType, value); - mParamType = null; - } - - private String encodeString(String originalString, String charsetForDecodedBytes) { - if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) { - return originalString; - } - Charset charset = Charset.forName(mInputCharset); - ByteBuffer byteBuffer = charset.encode(originalString); - // byteBuffer.array() "may" return byte array which is larger than - // byteBuffer.remaining(). Here, we keep on the safe side. - byte[] bytes = new byte[byteBuffer.remaining()]; - byteBuffer.get(bytes); - try { - return new String(bytes, charsetForDecodedBytes); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); - return null; - } - } - - private String handleOneValue(String value, String charsetForDecodedBytes, String encoding) { - if (encoding != null) { - if (encoding.equals("BASE64") || encoding.equals("B")) { - mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); - return value; - } else if (encoding.equals("QUOTED-PRINTABLE")) { - // "= " -> " ", "=\t" -> "\t". - // Previous code had done this replacement. Keep on the safe side. - StringBuilder builder = new StringBuilder(); - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '=' && i < length - 1) { - char nextCh = value.charAt(i + 1); - if (nextCh == ' ' || nextCh == '\t') { - - builder.append(nextCh); - i++; - continue; - } - } - builder.append(ch); - } - String quotedPrintable = builder.toString(); - - String[] lines; - if (mStrictLineBreakParsing) { - lines = quotedPrintable.split("\r\n"); - } else { - builder = new StringBuilder(); - length = quotedPrintable.length(); - ArrayList<String> list = new ArrayList<String>(); - for (int i = 0; i < length; i++) { - char ch = quotedPrintable.charAt(i); - if (ch == '\n') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else if (ch == '\r') { - list.add(builder.toString()); - builder = new StringBuilder(); - if (i < length - 1) { - char nextCh = quotedPrintable.charAt(i + 1); - if (nextCh == '\n') { - i++; - } - } - } else { - builder.append(ch); - } - } - String finalLine = builder.toString(); - if (finalLine.length() > 0) { - list.add(finalLine); - } - lines = list.toArray(new String[0]); - } - - builder = new StringBuilder(); - for (String line : lines) { - if (line.endsWith("=")) { - line = line.substring(0, line.length() - 1); - } - builder.append(line); - } - byte[] bytes; - try { - bytes = builder.toString().getBytes(mInputCharset); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset); - bytes = builder.toString().getBytes(); - } - - try { - bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); - } catch (DecoderException e) { - Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); - return ""; - } - - try { - return new String(bytes, charsetForDecodedBytes); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); - return new String(bytes); - } - } - // Unknown encoding. Fall back to default. - } - return encodeString(value, charsetForDecodedBytes); - } - - public void propertyValues(List<String> values) { - if (values == null || values.isEmpty()) { - return; - } - - final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); - final String charset = - ((charsetCollection != null) ? charsetCollection.iterator().next() : null); - final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); - final String encoding = - ((encodingCollection != null) ? encodingCollection.iterator().next() : null); - - String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset); - if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) { - charsetForDecodedBytes = mCharsetForDecodedBytes; - } - - for (final String value : values) { - mCurrentProperty.addToPropertyValueList( - handleOneValue(value, charsetForDecodedBytes, encoding)); - } - } - - public void showPerformanceInfo() { - Log.d(LOG_TAG, "time for insert ContactStruct to database: " + - mTimePushIntoContentResolver + " ms"); - } -} diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java deleted file mode 100644 index 7bab50d..0000000 --- a/core/java/android/pim/vcard/VCardEntryCounter.java +++ /dev/null @@ -1,63 +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.pim.vcard; - -import java.util.List; - -/** - * The class which just counts the number of vCard entries in the specified input. - */ -public class VCardEntryCounter implements VCardInterpreter { - private int mCount; - - public int getCount() { - return mCount; - } - - public void start() { - } - - public void end() { - } - - public void startEntry() { - } - - public void endEntry() { - mCount++; - } - - public void startProperty() { - } - - public void endProperty() { - } - - public void propertyGroup(String group) { - } - - public void propertyName(String name) { - } - - public void propertyParamType(String type) { - } - - public void propertyParamValue(String value) { - } - - public void propertyValues(List<String> values) { - } -} diff --git a/core/java/android/pim/vcard/VCardEntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java deleted file mode 100644 index 83a67fe..0000000 --- a/core/java/android/pim/vcard/VCardEntryHandler.java +++ /dev/null @@ -1,38 +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.pim.vcard; - -/** - * The interface called by {@link VCardEntryConstructor}. Useful when you don't want to - * handle detailed information as what {@link VCardParser} provides via {@link VCardInterpreter}. - */ -public interface VCardEntryHandler { - /** - * Called when the parsing started. - */ - public void onStart(); - - /** - * The method called when one VCard entry is successfully created - */ - public void onEntryCreated(final VCardEntry entry); - - /** - * Called when the parsing ended. - * Able to be use this method for showing performance log, etc. - */ - public void onEnd(); -} diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java deleted file mode 100644 index b5237c0..0000000 --- a/core/java/android/pim/vcard/VCardInterpreter.java +++ /dev/null @@ -1,102 +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.pim.vcard; - -import java.util.List; - -/** - * <P> - * The interface which should be implemented by the classes which have to analyze each - * vCard entry more minutely than {@link VCardEntry} class analysis. - * </P> - * <P> - * Here, there are several terms specific to vCard (and this library). - * </P> - * <P> - * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD" - * and end with "END:VCARD". - * </P> - * <P> - * The term "property" is one line in vCard entry, which consists of "group", "property name", - * "parameter(param) names and values", and "property values". - * </P> - * <P> - * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2... - * </P> - */ -public interface VCardInterpreter { - /** - * Called when vCard interpretation started. - */ - void start(); - - /** - * Called when vCard interpretation finished. - */ - void end(); - - /** - * Called when parsing one vCard entry started. - * More specifically, this method is called when "BEGIN:VCARD" is read. - */ - void startEntry(); - - /** - * Called when parsing one vCard entry ended. - * More specifically, this method is called when "END:VCARD" is read. - * Note that {@link #startEntry()} may be called since - * vCard (especially 2.1) allows nested vCard. - */ - void endEntry(); - - /** - * Called when reading one property started. - */ - void startProperty(); - - /** - * Called when reading one property ended. - */ - void endProperty(); - - /** - * @param group A group name. This method may be called more than once or may not be - * called at all, depending on how many gruoups are appended to the property. - */ - void propertyGroup(String group); - - /** - * @param name A property name like "N", "FN", "ADR", etc. - */ - void propertyName(String name); - - /** - * @param type A parameter name like "ENCODING", "CHARSET", etc. - */ - void propertyParamType(String type); - - /** - * @param value A parameter value. This method may be called without - * {@link #propertyParamType(String)} being called (when the vCard is vCard 2.1). - */ - void propertyParamValue(String value); - - /** - * @param values List of property values. The size of values would be 1 unless - * coressponding property name is "N", "ADR", or "ORG". - */ - void propertyValues(List<String> values); -} diff --git a/core/java/android/pim/vcard/VCardInterpreterCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java deleted file mode 100644 index 99f81f7..0000000 --- a/core/java/android/pim/vcard/VCardInterpreterCollection.java +++ /dev/null @@ -1,102 +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.pim.vcard; - -import java.util.Collection; -import java.util.List; - -/** - * The {@link VCardInterpreter} implementation which aggregates more than one - * {@link VCardInterpreter} objects and make a user object treat them as one - * {@link VCardInterpreter} object. - */ -public class VCardInterpreterCollection implements VCardInterpreter { - private final Collection<VCardInterpreter> mInterpreterCollection; - - public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) { - mInterpreterCollection = interpreterCollection; - } - - public Collection<VCardInterpreter> getCollection() { - return mInterpreterCollection; - } - - public void start() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.start(); - } - } - - public void end() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.end(); - } - } - - public void startEntry() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.startEntry(); - } - } - - public void endEntry() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.endEntry(); - } - } - - public void startProperty() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.startProperty(); - } - } - - public void endProperty() { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.endProperty(); - } - } - - public void propertyGroup(String group) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyGroup(group); - } - } - - public void propertyName(String name) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyName(name); - } - } - - public void propertyParamType(String type) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyParamType(type); - } - } - - public void propertyParamValue(String value) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyParamValue(value); - } - } - - public void propertyValues(List<String> values) { - for (VCardInterpreter builder : mInterpreterCollection) { - builder.propertyValues(values); - } - } -} diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java deleted file mode 100644 index 57c52a6..0000000 --- a/core/java/android/pim/vcard/VCardParser.java +++ /dev/null @@ -1,101 +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.pim.vcard; - -import android.pim.vcard.exception.VCardException; - -import java.io.IOException; -import java.io.InputStream; - -public abstract class VCardParser { - protected final int mParseType; - protected boolean mCanceled; - - public VCardParser() { - this(VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser(int parseType) { - mParseType = parseType; - } - - /** - * <P> - * Parses the given stream and send the VCard data into VCardBuilderBase object. - * </P. - * <P> - * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets - * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is - * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, - * In some exreme case, some VCard may have different charsets in one VCard (though - * we do not see any device which emits such kind of malicious data) - * </P> - * <P> - * In order to avoid "misunderstanding" charset as much as possible, this method - * use "ISO-8859-1" for reading the stream. When charset is specified in some property - * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to - * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit - * characters, which is not completely sure. In some cases, this "decoding-encoding" - * scheme may fail. To avoid the case, - * </P> - * <P> - * We recommend you to use {@link VCardSourceDetector} and detect which kind of source the - * VCard comes from and explicitly specify a charset using the result. - * </P> - * - * @param is The source to parse. - * @param interepreter A {@link VCardInterpreter} object which used to construct data. - * @return Returns true for success. Otherwise returns false. - * @throws IOException, VCardException - */ - public abstract boolean parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException; - - /** - * <P> - * The method variants which accept charset. - * </P> - * <P> - * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use - * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese - * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses - * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification (e.g. W53K). - * </P> - * - * @param is The source to parse. - * @param charset Charset to be used. - * @param builder The VCardBuilderBase object. - * @return Returns true when successful. Otherwise returns false. - * @throws IOException, VCardException - */ - public abstract boolean parse(InputStream is, String charset, VCardInterpreter builder) - throws IOException, VCardException; - - /** - * The method variants which tells this object the operation is already canceled. - */ - public abstract void parse(InputStream is, String charset, - VCardInterpreter builder, boolean canceled) - throws IOException, VCardException; - - /** - * Cancel parsing. - * Actual cancel is done after the end of the current one vcard entry parsing. - */ - public void cancel() { - mCanceled = true; - } -} diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java deleted file mode 100644 index fe8cfb0..0000000 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ /dev/null @@ -1,936 +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.pim.vcard; - -import android.pim.vcard.exception.VCardAgentNotSupportedException; -import android.pim.vcard.exception.VCardException; -import android.pim.vcard.exception.VCardInvalidCommentLineException; -import android.pim.vcard.exception.VCardInvalidLineException; -import android.pim.vcard.exception.VCardNestedException; -import android.pim.vcard.exception.VCardVersionException; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail. - */ -public class VCardParser_V21 extends VCardParser { - private static final String LOG_TAG = "VCardParser_V21"; - - /** Store the known-type */ - private static final HashSet<String> sKnownTypeSet = new HashSet<String>( - Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", - "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", - "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", - "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", - "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", - "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", - "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", - "WAVE", "AIFF", "PCM", "X509", "PGP")); - - /** Store the known-value */ - private static final HashSet<String> sKnownValueSet = new HashSet<String>( - Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); - - /** Store the property names available in vCard 2.1 */ - private static final HashSet<String> sAvailablePropertyNameSetV21 = - new HashSet<String>(Arrays.asList( - "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); - - /** - * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - * We allow it for safety... - */ - private static final HashSet<String> sAvailableEncodingV21 = - new HashSet<String>(Arrays.asList( - "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); - - // Used only for parsing END:VCARD. - private String mPreviousLine; - - /** The builder to build parsed data */ - protected VCardInterpreter mBuilder = null; - - /** - * The encoding type. "Encoding" in vCard is different from "Charset". - * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. - */ - protected String mEncoding = null; - - protected final String sDefaultEncoding = "8BIT"; - - // Should not directly read a line from this object. Use getLine() instead. - protected BufferedReader mReader; - - // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. - // See v21_foma_1.vcf in test directory for more information. - private int mNestCount; - - // In order to reduce warning message as much as possible, we hold the value which made Logger - // emit a warning message. - protected Set<String> mUnknownTypeMap = new HashSet<String>(); - protected Set<String> mUnknownValueMap = new HashSet<String>(); - - // For measuring performance. - private long mTimeTotal; - private long mTimeReadStartRecord; - private long mTimeReadEndRecord; - private long mTimeStartProperty; - private long mTimeEndProperty; - private long mTimeParseItems; - private long mTimeParseLineAndHandleGroup; - private long mTimeParsePropertyValues; - private long mTimeParseAdrOrgN; - private long mTimeHandleMiscPropertyValue; - private long mTimeHandleQuotedPrintable; - private long mTimeHandleBase64; - - public VCardParser_V21() { - this(null); - } - - public VCardParser_V21(VCardSourceDetector detector) { - this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser_V21(int parseType) { - super(parseType); - if (parseType == VCardConfig.PARSE_TYPE_FOMA) { - mNestCount = 1; - } - } - - /** - * Parses the file at the given position. - * - * vcard_file = [wsls] vcard [wsls] - */ - protected void parseVCardFile() throws IOException, VCardException { - boolean firstReading = true; - while (true) { - if (mCanceled) { - break; - } - if (!parseOneVCard(firstReading)) { - break; - } - firstReading = false; - } - - if (mNestCount > 0) { - boolean useCache = true; - for (int i = 0; i < mNestCount; i++) { - readEndVCard(useCache, true); - useCache = false; - } - } - } - - protected int getVersion() { - return VCardConfig.FLAG_V21; - } - - protected String getVersionString() { - return VCardConstants.VERSION_V21; - } - - /** - * @return true when the propertyName is a valid property name. - */ - protected boolean isValidPropertyName(String propertyName) { - if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); - } - return true; - } - - /** - * @return true when the encoding is a valid encoding. - */ - protected boolean isValidEncoding(String encoding) { - return sAvailableEncodingV21.contains(encoding.toUpperCase()); - } - - /** - * @return String. It may be null, or its length may be 0 - * @throws IOException - */ - protected String getLine() throws IOException { - return mReader.readLine(); - } - - /** - * @return String with it's length > 0 - * @throws IOException - * @throws VCardException when the stream reached end of line - */ - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Reached end of buffer."); - } else if (line.trim().length() > 0) { - return line; - } - } - } - - /** - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF - * "END" [ws] ":" [ws] "VCARD" - */ - private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { - boolean allowGarbage = false; - if (firstReading) { - if (mNestCount > 0) { - for (int i = 0; i < mNestCount; i++) { - if (!readBeginVCard(allowGarbage)) { - return false; - } - allowGarbage = true; - } - } - } - - if (!readBeginVCard(allowGarbage)) { - return false; - } - long start; - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.startEntry(); - mTimeReadStartRecord += System.currentTimeMillis() - start; - } - start = System.currentTimeMillis(); - parseItems(); - mTimeParseItems += System.currentTimeMillis() - start; - readEndVCard(true, false); - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.endEntry(); - mTimeReadEndRecord += System.currentTimeMillis() - start; - } - return true; - } - - /** - * @return True when successful. False when reaching the end of line - * @throws IOException - * @throws VCardException - */ - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - String line; - do { - while (true) { - line = getLine(); - if (line == null) { - return false; - } else if (line.trim().length() > 0) { - break; - } - } - String[] strArray = line.split(":", 2); - int length = strArray.length; - - // Though vCard 2.1/3.0 specification does not allow lower cases, - // vCard file emitted by some external vCard expoter have such invalid Strings. - // So we allow it. - // e.g. BEGIN:vCard - if (length == 2 && - strArray[0].trim().equalsIgnoreCase("BEGIN") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return true; - } else if (!allowGarbage) { - if (mNestCount > 0) { - mPreviousLine = line; - return false; - } else { - throw new VCardException( - "Expected String \"BEGIN:VCARD\" did not come " - + "(Instead, \"" + line + "\" came)"); - } - } - } while(allowGarbage); - - throw new VCardException("Reached where must not be reached."); - } - - /** - * The arguments useCache and allowGarbase are usually true and false accordingly when - * this function is called outside this function itself. - * - * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() - * is used. - * @param allowGarbage When true, ignore non "END:VCARD" line. - * @throws IOException - * @throws VCardException - */ - protected void readEndVCard(boolean useCache, boolean allowGarbage) - throws IOException, VCardException { - String line; - do { - if (useCache) { - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - line = mPreviousLine; - } else { - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Expected END:VCARD was not found."); - } else if (line.trim().length() > 0) { - break; - } - } - } - - String[] strArray = line.split(":", 2); - if (strArray.length == 2 && - strArray[0].trim().equalsIgnoreCase("END") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return; - } else if (!allowGarbage) { - throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); - } - useCache = false; - } while (allowGarbage); - } - - /** - * items = *CRLF item - * / item - */ - protected void parseItems() throws IOException, VCardException { - boolean ended = false; - - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - ended = parseItem(); - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } - - while (!ended) { - // follow VCARD ,it wont reach endProperty - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - try { - ended = parseItem(); - } catch (VCardInvalidCommentLineException e) { - Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); - ended = false; - } - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } - } - } - - /** - * item = [groups "."] name [params] ":" value CRLF - * / [groups "."] "ADR" [params] ":" addressparts CRLF - * / [groups "."] "ORG" [params] ":" orgparts CRLF - * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF - */ - protected boolean parseItem() throws IOException, VCardException { - mEncoding = sDefaultEncoding; - - final String line = getNonEmptyLine(); - long start = System.currentTimeMillis(); - - String[] propertyNameAndValue = separateLineAndHandleGroup(line); - if (propertyNameAndValue == null) { - return true; - } - if (propertyNameAndValue.length != 2) { - throw new VCardInvalidLineException("Invalid line \"" + line + "\""); - } - String propertyName = propertyNameAndValue[0].toUpperCase(); - String propertyValue = propertyNameAndValue[1]; - - mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - - if (propertyName.equals("ADR") || propertyName.equals("ORG") || - propertyName.equals("N")) { - start = System.currentTimeMillis(); - handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseAdrOrgN += System.currentTimeMillis() - start; - return false; - } else if (propertyName.equals("AGENT")) { - handleAgent(propertyValue); - return false; - } else if (isValidPropertyName(propertyName)) { - if (propertyName.equals("BEGIN")) { - if (propertyValue.equals("VCARD")) { - throw new VCardNestedException("This vCard has nested vCard data in it."); - } else { - throw new VCardException("Unknown BEGIN type: " + propertyValue); - } - } else if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersionString())) { - throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersionString()); - } - start = System.currentTimeMillis(); - handlePropertyValue(propertyName, propertyValue); - mTimeParsePropertyValues += System.currentTimeMillis() - start; - return false; - } - - throw new VCardException("Unknown property name: \"" + propertyName + "\""); - } - - static private final int STATE_GROUP_OR_PROPNAME = 0; - static private final int STATE_PARAMS = 1; - // vCard 3.0 specification allows double-quoted param-value, while vCard 2.1 does not. - // This is just for safety. - static private final int STATE_PARAMS_IN_DQUOTE = 2; - - protected String[] separateLineAndHandleGroup(String line) throws VCardException { - int state = STATE_GROUP_OR_PROPNAME; - int nameIndex = 0; - - final String[] propertyNameAndValue = new String[2]; - - final int length = line.length(); - if (length > 0 && line.charAt(0) == '#') { - throw new VCardInvalidCommentLineException(); - } - - for (int i = 0; i < length; i++) { - char ch = line.charAt(i); - switch (state) { - case STATE_GROUP_OR_PROPNAME: { - if (ch == ':') { - final String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } else if (ch == '.') { - String groupName = line.substring(nameIndex, i); - if (mBuilder != null) { - mBuilder.propertyGroup(groupName); - } - nameIndex = i + 1; - } else if (ch == ';') { - String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - nameIndex = i + 1; - state = STATE_PARAMS; - } - break; - } - case STATE_PARAMS: { - if (ch == '"') { - state = STATE_PARAMS_IN_DQUOTE; - } else if (ch == ';') { - handleParams(line.substring(nameIndex, i)); - nameIndex = i + 1; - } else if (ch == ':') { - handleParams(line.substring(nameIndex, i)); - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } - break; - } - case STATE_PARAMS_IN_DQUOTE: { - if (ch == '"') { - state = STATE_PARAMS; - } - break; - } - } - } - - throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); - } - - /** - * params = ";" [ws] paramlist - * paramlist = paramlist [ws] ";" [ws] param - * / param - * param = "TYPE" [ws] "=" [ws] ptypeval - * / "VALUE" [ws] "=" [ws] pvalueval - * / "ENCODING" [ws] "=" [ws] pencodingval - * / "CHARSET" [ws] "=" [ws] charsetval - * / "LANGUAGE" [ws] "=" [ws] langval - * / "X-" word [ws] "=" [ws] word - * / knowntype - */ - protected void handleParams(String params) throws VCardException { - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - final String paramName = strArray[0].trim().toUpperCase(); - String paramValue = strArray[1].trim(); - if (paramName.equals("TYPE")) { - handleType(paramValue); - } else if (paramName.equals("VALUE")) { - handleValue(paramValue); - } else if (paramName.equals("ENCODING")) { - handleEncoding(paramValue); - } else if (paramName.equals("CHARSET")) { - handleCharset(paramValue); - } else if (paramName.equals("LANGUAGE")) { - handleLanguage(paramValue); - } else if (paramName.startsWith("X-")) { - handleAnyParam(paramName, paramValue); - } else { - throw new VCardException("Unknown type \"" + paramName + "\""); - } - } else { - handleParamWithoutName(strArray[0]); - } - } - - /** - * vCard 3.0 parser may throw VCardException. - */ - @SuppressWarnings("unused") - protected void handleParamWithoutName(final String paramValue) throws VCardException { - handleType(paramValue); - } - - /** - * ptypeval = knowntype / "X-" word - */ - protected void handleType(final String ptypeval) { - String upperTypeValue = ptypeval; - if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && - !mUnknownTypeMap.contains(ptypeval)) { - mUnknownTypeMap.add(ptypeval); - Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("TYPE"); - mBuilder.propertyParamValue(upperTypeValue); - } - } - - /** - * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word - */ - protected void handleValue(final String pvalueval) { - if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && - pvalueval.startsWith("X-") && - !mUnknownValueMap.contains(pvalueval)) { - mUnknownValueMap.add(pvalueval); - Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("VALUE"); - mBuilder.propertyParamValue(pvalueval); - } - } - - /** - * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word - */ - protected void handleEncoding(String pencodingval) throws VCardException { - if (isValidEncoding(pencodingval) || - pencodingval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("ENCODING"); - mBuilder.propertyParamValue(pencodingval); - } - mEncoding = pencodingval; - } else { - throw new VCardException("Unknown encoding \"" + pencodingval + "\""); - } - } - - /** - * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), - * but today's vCard often contains other charset, so we allow them. - */ - protected void handleCharset(String charsetval) { - if (mBuilder != null) { - mBuilder.propertyParamType("CHARSET"); - mBuilder.propertyParamValue(charsetval); - } - } - - /** - * See also Section 7.1 of RFC 1521 - */ - protected void handleLanguage(String langval) throws VCardException { - String[] strArray = langval.split("-"); - if (strArray.length != 2) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - String tmp = strArray[0]; - int length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - tmp = strArray[1]; - length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - if (mBuilder != null) { - mBuilder.propertyParamType("LANGUAGE"); - mBuilder.propertyParamValue(langval); - } - } - - /** - * Mainly for "X-" type. This accepts any kind of type without check. - */ - protected void handleAnyParam(String paramName, String paramValue) { - if (mBuilder != null) { - mBuilder.propertyParamType(paramName); - mBuilder.propertyParamValue(paramValue); - } - } - - protected void handlePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - final long start = System.currentTimeMillis(); - final String result = getQuotedPrintable(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; - } else if (mEncoding.equalsIgnoreCase("BASE64") || - mEncoding.equalsIgnoreCase("B")) { - final long start = System.currentTimeMillis(); - // It is very rare, but some BASE64 data may be so big that - // OutOfMemoryError occurs. To ignore such cases, use try-catch. - try { - final String result = getBase64(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - } catch (OutOfMemoryError error) { - Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); - if (mBuilder != null) { - mBuilder.propertyValues(null); - } - } - mTimeHandleBase64 += System.currentTimeMillis() - start; - } else { - if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") - || mEncoding.equalsIgnoreCase("8BIT") - || mEncoding.toUpperCase().startsWith("X-"))) { - Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); - } - - final long start = System.currentTimeMillis(); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(maybeUnescapeText(propertyValue)); - mBuilder.propertyValues(v); - } - mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; - } - } - - protected String getQuotedPrintable(String firstString) throws IOException, VCardException { - // Specifically, there may be some padding between = and CRLF. - // See the following: - // - // qp-line := *(qp-segment transport-padding CRLF) - // qp-part transport-padding - // qp-segment := qp-section *(SPACE / TAB) "=" - // ; Maximum length of 76 characters - // - // e.g. (from RFC 2045) - // Now's the time = - // for all folk to come= - // to the aid of their country. - if (firstString.trim().endsWith("=")) { - // remove "transport-padding" - int pos = firstString.length() - 1; - while(firstString.charAt(pos) != '=') { - } - StringBuilder builder = new StringBuilder(); - builder.append(firstString.substring(0, pos + 1)); - builder.append("\r\n"); - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing quoted-printable String"); - } - if (line.trim().endsWith("=")) { - // remove "transport-padding" - pos = line.length() - 1; - while(line.charAt(pos) != '=') { - } - builder.append(line.substring(0, pos + 1)); - builder.append("\r\n"); - } else { - builder.append(line); - break; - } - } - return builder.toString(); - } else { - return firstString; - } - } - - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * Mainly for "ADR", "ORG", and "N" - * We do not care the number of strnosemi here. - * - * addressparts = 0*6(strnosemi ";") strnosemi - * ; PO Box, Extended Addr, Street, Locality, Region, - * Postal Code, Country Name - * orgparts = *(strnosemi ";") strnosemi - * ; First is Organization Name, - * remainder are Organization Units. - * nameparts = 0*4(strnosemi ";") strnosemi - * ; Family, Given, Middle, Prefix, Suffix. - * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. - * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi - * ; To include a semicolon in this string, it must be escaped - * ; with a "\" character. - * - * We are not sure whether we should add "\" CRLF to each value. - * For now, we exclude them. - */ - protected void handleMultiplePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - // vCard 2.1 does not allow QUOTED-PRINTABLE here, - // but some softwares/devices emit such data. - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - propertyValue = getQuotedPrintable(propertyValue); - } - - if (mBuilder != null) { - mBuilder.propertyValues(VCardUtils.constructListFromValue( - propertyValue, (getVersion() == VCardConfig.FLAG_V30))); - } - } - - /** - * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. - * - * item = ... - * / [groups "."] "AGENT" - * [params] ":" vcard CRLF - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF "END" [ws] ":" [ws] "VCARD" - */ - protected void handleAgent(final String propertyValue) throws VCardException { - if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { - // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. - return; - } else { - throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); - } - // TODO: Support AGENT property. - } - - /** - * For vCard 3.0. - */ - protected String maybeUnescapeText(final String text) { - return text; - } - - /** - * Returns unescaped String if the character should be unescaped. Return null otherwise. - * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. - */ - protected String maybeUnescapeCharacter(final char ch) { - return unescapeCharacter(ch); - } - - public static String unescapeCharacter(final char ch) { - // Original vCard 2.1 specification does not allow transformation - // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of - // this class allowed them, so keep it as is. - if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { - return String.valueOf(ch); - } else { - return null; - } - } - - @Override - public boolean parse(final InputStream is, final VCardInterpreter builder) - throws IOException, VCardException { - return parse(is, VCardConfig.DEFAULT_CHARSET, builder); - } - - @Override - public boolean parse(InputStream is, String charset, VCardInterpreter builder) - throws IOException, VCardException { - if (charset == null) { - charset = VCardConfig.DEFAULT_CHARSET; - } - final InputStreamReader tmpReader = new InputStreamReader(is, charset); - if (VCardConfig.showPerformanceLog()) { - mReader = new CustomBufferedReader(tmpReader); - } else { - mReader = new BufferedReader(tmpReader); - } - - mBuilder = builder; - - long start = System.currentTimeMillis(); - if (mBuilder != null) { - mBuilder.start(); - } - parseVCardFile(); - if (mBuilder != null) { - mBuilder.end(); - } - mTimeTotal += System.currentTimeMillis() - start; - - if (VCardConfig.showPerformanceLog()) { - showPerformanceInfo(); - } - - return true; - } - - @Override - public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled) - throws IOException, VCardException { - mCanceled = canceled; - parse(is, charset, builder); - } - - private void showPerformanceInfo() { - Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); - if (mReader instanceof CustomBufferedReader) { - Log.d(LOG_TAG, "Total readLine time: " + - ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); - } - Log.d(LOG_TAG, "Time for handling the beggining of the record: " + - mTimeReadStartRecord + " ms"); - Log.d(LOG_TAG, "Time for handling the end of the record: " + - mTimeReadEndRecord + " ms"); - Log.d(LOG_TAG, "Time for parsing line, and handling group: " + - mTimeParseLineAndHandleGroup + " ms"); - Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); - Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); - Log.d(LOG_TAG, "Time for handling normal property values: " + - mTimeHandleMiscPropertyValue + " ms"); - Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + - mTimeHandleQuotedPrintable + " ms"); - Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); - } - - private boolean isLetter(char ch) { - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - return true; - } - return false; - } -} - -class CustomBufferedReader extends BufferedReader { - private long mTime; - - public CustomBufferedReader(Reader in) { - super(in); - } - - @Override - public String readLine() throws IOException { - long start = System.currentTimeMillis(); - String ret = super.readLine(); - long end = System.currentTimeMillis(); - mTime += end - start; - return ret; - } - - public long getTotalmillisecond() { - return mTime; - } -} diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java deleted file mode 100644 index 4ecfe97..0000000 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ /dev/null @@ -1,358 +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.pim.vcard; - -import android.pim.vcard.exception.VCardException; -import android.util.Log; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; - -/** - * The class used to parse vCard 3.0. - * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426). - */ -public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V30"; - - private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( - Arrays.asList( - "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 - "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", - "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 - - // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. - private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( - Arrays.asList("7BIT", "8BIT", "BASE64", "B")); - - // Although RFC 2426 specifies some property must not have parameters, we allow it, - // since there may be some careers which violates the RFC... - private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); - - private String mPreviousLine; - - private boolean mEmittedAgentWarning = false; - - /** - * True when the caller wants the parser to be strict about the input. - * Currently this is only for testing. - */ - private final boolean mStrictParsing; - - public VCardParser_V30() { - super(); - mStrictParsing = false; - } - - /** - * @param strictParsing when true, this object throws VCardException when the vcard is not - * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class - * is not fully yet for being used with this flag and may not notice invalid line(s). - * - * @hide currently only for testing! - */ - public VCardParser_V30(boolean strictParsing) { - super(); - mStrictParsing = strictParsing; - } - - public VCardParser_V30(int parseMode) { - super(parseMode); - mStrictParsing = false; - } - - @Override - protected int getVersion() { - return VCardConfig.FLAG_V30; - } - - @Override - protected String getVersionString() { - return VCardConstants.VERSION_V30; - } - - @Override - protected boolean isValidPropertyName(String propertyName) { - if (!(sAcceptablePropsWithParam.contains(propertyName) || - acceptablePropsWithoutParam.contains(propertyName) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); - } - return true; - } - - @Override - protected boolean isValidEncoding(String encoding) { - return sAcceptableEncodingV30.contains(encoding.toUpperCase()); - } - - @Override - protected String getLine() throws IOException { - if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } else { - return mReader.readLine(); - } - } - - /** - * vCard 3.0 requires that the line with space at the beginning of the line - * must be combined with previous line. - */ - @Override - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - StringBuilder builder = null; - while (true) { - line = mReader.readLine(); - if (line == null) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - throw new VCardException("Reached end of buffer."); - } else if (line.length() == 0) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { - if (builder != null) { - // See Section 5.8.1 of RFC 2425 (MIME-DIR document). - // Following is the excerpts from it. - // - // DESCRIPTION:This is a long description that exists on a long line. - // - // Can be represented as: - // - // DESCRIPTION:This is a long description - // that exists on a long line. - // - // It could also be represented as: - // - // DESCRIPTION:This is a long descrip - // tion that exists o - // n a long line. - builder.append(line.substring(1)); - } else if (mPreviousLine != null) { - builder = new StringBuilder(); - builder.append(mPreviousLine); - mPreviousLine = null; - builder.append(line.substring(1)); - } else { - throw new VCardException("Space exists at the beginning of the line"); - } - } else { - if (mPreviousLine == null) { - mPreviousLine = line; - if (builder != null) { - return builder.toString(); - } - } else { - String ret = mPreviousLine; - mPreviousLine = line; - return ret; - } - } - } - } - - - /** - * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF - * 1 * (contentline) - * ;A vCard object MUST include the VERSION, FN and N types. - * [group "."] "END" ":" "VCARD" 1 * CRLF - */ - @Override - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - return super.readBeginVCard(allowGarbage); - } - - @Override - protected void readEndVCard(boolean useCache, boolean allowGarbage) - throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - super.readEndVCard(useCache, allowGarbage); - } - - /** - * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. - */ - @Override - protected void handleParams(String params) throws VCardException { - try { - super.handleParams(params); - } catch (VCardException e) { - // maybe IANA type - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - handleAnyParam(strArray[0], strArray[1]); - } else { - // Must not come here in the current implementation. - throw new VCardException( - "Unknown params value: " + params); - } - } - } - - @Override - protected void handleAnyParam(String paramName, String paramValue) { - super.handleAnyParam(paramName, paramValue); - } - - @Override - protected void handleParamWithoutName(final String paramValue) throws VCardException { - if (mStrictParsing) { - throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); - } else { - super.handleParamWithoutName(paramValue); - } - } - - /** - * vCard 3.0 defines - * - * param = param-name "=" param-value *("," param-value) - * param-name = iana-token / x-name - * param-value = ptext / quoted-string - * quoted-string = DQUOTE QSAFE-CHAR DQUOTE - */ - @Override - protected void handleType(String ptypevalues) { - String[] ptypeArray = ptypevalues.split(","); - mBuilder.propertyParamType("TYPE"); - for (String value : ptypeArray) { - int length = value.length(); - if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { - mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); - } else { - mBuilder.propertyParamValue(value); - } - } - } - - @Override - protected void handleAgent(String propertyValue) { - // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. - // - // e.g. - // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n - // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n - // ET:jfriday@host.com\nEND:VCARD\n - // - // TODO: fix this. - // - // issue: - // vCard 3.0 also allows this as an example. - // - // AGENT;VALUE=uri: - // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com - // - // This is not vCard. Should we support this? - // - // Just ignore the line for now, since we cannot know how to handle it... - if (!mEmittedAgentWarning) { - Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); - mEmittedAgentWarning = true; - } - } - - /** - * vCard 3.0 does not require two CRLF at the last of BASE64 data. - * It only requires that data should be MIME-encoded. - */ - @Override - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } else if (!line.startsWith(" ") && !line.startsWith("\t")) { - mPreviousLine = line; - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") - * ; \\ encodes \, \n or \N encodes newline - * ; \; encodes ;, \, encodes , - * - * Note: Apple escapes ':' into '\:' while does not escape '\' - */ - @Override - protected String maybeUnescapeText(String text) { - return unescapeText(text); - } - - public static String unescapeText(String text) { - StringBuilder builder = new StringBuilder(); - int length = text.length(); - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - if (ch == '\\' && i < length - 1) { - char next_ch = text.charAt(++i); - if (next_ch == 'n' || next_ch == 'N') { - builder.append("\n"); - } else { - builder.append(next_ch); - } - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - @Override - protected String maybeUnescapeCharacter(char ch) { - return unescapeCharacter(ch); - } - - public static String unescapeCharacter(char ch) { - if (ch == 'n' || ch == 'N') { - return "\n"; - } else { - return String.valueOf(ch); - } - } -} diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java deleted file mode 100644 index 7297c50..0000000 --- a/core/java/android/pim/vcard/VCardSourceDetector.java +++ /dev/null @@ -1,129 +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.pim.vcard; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Class which tries to detects the source of the vCard from its properties. - * Currently this implementation is very premature. - * @hide - */ -public class VCardSourceDetector implements VCardInterpreter { - private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( - "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", - "X-ABADR", "X-ABUID")); - - private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( - "X-GNO", "X-GN", "X-REDUCTION")); - - private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( - "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC")); - - // Note: these signes appears before the signs of the other type (e.g. "X-GN"). - // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES. - private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList( - "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", - "X-SD-DESCRIPTION")); - private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; - - private int mType = VCardConfig.PARSE_TYPE_UNKNOWN; - // Some mobile phones (like FOMA) tells us the charset of the data. - private boolean mNeedParseSpecifiedCharset; - private String mSpecifiedCharset; - - public void start() { - } - - public void end() { - } - - public void startEntry() { - } - - public void startProperty() { - mNeedParseSpecifiedCharset = false; - } - - public void endProperty() { - } - - public void endEntry() { - } - - public void propertyGroup(String group) { - } - - public void propertyName(String name) { - if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { - mType = VCardConfig.PARSE_TYPE_FOMA; - mNeedParseSpecifiedCharset = true; - return; - } - if (mType != VCardConfig.PARSE_TYPE_UNKNOWN) { - return; - } - if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP; - } else if (FOMA_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_FOMA; - } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP; - } else if (APPLE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_APPLE; - } - } - - public void propertyParamType(String type) { - } - - public void propertyParamValue(String value) { - } - - public void propertyValues(List<String> values) { - if (mNeedParseSpecifiedCharset && values.size() > 0) { - mSpecifiedCharset = values.get(0); - } - } - - /* package */ int getEstimatedType() { - return mType; - } - - /** - * Return charset String guessed from the source's properties. - * This method must be called after parsing target file(s). - * @return Charset String. Null is returned if guessing the source fails. - */ - public String getEstimatedCharset() { - if (mSpecifiedCharset != null) { - return mSpecifiedCharset; - } - switch (mType) { - case VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP: - case VCardConfig.PARSE_TYPE_FOMA: - case VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP: - return "SHIFT_JIS"; - case VCardConfig.PARSE_TYPE_APPLE: - return "UTF-8"; - default: - return null; - } - } -} diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java deleted file mode 100644 index 11b112b..0000000 --- a/core/java/android/pim/vcard/VCardUtils.java +++ /dev/null @@ -1,545 +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.pim.vcard; - -import android.content.ContentProviderOperation; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utilities for VCard handling codes. - */ -public class VCardUtils { - // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is - // converted to two parameter Strings. These only contain some minor fields valid in both - // vCard and current (as of 2009-08-07) Contacts structure. - private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; - private static final Set<String> sPhoneTypesUnknownToContactsSet; - private static final Map<String, Integer> sKnownPhoneTypeMap_StoI; - private static final Map<Integer, String> sKnownImPropNameMap_ItoS; - private static final Set<String> sMobilePhoneLabelSet; - - static { - sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); - sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>(); - - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); - - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); - - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, - Phone.TYPE_CALLBACK); - sKnownPhoneTypeMap_StoI.put( - VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, - Phone.TYPE_TTY_TDD); - sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, - Phone.TYPE_ASSISTANT); - - sPhoneTypesUnknownToContactsSet = new HashSet<String>(); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); - sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); - - sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, - VCardConstants.PROPERTY_X_GOOGLE_TALK); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); - - // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) - // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) - // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone) - // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone) - sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList( - "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4", - "\uFF79\uFF72\uFF80\uFF72")); - } - - public static String getPhoneTypeString(Integer type) { - return sKnownPhoneTypesMap_ItoS.get(type); - } - - /** - * Returns Interger when the given types can be parsed as known type. Returns String object - * when not, which should be set to label. - */ - public static Object getPhoneTypeFromStrings(Collection<String> types, - String number) { - if (number == null) { - number = ""; - } - int type = -1; - String label = null; - boolean isFax = false; - boolean hasPref = false; - - if (types != null) { - for (String typeString : types) { - if (typeString == null) { - continue; - } - typeString = typeString.toUpperCase(); - if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { - hasPref = true; - } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) { - isFax = true; - } else { - if (typeString.startsWith("X-") && type < 0) { - typeString = typeString.substring(2); - } - if (typeString.length() == 0) { - continue; - } - final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); - if (tmp != null) { - final int typeCandidate = tmp; - // TYPE_PAGER is prefered when the number contains @ surronded by - // a pager number and a domain name. - // e.g. - // o 1111@domain.com - // x @domain.com - // x 1111@ - final int indexOfAt = number.indexOf("@"); - if ((typeCandidate == Phone.TYPE_PAGER - && 0 < indexOfAt && indexOfAt < number.length() - 1) - || type < 0 - || type == Phone.TYPE_CUSTOM) { - type = tmp; - } - } else if (type < 0) { - type = Phone.TYPE_CUSTOM; - label = typeString; - } - } - } - } - if (type < 0) { - if (hasPref) { - type = Phone.TYPE_MAIN; - } else { - // default to TYPE_HOME - type = Phone.TYPE_HOME; - } - } - if (isFax) { - if (type == Phone.TYPE_HOME) { - type = Phone.TYPE_FAX_HOME; - } else if (type == Phone.TYPE_WORK) { - type = Phone.TYPE_FAX_WORK; - } else if (type == Phone.TYPE_OTHER) { - type = Phone.TYPE_OTHER_FAX; - } - } - if (type == Phone.TYPE_CUSTOM) { - return label; - } else { - return type; - } - } - - @SuppressWarnings("deprecation") - public static boolean isMobilePhoneLabel(final String label) { - // For backward compatibility. - // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. - // To support mobile type at that time, this custom label had been used. - return (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME.equals(label) - || sMobilePhoneLabelSet.contains(label)); - } - - public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { - return sPhoneTypesUnknownToContactsSet.contains(label); - } - - public static String getPropertyNameForIm(final int protocol) { - return sKnownImPropNameMap_ItoS.get(protocol); - } - - public static String[] sortNameElements(final int vcardType, - final String familyName, final String middleName, final String givenName) { - final String[] list = new String[3]; - final int nameOrderType = VCardConfig.getNameOrderType(vcardType); - switch (nameOrderType) { - case VCardConfig.NAME_ORDER_JAPANESE: { - if (containsOnlyPrintableAscii(familyName) && - containsOnlyPrintableAscii(givenName)) { - list[0] = givenName; - list[1] = middleName; - list[2] = familyName; - } else { - list[0] = familyName; - list[1] = middleName; - list[2] = givenName; - } - break; - } - case VCardConfig.NAME_ORDER_EUROPE: { - list[0] = middleName; - list[1] = givenName; - list[2] = familyName; - break; - } - default: { - list[0] = givenName; - list[1] = middleName; - list[2] = familyName; - break; - } - } - return list; - } - - public static int getPhoneNumberFormat(final int vcardType) { - if (VCardConfig.isJapaneseDevice(vcardType)) { - return PhoneNumberUtils.FORMAT_JAPAN; - } else { - return PhoneNumberUtils.FORMAT_NANP; - } - } - - /** - * Inserts postal data into the builder object. - * - * Note that the data structure of ContactsContract is different from that defined in vCard. - * So some conversion may be performed in this method. - */ - public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, - final ContentProviderOperation.Builder builder, - final VCardEntry.PostalData postalData) { - builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); - builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); - - builder.withValue(StructuredPostal.TYPE, postalData.type); - if (postalData.type == StructuredPostal.TYPE_CUSTOM) { - builder.withValue(StructuredPostal.LABEL, postalData.label); - } - - final String streetString; - if (TextUtils.isEmpty(postalData.street)) { - if (TextUtils.isEmpty(postalData.extendedAddress)) { - streetString = null; - } else { - streetString = postalData.extendedAddress; - } - } else { - if (TextUtils.isEmpty(postalData.extendedAddress)) { - streetString = postalData.street; - } else { - streetString = postalData.street + " " + postalData.extendedAddress; - } - } - builder.withValue(StructuredPostal.POBOX, postalData.pobox); - builder.withValue(StructuredPostal.STREET, streetString); - builder.withValue(StructuredPostal.CITY, postalData.localty); - builder.withValue(StructuredPostal.REGION, postalData.region); - builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); - builder.withValue(StructuredPostal.COUNTRY, postalData.country); - - builder.withValue(StructuredPostal.FORMATTED_ADDRESS, - postalData.getFormattedAddress(vcardType)); - if (postalData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); - } - } - - public static String constructNameFromElements(final int vcardType, - final String familyName, final String middleName, final String givenName) { - return constructNameFromElements(vcardType, familyName, middleName, givenName, - null, null); - } - - public static String constructNameFromElements(final int vcardType, - final String familyName, final String middleName, final String givenName, - final String prefix, final String suffix) { - final StringBuilder builder = new StringBuilder(); - final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName); - boolean first = true; - if (!TextUtils.isEmpty(prefix)) { - first = false; - builder.append(prefix); - } - for (final String namePart : nameList) { - if (!TextUtils.isEmpty(namePart)) { - if (first) { - first = false; - } else { - builder.append(' '); - } - builder.append(namePart); - } - } - if (!TextUtils.isEmpty(suffix)) { - if (!first) { - builder.append(' '); - } - builder.append(suffix); - } - return builder.toString(); - } - - public static List<String> constructListFromValue(final String value, - final boolean isV30) { - final List<String> list = new ArrayList<String>(); - StringBuilder builder = new StringBuilder(); - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '\\' && i < length - 1) { - char nextCh = value.charAt(i + 1); - final String unescapedString = - (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) : - VCardParser_V21.unescapeCharacter(nextCh)); - if (unescapedString != null) { - builder.append(unescapedString); - i++; - } else { - builder.append(ch); - } - } else if (ch == ';') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else { - builder.append(ch); - } - } - list.add(builder.toString()); - return list; - } - - public static boolean containsOnlyPrintableAscii(final String...values) { - if (values == null) { - return true; - } - return containsOnlyPrintableAscii(Arrays.asList(values)); - } - - public static boolean containsOnlyPrintableAscii(final Collection<String> values) { - if (values == null) { - return true; - } - for (final String value : values) { - if (TextUtils.isEmpty(value)) { - continue; - } - if (!TextUtils.isPrintableAsciiOnly(value)) { - return false; - } - } - return true; - } - - /** - * This is useful when checking the string should be encoded into quoted-printable - * or not, which is required by vCard 2.1. - * See the definition of "7bit" in vCard 2.1 spec for more information. - */ - public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { - if (values == null) { - return true; - } - return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); - } - - public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) { - if (values == null) { - return true; - } - final int asciiFirst = 0x20; - final int asciiLast = 0x7E; // included - for (final String value : values) { - if (TextUtils.isEmpty(value)) { - continue; - } - final int length = value.length(); - for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { - final int c = value.codePointAt(i); - if (!(asciiFirst <= c && c <= asciiLast)) { - return false; - } - } - } - return true; - } - - private static final Set<Character> sUnAcceptableAsciiInV21WordSet = - new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); - - /** - * This is useful since vCard 3.0 often requires the ("X-") properties and groups - * should contain only alphabets, digits, and hyphen. - * - * Note: It is already known some devices (wrongly) outputs properties with characters - * which should not be in the field. One example is "X-GOOGLE TALK". We accept - * such kind of input but must never output it unless the target is very specific - * to the device which is able to parse the malformed input. - */ - public static boolean containsOnlyAlphaDigitHyphen(final String...values) { - if (values == null) { - return true; - } - return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); - } - - public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) { - if (values == null) { - return true; - } - final int upperAlphabetFirst = 0x41; // A - final int upperAlphabetAfterLast = 0x5b; // [ - final int lowerAlphabetFirst = 0x61; // a - final int lowerAlphabetAfterLast = 0x7b; // { - final int digitFirst = 0x30; // 0 - final int digitAfterLast = 0x3A; // : - final int hyphen = '-'; - for (final String str : values) { - if (TextUtils.isEmpty(str)) { - continue; - } - final int length = str.length(); - for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { - int codepoint = str.codePointAt(i); - if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) || - (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) || - (digitFirst <= codepoint && codepoint < digitAfterLast) || - (codepoint == hyphen))) { - return false; - } - } - } - return true; - } - - /** - * <P> - * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. - * </P> - * <P> - * vCard 2.1 specifies:<BR /> - * word = <any printable 7bit us-ascii except []=:., > - * </P> - */ - public static boolean isV21Word(final String value) { - if (TextUtils.isEmpty(value)) { - return true; - } - final int asciiFirst = 0x20; - final int asciiLast = 0x7E; // included - final int length = value.length(); - for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { - final int c = value.codePointAt(i); - if (!(asciiFirst <= c && c <= asciiLast) || - sUnAcceptableAsciiInV21WordSet.contains((char)c)) { - return false; - } - } - return true; - } - - public static String toHalfWidthString(final String orgString) { - if (TextUtils.isEmpty(orgString)) { - return null; - } - final StringBuilder builder = new StringBuilder(); - final int length = orgString.length(); - for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) { - // All Japanese character is able to be expressed by char. - // Do not need to use String#codepPointAt(). - final char ch = orgString.charAt(i); - final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); - if (halfWidthText != null) { - builder.append(halfWidthText); - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - /** - * Guesses the format of input image. Currently just the first few bytes are used. - * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when - * the guess failed. - * @param input Image as byte array. - * @return The image type or null when the type cannot be determined. - */ - public static String guessImageType(final byte[] input) { - if (input == null) { - return null; - } - if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') { - return "GIF"; - } else if (input.length >= 4 && input[0] == (byte) 0x89 - && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') { - // Note: vCard 2.1 officially does not support PNG, but we may have it and - // using X- word like "X-PNG" may not let importers know it is PNG. - // So we use the String "PNG" as is... - return "PNG"; - } else if (input.length >= 2 && input[0] == (byte) 0xff - && input[1] == (byte) 0xd8) { - return "JPEG"; - } else { - return null; - } - } - - /** - * @return True when all the given values are null or empty Strings. - */ - public static boolean areAllEmpty(final String...values) { - if (values == null) { - return true; - } - - for (final String value : values) { - if (!TextUtils.isEmpty(value)) { - return false; - } - } - return true; - } - - private VCardUtils() { - } -} diff --git a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java deleted file mode 100644 index e72c7df..0000000 --- a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java +++ /dev/null @@ -1,27 +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.pim.vcard.exception; - -public class VCardAgentNotSupportedException extends VCardNotSupportedException { - public VCardAgentNotSupportedException() { - super(); - } - - public VCardAgentNotSupportedException(String message) { - super(message); - } - -}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java deleted file mode 100644 index 67db62c..0000000 --- a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java +++ /dev/null @@ -1,32 +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.pim.vcard.exception; - -/** - * Thrown when the vCard has some line starting with '#'. In the specification, - * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit - * such lines. - */ -public class VCardInvalidCommentLineException extends VCardInvalidLineException { - public VCardInvalidCommentLineException() { - super(); - } - - public VCardInvalidCommentLineException(final String message) { - super(message); - } -} diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java deleted file mode 100644 index 330153e..0000000 --- a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java +++ /dev/null @@ -1,32 +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.pim.vcard.exception; - -/** - * Thrown when the vCard has some line starting with '#'. In the specification, - * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit - * such lines. - */ -public class VCardInvalidLineException extends VCardException { - public VCardInvalidLineException() { - super(); - } - - public VCardInvalidLineException(final String message) { - super(message); - } -} diff --git a/core/java/android/pim/vcard/exception/VCardNestedException.java b/core/java/android/pim/vcard/exception/VCardNestedException.java deleted file mode 100644 index 503c2fb..0000000 --- a/core/java/android/pim/vcard/exception/VCardNestedException.java +++ /dev/null @@ -1,29 +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.pim.vcard.exception; - -/** - * VCardException thrown when VCard is nested without VCardParser's being notified. - */ -public class VCardNestedException extends VCardNotSupportedException { - public VCardNestedException() { - super(); - } - public VCardNestedException(String message) { - super(message); - } -} diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java deleted file mode 100644 index 616aa77..0000000 --- a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java +++ /dev/null @@ -1,33 +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.pim.vcard.exception; - -/** - * The exception which tells that the input VCard is probably valid from the view of - * specification but not supported in the current framework for now. - * - * This is a kind of a good news from the view of development. - * It may be good to ask users to send a report with the VCard example - * for the future development. - */ -public class VCardNotSupportedException extends VCardException { - public VCardNotSupportedException() { - super(); - } - public VCardNotSupportedException(String message) { - super(message); - } -}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/package.html b/core/java/android/pim/vcard/exception/package.html deleted file mode 100644 index 26b8a32..0000000 --- a/core/java/android/pim/vcard/exception/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<HTML> -<BODY> -{@hide} -</BODY> -</HTML>
\ No newline at end of file diff --git a/core/java/android/pim/vcard/package.html b/core/java/android/pim/vcard/package.html deleted file mode 100644 index 26b8a32..0000000 --- a/core/java/android/pim/vcard/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<HTML> -<BODY> -{@hide} -</BODY> -</HTML>
\ No newline at end of file diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java new file mode 100644 index 0000000..42d555c --- /dev/null +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -0,0 +1,274 @@ +/* + * 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.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; + +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link Preference} that displays a list of entries as + * a dialog. + * <p> + * This preference will store a set of strings into the SharedPreferences. + * This set will contain one or more values from the + * {@link #setEntryValues(CharSequence[])} array. + * + * @attr ref android.R.styleable#MultiSelectListPreference_entries + * @attr ref android.R.styleable#MultiSelectListPreference_entryValues + */ +public class MultiSelectListPreference extends DialogPreference { + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private Set<String> mValues = new HashSet<String>(); + private Set<String> mNewValues = new HashSet<String>(); + private boolean mPreferenceChanged; + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MultiSelectListPreference, 0, 0); + mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries); + mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues); + a.recycle(); + } + + public MultiSelectListPreference(Context context) { + this(context, null); + } + + /** + * Sets the human-readable entries to be shown in the list. This will be + * shown in subsequent dialogs. + * <p> + * Each entry must have a corresponding index in + * {@link #setEntryValues(CharSequence[])}. + * + * @param entries The entries. + * @see #setEntryValues(CharSequence[]) + */ + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + /** + * @see #setEntries(CharSequence[]) + * @param entriesResId The entries array as a resource. + */ + public void setEntries(int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + /** + * The list of entries to be shown in the list in subsequent dialogs. + * + * @return The list as an array. + */ + public CharSequence[] getEntries() { + return mEntries; + } + + /** + * The array to find the value to save for a preference when an entry from + * entries is selected. If a user clicks on the second item in entries, the + * second item in this array will be saved to the preference. + * + * @param entryValues The array to be used as values to save for the preference. + */ + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + /** + * @see #setEntryValues(CharSequence[]) + * @param entryValuesResId The entry values array as a resource. + */ + public void setEntryValues(int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + /** + * Returns the array of values to be saved for the preference. + * + * @return The array of values. + */ + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + /** + * Sets the value of the key. This should contain entries in + * {@link #getEntryValues()}. + * + * @param values The values to set for the key. + */ + public void setValues(Set<String> values) { + mValues = values; + + persistStringSet(values); + } + + /** + * Retrieves the current value of the key. + */ + public Set<String> getValues() { + return mValues; + } + + /** + * Returns the index of the given value (in the entry values array). + * + * @param value The value whose index should be returned. + * @return The index of the value, or -1 if not found. + */ + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "MultiSelectListPreference requires an entries array and " + + "an entryValues array."); + } + + boolean[] checkedItems = getSelectedItems(); + builder.setMultiChoiceItems(mEntries, checkedItems, + new DialogInterface.OnMultiChoiceClickListener() { + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + mPreferenceChanged |= mNewValues.add(mEntries[which].toString()); + } else { + mPreferenceChanged |= mNewValues.remove(mEntries[which].toString()); + } + } + }); + mNewValues.clear(); + mNewValues.addAll(mValues); + } + + private boolean[] getSelectedItems() { + final CharSequence[] entries = mEntries; + final int entryCount = entries.length; + final Set<String> values = mValues; + boolean[] result = new boolean[entryCount]; + + for (int i = 0; i < entryCount; i++) { + result[i] = values.contains(entries[i].toString()); + } + + return result; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && mPreferenceChanged) { + final Set<String> values = mNewValues; + if (callChangeListener(values)) { + setValues(values); + } + } + mPreferenceChanged = false; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + final CharSequence[] defaultValues = a.getTextArray(index); + final int valueCount = defaultValues.length; + final Set<String> result = new HashSet<String>(); + + for (int i = 0; i < valueCount; i++) { + result.add(defaultValues[i].toString()); + } + + return result; + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValues(restoreValue ? getPersistedStringSet(mValues) : (Set<String>) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.values = getValues(); + return myState; + } + + private static class SavedState extends BaseSavedState { + Set<String> values; + + public SavedState(Parcel source) { + super(source); + values = new HashSet<String>(); + String[] strings = source.readStringArray(); + + final int stringCount = strings.length; + for (int i = 0; i < stringCount; i++) { + values.add(strings[i]); + } + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeStringArray(values.toArray(new String[0])); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 197d976..381f794 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -16,8 +16,7 @@ package android.preference; -import java.util.ArrayList; -import java.util.List; +import com.android.internal.util.CharSequences; import android.content.Context; import android.content.Intent; @@ -28,7 +27,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; -import com.android.internal.util.CharSequences; import android.view.AbsSavedState; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +34,10 @@ import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + /** * Represents the basic Preference UI building * block displayed by a {@link PreferenceActivity} in the form of a @@ -1250,6 +1252,61 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** + * Attempts to persist a set of Strings to the {@link android.content.SharedPreferences}. + * <p> + * This will check if this Preference is persistent, get an editor from + * the {@link PreferenceManager}, put in the strings, and check if we should commit (and + * commit if so). + * + * @param values The values to persist. + * @return True if the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #getPersistedString(Set) + * + * @hide Pending API approval + */ + protected boolean persistStringSet(Set<String> values) { + if (shouldPersist()) { + // Shouldn't store null + if (values.equals(getPersistedStringSet(null))) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putStringSet(mKey, values); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted set of Strings from the + * {@link android.content.SharedPreferences}. + * <p> + * This will check if this Preference is persistent, get the SharedPreferences + * from the {@link PreferenceManager}, and get the value. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the SharedPreferences or the default return + * value. + * @see #persistStringSet(Set) + * + * @hide Pending API approval + */ + protected Set<String> getPersistedStringSet(Set<String> defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getStringSet(mKey, defaultReturnValue); + } + + /** * Attempts to persist an int to the {@link android.content.SharedPreferences}. * * @param value The value to persist. diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 726793d..4686978 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -23,7 +23,10 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.text.TextUtils; import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; /** * Shows a hierarchy of {@link Preference} objects as @@ -69,30 +72,43 @@ import android.view.View; * As a convenience, this activity implements a click listener for any * preference in the current hierarchy, see * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. - * + * * @see Preference * @see PreferenceScreen */ public abstract class PreferenceActivity extends ListActivity implements PreferenceManager.OnPreferenceTreeClickListener { - + private static final String PREFERENCES_TAG = "android:preferences"; - + + // extras that allow any preference activity to be launched as part of a wizard + + // show Back and Next buttons? takes boolean parameter + // Back will then return RESULT_CANCELED and Next RESULT_OK + private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; + + // specify custom text for the Back or Next buttons, or cause a button to not appear + // at all by setting it to null + private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; + private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; + + private Button mNextButton; + private PreferenceManager mPreferenceManager; - + private Bundle mSavedInstanceState; /** * The starting request code given out to preference framework. */ private static final int FIRST_REQUEST_CODE = 100; - + private static final int MSG_BIND_PREFERENCES = 0; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { - + case MSG_BIND_PREFERENCES: bindPreferences(); break; @@ -105,7 +121,49 @@ public abstract class PreferenceActivity extends ListActivity implements super.onCreate(savedInstanceState); setContentView(com.android.internal.R.layout.preference_list_content); - + + // see if we should show Back/Next buttons + Intent intent = getIntent(); + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { + + findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); + + Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); + backButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); + mNextButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK); + finish(); + } + }); + + // set our various button parameters + if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); + if (TextUtils.isEmpty(buttonText)) { + mNextButton.setVisibility(View.GONE); + } + else { + mNextButton.setText(buttonText); + } + } + if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); + if (TextUtils.isEmpty(buttonText)) { + backButton.setVisibility(View.GONE); + } + else { + backButton.setText(buttonText); + } + } + } + mPreferenceManager = onCreatePreferenceManager(); getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); } @@ -113,14 +171,13 @@ public abstract class PreferenceActivity extends ListActivity implements @Override protected void onStop() { super.onStop(); - + mPreferenceManager.dispatchActivityStop(); } @Override protected void onDestroy() { super.onDestroy(); - mPreferenceManager.dispatchActivityDestroy(); } @@ -156,7 +213,7 @@ public abstract class PreferenceActivity extends ListActivity implements @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); } @@ -176,7 +233,7 @@ public abstract class PreferenceActivity extends ListActivity implements if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); } - + private void bindPreferences() { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { @@ -187,10 +244,10 @@ public abstract class PreferenceActivity extends ListActivity implements } } } - + /** * Creates the {@link PreferenceManager}. - * + * * @return The {@link PreferenceManager} used by this activity. */ private PreferenceManager onCreatePreferenceManager() { @@ -198,7 +255,7 @@ public abstract class PreferenceActivity extends ListActivity implements preferenceManager.setOnPreferenceTreeClickListener(this); return preferenceManager; } - + /** * Returns the {@link PreferenceManager} used by this activity. * @return The {@link PreferenceManager}. @@ -206,7 +263,7 @@ public abstract class PreferenceActivity extends ListActivity implements public PreferenceManager getPreferenceManager() { return mPreferenceManager; } - + private void requirePreferenceManager() { if (mPreferenceManager == null) { throw new RuntimeException("This should be called after super.onCreate."); @@ -215,7 +272,7 @@ public abstract class PreferenceActivity extends ListActivity implements /** * Sets the root of the preference hierarchy that this activity is showing. - * + * * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. */ public void setPreferenceScreen(PreferenceScreen preferenceScreen) { @@ -228,37 +285,37 @@ public abstract class PreferenceActivity extends ListActivity implements } } } - + /** * Gets the root of the preference hierarchy that this activity is showing. - * + * * @return The {@link PreferenceScreen} that is the root of the preference * hierarchy. */ public PreferenceScreen getPreferenceScreen() { return mPreferenceManager.getPreferenceScreen(); } - + /** * Adds preferences from activities that match the given {@link Intent}. - * + * * @param intent The {@link Intent} to query activities. */ public void addPreferencesFromIntent(Intent intent) { requirePreferenceManager(); - + setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); } - + /** * Inflates the given XML resource and adds the preference hierarchy to the current * preference hierarchy. - * + * * @param preferencesResId The XML resource ID to inflate. */ public void addPreferencesFromResource(int preferencesResId) { requirePreferenceManager(); - + setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, getPreferenceScreen())); } @@ -269,20 +326,20 @@ public abstract class PreferenceActivity extends ListActivity implements public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { return false; } - + /** * Finds a {@link Preference} based on its key. - * + * * @param key The key of the preference to retrieve. * @return The {@link Preference} with the key, or null. * @see PreferenceGroup#findPreference(CharSequence) */ public Preference findPreference(CharSequence key) { - + if (mPreferenceManager == null) { return null; } - + return mPreferenceManager.findPreference(key); } @@ -292,5 +349,14 @@ public abstract class PreferenceActivity extends ListActivity implements mPreferenceManager.dispatchNewIntent(intent); } } - + + // give subclasses access to the Next button + /** @hide */ + protected boolean hasNextButton() { + return mNextButton != null; + } + /** @hide */ + protected Button getNextButton() { + return mNextButton; + } } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index 9a09805..a23a5a7 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -76,64 +76,28 @@ public final class Calendar { */ public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + /** - * Columns from the Calendars table that other tables join into themselves. + * Generic columns for use by sync adapters. The specific functions of + * these columns are private to the sync adapter. Other clients of the API + * should not attempt to either read or write this column. */ - public interface CalendarsColumns - { - /** - * The color of the calendar - * <P>Type: INTEGER (color value)</P> - */ - public static final String COLOR = "color"; - - /** - * The level of access that the user has for the calendar - * <P>Type: INTEGER (one of the values below)</P> - */ - public static final String ACCESS_LEVEL = "access_level"; - - /** Cannot access the calendar */ - public static final int NO_ACCESS = 0; - /** Can only see free/busy information about the calendar */ - public static final int FREEBUSY_ACCESS = 100; - /** Can read all event details */ - public static final int READ_ACCESS = 200; - public static final int RESPOND_ACCESS = 300; - public static final int OVERRIDE_ACCESS = 400; - /** Full access to modify the calendar, but not the access control settings */ - public static final int CONTRIBUTOR_ACCESS = 500; - public static final int EDITOR_ACCESS = 600; - /** Full access to the calendar */ - public static final int OWNER_ACCESS = 700; - /** Domain admin */ - public static final int ROOT_ACCESS = 800; - - /** - * Is the calendar selected to be displayed? - * <P>Type: INTEGER (boolean)</P> - */ - public static final String SELECTED = "selected"; - - /** - * The timezone the calendar's events occurs in - * <P>Type: TEXT</P> - */ - public static final String TIMEZONE = "timezone"; - - /** - * If this calendar is in the list of calendars that are selected for - * syncing then "sync_events" is 1, otherwise 0. - * <p>Type: INTEGER (boolean)</p> - */ - public static final String SYNC_EVENTS = "sync_events"; - - /** - * Sync state data. - * <p>Type: String (blob)</p> - */ - public static final String SYNC_STATE = "sync_state"; + protected interface BaseSyncColumns { + + /** Generic column for use by sync adapters. */ + public static final String SYNC1 = "sync1"; + /** Generic column for use by sync adapters. */ + public static final String SYNC2 = "sync2"; + /** Generic column for use by sync adapters. */ + public static final String SYNC3 = "sync3"; + /** Generic column for use by sync adapters. */ + public static final String SYNC4 = "sync4"; + } + /** + * Columns for Sync information used by Calendars and Events tables. + */ + public interface SyncColumns extends BaseSyncColumns { /** * The account that was used to sync the entry to the device. * <P>Type: TEXT</P> @@ -185,6 +149,12 @@ public final class Calendar { */ public static final String _SYNC_DIRTY = "_sync_dirty"; + } + + /** + * Columns from the Account information used by Calendars and Events tables. + */ + public interface AccountColumns { /** * The name of the account instance to which this row belongs, which when paired with * {@link #ACCOUNT_TYPE} identifies a specific account. @@ -201,9 +171,159 @@ public final class Calendar { } /** + * Columns from the Calendars table that other tables join into themselves. + */ + public interface CalendarsColumns { + /** + * The color of the calendar + * <P>Type: INTEGER (color value)</P> + */ + public static final String COLOR = "color"; + + /** + * The level of access that the user has for the calendar + * <P>Type: INTEGER (one of the values below)</P> + */ + public static final String ACCESS_LEVEL = "access_level"; + + /** Cannot access the calendar */ + public static final int NO_ACCESS = 0; + /** Can only see free/busy information about the calendar */ + public static final int FREEBUSY_ACCESS = 100; + /** Can read all event details */ + public static final int READ_ACCESS = 200; + public static final int RESPOND_ACCESS = 300; + public static final int OVERRIDE_ACCESS = 400; + /** Full access to modify the calendar, but not the access control settings */ + public static final int CONTRIBUTOR_ACCESS = 500; + public static final int EDITOR_ACCESS = 600; + /** Full access to the calendar */ + public static final int OWNER_ACCESS = 700; + /** Domain admin */ + public static final int ROOT_ACCESS = 800; + + /** + * Is the calendar selected to be displayed? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String SELECTED = "selected"; + + /** + * The timezone the calendar's events occurs in + * <P>Type: TEXT</P> + */ + public static final String TIMEZONE = "timezone"; + + /** + * If this calendar is in the list of calendars that are selected for + * syncing then "sync_events" is 1, otherwise 0. + * <p>Type: INTEGER (boolean)</p> + */ + public static final String SYNC_EVENTS = "sync_events"; + + /** + * Sync state data. + * <p>Type: String (blob)</p> + */ + public static final String SYNC_STATE = "sync_state"; + + /** + * Whether the row has been deleted. A deleted row should be ignored. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DELETED = "deleted"; + } + + /** + * Class that represents a Calendar Entity. There is one entry per calendar. + */ + public static class CalendarsEntity implements BaseColumns, SyncColumns, CalendarsColumns { + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/calendar_entities"); + + public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { + return new EntityIteratorImpl(cursor, resolver); + } + + public static EntityIterator newEntityIterator(Cursor cursor, + ContentProviderClient provider) { + return new EntityIteratorImpl(cursor, provider); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + private final ContentResolver mResolver; + private final ContentProviderClient mProvider; + + public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) { + super(cursor); + mResolver = resolver; + mProvider = null; + } + + public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) { + super(cursor); + mResolver = null; + mProvider = provider; + } + + @Override + public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException { + // we expect the cursor is already at the row we need to read from + final long calendarId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID)); + + // Create the content value + ContentValues cv = new ContentValues(); + cv.put(_ID, calendarId); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT_TYPE); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_TIME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_MARK); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC3); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.DISPLAY_NAME); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.HIDDEN); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.COLOR); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELECTED); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.LOCATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.OWNER_ACCOUNT); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + Calendars.ORGANIZER_CAN_RESPOND); + + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); + + // Create the Entity from the ContentValue + Entity entity = new Entity(cv); + + // Set cursor to next row + cursor.moveToNext(); + + // Return the created Entity + return entity; + } + } + } + + /** * Contains a list of available calendars. */ - public static class Calendars implements BaseColumns, CalendarsColumns + public static class Calendars implements BaseColumns, SyncColumns, AccountColumns, + CalendarsColumns { private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?" + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; @@ -258,6 +378,24 @@ public final class Calendar { public static final String URL = "url"; /** + * The URL for the calendar itself + * <P>Type: TEXT (URL)</P> + */ + public static final String SELF_URL = "selfUrl"; + + /** + * The URL for the calendar to be edited + * <P>Type: TEXT (URL)</P> + */ + public static final String EDIT_URL = "editUrl"; + + /** + * The URL for the calendar events + * <P>Type: TEXT (URL)</P> + */ + public static final String EVENTS_URL = "eventsUrl"; + + /** * The name of the calendar * <P>Type: TEXT</P> */ @@ -296,6 +434,9 @@ public final class Calendar { public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond"; } + /** + * Columns from the Attendees table that other tables join into themselves. + */ public interface AttendeesColumns { /** @@ -361,8 +502,7 @@ public final class Calendar { /** * Columns from the Events table that other tables join into themselves. */ - public interface EventsColumns - { + public interface EventsColumns { /** * The calendar the event belongs to * <P>Type: INTEGER (foreign key to the Calendars table)</P> @@ -438,6 +578,18 @@ public final class Calendar { public static final String DTEND = "dtend"; /** + * The time the event starts with allDay events in a local tz + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTSTART2 = "dtstart2"; + + /** + * The time the event ends with allDay events in a local tz + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTEND2 = "dtend2"; + + /** * The duration of the event * <P>Type: TEXT (duration in RFC2445 format)</P> */ @@ -450,6 +602,12 @@ public final class Calendar { public static final String EVENT_TIMEZONE = "eventTimezone"; /** + * The timezone for the event, allDay events will have a local tz instead of UTC + * <P>Type: TEXT + */ + public static final String EVENT_TIMEZONE2 = "eventTimezone2"; + + /** * Whether the event lasts all day or not * <P>Type: INTEGER (boolean)</P> */ @@ -598,7 +756,8 @@ public final class Calendar { /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns { + public static final class EventsEntity implements BaseColumns, SyncColumns, AccountColumns, + EventsColumns { /** * The content:// style URL for this table */ @@ -703,8 +862,8 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EventsColumns.DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); Entity entity = new Entity(cv); Cursor subCursor; @@ -795,7 +954,8 @@ public final class Calendar { /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns { + public static final class Events implements BaseColumns, SyncColumns, AccountColumns, + EventsColumns { private static final String[] FETCH_ENTRY_COLUMNS = new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; @@ -981,7 +1141,7 @@ public final class Calendar { public static final String MAX_EVENTDAYS = "maxEventDays"; } - public static final class CalendarMetaData implements CalendarMetaDataColumns { + public static final class CalendarMetaData implements CalendarMetaDataColumns, BaseColumns { } public interface EventDaysColumns { @@ -1379,4 +1539,43 @@ public final class Calendar { public static final Uri CONTENT_URI = Uri.withAppendedPath(Calendar.CONTENT_URI, CONTENT_DIRECTORY); } + + /** + * Columns from the EventsRawTimes table + */ + public interface EventsRawTimesColumns { + /** + * The corresponding event id + * <P>Type: INTEGER (long)</P> + */ + public static final String EVENT_ID = "event_id"; + + /** + * The RFC2445 compliant time the event starts + * <P>Type: TEXT</P> + */ + public static final String DTSTART_2445 = "dtstart2445"; + + /** + * The RFC2445 compliant time the event ends + * <P>Type: TEXT</P> + */ + public static final String DTEND_2445 = "dtend2445"; + + /** + * The RFC2445 compliant original instance time of the recurring event for which this + * event is an exception. + * <P>Type: TEXT</P> + */ + public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445"; + + /** + * The RFC2445 compliant last date this event repeats on, or NULL if it never ends + * <P>Type: TEXT</P> + */ + public static final String LAST_DATE_2445 = "lastDate2445"; + } + + public static final class EventsRawTimes implements BaseColumns, EventsRawTimesColumns { + } } diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 40a408a..218f21c 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -130,6 +130,17 @@ public final class ContactsContract { public static final String REQUESTING_PACKAGE_PARAM_KEY = "requesting_package"; /** + * Query parameter that should be used by the client to access a specific + * {@link Directory}. The parameter value should be the _ID of the corresponding + * directory, e.g. + * {@code content://com.android.contacts/data/emails/filter/acme?directory=3} + * + * @hide + */ + public static final String DIRECTORY_PARAM_KEY = "directory"; + + + /** * @hide */ public static final class Preferences { @@ -181,6 +192,227 @@ public final class ContactsContract { } /** + * A Directory represents a contacts corpus, e.g. Local contacts, + * Google Apps Global Address List or Corporate Global Address List. + * <p> + * A Directory is implemented as a content provider with its unique authority and + * the same API as the main Contacts Provider. However, there is no expectation that + * every directory provider will implement this Contract in its entirety. If a + * directory provider does not have an implementation for a specific request, it + * should throw an UnsupportedOperationException. + * </p> + * <p> + * The most important use case for Directories is search. A Directory provider is + * expected to support at least {@link Contacts#CONTENT_FILTER_URI + * Contacts#CONTENT_FILTER_URI}. If a Directory provider wants to participate + * in email and phone lookup functionalities, it should also implement + * {@link CommonDataKinds.Email#CONTENT_FILTER_URI CommonDataKinds.Email.CONTENT_FILTER_URI} + * and + * {@link CommonDataKinds.Phone#CONTENT_FILTER_URI CommonDataKinds.Phone.CONTENT_FILTER_URI}. + * </p> + * <p> + * A directory provider should return NULL for every projection field it does not + * recognize, rather than throwing an exception. This way it will not be broken + * if ContactsContract is extended with new fields in the future. + * </p> + * <p> + * The client interacts with a directory via Contacts Provider by supplying an + * optional {@code directory=} query parameter. + * <p> + * <p> + * When the Contacts Provider receives the request, it transforms the URI and forwards + * the request to the corresponding directory content provider. + * The URI is transformed in the following fashion: + * <ul> + * <li>The URI authority is replaced with the corresponding {@link #DIRECTORY_AUTHORITY}.</li> + * <li>The {@code accountName=} and {@code accountType=} parameters are added or + * replaced using the corresponding {@link #ACCOUNT_TYPE} and {@link #ACCOUNT_NAME} values.</li> + * <li>If the URI is missing a {@link ContactsContract#REQUESTING_PACKAGE_PARAM_KEY} + * parameter, this parameter is added.</li> + * </ul> + * </p> + * <p> + * Clients should send directory requests to Contacts Provider and let it + * forward them to the respective providers rather than constructing directory provider + * URIs by themselves. This level of indirection allows Contacts Provider to + * implement additional system-level features and optimizations. + * Also, directory providers may reject requests coming from other + * clients than the Contacts Provider itself. + * </p> + * <p> + * The Directory table always has at least these two rows: + * <ul> + * <li> + * The local directory. It has {@link Directory#_ID Directory._ID} = + * {@link Directory#DEFAULT Directory.DEFAULT}. This directory can be used to access locally + * stored contacts. The same can be achieved by omitting the {@code directory=} + * parameter altogether. + * </li> + * <li> + * The local invisible contacts. The corresponding directory ID is + * {@link Directory#LOCAL_INVISIBLE Directory.LOCAL_INVISIBLE}. + * </li> + * </ul> + * </p> + * <p> + * Other directories should register themselves by explicitly adding rows to this table. + * </p> + * <p> + * When a row is inserted in this table, it is automatically associated with the package + * (apk) that made the request. If the package is later uninstalled, all directory rows + * it inserted are automatically removed. + * </p> + * <p> + * A directory row can be optionally associated with an account. + * If the account is later removed, the corresponding directory rows are + * automatically removed. + * </p> + * + * @hide + */ + public static final class Directory implements BaseColumns { + + /** + * Not instantiable. + */ + private Directory() { + } + + /** + * The content:// style URI for this table. Requests to this URI can be + * performed on the UI thread because they are always unblocking. + * + * @hide + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "directories"); + + /** + * The MIME-type of {@link #CONTENT_URI} providing a directory of + * contact directories. + * + * @hide + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/contact_directories"; + + /** + * The MIME type of a {@link #CONTENT_URI} item. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/contact_directory"; + + /** + * The name of the package that owns this directory. This field is + * required in an insert request and must match the name of the package + * making the request. If the package is later uninstalled, the + * directories it owns are automatically removed from this table. Only + * the specified package is allowed to modify or delete this row later. + * + * <p>TYPE: TEXT</p> + * + * @hide + */ + public static final String PACKAGE_NAME = "packageName"; + + /** + * The type of directory captured as a resource ID in the context of the + * package {@link #PACKAGE_NAME}, e.g. "Corporate Directory" + * + * <p>TYPE: INTEGER</p> + * + * @hide + */ + public static final String TYPE_RESOURCE_ID = "typeResourceId"; + + /** + * An optional name that can be used in the UI to represent this directory, + * e.g. "Acme Corp" + * <p>TYPE: text</p> + * + * @hide + */ + public static final String DISPLAY_NAME = "displayName"; + + /** + * The authority to which the request should forwarded in order to access + * this directory. + * + * <p>TYPE: text</p> + * + * @hide + */ + public static final String DIRECTORY_AUTHORITY = "authority"; + + /** + * The account type which this directory is associated. + * + * <p>TYPE: text</p> + * + * @hide + */ + public static final String ACCOUNT_TYPE = "accountType"; + + /** + * The account with which this directory is associated. If the account is later + * removed, the directories it owns are automatically removed from this table. + * + * <p>TYPE: text</p> + * + * @hide + */ + public static final String ACCOUNT_NAME = "accountName"; + + /** + * One of {@link #EXPORT_SUPPORT_NONE}, {@link #EXPORT_SUPPORT_ANY_ACCOUNT}, + * {@link #EXPORT_SUPPORT_SAME_ACCOUNT_ONLY}. This is the expectation the + * directory has for data exported from it. Clients must obey this setting. + * + * @hide + */ + public static final String EXPORT_SUPPORT = "exportSupport"; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * does not allow any data to be copied out of it. + * + * @hide + */ + public static final int EXPORT_SUPPORT_NONE = 0; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * allow its data copied only to the account specified by + * {@link #ACCOUNT_TYPE}/{@link #ACCOUNT_NAME}. + * + * @hide + */ + public static final int EXPORT_SUPPORT_SAME_ACCOUNT_ONLY = 1; + + /** + * An {@link #EXPORT_SUPPORT} setting that indicates that the directory + * allow its data copied to any contacts account. + * + * @hide + */ + public static final int EXPORT_SUPPORT_ANY_ACCOUNT = 2; + + /** + * _ID of the default directory, which represents locally stored contacts. + * + * @hide + */ + public static final long DEFAULT = 0; + + /** + * _ID of the directory that represents locally stored invisible contacts. + * + * @hide + */ + public static final long LOCAL_INVISIBLE = 1; + } + + /** * @hide should be removed when users are updated to refer to SyncState * @deprecated use SyncState instead */ @@ -4902,6 +5134,23 @@ public final class ContactsContract { * Type: INTEGER (boolean) */ public static final String SHOULD_SYNC = "should_sync"; + + /** + * Any newly created contacts will automatically be added to groups that have this + * flag set to true. + * <p> + * Type: INTEGER (boolean) + */ + public static final String AUTO_ADD = "auto_add"; + + /** + * When a contacts is marked as a favorites it will be automatically added + * to the groups that have this flag set, and when it is removed from favorites + * it will be removed from these groups. + * <p> + * Type: INTEGER (boolean) + */ + public static final String FAVORITES = "favorites"; } /** @@ -5042,6 +5291,8 @@ public final class ContactsContract { DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DELETED); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, NOTES); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SHOULD_SYNC); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, FAVORITES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, AUTO_ADD); cursor.moveToNext(); return new Entity(values); } @@ -5558,6 +5809,28 @@ public final class ContactsContract { "com.android.contacts.action.SHOW_OR_CREATE_CONTACT"; /** + * Starts an Activity that lets the user select the multiple phones from a + * list of phone numbers which come from the contacts or + * {@link #EXTRA_PHONE_URIS}. + * <p> + * The phone numbers being passed in through {@link #EXTRA_PHONE_URIS} + * could belong to the contacts or not, and will be selected by default. + * <p> + * The user's selection will be returned from + * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} + * if the resultCode is + * {@link android.app.Activity#RESULT_OK}, the array of picked phone + * numbers are in the Intent's + * {@link #EXTRA_PHONE_URIS}; otherwise, the + * {@link android.app.Activity#RESULT_CANCELED} is returned if the user + * left the Activity without changing the selection. + * + * @hide + */ + public static final String ACTION_GET_MULTIPLE_PHONES = + "com.android.contacts.action.GET_MULTIPLE_PHONES"; + + /** * Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new * contact if no matching contact found. Otherwise, default behavior is * to prompt user with dialog before creating. @@ -5578,6 +5851,23 @@ public final class ContactsContract { "com.android.contacts.action.CREATE_DESCRIPTION"; /** + * Used with {@link #ACTION_GET_MULTIPLE_PHONES} as the input or output value. + * <p> + * The phone numbers want to be picked by default should be passed in as + * input value. These phone numbers could belong to the contacts or not. + * <p> + * The phone numbers which were picked by the user are returned as output + * value. + * <p> + * Type: array of URIs, the tel URI is used for the phone numbers which don't + * belong to any contact, the content URI is used for phone id in contacts. + * + * @hide + */ + public static final String EXTRA_PHONE_URIS = + "com.android.contacts.extra.PHONE_URIS"; + + /** * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a * dialog location using screen coordinates. When not specified, the * dialog will be centered. diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 40ed980..293d31c 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -237,8 +237,67 @@ public final class MediaStore { * <P>Type: TEXT</P> */ public static final String MIME_TYPE = "mime_type"; + + /** + * The MTP object handle of a newly transfered file. + * Used internally by the MediaScanner + * <P>Type: INTEGER</P> + * @hide + */ + public static final String MTP_OBJECT_HANDLE = "mtp_object_handle"; } + + + /** + * Media provider interface used by MTP implementation. + * @hide + */ + public static final class MtpObjects { + + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/object"); + } + + public static final Uri getContentUri(String volumeName, + long objectId) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/object/" + objectId); + } + + /** + * Fields for master table for all media files. + * Table also contains MediaColumns._ID, DATA, SIZE and DATE_MODIFIED. + */ + public interface ObjectColumns extends MediaColumns { + /** + * The MTP format code of the file + * <P>Type: INTEGER</P> + */ + public static final String FORMAT = "format"; + + /** + * The index of the parent directory of the file + * <P>Type: INTEGER</P> + */ + public static final String PARENT = "parent"; + + /** + * Identifier for the media table containing the object. + * Used internally by MediaProvider + * <P>Type: INTEGER</P> + */ + public static final String MEDIA_TABLE = "media_table"; + + /** + * The ID of the object in its media table. + * <P>Type: INTEGER</P> + */ + public static final String MEDIA_ID = "media_id"; + } + } + /** * This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended * to be accessed elsewhere. @@ -317,22 +376,23 @@ public final class MediaStore { // Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo); // If the magic is non-zero, we simply return thumbnail if it does exist. // querying MediaProvider and simply return thumbnail. - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); - long magic = thumbFile.getMagic(origId); - if (magic != 0) { - if (kind == MICRO_KIND) { - byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; - if (thumbFile.getMiniThumbFromFile(origId, data) != null) { - bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap == null) { - Log.w(TAG, "couldn't decode byte array."); + MiniThumbFile thumbFile = new MiniThumbFile(isVideo ? Video.Media.EXTERNAL_CONTENT_URI + : Images.Media.EXTERNAL_CONTENT_URI); + Cursor c = null; + try { + long magic = thumbFile.getMagic(origId); + if (magic != 0) { + if (kind == MICRO_KIND) { + byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; + if (thumbFile.getMiniThumbFromFile(origId, data) != null) { + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + if (bitmap == null) { + Log.w(TAG, "couldn't decode byte array."); + } } - } - return bitmap; - } else if (kind == MINI_KIND) { - String column = isVideo ? "video_id=" : "image_id="; - Cursor c = null; - try { + return bitmap; + } else if (kind == MINI_KIND) { + String column = isVideo ? "video_id=" : "image_id="; c = cr.query(baseUri, PROJECTION, column + origId, null, null); if (c != null && c.moveToFirst()) { bitmap = getMiniThumbFromFile(c, baseUri, cr, options); @@ -340,17 +400,13 @@ public final class MediaStore { return bitmap; } } - } finally { - if (c != null) c.close(); } } - } - Cursor c = null; - try { Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1") .appendQueryParameter("orig_id", String.valueOf(origId)) .appendQueryParameter("group_id", String.valueOf(groupId)).build(); + if (c != null) c.close(); c = cr.query(blockingUri, PROJECTION, null, null, null); // This happens when original image/video doesn't exist. if (c == null) return null; @@ -397,6 +453,9 @@ public final class MediaStore { Log.w(TAG, ex); } finally { if (c != null) c.close(); + // To avoid file descriptor leak in application process. + thumbFile.deactivate(); + thumbFile = null; } return bitmap; } diff --git a/core/java/android/provider/Mtp.java b/core/java/android/provider/Mtp.java new file mode 100644 index 0000000..15f8666 --- /dev/null +++ b/core/java/android/provider/Mtp.java @@ -0,0 +1,335 @@ +/* + * 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.provider; + +import android.content.ContentUris; +import android.net.Uri; +import android.util.Log; + + +/** + * The MTP provider supports accessing content on MTP and PTP devices. + * @hide + */ +public final class Mtp +{ + private final static String TAG = "Mtp"; + + public static final String AUTHORITY = "mtp"; + + private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; + private static final String CONTENT_AUTHORITY_DEVICE_SLASH = "content://" + AUTHORITY + "/device/"; + + /** + * Contains list of all MTP/PTP devices + */ + public static final class Device implements BaseColumns { + + public static final Uri CONTENT_URI = Uri.parse(CONTENT_AUTHORITY_SLASH + "device"); + + public static Uri getContentUri(int deviceID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID); + } + + /** + * The manufacturer of the device + * <P>Type: TEXT</P> + */ + public static final String MANUFACTURER = "manufacturer"; + + /** + * The model name of the device + * <P>Type: TEXT</P> + */ + public static final String MODEL = "model"; + } + + /** + * Contains list of storage units for an MTP/PTP device + */ + public static final class Storage implements BaseColumns { + + public static Uri getContentUri(int deviceID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage"); + } + + public static Uri getContentUri(int deviceID, int storageID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage/" + storageID); + } + + /** + * Storage unit identifier + * <P>Type: TEXT</P> + */ + public static final String IDENTIFIER = "identifier"; + + /** + * Storage unit description + * <P>Type: TEXT</P> + */ + public static final String DESCRIPTION = "description"; + } + + /** + * Contains list of objects on an MTP/PTP device + */ + public static final class Object implements BaseColumns { + + public static Uri getContentUri(int deviceID, int objectID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/object/" + objectID); + } + + public static Uri getContentUriForObjectChildren(int deviceID, int objectID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/object/" + objectID + "/child"); + } + + public static Uri getContentUriForStorageChildren(int deviceID, int storageID) { + return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + + "/storage/" + storageID + "/child"); + } + + /** + * The following columns correspond to the fields in the ObjectInfo dataset + * as described in the MTP specification. + */ + + /** + * The ID of the storage unit containing the object. + * <P>Type: INTEGER</P> + */ + public static final String STORAGE_ID = "storage_id"; + + /** + * The object's format. Can be one of the FORMAT_* symbols below, + * or any of the valid MTP object formats as defined in the MTP specification. + * <P>Type: INTEGER</P> + */ + public static final String FORMAT = "format"; + + /** + * The protection status of the object. See the PROTECTION_STATUS_*symbols below. + * <P>Type: INTEGER</P> + */ + public static final String PROTECTION_STATUS = "protection_status"; + + /** + * The size of the object in bytes. + * <P>Type: INTEGER</P> + */ + public static final String SIZE = "size"; + + /** + * The object's thumbnail format. Can be one of the FORMAT_* symbols below, + * or any of the valid MTP object formats as defined in the MTP specification. + * <P>Type: INTEGER</P> + */ + public static final String THUMB_FORMAT = "format"; + + /** + * The size of the object's thumbnail in bytes. + * <P>Type: INTEGER</P> + */ + public static final String THUMB_SIZE = "thumb_size"; + + /** + * The width of the object's thumbnail in pixels. + * <P>Type: INTEGER</P> + */ + public static final String THUMB_WIDTH = "thumb_width"; + + /** + * The height of the object's thumbnail in pixels. + * <P>Type: INTEGER</P> + */ + public static final String THUMB_HEIGHT = "thumb_height"; + + /** + * The object's thumbnail. + * <P>Type: BLOB</P> + */ + public static final String THUMB = "thumb"; + + /** + * The width of the object in pixels. + * <P>Type: INTEGER</P> + */ + public static final String IMAGE_WIDTH = "image_width"; + + /** + * The height of the object in pixels. + * <P>Type: INTEGER</P> + */ + public static final String IMAGE_HEIGHT = "image_height"; + + /** + * The depth of the object in bits per pixel. + * <P>Type: INTEGER</P> + */ + public static final String IMAGE_DEPTH = "image_depth"; + + /** + * The ID of the object's parent, or zero if the object + * is in the root of its storage unit. + * <P>Type: INTEGER</P> + */ + public static final String PARENT = "parent"; + + /** + * The association type for a container object. + * For folders this is typically {@link #ASSOCIATION_TYPE_GENERIC_FOLDER} + * <P>Type: INTEGER</P> + */ + public static final String ASSOCIATION_TYPE = "association_type"; + + /** + * Contains additional information about container objects. + * <P>Type: INTEGER</P> + */ + public static final String ASSOCIATION_DESC = "association_desc"; + + /** + * The sequence number of the object, typically used for an association + * containing images taken in sequence. + * <P>Type: INTEGER</P> + */ + public static final String SEQUENCE_NUMBER = "sequence_number"; + + /** + * The name of the object. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The date the object was created, in seconds since January 1, 1970. + * <P>Type: INTEGER</P> + */ + public static final String DATE_CREATED = "date_created"; + + /** + * The date the object was last modified, in seconds since January 1, 1970. + * <P>Type: INTEGER</P> + */ + public static final String DATE_MODIFIED = "date_modified"; + + /** + * A list of keywords associated with an object, separated by spaces. + * <P>Type: TEXT</P> + */ + public static final String KEYWORDS = "keywords"; + + /** + * Contants for {@link #FORMAT} and {@link #THUMB_FORMAT} + */ + public static final int FORMAT_UNDEFINED = 0x3000; + public static final int FORMAT_ASSOCIATION = 0x3001; + public static final int FORMAT_SCRIPT = 0x3002; + public static final int FORMAT_EXECUTABLE = 0x3003; + public static final int FORMAT_TEXT = 0x3004; + public static final int FORMAT_HTML = 0x3005; + public static final int FORMAT_DPOF = 0x3006; + public static final int FORMAT_AIFF = 0x3007; + public static final int FORMAT_WAV = 0x3008; + public static final int FORMAT_MP3 = 0x3009; + public static final int FORMAT_AVI = 0x300A; + public static final int FORMAT_MPEG = 0x300B; + public static final int FORMAT_ASF = 0x300C; + public static final int FORMAT_DEFINED = 0x3800; + public static final int FORMAT_EXIF_JPEG = 0x3801; + public static final int FORMAT_TIFF_EP = 0x3802; + public static final int FORMAT_FLASHPIX = 0x3803; + public static final int FORMAT_BMP = 0x3804; + public static final int FORMAT_CIFF = 0x3805; + public static final int FORMAT_GIF = 0x3807; + public static final int FORMAT_JFIF = 0x3808; + public static final int FORMAT_CD = 0x3809; + public static final int FORMAT_PICT = 0x380A; + public static final int FORMAT_PNG = 0x380B; + public static final int FORMAT_TIFF = 0x380D; + public static final int FORMAT_TIFF_IT = 0x380E; + public static final int FORMAT_JP2 = 0x380F; + public static final int FORMAT_JPX = 0x3810; + public static final int FORMAT_UNDEFINED_FIRMWARE = 0xB802; + public static final int FORMAT_WINDOWS_IMAGE_FORMAT = 0xB881; + public static final int FORMAT_UNDEFINED_AUDIO = 0xB900; + public static final int FORMAT_WMA = 0xB901; + public static final int FORMAT_OGG = 0xB902; + public static final int FORMAT_AAC = 0xB903; + public static final int FORMAT_AUDIBLE = 0xB904; + public static final int FORMAT_FLAC = 0xB906; + public static final int FORMAT_UNDEFINED_VIDEO = 0xB980; + public static final int FORMAT_WMV = 0xB981; + public static final int FORMAT_MP4_CONTAINER = 0xB982; + public static final int FORMAT_MP2 = 0xB983; + public static final int FORMAT_3GP_CONTAINER = 0xB984; + public static final int FORMAT_UNDEFINED_COLLECTION = 0xBA00; + public static final int FORMAT_ABSTRACT_MULTIMEDIA_ALBUM = 0xBA01; + public static final int FORMAT_ABSTRACT_IMAGE_ALBUM = 0xBA02; + public static final int FORMAT_ABSTRACT_AUDIO_ALBUM = 0xBA03; + public static final int FORMAT_ABSTRACT_VIDEO_ALBUM = 0xBA04; + public static final int FORMAT_ABSTRACT_AV_PLAYLIST = 0xBA05; + public static final int FORMAT_ABSTRACT_CONTACT_GROUP = 0xBA06; + public static final int FORMAT_ABSTRACT_MESSAGE_FOLDER = 0xBA07; + public static final int FORMAT_ABSTRACT_CHAPTERED_PRODUCTION = 0xBA08; + public static final int FORMAT_ABSTRACT_AUDIO_PLAYLIST = 0xBA09; + public static final int FORMAT_ABSTRACT_VIDEO_PLAYLIST = 0xBA0A; + public static final int FORMAT_ABSTRACT_MEDIACAST = 0xBA0B; + public static final int FORMAT_WPL_PLAYLIST = 0xBA10; + public static final int FORMAT_M3U_PLAYLIST = 0xBA11; + public static final int FORMAT_MPL_PLAYLIST = 0xBA12; + public static final int FORMAT_ASX_PLAYLIST = 0xBA13; + public static final int FORMAT_PLS_PLAYLIST = 0xBA14; + public static final int FORMAT_UNDEFINED_DOCUMENT = 0xBA80; + public static final int FORMAT_ABSTRACT_DOCUMENT = 0xBA81; + public static final int FORMAT_XML_DOCUMENT = 0xBA82; + public static final int FORMAT_MS_WORD_DOCUMENT = 0xBA83; + public static final int FORMAT_MHT_COMPILED_HTML_DOCUMENT = 0xBA84; + public static final int FORMAT_MS_EXCEL_SPREADSHEET = 0xBA85; + public static final int FORMAT_MS_POWERPOINT_PRESENTATION = 0xBA86; + public static final int FORMAT_UNDEFINED_MESSAGE = 0xBB00; + public static final int FORMAT_ABSTRACT_MESSSAGE = 0xBB01; + public static final int FORMAT_UNDEFINED_CONTACT = 0xBB80; + public static final int FORMAT_ABSTRACT_CONTACT = 0xBB81; + public static final int FORMAT_VCARD_2 = 0xBB82; + + /** + * Object is not protected. It may be modified and deleted, and its properties + * may be modified. + */ + public static final int PROTECTION_STATUS_NONE = 0; + + /** + * Object can not be modified or deleted and its properties can not be modified. + */ + public static final int PROTECTION_STATUS_READ_ONLY = 0x8001; + + /** + * Object can not be modified or deleted but its properties are modifiable. + */ + public static final int PROTECTION_STATUS_READ_ONLY_DATA = 0x8002; + + /** + * Object's contents can not be transfered from the device, but the object + * may be moved or deleted and its properties may be modified. + */ + public static final int PROTECTION_STATUS_NON_TRANSFERABLE_DATA = 0x8003; + + public static final int ASSOCIATION_TYPE_GENERIC_FOLDER = 0x0001; + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ea4738f..4ec5363 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -16,14 +16,11 @@ package android.provider; -import com.google.android.collect.Maps; -import org.apache.commons.codec.binary.Base64; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; -import android.content.ContentQueryMap; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -38,19 +35,14 @@ import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.*; -import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.AndroidException; import android.util.Config; import android.util.Log; import java.net.URISyntaxException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; /** @@ -1009,7 +1001,7 @@ public final class Settings { public static boolean hasInterestingConfigurationChanges(int changes) { return (changes&ActivityInfo.CONFIG_FONT_SCALE) != 0; } - + public static boolean getShowGTalkServiceStatus(ContentResolver cr) { return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0; } @@ -1216,7 +1208,7 @@ public final class Settings { public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; /** - * @deprecated Use + * @deprecated Use * {@link android.provider.Settings.Secure#LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED} * instead */ @@ -2290,6 +2282,14 @@ public final class Settings { } /** + * Get the key that retrieves a bluetooth Input Device's priority. + * @hide + */ + public static final String getBluetoothInputDevicePriorityKey(String address) { + return ("bluetooth_input_device_priority_" + address.toUpperCase()); + } + + /** * Whether or not data roaming is enabled. (0 = false, 1 = true) */ public static final String DATA_ROAMING = "data_roaming"; @@ -2416,6 +2416,14 @@ public final class Settings { public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; /** + * A positive value indicates the frequency of SamplingProfiler + * taking snapshots in hertz. Zero value means SamplingProfiler is disabled. + * + * @hide + */ + public static final String SAMPLING_PROFILER_HZ = "sampling_profiler_hz"; + + /** * Settings classname to launch when Settings is clicked from All * Applications. Needed because of user testing between the old * and new Settings apps. @@ -3573,20 +3581,8 @@ public final class Settings { // If a shortcut is supplied, and it is already defined for // another bookmark, then remove the old definition. if (shortcut != 0) { - Cursor c = cr.query(CONTENT_URI, - sShortcutProjection, sShortcutSelection, - new String[] { String.valueOf((int) shortcut) }, null); - try { - if (c.moveToFirst()) { - while (c.getCount() > 0) { - if (!c.deleteRow()) { - Log.w(TAG, "Could not delete existing shortcut row"); - } - } - } - } finally { - if (c != null) c.close(); - } + cr.delete(CONTENT_URI, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }); } ContentValues values = new ContentValues(); diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index 35a582d..9b7a73d 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -20,6 +20,7 @@ import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothInputDevice; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.Intent; @@ -427,6 +428,20 @@ class BluetoothEventLoop { } } + private void onInputDevicePropertyChanged(String path, String[] propValues) { + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) { + Log.e(TAG, "onInputDevicePropertyChanged: Address of the remote device in null"); + return; + } + log(" Input Device : Name of Property is:" + propValues[0]); + boolean state = false; + if (propValues[1].equals("true")) { + state = true; + } + mBluetoothService.handleInputDevicePropertyChange(address, state); + } + private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) { String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { @@ -573,6 +588,8 @@ class BluetoothEventLoop { } private boolean onAgentAuthorize(String objectPath, String deviceUuid) { + if (!mBluetoothService.isEnabled()) return false; + String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); @@ -581,15 +598,15 @@ class BluetoothEventLoop { boolean authorized = false; ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); - BluetoothA2dp a2dp = new BluetoothA2dp(mContext); + BluetoothDevice device = mAdapter.getRemoteDevice(address); // Bluez sends the UUID of the local service being accessed, _not_ the // remote service - if (mBluetoothService.isEnabled() && - (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) - || BluetoothUuid.isAdvAudioDist(uuid)) && - !isOtherSinkInNonDisconnectingState(address)) { - BluetoothDevice device = mAdapter.getRemoteDevice(address); + if ((BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) + || BluetoothUuid.isAdvAudioDist(uuid)) && + !isOtherSinkInNonDisconnectingState(address)) { + BluetoothA2dp a2dp = new BluetoothA2dp(mContext); + authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; if (authorized) { Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); @@ -597,6 +614,15 @@ class BluetoothEventLoop { } else { Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); } + } else if (BluetoothUuid.isInputDevice(uuid) && !isOtherInputDeviceConnected(address)) { + BluetoothInputDevice inputDevice = new BluetoothInputDevice(mContext); + authorized = inputDevice.getInputDevicePriority(device) > + BluetoothInputDevice.PRIORITY_OFF; + if (authorized) { + Log.i(TAG, "Allowing incoming HID connection from " + address); + } else { + Log.i(TAG, "Rejecting incoming HID connection from " + address); + } } else { Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } @@ -604,7 +630,19 @@ class BluetoothEventLoop { return authorized; } - boolean isOtherSinkInNonDisconnectingState(String address) { + private boolean isOtherInputDeviceConnected(String address) { + Set<BluetoothDevice> devices = + mBluetoothService.lookupInputDevicesMatchingStates(new int[] { + BluetoothInputDevice.STATE_CONNECTING, + BluetoothInputDevice.STATE_CONNECTED}); + + for (BluetoothDevice device : devices) { + if (!device.getAddress().equals(address)) return true; + } + return false; + } + + private boolean isOtherSinkInNonDisconnectingState(String address) { BluetoothA2dp a2dp = new BluetoothA2dp(mContext); Set<BluetoothDevice> devices = a2dp.getNonDisconnectedSinks(); if (devices.size() == 0) return false; diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index 31e5a7b..ec99b0d 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -24,16 +24,19 @@ package android.server; +import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothDeviceProfileState; import android.bluetooth.BluetoothProfileState; +import android.bluetooth.BluetoothInputDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothCallback; +import android.bluetooth.IBluetoothHeadset; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -70,8 +73,10 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.Set; public class BluetoothService extends IBluetooth.Stub { private static final String TAG = "BluetoothService"; @@ -129,6 +134,8 @@ public class BluetoothService extends IBluetooth.Stub { private final BluetoothProfileState mHfpProfileState; private BluetoothA2dpService mA2dpService; + private final HashMap<BluetoothDevice, Integer> mInputDevices; + private static String mDockAddress; private String mDockPin; @@ -198,6 +205,7 @@ public class BluetoothService extends IBluetooth.Stub { filter.addAction(Intent.ACTION_DOCK_EVENT); mContext.registerReceiver(mReceiver, filter); + mInputDevices = new HashMap<BluetoothDevice, Integer>(); } public static synchronized String readDockBluetoothAddress() { @@ -1220,6 +1228,127 @@ public class BluetoothService extends IBluetooth.Stub { return sp.contains(SHARED_PREFERENCE_DOCK_ADDRESS + address); } + public synchronized boolean connectInputDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (objectPath == null || getConnectedInputDevices().length != 0 || + getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_OFF) { + return false; + } + if(connectInputDeviceNative(objectPath)) { + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING); + return true; + } + return false; + } + + public synchronized boolean disconnectInputDevice(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + String objectPath = getObjectPathFromAddress(device.getAddress()); + if (objectPath == null || getConnectedInputDevices().length == 0) { + return false; + } + if(disconnectInputDeviceNative(objectPath)) { + handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING); + return true; + } + return false; + } + + public synchronized int getInputDeviceState(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + + if (mInputDevices.get(device) == null) { + return BluetoothInputDevice.STATE_DISCONNECTED; + } + return mInputDevices.get(device); + } + + public synchronized BluetoothDevice[] getConnectedInputDevices() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + Set<BluetoothDevice> devices = lookupInputDevicesMatchingStates( + new int[] {BluetoothInputDevice.STATE_CONNECTED}); + return devices.toArray(new BluetoothDevice[devices.size()]); + } + + public synchronized int getInputDevicePriority(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), + BluetoothInputDevice.PRIORITY_UNDEFINED); + } + + public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { + return false; + } + return Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), + priority); + } + + /*package*/synchronized Set<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { + Set<BluetoothDevice> inputDevices = new HashSet<BluetoothDevice>(); + if (mInputDevices.isEmpty()) { + return inputDevices; + } + for (BluetoothDevice device: mInputDevices.keySet()) { + int inputDeviceState = getInputDeviceState(device); + for (int state : states) { + if (state == inputDeviceState) { + inputDevices.add(device); + break; + } + } + } + return inputDevices; + } + + private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) { + int prevState; + if (mInputDevices.get(device) == null) { + prevState = BluetoothInputDevice.STATE_DISCONNECTED; + } else { + prevState = mInputDevices.get(device); + } + if (prevState == state) return; + + mInputDevices.put(device, state); + + if (getInputDevicePriority(device) > + BluetoothInputDevice.PRIORITY_OFF && + state == BluetoothInputDevice.STATE_CONNECTING || + state == BluetoothInputDevice.STATE_CONNECTED) { + // We have connected or attempting to connect. + // Bump priority + setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_AUTO_CONNECT); + } + + Intent intent = new Intent(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothInputDevice.EXTRA_PREVIOUS_INPUT_DEVICE_STATE, prevState); + intent.putExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (DBG) log("InputDevice state : device: " + device + " State:" + prevState + "->" + state); + + } + + /*package*/ void handleInputDevicePropertyChange(String path, boolean connected) { + String address = getAddressFromObjectPath(path); + if (address == null) return; + int state = connected ? BluetoothInputDevice.STATE_CONNECTED : + BluetoothInputDevice.STATE_DISCONNECTED; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + handleInputDeviceStateChange(device, state); + } + /*package*/ boolean isRemoteDeviceInCache(String address) { return (mDeviceProperties.get(address) != null); } @@ -2103,4 +2232,6 @@ public class BluetoothService extends IBluetooth.Stub { short channel); private native boolean removeServiceRecordNative(int handle); private native boolean setLinkTimeoutNative(String path, int num_slots); + private native boolean connectInputDeviceNative(String path); + private native boolean disconnectInputDeviceNative(String path); } diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java index e4f934e..eacd40d 100644 --- a/core/java/android/text/AndroidBidi.java +++ b/core/java/android/text/AndroidBidi.java @@ -16,6 +16,8 @@ package android.text; +import android.text.Layout.Directions; + /** * Access the ICU bidi implementation. * @hide @@ -44,5 +46,132 @@ package android.text; return result; } + /** + * Returns run direction information for a line within a paragraph. + * + * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or + * Layout.DIR_RIGHT_TO_LEFT + * @param levels levels as returned from {@link #bidi} + * @param lstart start of the line in the levels array + * @param chars the character array (used to determine whitespace) + * @param cstart the start of the line in the chars array + * @param len the length of the line + * @return the directions + */ + public static Directions directions(int dir, byte[] levels, int lstart, + char[] chars, int cstart, int len) { + + int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1; + int curLevel = levels[lstart]; + int minLevel = curLevel; + int runCount = 1; + for (int i = lstart + 1, e = lstart + len; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + ++runCount; + } + } + + // add final run for trailing counter-directional whitespace + int visLen = len; + if ((curLevel & 1) != (baseLevel & 1)) { + // look for visible end + while (--visLen >= 0) { + char ch = chars[cstart + visLen]; + + if (ch == '\n') { + --visLen; + break; + } + + if (ch != ' ' && ch != '\t') { + break; + } + } + ++visLen; + if (visLen != len) { + ++runCount; + } + } + + if (runCount == 1 && minLevel == baseLevel) { + // we're done, only one run on this line + if ((minLevel & 1) != 0) { + return Layout.DIRS_ALL_RIGHT_TO_LEFT; + } + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + int[] ld = new int[runCount * 2]; + int maxLevel = minLevel; + int levelBits = minLevel << Layout.RUN_LEVEL_SHIFT; + { + // Start of first pair is always 0, we write + // length then start at each new run, and the + // last run length after we're done. + int n = 1; + int prev = lstart; + curLevel = minLevel; + for (int i = lstart, e = lstart + visLen; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + if (level > maxLevel) { + maxLevel = level; + } else if (level < minLevel) { + minLevel = level; + } + // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT + ld[n++] = (i - prev) | levelBits; + ld[n++] = i - lstart; + levelBits = curLevel << Layout.RUN_LEVEL_SHIFT; + prev = i; + } + } + ld[n] = (lstart + visLen - prev) | levelBits; + if (visLen < len) { + ld[++n] = visLen; + ld[++n] = (len - visLen) | (baseLevel << Layout.RUN_LEVEL_SHIFT); + } + } + + // See if we need to swap any runs. + // If the min level run direction doesn't match the base + // direction, we always need to swap (at this point + // we have more than one run). + // Otherwise, we don't need to swap the lowest level. + // Since there are no logically adjacent runs at the same + // level, if the max level is the same as the (new) min + // level, we have a series of alternating levels that + // is already in order, so there's no more to do. + // + boolean swap; + if ((minLevel & 1) == baseLevel) { + minLevel += 1; + swap = maxLevel > minLevel; + } else { + swap = runCount > 1; + } + if (swap) { + for (int level = maxLevel - 1; level >= minLevel; --level) { + for (int i = 0; i < ld.length; i += 2) { + if (levels[ld[i]] >= level) { + int e = i + 2; + while (e < ld.length && levels[ld[e]] >= level) { + e += 2; + } + for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) { + int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x; + x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x; + } + i = e + 2; + } + } + } + } + return new Directions(ld); + } + private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo); }
\ No newline at end of file diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 944f735..9309b05 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * width because the width that was passed in was for the * full text, not the ellipsized form. */ - synchronized (sTemp) { - mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - source, 0, source.length(), - null))); - } + TextLine line = TextLine.obtain(); + line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + mMax = (int) FloatMath.ceil(line.metrics(null)); + TextLine.recycle(line); } if (includepad) { @@ -276,14 +276,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (fm == null) { fm = new Metrics(); } - - int wid; - synchronized (sTemp) { - wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - text, 0, text.length(), fm))); - } - fm.width = wid; + TextLine line = TextLine.obtain(); + line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + fm.width = (int) FloatMath.ceil(line.metrics(fm)); + TextLine.recycle(line); + return fm; } else { return null; @@ -389,7 +388,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public static class Metrics extends Paint.FontMetricsInt { public int width; - + @Override public String toString() { return super.toString() + " width=" + width; } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 14e5655..b6aa03a 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -310,7 +310,6 @@ extends Layout Directions[] objects = new Directions[1]; - for (int i = 0; i < n; i++) { ints[START] = reflowed.getLineStart(i) | (reflowed.getParagraphDirection(i) << DIR_SHIFT) | diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index c3bd0ae..d426d12 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -34,13 +34,33 @@ extends CharSequence float x, float y, Paint p); /** + * Just like {@link Canvas#drawTextRun}. + * {@hide} + */ + void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, + float x, float y, int flags, Paint p); + + /** * Just like {@link Paint#measureText}. */ float measureText(int start, int end, Paint p); - /** * Just like {@link Paint#getTextWidths}. */ public int getTextWidths(int start, int end, float[] widths, Paint p); + + /** + * Just like {@link Paint#getTextRunAdvances}. + * @hide + */ + float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex, Paint paint); + + /** + * Just like {@link Paint#getTextRunCursor}. + * @hide + */ + int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, + int cursorOpt, Paint p); } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 38ac9b7..f533944 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,29 +16,33 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.emoji.EmojiFactory; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Path; -import com.android.internal.util.ArrayUtils; - -import junit.framework.Assert; -import android.text.style.*; +import android.graphics.Rect; import android.text.method.TextKeyListener; +import android.text.style.AlignmentSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineBackgroundSpan; +import android.text.style.ParagraphStyle; +import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.view.KeyEvent; +import java.util.Arrays; + /** - * A base class that manages text layout in visual elements on - * the screen. - * <p>For text that will be edited, use a {@link DynamicLayout}, - * which will be updated as the text changes. + * A base class that manages text layout in visual elements on + * the screen. + * <p>For text that will be edited, use a {@link DynamicLayout}, + * which will be updated as the text changes. * For text that will not change, use a {@link StaticLayout}. */ public abstract class Layout { - private static final boolean DEBUG = false; private static final ParagraphStyle[] NO_PARA_SPANS = ArrayUtils.emptyArray(ParagraphStyle.class); @@ -54,9 +58,7 @@ public abstract class Layout { MIN_EMOJI = -1; MAX_EMOJI = -1; } - }; - - private RectF mEmojiRect; + } /** * Return how wide a layout must be in order to display the @@ -66,7 +68,7 @@ public abstract class Layout { TextPaint paint) { return getDesiredWidth(source, 0, source.length(), paint); } - + /** * Return how wide a layout must be in order to display the * specified text slice with one line per paragraph. @@ -85,8 +87,7 @@ public abstract class Layout { next = end; // note, omits trailing paragraph char - float w = measureText(paint, workPaint, - source, i, next, null, true, null); + float w = measurePara(paint, workPaint, source, i, next); if (w > need) need = w; @@ -116,6 +117,15 @@ public abstract class Layout { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); + // Ensure paint doesn't have baselineShift set. + // While normally we don't modify the paint the user passed in, + // we were already doing this in Styled.drawUniformRun with both + // baselineShift and bgColor. We probably should reevaluate bgColor. + if (paint != null) { + paint.bgColor = 0; + paint.baselineShift = 0; + } + mText = text; mPaint = paint; mWorkPaint = new TextPaint(); @@ -175,7 +185,6 @@ public abstract class Layout { dbottom = sTempRect.bottom; } - int top = 0; int bottom = getLineTop(getLineCount()); @@ -185,26 +194,28 @@ public abstract class Layout { if (dbottom < bottom) { bottom = dbottom; } - - int first = getLineForVertical(top); + + int first = getLineForVertical(top); int last = getLineForVertical(bottom); - + int previousLineBottom = getLineTop(first); int previousLineEnd = getLineStart(first); - + TextPaint paint = mPaint; CharSequence buf = mText; int width = mWidth; boolean spannedText = mSpannedText; ParagraphStyle[] spans = NO_PARA_SPANS; - int spanend = 0; + int spanEnd = 0; int textLength = 0; // First, draw LineBackgroundSpans. - // LineBackgroundSpans know nothing about the alignment or direction of - // the layout or line. XXX: Should they? + // LineBackgroundSpans know nothing about the alignment, margins, or + // direction of the layout or line. XXX: Should they? + // They are evaluated at each line. if (spannedText) { + Spanned sp = (Spanned) buf; textLength = buf.length(); for (int i = first; i <= last; i++) { int start = previousLineEnd; @@ -216,12 +227,14 @@ public abstract class Layout { previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); - if (start >= spanend) { - Spanned sp = (Spanned) buf; - spanend = sp.nextSpanTransition(start, textLength, - LineBackgroundSpan.class); - spans = sp.getSpans(start, spanend, - LineBackgroundSpan.class); + if (start >= spanEnd) { + // These should be infrequent, so we'll use this so that + // we don't have to check as often. + spanEnd = sp.nextSpanTransition(start, textLength, + LineBackgroundSpan.class); + // All LineBackgroundSpans on a line contribute to its + // background. + spans = sp.getSpans(start, end, LineBackgroundSpan.class); } for (int n = 0; n < spans.length; n++) { @@ -234,11 +247,11 @@ public abstract class Layout { } } // reset to their original values - spanend = 0; + spanEnd = 0; previousLineBottom = getLineTop(first); previousLineEnd = getLineStart(first); spans = NO_PARA_SPANS; - } + } // There can be a highlight even without spans if we are drawing // a non-spanned transformation of a spanned editing buffer. @@ -255,7 +268,11 @@ public abstract class Layout { } Alignment align = mAlignment; - + TabStops tabStops = null; + boolean tabStopsIsInitialized = false; + + TextLine tl = TextLine.obtain(); + // Next draw the lines, one at a time. // the baseline is the top of the following line minus the current // line's descent. @@ -270,19 +287,30 @@ public abstract class Layout { previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); - boolean isFirstParaLine = false; - if (spannedText) { - if (start == 0 || buf.charAt(start - 1) == '\n') { - isFirstParaLine = true; - } - // New batch of paragraph styles, compute the alignment. - // Last alignment style wins. - if (start >= spanend) { - Spanned sp = (Spanned) buf; - spanend = sp.nextSpanTransition(start, textLength, + int dir = getParagraphDirection(i); + int left = 0; + int right = mWidth; + + if (spannedText) { + Spanned sp = (Spanned) buf; + boolean isFirstParaLine = (start == 0 || + buf.charAt(start - 1) == '\n'); + + // New batch of paragraph styles, collect into spans array. + // Compute the alignment, last alignment style wins. + // Reset tabStops, we'll rebuild if we encounter a line with + // tabs. + // We expect paragraph spans to be relatively infrequent, use + // spanEnd so that we can check less frequently. Since + // paragraph styles ought to apply to entire paragraphs, we can + // just collect the ones present at the start of the paragraph. + // If spanEnd is before the end of the paragraph, that's not + // our problem. + if (start >= spanEnd && (i == first || isFirstParaLine)) { + spanEnd = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); - spans = sp.getSpans(start, spanend, ParagraphStyle.class); - + spans = sp.getSpans(start, spanEnd, ParagraphStyle.class); + align = mAlignment; for (int n = spans.length-1; n >= 0; n--) { if (spans[n] instanceof AlignmentSpan) { @@ -290,45 +318,49 @@ public abstract class Layout { break; } } + + tabStopsIsInitialized = false; } - } - - int dir = getParagraphDirection(i); - int left = 0; - int right = mWidth; - // Draw all leading margin spans. Adjust left or right according - // to the paragraph direction of the line. - if (spannedText) { + // Draw all leading margin spans. Adjust left or right according + // to the paragraph direction of the line. final int length = spans.length; for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan) { LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; + boolean useFirstLineMargin = isFirstParaLine; + if (margin instanceof LeadingMarginSpan2) { + int count = ((LeadingMarginSpan2) margin).getLeadingMarginLineCount(); + int startLine = getLineForOffset(sp.getSpanStart(margin)); + useFirstLineMargin = i < startLine + count; + } if (dir == DIR_RIGHT_TO_LEFT) { margin.drawLeadingMargin(c, paint, right, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); - - right -= margin.getLeadingMargin(isFirstParaLine); + right -= margin.getLeadingMargin(useFirstLineMargin); } else { margin.drawLeadingMargin(c, paint, left, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); - - boolean useMargin = isFirstParaLine; - if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) { - int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount(); - useMargin = count > i; - } - left += margin.getLeadingMargin(useMargin); + left += margin.getLeadingMargin(useFirstLineMargin); } } } } - // Adjust the point at which to start rendering depending on the - // alignment of the paragraph. + boolean hasTabOrEmoji = getLineContainsTab(i); + // Can't tell if we have tabs for sure, currently + if (hasTabOrEmoji && !tabStopsIsInitialized) { + if (tabStops == null) { + tabStops = new TabStops(TAB_INCREMENT, spans); + } else { + tabStops.reset(TAB_INCREMENT, spans); + } + tabStopsIsInitialized = true; + } + int x; if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_LEFT_TO_RIGHT) { @@ -337,41 +369,80 @@ public abstract class Layout { x = right; } } else { - int max = (int)getLineMax(i, spans, false); + int max = (int)getLineExtent(i, tabStops, false); if (align == Alignment.ALIGN_OPPOSITE) { - if (dir == DIR_RIGHT_TO_LEFT) { - x = left + max; - } else { + if (dir == DIR_LEFT_TO_RIGHT) { x = right - max; - } - } else { - // Alignment.ALIGN_CENTER - max = max & ~1; - int half = (right - left - max) >> 1; - if (dir == DIR_RIGHT_TO_LEFT) { - x = right - half; } else { - x = left + half; + x = left - max; } + } else { // Alignment.ALIGN_CENTER + max = max & ~1; + x = (right + left - max) >> 1; } } Directions directions = getLineDirections(i); - boolean hasTab = getLineContainsTab(i); if (directions == DIRS_ALL_LEFT_TO_RIGHT && - !spannedText && !hasTab) { - if (DEBUG) { - Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT); - Assert.assertNotNull(c); - } + !spannedText && !hasTabOrEmoji) { // XXX: assumes there's nothing additional to be done c.drawText(buf, start, end, x, lbaseline, paint); } else { - drawText(c, buf, start, end, dir, directions, - x, ltop, lbaseline, lbottom, paint, mWorkPaint, - hasTab, spans); + tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); + tl.draw(c, x, ltop, lbaseline, lbottom); + } + } + + TextLine.recycle(tl); + } + + /** + * Return the start position of the line, given the left and right bounds + * of the margins. + * + * @param line the line index + * @param left the left bounds (0, or leading margin if ltr para) + * @param right the right bounds (width, minus leading margin if rtl para) + * @return the start position of the line (to right of line if rtl para) + */ + private int getLineStartPos(int line, int left, int right) { + // Adjust the point at which to start rendering depending on the + // alignment of the paragraph. + Alignment align = getParagraphAlignment(line); + int dir = getParagraphDirection(line); + + int x; + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = left; + } else { + x = right; + } + } else { + TabStops tabStops = null; + if (mSpannedText && getLineContainsTab(line)) { + Spanned spanned = (Spanned) mText; + int start = getLineStart(line); + int spanEnd = spanned.nextSpanTransition(start, spanned.length(), + TabStopSpan.class); + TabStopSpan[] tabSpans = spanned.getSpans(start, spanEnd, TabStopSpan.class); + if (tabSpans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabSpans); + } + } + int max = (int)getLineExtent(line, tabStops, false); + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = right - max; + } else { + x = left - max; + } + } else { // Alignment.ALIGN_CENTER + max = max & ~1; + x = (left + right - max) >> 1; } } + return x; } /** @@ -417,7 +488,7 @@ public abstract class Layout { mWidth = wid; } - + /** * Return the total height of this layout. */ @@ -450,7 +521,7 @@ public abstract class Layout { * Return the number of lines of text in this layout. */ public abstract int getLineCount(); - + /** * Return the baseline for the specified line (0…getLineCount() - 1) * If bounds is not null, return the top, left, right, bottom extents @@ -524,13 +595,95 @@ public abstract class Layout { */ public abstract int getBottomPadding(); + + /** + * Returns true if the character at offset and the preceding character + * are at different run levels (and thus there's a split caret). + * @param offset the offset + * @return true if at a level boundary + */ + private boolean isLevelBoundary(int offset) { + int line = getLineForOffset(offset); + Directions dirs = getLineDirections(line); + if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { + return false; + } + + int[] runs = dirs.mDirections; + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + if (offset == lineStart || offset == lineEnd) { + int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; + int runIndex = offset == lineStart ? 0 : runs.length - 2; + return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; + } + + offset -= lineStart; + for (int i = 0; i < runs.length; i += 2) { + if (offset == runs[i]) { + return true; + } + } + return false; + } + + private boolean primaryIsTrailingPrevious(int offset) { + int line = getLineForOffset(offset); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int[] runs = getLineDirections(line).mDirections; + + int levelAt = -1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + if (offset > start) { + // Previous character is at same level, so don't use trailing. + return false; + } + levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + if (levelAt == -1) { + // Offset was limit of line. + levelAt = getParagraphDirection(line) == 1 ? 0 : 1; + } + + // At level boundary, check previous level. + int levelBefore = -1; + if (offset == lineStart) { + levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; + } else { + offset -= 1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + } + + return levelBefore < levelAt; + } + /** * Get the primary horizontal position for the specified text offset. * This is the location where a new character would be inserted in * the paragraph's primary direction. */ public float getPrimaryHorizontal(int offset) { - return getHorizontal(offset, false, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, trailing); } /** @@ -539,66 +692,42 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. */ public float getSecondaryHorizontal(int offset) { - return getHorizontal(offset, true, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, !trailing); } - private float getHorizontal(int offset, boolean trailing, boolean alt) { + private float getHorizontal(int offset, boolean trailing) { int line = getLineForOffset(offset); - return getHorizontal(offset, trailing, alt, line); + return getHorizontal(offset, trailing, line); } - private float getHorizontal(int offset, boolean trailing, boolean alt, - int line) { + private float getHorizontal(int offset, boolean trailing, int line) { int start = getLineStart(line); - int end = getLineVisibleEnd(line); + int end = getLineEnd(line); int dir = getParagraphDirection(line); - boolean tab = getLineContainsTab(line); + boolean hasTabOrEmoji = getLineContainsTab(line); Directions directions = getLineDirections(line); - TabStopSpan[] tabs = null; - if (tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + TabStops tabStops = null; + if (hasTabOrEmoji && mText instanceof Spanned) { + // Just checking this line should be good enough, tabs should be + // consistent across all lines in a paragraph. + TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + if (tabs.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse + } } - float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, - dir, directions, trailing, alt, tab, tabs); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops); + float wid = tl.measure(offset - start, trailing, null); + TextLine.recycle(tl); - if (offset > end) { - if (dir == DIR_RIGHT_TO_LEFT) - wid -= measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - else - wid += measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - } - - Alignment align = getParagraphAlignment(line); int left = getParagraphLeft(line); int right = getParagraphRight(line); - if (align == Alignment.ALIGN_NORMAL) { - if (dir == DIR_RIGHT_TO_LEFT) - return right + wid; - else - return left + wid; - } - - float max = getLineMax(line); - - if (align == Alignment.ALIGN_OPPOSITE) { - if (dir == DIR_RIGHT_TO_LEFT) - return left + max + wid; - else - return right - max + wid; - } else { /* align == Alignment.ALIGN_CENTER */ - int imax = ((int) max) & ~1; - - if (dir == DIR_RIGHT_TO_LEFT) - return right - (((right - left) - imax) / 2) + wid; - else - return left + ((right - left) - imax) / 2 + wid; - } + return getLineStartPos(line, left, right) + wid; } /** @@ -656,38 +785,76 @@ public abstract class Layout { } /** - * Gets the horizontal extent of the specified line, excluding - * trailing whitespace. + * Gets the unsigned horizontal extent of the specified line, including + * leading margin indent, but excluding trailing whitespace. */ public float getLineMax(int line) { - return getLineMax(line, null, false); + float margin = getParagraphLeadingMargin(line); + float signedExtent = getLineExtent(line, false); + return margin + signedExtent >= 0 ? signedExtent : -signedExtent; } /** - * Gets the horizontal extent of the specified line, including - * trailing whitespace. + * Gets the unsigned horizontal extent of the specified line, including + * leading margin indent and trailing whitespace. */ public float getLineWidth(int line) { - return getLineMax(line, null, true); + float margin = getParagraphLeadingMargin(line); + float signedExtent = getLineExtent(line, true); + return margin + signedExtent >= 0 ? signedExtent : -signedExtent; } - private float getLineMax(int line, Object[] tabs, boolean full) { + /** + * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the + * tab stops instead of using the ones passed in. + * @param line the index of the line + * @param full whether to include trailing whitespace + * @return the extent of the line + */ + private float getLineExtent(int line, boolean full) { int start = getLineStart(line); - int end; + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + + boolean hasTabsOrEmoji = getLineContainsTab(line); + TabStops tabStops = null; + if (hasTabsOrEmoji && mText instanceof Spanned) { + // Just checking this line should be good enough, tabs should be + // consistent across all lines in a paragraph. + TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + if (tabs.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse + } + } + Directions directions = getLineDirections(line); + int dir = getParagraphDirection(line); - if (full) { - end = getLineEnd(line); - } else { - end = getLineVisibleEnd(line); - } - boolean tab = getLineContainsTab(line); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; + } - if (tabs == null && tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); - } + /** + * Returns the signed horizontal extent of the specified line, excluding + * leading margin. If full is false, excludes trailing whitespace. + * @param line the index of the line + * @param tabStops the tab stops, can be null if we know they're not used. + * @param full whether to include trailing whitespace + * @return the extent of the text on this line + */ + private float getLineExtent(int line, TabStops tabStops, boolean full) { + int start = getLineStart(line); + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + boolean hasTabsOrEmoji = getLineContainsTab(line); + Directions directions = getLineDirections(line); + int dir = getParagraphDirection(line); - return measureText(mPaint, mWorkPaint, - mText, start, end, null, tab, tabs); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; } /** @@ -738,7 +905,7 @@ public abstract class Layout { } /** - * Get the character offset on the specfied line whose position is + * Get the character offset on the specified line whose position is * closest to the specified horizontal position. */ public int getOffsetForHorizontal(int line, float horiz) { @@ -752,14 +919,13 @@ public abstract class Layout { int best = min; float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); - int here = min; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - int swap = ((i & 1) == 0) ? 1 : -1; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = min + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; if (there > max) there = max; - int high = there - 1 + 1, low = here + 1 - 1, guess; while (high - low > 1) { @@ -792,7 +958,7 @@ public abstract class Layout { if (dist < bestdist) { bestdist = dist; - best = low; + best = low; } } @@ -802,8 +968,6 @@ public abstract class Layout { bestdist = dist; best = here; } - - here = there; } float dist = Math.abs(getPrimaryHorizontal(max) - horiz); @@ -823,19 +987,15 @@ public abstract class Layout { return getLineStart(line + 1); } - /** + /** * Return the text offset after the last visible character (so whitespace * is not counted) on the specified line. */ public int getLineVisibleEnd(int line) { return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); } - - private int getLineVisibleEnd(int line, int start, int end) { - if (DEBUG) { - Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); - } + private int getLineVisibleEnd(int line, int start, int end) { CharSequence text = mText; char ch; if (line == getLineCount() - 1) { @@ -882,207 +1042,62 @@ public abstract class Layout { return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); } - /** - * Return the text offset that would be reached by moving left - * (possibly onto another line) from the specified offset. - */ public int getOffsetToLeftOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MIN_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h < horiz && h > besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h < horiz && h > besth) { - best = end; - besth = h; - } - - if (best != offset) - return best; - - int dir = getParagraphDirection(line); - - if (dir > 0) { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, 10000); - } else { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, 10000); - } + return getOffsetToLeftRightOf(offset, true); } - /** - * Return the text offset that would be reached by moving right - * (possibly onto another line) from the specified offset. - */ public int getOffsetToRightOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MAX_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h > horiz && h < besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); + return getOffsetToLeftRightOf(offset, false); + } - if (h > horiz && h < besth) { - best = candidate; - besth = h; + private int getOffsetToLeftRightOf(int caret, boolean toLeft) { + int line = getLineForOffset(caret); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int lineDir = getParagraphDirection(line); + + boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); + if (caret == (advance ? lineEnd : lineStart)) { + // walking off line, so look at the line we're headed to + if (caret == lineStart) { + if (line > 0) { + --line; + } else { + return caret; // at very start, don't move } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; + } else { + if (line < getLineCount() - 1) { + ++line; + } else { + return caret; // at very end, don't move } } - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h > horiz && h < besth) { - best = end; - besth = h; + lineStart = getLineStart(line); + lineEnd = getLineEnd(line); + int newDir = getParagraphDirection(line); + if (newDir != lineDir) { + // unusual case. we want to walk onto the line, but it runs + // in a different direction than this one, so we fake movement + // in the opposite direction. + toLeft = !toLeft; + lineDir = newDir; + } } - if (best != offset) - return best; - - int dir = getParagraphDirection(line); + Directions directions = getLineDirections(line); - if (dir > 0) { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, -10000); - } else { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, -10000); - } + TextLine tl = TextLine.obtain(); + // XXX: we don't care about tabs + tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); + caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); + tl = TextLine.recycle(tl); + return caret; } private int getOffsetAtStartOf(int offset) { + // XXX this probably should skip local reorderings and + // zero-width characters, look at callers if (offset == 0) return 0; @@ -1115,7 +1130,7 @@ public abstract class Layout { /** * Fills in the specified Path with a representation of a cursor * at the specified offset. This will often be a vertical line - * but can be multiple discontinous lines in text with multiple + * but can be multiple discontinuous lines in text with multiple * directionalities. */ public void getCursorPath(int point, Path dest, @@ -1127,7 +1142,8 @@ public abstract class Layout { int bottom = getLineTop(line+1); float h1 = getPrimaryHorizontal(point) - 0.5f; - float h2 = getSecondaryHorizontal(point) - 0.5f; + float h2 = isLevelBoundary(point) ? + getSecondaryHorizontal(point) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, KeyEvent.META_SHIFT_ON) | @@ -1204,9 +1220,10 @@ public abstract class Layout { if (lineend > linestart && mText.charAt(lineend - 1) == '\n') lineend--; - int here = linestart; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = linestart + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + if (there > lineend) there = lineend; @@ -1215,14 +1232,12 @@ public abstract class Layout { int en = Math.min(end, there); if (st != en) { - float h1 = getHorizontal(st, false, false, line); - float h2 = getHorizontal(en, true, false, line); + float h1 = getHorizontal(st, false, line); + float h2 = getHorizontal(en, true, line); dest.addRect(h1, top, h2, bottom, Path.Direction.CW); } } - - here = there; } } @@ -1257,7 +1272,7 @@ public abstract class Layout { addSelection(startline, start, getLineEnd(startline), top, getLineBottom(startline), dest); - + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) dest.addRect(getLineLeft(startline), top, 0, getLineBottom(startline), Path.Direction.CW); @@ -1310,422 +1325,173 @@ public abstract class Layout { * Get the left edge of the specified paragraph, inset by left margins. */ public final int getParagraphLeft(int line) { - int dir = getParagraphDirection(line); - int left = 0; - - boolean par = false; - int off = getLineStart(line); - if (off == 0 || mText.charAt(off - 1) == '\n') - par = true; - - if (dir == DIR_LEFT_TO_RIGHT) { - if (mSpannedText) { - Spanned sp = (Spanned) mText; - LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), - getLineEnd(line), - LeadingMarginSpan.class); - - for (int i = 0; i < spans.length; i++) { - boolean margin = par; - LeadingMarginSpan span = spans[i]; - if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { - int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); - margin = count >= line; - } - left += span.getLeadingMargin(margin); - } - } + int dir = getParagraphDirection(line); + if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { + return left; // leading margin has no impact, or no styles } - - return left; + return getParagraphLeadingMargin(line); } /** * Get the right edge of the specified paragraph, inset by right margins. */ public final int getParagraphRight(int line) { - int dir = getParagraphDirection(line); - int right = mWidth; - - boolean par = false; - int off = getLineStart(line); - if (off == 0 || mText.charAt(off - 1) == '\n') - par = true; - - - if (dir == DIR_RIGHT_TO_LEFT) { - if (mSpannedText) { - Spanned sp = (Spanned) mText; - LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), - getLineEnd(line), - LeadingMarginSpan.class); - - for (int i = 0; i < spans.length; i++) { - right -= spans[i].getLeadingMargin(par); - } - } + int dir = getParagraphDirection(line); + if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { + return right; // leading margin has no impact, or no styles } - - return right; + return right - getParagraphLeadingMargin(line); } - private void drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, Directions directions, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean hasTabs, Object[] parspans) { - char[] buf; - if (!hasTabs) { - if (directions == DIRS_ALL_LEFT_TO_RIGHT) { - if (DEBUG) { - Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); - } - Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); - return; - } - buf = null; - } else { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - if (j == there || buf[j] == '\t') { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (j != there && buf[j] == '\t') - h = dir * nextTab(text, start, end, h * dir, parspans); - - segstart = j + 1; - } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) { - int emoji = Character.codePointAt(buf, j); - - if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { - Bitmap bm = EMOJI_FACTORY. - getBitmapFromAndroidPua(emoji); - - if (bm != null) { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (mEmojiRect == null) { - mEmojiRect = new RectF(); - } + /** + * Returns the effective leading margin (unsigned) for this line, + * taking into account LeadingMarginSpan and LeadingMarginSpan2. + * @param line the line index + * @return the leading margin of this line + */ + private int getParagraphLeadingMargin(int line) { + if (!mSpannedText) { + return 0; + } + Spanned spanned = (Spanned) mText; - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + j, start + j + 1, - null); - - float bitmapHeight = bm.getHeight(); - float textHeight = -workPaint.ascent(); - float scale = textHeight / bitmapHeight; - float width = bm.getWidth() * scale; + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, + LeadingMarginSpan.class); + LeadingMarginSpan[] spans = spanned.getSpans(lineStart, spanEnd, + LeadingMarginSpan.class); + if (spans.length == 0) { + return 0; // no leading margin span; + } - mEmojiRect.set(x + h, y - textHeight, - x + h + width, y); + int margin = 0; - canvas.drawBitmap(bm, null, mEmojiRect, paint); - h += width; + boolean isFirstParaLine = lineStart == 0 || + spanned.charAt(lineStart - 1) == '\n'; - j++; - segstart = j + 1; - } - } - } + for (int i = 0; i < spans.length; i++) { + LeadingMarginSpan span = spans[i]; + boolean useFirstLineMargin = isFirstParaLine; + if (span instanceof LeadingMarginSpan2) { + int spStart = spanned.getSpanStart(span); + int spanLine = getLineForOffset(spStart); + int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount(); + useFirstLineMargin = line < spanLine + count; } - - here = there; + margin += span.getLeadingMargin(useFirstLineMargin); } - if (hasTabs) - TextUtils.recycle(buf); + return margin; } - private static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int offset, int end, - int dir, Directions directions, - boolean trailing, boolean alt, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - if (alt) { - if (dir == DIR_RIGHT_TO_LEFT) - trailing = !trailing; - } - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - if (alt) - trailing = !trailing; - - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && j < there) { - codept = buf[j]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { - codept = Character.codePointAt(buf, j); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (j == there || codept == '\t' || bm != null) { - float segw; - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { - h += Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { - h -= Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - } - - segw = Styled.measureText(paint, workPaint, text, - start + segstart, start + j, - null); - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT) { - h += segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - } - - if (dir == DIR_RIGHT_TO_LEFT) - h -= segw; - else - h += segw; - - if (j != there && buf[j] == '\t') { - if (offset == start + j) - return h; - - h = dir * nextTab(text, start, end, h * dir, tabs); - } - - if (bm != null) { - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - j, j + 2, null); - - float wid = (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= wid; - } else { - h += wid; + /* package */ + static float measurePara(TextPaint paint, TextPaint workPaint, + CharSequence text, int start, int end) { + + MeasuredText mt = MeasuredText.obtain(); + TextLine tl = TextLine.obtain(); + try { + mt.setPara(text, start, end, DIR_REQUEST_LTR); + Directions directions; + int dir; + if (mt.mEasy) { + directions = DIRS_ALL_LEFT_TO_RIGHT; + dir = Layout.DIR_LEFT_TO_RIGHT; + } else { + directions = AndroidBidi.directions(mt.mDir, mt.mLevels, + 0, mt.mChars, 0, mt.mLen); + dir = mt.mDir; + } + char[] chars = mt.mChars; + int len = mt.mLen; + boolean hasTabs = false; + TabStops tabStops = null; + for (int i = 0; i < len; ++i) { + if (chars[i] == '\t') { + hasTabs = true; + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + int spanEnd = spanned.nextSpanTransition(start, end, + TabStopSpan.class); + TabStopSpan[] spans = spanned.getSpans(start, spanEnd, + TabStopSpan.class); + if (spans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, spans); } - - j++; } - - segstart = j + 1; + break; } } - - here = there; + tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); + return tl.metrics(null); + } finally { + TextLine.recycle(tl); + MeasuredText.recycle(mt); } - - if (hasTabs) - TextUtils.recycle(buf); - - return h; } /** - * Measure width of a run of text on a single line that is known to all be - * in the same direction as the paragraph base direction. Returns the width, - * and the line metrics in fm if fm is not null. - * - * @param paint the paint for the text; will not be modified - * @param workPaint paint available for modification - * @param text text - * @param start start of the line - * @param end limit of the line - * @param fm object to return integer metrics in, can be null - * @param hasTabs true if it is known that the line has tabs - * @param tabs tab position information - * @return the width of the text from start to end + * @hide */ - /* package */ static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int end, - Paint.FontMetricsInt fm, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - int len = end - start; - - int lastPos = 0; - float width = 0; - int ascent = 0, descent = 0, top = 0, bottom = 0; - - if (fm != null) { - fm.ascent = 0; - fm.descent = 0; - } - - for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && pos < len) { - codept = buf[pos]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { - codept = Character.codePointAt(buf, pos); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (pos == len || codept == '\t' || bm != null) { - workPaint.baselineShift = 0; - - width += Styled.measureText(paint, workPaint, text, - start + lastPos, start + pos, - fm); - - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; - } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; + /* package */ static class TabStops { + private int[] mStops; + private int mNumStops; + private int mIncrement; + + TabStops(int increment, Object[] spans) { + reset(increment, spans); + } + + void reset(int increment, Object[] spans) { + this.mIncrement = increment; + + int ns = 0; + if (spans != null) { + int[] stops = this.mStops; + for (Object o : spans) { + if (o instanceof TabStopSpan) { + if (stops == null) { + stops = new int[10]; + } else if (ns == stops.length) { + int[] nstops = new int[ns * 2]; + for (int i = 0; i < ns; ++i) { + nstops[i] = stops[i]; + } + stops = nstops; + } + stops[ns++] = ((TabStopSpan) o).getTabStop(); } } - - if (pos != len) { - if (bm == null) { - // no emoji, must have hit a tab - width = nextTab(text, start, end, width, tabs); - } else { - // This sets up workPaint with the font on the emoji - // text, so that we can extract the ascent and scale. - - // We can't use the result of the previous call to - // measureText because the emoji might have its own style. - // We have to initialize workPaint here because if the - // text is unstyled measureText might not use workPaint - // at all. - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + pos, start + pos + 1, null); - - width += (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - // Since we had an emoji, we bump past the second half - // of the surrogate pair. - pos++; - } + if (ns > 1) { + Arrays.sort(stops, 0, ns); } + if (stops != this.mStops) { + this.mStops = stops; + } + } + this.mNumStops = ns; + } - if (fm != null) { - if (fm.ascent < ascent) { - ascent = fm.ascent; - } - if (fm.descent > descent) { - descent = fm.descent; - } - - if (fm.top < top) { - top = fm.top; - } - if (fm.bottom > bottom) { - bottom = fm.bottom; + float nextTab(float h) { + int ns = this.mNumStops; + if (ns > 0) { + int[] stops = this.mStops; + for (int i = 0; i < ns; ++i) { + int stop = stops[i]; + if (stop > h) { + return stop; } - - // No need to take bitmap height into account here, - // since it is scaled to match the text height. } - - lastPos = pos + 1; } + return nextDefaultStop(h, mIncrement); } - if (fm != null) { - fm.ascent = ascent; - fm.descent = descent; - fm.top = top; - fm.bottom = bottom; + public static float nextDefaultStop(float h, int inc) { + return ((int) ((h + inc) / inc)) * inc; } - - if (hasTabs) - TextUtils.recycle(buf); - - return width; } /** @@ -1804,23 +1570,22 @@ public abstract class Layout { /** * Stores information about bidirectional (left-to-right or right-to-left) - * text within the layout of a line. TODO: This work is not complete - * or correct and will be fleshed out in a later revision. + * text within the layout of a line. */ public static class Directions { - private short[] mDirections; - - // The values in mDirections are the offsets from the first character - // in the line to the next flip in direction. Runs at even indices - // are left-to-right, the others are right-to-left. So, for example, - // a line that starts with a right-to-left run has 0 at mDirections[0], - // since the 'first' (ltr) run is zero length. - // - // The code currently assumes that each run is adjacent to the previous - // one, progressing in the base line direction. This isn't sufficient - // to handle nested runs, for example numeric text in an rtl context - // in an ltr paragraph. - /* package */ Directions(short[] dirs) { + // Directions represents directional runs within a line of text. + // Runs are pairs of ints listed in visual order, starting from the + // leading margin. The first int of each pair is the offset from + // the first character of the line to the start of the run. The + // second int represents both the length and level of the run. + // The length is in the lower bits, accessed by masking with + // DIR_LENGTH_MASK. The level is in the higher bits, accessed + // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. + // To simply test for an RTL direction, test the bit using + // DIR_RTL_FLAG, if set then the direction is rtl. + + /* package */ int[] mDirections; + /* package */ Directions(int[] dirs) { mDirections = dirs; } } @@ -1831,6 +1596,7 @@ public abstract class Layout { * line is ellipsized, not getLineStart().) */ public abstract int getEllipsisStart(int line); + /** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. @@ -1870,7 +1636,7 @@ public abstract class Layout { public int length() { return mText.length(); } - + public CharSequence subSequence(int start, int end) { char[] s = new char[end - start]; getChars(start, end, s, 0); @@ -1936,12 +1702,17 @@ public abstract class Layout { public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; - + /* package */ static final int DIR_REQUEST_LTR = 1; /* package */ static final int DIR_REQUEST_RTL = -1; /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; + /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; + /* package */ static final int RUN_LEVEL_SHIFT = 26; + /* package */ static final int RUN_LEVEL_MASK = 0x3f; + /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; + public enum Alignment { ALIGN_NORMAL, ALIGN_OPPOSITE, @@ -1953,9 +1724,7 @@ public abstract class Layout { private static final int TAB_INCREMENT = 20; /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = - new Directions(new short[] { 32767 }); + new Directions(new int[] { 0, RUN_LENGTH_MASK }); /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = - new Directions(new short[] { 0, 32767 }); - + new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); } - diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java new file mode 100644 index 0000000..d5699f1 --- /dev/null +++ b/core/java/android/text/MeasuredText.java @@ -0,0 +1,229 @@ +/* + * 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.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * @hide + */ +class MeasuredText { + /* package */ CharSequence mText; + /* package */ int mTextStart; + /* package */ float[] mWidths; + /* package */ char[] mChars; + /* package */ byte[] mLevels; + /* package */ int mDir; + /* package */ boolean mEasy; + /* package */ int mLen; + private int mPos; + private TextPaint mWorkPaint; + + private MeasuredText() { + mWorkPaint = new TextPaint(); + } + + private static MeasuredText[] cached = new MeasuredText[3]; + + /* package */ + static MeasuredText obtain() { + MeasuredText mt; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + mt = cached[i]; + cached[i] = null; + return mt; + } + } + } + mt = new MeasuredText(); + Log.e("MEAS", "new: " + mt); + return mt; + } + + /* package */ + static MeasuredText recycle(MeasuredText mt) { + mt.mText = null; + if (mt.mLen < 1000) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = mt; + break; + } + } + } + } + return null; + } + + /** + * Analyzes text for bidirectional runs. Allocates working buffers. + */ + /* package */ + void setPara(CharSequence text, int start, int end, int bidiRequest) { + mText = text; + mTextStart = start; + + int len = end - start; + mLen = len; + mPos = 0; + + if (mWidths == null || mWidths.length < len) { + mWidths = new float[ArrayUtils.idealFloatArraySize(len)]; + } + if (mChars == null || mChars.length < len) { + mChars = new char[ArrayUtils.idealCharArraySize(len)]; + } + TextUtils.getChars(text, start, end, mChars, 0); + + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + ReplacementSpan[] spans = spanned.getSpans(start, end, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int startInPara = spanned.getSpanStart(spans[i]) - start; + int endInPara = spanned.getSpanEnd(spans[i]) - start; + for (int j = startInPara; j < endInPara; j++) { + mChars[j] = '\uFFFC'; + } + } + } + + if (TextUtils.doesNotNeedBidi(mChars, 0, len)) { + mDir = Layout.DIR_LEFT_TO_RIGHT; + mEasy = true; + } else { + if (mLevels == null || mLevels.length < len) { + mLevels = new byte[ArrayUtils.idealByteArraySize(len)]; + } + mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); + mEasy = false; + } + } + + float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { + if (fm != null) { + paint.getFontMetricsInt(fm); + } + + int p = mPos; + mPos = p + len; + + if (mEasy) { + int flags = mDir == Layout.DIR_LEFT_TO_RIGHT + ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; + return paint.getTextRunAdvances(mChars, p, len, p, len, flags, mWidths, p); + } + + float totalAdvance = 0; + int level = mLevels[p]; + for (int q = p, i = p + 1, e = p + len;; ++i) { + if (i == e || mLevels[i] != level) { + int flags = (level & 0x1) == 0 ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL; + totalAdvance += + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, flags, mWidths, q); + if (i == e) { + break; + } + q = i; + level = mLevels[i]; + } + } + return totalAdvance; + } + + float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, + Paint.FontMetricsInt fm) { + + TextPaint workPaint = mWorkPaint; + workPaint.set(paint); + // XXX paint should not have a baseline shift, but... + workPaint.baselineShift = 0; + + ReplacementSpan replacement = null; + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(workPaint); + } + } + + float wid; + if (replacement == null) { + wid = addStyleRun(workPaint, len, fm); + } else { + // Use original text. Shouldn't matter. + wid = replacement.getSize(workPaint, mText, mTextStart + mPos, + mTextStart + mPos + len, fm); + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + return wid; + } + + int breakText(int start, int limit, boolean forwards, float width) { + float[] w = mWidths; + if (forwards) { + for (int i = start; i < limit; ++i) { + if ((width -= w[i]) < 0) { + return i - start; + } + } + } else { + for (int i = limit; --i >= start;) { + if ((width -= w[i]) < 0) { + return limit - i -1; + } + } + } + + return limit - start; + } + + float measure(int start, int limit) { + float width = 0; + float[] w = mWidths; + for (int i = start; i < limit; ++i) { + width += w[i]; + } + return width; + } +}
\ No newline at end of file diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index bb98bce..13cb5e6 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -417,8 +417,8 @@ public class Selection { } } - private static final class START implements NoCopySpan { }; - private static final class END implements NoCopySpan { }; + private static final class START implements NoCopySpan { } + private static final class END implements NoCopySpan { } /* * Public constants diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index caaafa1..fc01ef2 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -17,8 +17,9 @@ package android.text; import com.android.internal.util.ArrayUtils; -import android.graphics.Paint; + import android.graphics.Canvas; +import android.graphics.Paint; import java.lang.reflect.Array; @@ -312,12 +313,15 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, moveGapTo(end); - if (tbend - tbstart >= mGapLength + (end - start)) - resizeFor(mText.length - mGapLength + - tbend - tbstart - (end - start)); + // Can be negative + final int nbNewChars = (tbend - tbstart) - (end - start); - mGapStart += tbend - tbstart - (end - start); - mGapLength -= tbend - tbstart - (end - start); + if (nbNewChars >= mGapLength) { + resizeFor(mText.length + nbNewChars - mGapLength); + } + + mGapStart += nbNewChars; + mGapLength -= nbNewChars; if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); @@ -707,6 +711,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, * the specified range of the buffer. The kind may be Object.class to get * a list of all the spans regardless of type. */ + @SuppressWarnings("unchecked") public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { int spanCount = mSpanCount; Object[] spans = mSpans; @@ -717,8 +722,8 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, int gaplen = mGapLength; int count = 0; - Object[] ret = null; - Object ret1 = null; + T[] ret = null; + T ret1 = null; for (int i = 0; i < spanCount; i++) { int spanStart = starts[i]; @@ -750,11 +755,13 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } if (count == 0) { - ret1 = spans[i]; + // Safe conversion thanks to the isInstance test above + ret1 = (T) spans[i]; count++; } else { if (count == 1) { - ret = (Object[]) Array.newInstance(kind, spanCount - i + 1); + // Safe conversion, but requires a suppressWarning + ret = (T[]) Array.newInstance(kind, spanCount - i + 1); ret[0] = ret1; } @@ -771,29 +778,33 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } System.arraycopy(ret, j, ret, j + 1, count - j); - ret[j] = spans[i]; + // Safe conversion thanks to the isInstance test above + ret[j] = (T) spans[i]; count++; } else { - ret[count++] = spans[i]; + // Safe conversion thanks to the isInstance test above + ret[count++] = (T) spans[i]; } } } if (count == 0) { - return (T[]) ArrayUtils.emptyArray(kind); + return ArrayUtils.emptyArray(kind); } if (count == 1) { - ret = (Object[]) Array.newInstance(kind, 1); + // Safe conversion, but requires a suppressWarning + ret = (T[]) Array.newInstance(kind, 1); ret[0] = ret1; - return (T[]) ret; + return ret; } if (count == ret.length) { - return (T[]) ret; + return ret; } - Object[] nret = (Object[]) Array.newInstance(kind, count); + // Safe conversion, but requires a suppressWarning + T[] nret = (T[]) Array.newInstance(kind, count); System.arraycopy(ret, 0, nret, 0, count); - return (T[]) nret; + return nret; } /** @@ -862,6 +873,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, /** * Return a String containing a copy of the chars in this buffer. */ + @Override public String toString() { int len = length(); char[] buf = new char[len]; @@ -952,6 +964,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } } +/* private boolean isprint(char c) { // XXX if (c >= ' ' && c <= '~') return true; @@ -959,7 +972,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return false; } -/* private static final int startFlag(int flag) { return (flag >> 4) & 0x0F; } @@ -1054,7 +1066,32 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } } + /** + * Don't call this yourself -- exists for Canvas to use internally. + * {@hide} + */ + public void drawTextRun(Canvas c, int start, int end, + int contextStart, int contextEnd, + float x, float y, int flags, Paint p) { + checkRange("drawTextRun", start, end); + + int contextLen = contextEnd - contextStart; + int len = end - start; + if (contextEnd <= mGapStart) { + c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, flags, p); + } else if (contextStart >= mGapStart) { + c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength, + contextLen, x, y, flags, p); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, flags, p); + TextUtils.recycle(buf); + } + } + + /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ @@ -1103,6 +1140,58 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return ret; } + /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, + float[] advances, int advancesPos, Paint p) { + + float ret; + + int contextLen = contextEnd - contextStart; + int len = end - start; + + if (end <= mGapStart) { + ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, + flags, advances, advancesPos); + } else if (start >= mGapStart) { + ret = p.getTextRunAdvances(mText, start + mGapLength, len, + contextStart + mGapLength, contextLen, flags, advances, advancesPos); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunAdvances(buf, start - contextStart, len, + 0, contextLen, flags, advances, advancesPos); + TextUtils.recycle(buf); + } + + return ret; + } + + public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, + int cursorOpt, Paint p) { + + int ret; + + int contextLen = contextEnd - contextStart; + if (contextEnd <= mGapStart) { + ret = p.getTextRunCursor(mText, contextStart, contextLen, + flags, offset, cursorOpt); + } else if (contextStart >= mGapStart) { + ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen, + flags, offset + mGapLength, cursorOpt) - mGapLength; + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunCursor(buf, 0, contextLen, + flags, offset - contextStart, cursorOpt) + contextStart; + TextUtils.recycle(buf); + } + + return ret; + } + // Documentation from interface public void setFilters(InputFilter[] filters) { if (filters == null) { diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java index 154497d..d14fcbc 100644 --- a/core/java/android/text/Spanned.java +++ b/core/java/android/text/Spanned.java @@ -91,7 +91,7 @@ extends CharSequence public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK; /** - * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand + * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand * to include text inserted at their ending point but not at their * starting point. When 0-length, they behave like points. */ diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f02ad2a..44157de 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,14 +16,15 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.graphics.Bitmap; import android.graphics.Paint; -import com.android.internal.util.ArrayUtils; -import android.util.Log; import android.text.style.LeadingMarginSpan; import android.text.style.LineHeightSpan; import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; /** * StaticLayout is a Layout for text that will not be edited after it @@ -31,8 +32,9 @@ import android.text.style.ReplacementSpan; * <p>This is used by widgets to control text layout. You should not need * to use this class directly unless you are implementing your own widget * or custom display object, or would be tempted to call - * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) - * Canvas.drawText()} directly.</p> + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, + * float, float, android.graphics.Paint) + * Canvas.drawText()} directly.</p> */ public class StaticLayout @@ -62,7 +64,7 @@ extends Layout boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { super((ellipsize == null) - ? source + ? source : (source instanceof Spanned) ? new SpannedEllipsizer(source) : new Ellipsizer(source), @@ -72,7 +74,7 @@ extends Layout * This is annoying, but we can't refer to the layout until * superclass construction is finished, and the superclass * constructor wants the reference to the display text. - * + * * This will break if the superclass constructor ever actually * cares about the content instead of just holding the reference. */ @@ -94,13 +96,13 @@ extends Layout mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); + generate(source, bufstart, bufend, paint, outerwidth, align, spacingmult, spacingadd, includepad, includepad, ellipsize != null, ellipsizedWidth, ellipsize); - mChdirs = null; - mChs = null; - mWidths = null; + mMeasured = MeasuredText.recycle(mMeasured); mFontMetricsInt = null; } @@ -111,6 +113,7 @@ extends Layout mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); } /* package */ void generate(CharSequence source, int bufstart, int bufend, @@ -128,59 +131,50 @@ extends Layout Paint.FontMetricsInt fm = mFontMetricsInt; int[] choosehtv = null; - int end = TextUtils.indexOf(source, '\n', bufstart, bufend); - int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart; - boolean first = true; - - if (mChdirs == null) { - mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)]; - mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)]; - mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)]; - } - - byte[] chdirs = mChdirs; - char[] chs = mChs; - float[] widths = mWidths; + MeasuredText measured = mMeasured; - AlteredCharSequence alter = null; Spanned spanned = null; - if (source instanceof Spanned) spanned = (Spanned) source; int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX - for (int start = bufstart; start <= bufend; start = end) { - if (first) - first = false; - else - end = TextUtils.indexOf(source, '\n', start, bufend); - - if (end < 0) - end = bufend; + int paraEnd; + for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend); + if (paraEnd < 0) + paraEnd = bufend; else - end++; + paraEnd++; + int paraLen = paraEnd - paraStart; - int firstWidthLineCount = 1; + int firstWidthLineLimit = mLineCount + 1; int firstwidth = outerwidth; int restwidth = outerwidth; LineHeightSpan[] chooseht = null; if (spanned != null) { - LeadingMarginSpan[] sp; - - sp = spanned.getSpans(start, end, LeadingMarginSpan.class); + LeadingMarginSpan[] sp = spanned.getSpans(paraStart, paraEnd, + LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); - if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { - firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + + // LeadingMarginSpan2 is odd. The count affects all + // leading margin spans, not just this particular one, + // and start from the top of the span, not the top of the + // paragraph. + if (lms instanceof LeadingMarginSpan2) { + LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; + int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2)); + firstWidthLineLimit = lmsFirstLine + + lms2.getLeadingMarginLineCount(); } } - chooseht = spanned.getSpans(start, end, LineHeightSpan.class); + chooseht = spanned.getSpans(paraStart, paraEnd, LineHeightSpan.class); if (chooseht.length != 0) { if (choosehtv == null || @@ -192,11 +186,11 @@ extends Layout for (int i = 0; i < chooseht.length; i++) { int o = spanned.getSpanStart(chooseht[i]); - if (o < start) { + if (o < paraStart) { // starts in this layout, before the // current paragraph - choosehtv[i] = getLineTop(getLineForOffset(o)); + choosehtv[i] = getLineTop(getLineForOffset(o)); } else { // starts in this paragraph @@ -206,162 +200,87 @@ extends Layout } } - if (end - start > chdirs.length) { - chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)]; - mChdirs = chdirs; - } - if (end - start > chs.length) { - chs = new char[ArrayUtils.idealCharArraySize(end - start)]; - mChs = chs; - } - if ((end - start) * 2 > widths.length) { - widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)]; - mWidths = widths; - } - - TextUtils.getChars(source, start, end, chs, 0); - final int n = end - start; - - boolean easy = true; - boolean altered = false; - int dir = DEFAULT_DIR; // XXX - - for (int i = 0; i < n; i++) { - if (chs[i] >= FIRST_RIGHT_TO_LEFT) { - easy = false; - break; - } - } + measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); + char[] chs = measured.mChars; + float[] widths = measured.mWidths; + byte[] chdirs = measured.mLevels; + int dir = measured.mDir; + boolean easy = measured.mEasy; - // Ensure that none of the underlying characters are treated - // as viable breakpoints, and that the entire run gets the - // same bidi direction. - - if (source instanceof Spanned) { - Spanned sp = (Spanned) source; - ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class); - - for (int y = 0; y < spans.length; y++) { - int a = sp.getSpanStart(spans[y]); - int b = sp.getSpanEnd(spans[y]); - - for (int x = a; x < b; x++) { - chs[x - start] = '\uFFFC'; - } - } - } - - if (!easy) { - // XXX put override flags, etc. into chdirs - dir = bidi(dir, chs, chdirs, n, false); - - // Do mirroring for right-to-left segments - - for (int i = 0; i < n; i++) { - if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - int j; - - for (j = i; j < n; j++) { - if (chdirs[j] != - Character.DIRECTIONALITY_RIGHT_TO_LEFT) - break; - } - - if (AndroidCharacter.mirror(chs, i, j - i)) - altered = true; - - i = j - 1; - } - } - } - - CharSequence sub; - - if (altered) { - if (alter == null) - alter = AlteredCharSequence.make(source, chs, start, end); - else - alter.update(chs, start, end); - - sub = alter; - } else { - sub = source; - } + CharSequence sub = source; int width = firstwidth; float w = 0; - int here = start; + int here = paraStart; - int ok = start; + int ok = paraStart; float okwidth = w; int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; - int fit = start; + int fit = paraStart; float fitwidth = w; int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; - boolean tab = false; - - int next; - for (int i = start; i < end; i = next) { - if (spanned == null) - next = end; - else - next = spanned.nextSpanTransition(i, end, - MetricAffectingSpan. - class); - - if (spanned == null) { - paint.getTextWidths(sub, i, next, widths); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - paint.getFontMetricsInt(fm); - } else { - mWorkPaint.baselineShift = 0; + boolean hasTabOrEmoji = false; + boolean hasTab = false; + TabStops tabStops = null; + + for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart; + spanStart < paraEnd; spanStart = nextSpanStart) { - Styled.getTextWidths(paint, mWorkPaint, - spanned, i, next, - widths, fm); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); + if (spanStart == spanEnd) { + if (spanned == null) + spanEnd = paraEnd; + else + spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, + MetricAffectingSpan.class); - if (mWorkPaint.baselineShift < 0) { - fm.ascent += mWorkPaint.baselineShift; - fm.top += mWorkPaint.baselineShift; + int spanLen = spanEnd - spanStart; + if (spanned == null) { + measured.addStyleRun(paint, spanLen, fm); } else { - fm.descent += mWorkPaint.baselineShift; - fm.bottom += mWorkPaint.baselineShift; + MetricAffectingSpan[] spans = + spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + measured.addStyleRun(paint, spans, spanLen, fm); } } + nextSpanStart = spanEnd; + int startInPara = spanStart - paraStart; + int endInPara = spanEnd - paraStart; + int fmtop = fm.top; int fmbottom = fm.bottom; int fmascent = fm.ascent; int fmdescent = fm.descent; - if (false) { - StringBuilder sb = new StringBuilder(); - for (int j = i; j < next; j++) { - sb.append(widths[j - start + (end - start)]); - sb.append(' '); - } - - Log.e("text", sb.toString()); - } - - for (int j = i; j < next; j++) { - char c = chs[j - start]; + for (int j = spanStart; j < spanEnd; j++) { + char c = chs[j - paraStart]; float before = w; if (c == '\n') { ; } else if (c == '\t') { - w = Layout.nextTab(sub, start, end, w, null); - tab = true; - } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) { - int emoji = Character.codePointAt(chs, j - start); + if (hasTab == false) { + hasTab = true; + hasTabOrEmoji = true; + if (spanned != null) { + // First tab this para, check for tabstops + TabStopSpan[] spans = spanned.getSpans(paraStart, + paraEnd, TabStopSpan.class); + if (spans.length > 0) { + tabStops = new TabStops(TAB_INCREMENT, spans); + } + } + } + if (tabStops != null) { + w = tabStops.nextTab(w); + } else { + w = TabStops.nextDefaultStop(w, TAB_INCREMENT); + } + } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) { + int emoji = Character.codePointAt(chs, j - paraStart); if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { Bitmap bm = EMOJI_FACTORY. @@ -376,21 +295,21 @@ extends Layout whichPaint = mWorkPaint; } - float wid = (float) bm.getWidth() * + float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight(); w += wid; - tab = true; + hasTabOrEmoji = true; j++; } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); @@ -411,7 +330,7 @@ extends Layout /* * From the Unicode Line Breaking Algorithm: * (at least approximately) - * + * * .,:; are class IS: breakpoints * except when adjacent to digits * / is class SY: a breakpoint @@ -426,12 +345,12 @@ extends Layout if (c == ' ' || c == '\t' || ((c == '.' || c == ',' || c == ':' || c == ';') && - (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || ((c == '/' || c == '-') && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || (c >= FIRST_CJK && isIdeographic(c, true) && - j + 1 < next && isIdeographic(chs[j + 1 - start], false))) { + j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { okwidth = w; ok = j + 1; @@ -448,7 +367,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -457,10 +376,10 @@ extends Layout okascent, okdescent, oktop, okbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -484,7 +403,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -493,10 +412,10 @@ extends Layout okascent, okdescent, oktop, okbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -509,19 +428,20 @@ extends Layout fittop, fitbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, fit == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, fitwidth, paint); here = fit; } else { // Log.e("text", "output one " + here + " to " +(here + 1)); - measureText(paint, mWorkPaint, - source, here, here + 1, fm, tab, - null); + // XXX not sure why the existing fm wasn't ok. + // measureText(paint, mWorkPaint, + // source, here, here + 1, fm, tab, + // null); v = out(source, here, here+1, @@ -529,19 +449,22 @@ extends Layout fm.top, fm.bottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, here + 1 == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, - widths[here - start], paint); + widths[here - paraStart], paint); here = here + 1; } - if (here < i) { - j = next = here; // must remeasure + if (here < spanStart) { + // didn't output all the text for this span + // we've measured the raw widths, though, so + // just reset the start point + j = nextSpanStart = here; } else { j = here - 1; // continue looping } @@ -551,14 +474,14 @@ extends Layout fitascent = fitdescent = fittop = fitbottom = 0; okascent = okdescent = oktop = okbottom = 0; - if (--firstWidthLineCount <= 0) { + if (--firstWidthLineLimit <= 0) { width = restwidth; } } } } - if (end != here) { + if (paraEnd != here) { if ((fittop | fitbottom | fitdescent | fitascent) == 0) { paint.getFontMetricsInt(fm); @@ -571,20 +494,20 @@ extends Layout // Log.e("text", "output rest " + here + " to " + end); v = out(source, - here, end, fitascent, fitdescent, + here, paraEnd, fitascent, fitdescent, fittop, fitbottom, v, spacingmult, spacingadd, chooseht, - choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, - end == bufend, includepad, trackpad, - widths, start, end - start, + choosehtv, fm, hasTabOrEmoji, + needMultiply, paraStart, chdirs, dir, easy, + paraEnd == bufend, includepad, trackpad, + chs, widths, here - paraStart, where, ellipsizedWidth, w, paint); } - start = end; + paraStart = paraEnd; - if (end == bufend) + if (paraEnd == bufend) break; } @@ -599,246 +522,13 @@ extends Layout v, spacingmult, spacingadd, null, null, fm, false, - needMultiply, bufend, chdirs, DEFAULT_DIR, true, + needMultiply, bufend, null, DEFAULT_DIR, true, true, includepad, trackpad, - widths, bufstart, 0, + null, null, bufstart, where, ellipsizedWidth, 0, paint); } } - /** - * Runs the unicode bidi algorithm on the first n chars in chs, returning - * the char dirs in chInfo and the base line direction of the first - * paragraph. - * - * XXX change result from dirs to levels - * - * @param dir the direction flag, either DIR_REQUEST_LTR, - * DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL. - * @param chs the text to examine - * @param chInfo on input, if hasInfo is true, override and other flags - * representing out-of-band embedding information. On output, the generated - * dirs of the text. - * @param n the length of the text/information in chs and chInfo - * @param hasInfo true if chInfo has input information, otherwise the - * input data in chInfo is ignored. - * @return the resolved direction level of the first paragraph, either - * DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT. - */ - /* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n, - boolean hasInfo) { - - AndroidCharacter.getDirectionalities(chs, chInfo, n); - - /* - * Determine primary paragraph direction if not specified - */ - if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) { - // set up default - dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT; - for (int j = 0; j < n; j++) { - int d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { - dir = DIR_LEFT_TO_RIGHT; - break; - } - if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - dir = DIR_RIGHT_TO_LEFT; - break; - } - } - } - - final byte SOR = dir == DIR_LEFT_TO_RIGHT ? - Character.DIRECTIONALITY_LEFT_TO_RIGHT : - Character.DIRECTIONALITY_RIGHT_TO_LEFT; - - /* - * XXX Explicit overrides should go here - */ - - /* - * Weak type resolution - */ - - // dump(chdirs, n, "initial"); - - // W1 non spacing marks - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.NON_SPACING_MARK) { - if (j == 0) - chInfo[j] = SOR; - else - chInfo[j] = chInfo[j - 1]; - } - } - - // dump(chdirs, n, "W1"); - - // W2 european numbers - byte cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - cur = d; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) { - if (cur == - Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W2"); - - // W3 arabic letters - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } - - // dump(chdirs, n, "W3"); - - // W4 single separator between numbers - for (int j = 1; j < n - 1; j++) { - byte d = chInfo[j]; - byte prev = chInfo[j - 1]; - byte next = chInfo[j + 1]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER && - next == Character.DIRECTIONALITY_ARABIC_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W4"); - - // W5 european number terminators - boolean adjacent = false; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - adjacent = false; - } - - //dump(chdirs, n, "W5"); - - // W5 european number terminators part 2, - // W6 separators and terminators - adjacent = false; - for (int j = n - 1; j >= 0; j--) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) { - if (adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - else { - adjacent = false; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR || - d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR) - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - } - - // dump(chdirs, n, "W6"); - - // W7 strong direction of european numbers - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == SOR || - d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) - cur = d; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = cur; - } - - // dump(chdirs, n, "W7"); - - // N1, N2 neutrals - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - cur = d; - } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - d == Character.DIRECTIONALITY_ARABIC_NUMBER) { - cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } else { - byte dd = SOR; - int k; - - for (k = j + 1; k < n; k++) { - dd = chInfo[k]; - - if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - break; - } - if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - dd == Character.DIRECTIONALITY_ARABIC_NUMBER) { - dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - break; - } - } - - for (int y = j; y < k; y++) { - if (dd == cur) - chInfo[y] = cur; - else - chInfo[y] = SOR; - } - - j = k - 1; - } - } - - // dump(chdirs, n, "final"); - - // extra: enforce that all tabs and surrogate characters go the - // primary direction - // TODO: actually do directions right for surrogates - - for (int j = 0; j < n; j++) { - char c = chs[j]; - - if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) { - chInfo[j] = SOR; - } - } - - return dir; - } - private static final char FIRST_CJK = '\u2E80'; /** * Returns true if the specified character is one of those specified @@ -944,37 +634,15 @@ extends Layout } */ - private static int getFit(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - float wid) { - int high = end + 1, low = start - 1, guess; - - while (high - low > 1) { - guess = (high + low) / 2; - - if (measureText(paint, workPaint, - text, start, guess, null, true, null) > wid) - high = guess; - else - low = guess; - } - - if (low < start) - return start; - else - return low; - } - private int out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, LineHeightSpan[] chooseht, int[] choosehtv, - Paint.FontMetricsInt fm, boolean tab, + Paint.FontMetricsInt fm, boolean hasTabOrEmoji, boolean needMultiply, int pstart, byte[] chdirs, int dir, boolean easy, boolean last, boolean includepad, boolean trackpad, - float[] widths, int widstart, int widoff, + char[] chs, float[] widths, int widstart, TextUtils.TruncateAt ellipsize, float ellipsiswidth, float textwidth, TextPaint paint) { int j = mLineCount; @@ -982,8 +650,6 @@ extends Layout int want = off + mColumns + TOP; int[] lines = mLines; - // Log.e("text", "line " + start + " to " + end + (last ? "===" : "")); - if (want >= lines.length) { int nlen = ArrayUtils.idealIntArraySize(want + 1); int[] grow = new int[nlen]; @@ -1059,59 +725,23 @@ extends Layout lines[off + mColumns + START] = end; lines[off + mColumns + TOP] = v; - if (tab) + if (hasTabOrEmoji) lines[off + TAB] |= TAB_MASK; - { - lines[off + DIR] |= dir << DIR_SHIFT; - - int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - int count = 0; - - if (!easy) { - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - count++; - cur = chdirs[k - pstart]; - } - } - } - - Directions linedirs; - - if (count == 0) { - linedirs = DIRS_ALL_LEFT_TO_RIGHT; - } else { - short[] ld = new short[count + 1]; - - cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - count = 0; - int here = start; - - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - // XXX check to make sure we don't - // overflow short - ld[count++] = (short) (k - here); - cur = chdirs[k - pstart]; - here = k; - } - } - - ld[count] = (short) (end - here); - - if (count == 1 && ld[0] == 0) { - linedirs = DIRS_ALL_RIGHT_TO_LEFT; - } else { - linedirs = new Directions(ld); - } - } - + lines[off + DIR] |= dir << DIR_SHIFT; + Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; + // easy means all chars < the first RTL, so no emoji, no nothing + // XXX a run with no text or all spaces is easy but might be an empty + // RTL paragraph. Make sure easy is false if this is the case. + if (easy) { mLineDirections[j] = linedirs; + } else { + mLineDirections[j] = AndroidBidi.directions(dir, chdirs, widstart, chs, + widstart, end - start); // If ellipsize is in marquee mode, do not apply ellipsis on the first line if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) { - calculateEllipsis(start, end, widths, widstart, widoff, + calculateEllipsis(start, end, widths, widstart, ellipsiswidth, ellipsize, j, textwidth, paint); } @@ -1122,7 +752,7 @@ extends Layout } private void calculateEllipsis(int linestart, int lineend, - float[] widths, int widstart, int widoff, + float[] widths, int widstart, float avail, TextUtils.TruncateAt where, int line, float textwidth, TextPaint paint) { int len = lineend - linestart; @@ -1142,7 +772,7 @@ extends Layout int i; for (i = len; i >= 0; i--) { - float w = widths[i - 1 + linestart - widstart + widoff]; + float w = widths[i - 1 + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1158,7 +788,7 @@ extends Layout int i; for (i = 0; i < len; i++) { - float w = widths[i + linestart - widstart + widoff]; + float w = widths[i + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1175,7 +805,7 @@ extends Layout float ravail = (avail - ellipsiswid) / 2; for (right = len; right >= 0; right--) { - float w = widths[right - 1 + linestart - widstart + widoff]; + float w = widths[right - 1 + linestart - widstart]; if (w + rsum > ravail) { break; @@ -1186,7 +816,7 @@ extends Layout float lavail = avail - ellipsiswid - rsum; for (left = 0; left < right; left++) { - float w = widths[left + linestart - widstart + widoff]; + float w = widths[left + linestart - widstart]; if (w + lsum > lavail) { break; @@ -1203,7 +833,7 @@ extends Layout mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; } - // Override the baseclass so we can directly access our members, + // Override the base class so we can directly access our members, // rather than relying on member functions. // The logic mirrors that of Layout.getLineForVertical // FIXME: It may be faster to do a linear search for layouts without many lines. @@ -1232,11 +862,11 @@ extends Layout } public int getLineTop(int line) { - return mLines[mColumns * line + TOP]; + return mLines[mColumns * line + TOP]; } public int getLineDescent(int line) { - return mLines[mColumns * line + DESCENT]; + return mLines[mColumns * line + DESCENT]; } public int getLineStart(int line) { @@ -1309,13 +939,11 @@ extends Layout private static final int DIR_SHIFT = 30; private static final int TAB_MASK = 0x20000000; - private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + private static final int TAB_INCREMENT = 20; // same as Layout, but that's private /* - * These are reused across calls to generate() + * This is reused across calls to generate() */ - private byte[] mChdirs; - private char[] mChs; - private float[] mWidths; + private MeasuredText mMeasured; private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); } diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java deleted file mode 100644 index 513b2cd..0000000 --- a/core/java/android/text/Styled.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * 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.text; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.text.style.CharacterStyle; -import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; - -/** - * This class provides static methods for drawing and measuring styled text, - * like {@link android.text.Spanned} object with - * {@link android.text.style.ReplacementSpan}. - * - * @hide - */ -public class Styled -{ - /** - * Draws and/or measures a uniform run of text on a single line. No span of - * interest should start or end in the middle of this run (if not - * drawing, character spans that don't affect metrics can be ignored). - * Neither should the run direction change in the middle of the run. - * - * <p>The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - * <p>On return, workPaint will reflect the original paint plus any - * modifications made by character styles on the run. - * - * <p>The returned width is signed and will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawUniformRun(Canvas canvas, - Spanned text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - boolean haveWidth = false; - float ret = 0; - CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); - - ReplacementSpan replacement = null; - - // XXX: This shouldn't be modifying paint, only workPaint. - // However, the members belonging to TextPaint should have default - // values anyway. Better to ensure this in the Layout constructor. - paint.bgColor = 0; - paint.baselineShift = 0; - workPaint.set(paint); - - if (spans.length > 0) { - for (int i = 0; i < spans.length; i++) { - CharacterStyle span = spans[i]; - - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateDrawState(workPaint); - } - } - } - - if (replacement == null) { - CharSequence tmp; - int tmpstart, tmpend; - - if (runIsRtl) { - tmp = TextUtils.getReverse(text, start, end); - tmpstart = 0; - // XXX: assumes getReverse doesn't change the length of the text - tmpend = end - start; - } else { - tmp = text; - tmpstart = start; - tmpend = end; - } - - if (fmi != null) { - workPaint.getFontMetricsInt(fmi); - } - - if (canvas != null) { - if (workPaint.bgColor != 0) { - int c = workPaint.getColor(); - Paint.Style s = workPaint.getStyle(); - workPaint.setColor(workPaint.bgColor); - workPaint.setStyle(Paint.Style.FILL); - - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - canvas.drawRect(x - ret, top, x, bottom, workPaint); - else - canvas.drawRect(x, top, x + ret, bottom, workPaint); - - workPaint.setStyle(s); - workPaint.setColor(c); - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - canvas.drawText(tmp, tmpstart, tmpend, - x - ret, y + workPaint.baselineShift, workPaint); - } else { - if (needWidth) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - - canvas.drawText(tmp, tmpstart, tmpend, - x, y + workPaint.baselineShift, workPaint); - } - } else { - if (needWidth && !haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - } else { - ret = replacement.getSize(workPaint, text, start, end, fmi); - - if (canvas != null) { - if (dir == Layout.DIR_RIGHT_TO_LEFT) - replacement.draw(canvas, text, start, end, - x - ret, top, y, bottom, workPaint); - else - replacement.draw(canvas, text, start, end, - x, top, y, bottom, workPaint); - } - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - return -ret; - else - return ret; - } - - /** - * Returns the advance widths for a uniform left-to-right run of text with - * no style changes in the middle of the run. If any style is replacement - * text, the first character will get the width of the replacement and the - * remaining characters will get a width of 0. - * - * @param paint the paint, will not be modified - * @param workPaint a paint to modify; on return will reflect the original - * paint plus the effect of all spans on the run - * @param text the text - * @param start the start of the run - * @param end the limit of the run - * @param widths array to receive the advance widths of the characters. Must - * be at least a large as (end - start). - * @param fmi FontMetrics information; can be null - * @return the actual number of widths returned - */ - public static int getTextWidths(TextPaint paint, - TextPaint workPaint, - Spanned text, int start, int end, - float[] widths, Paint.FontMetricsInt fmi) { - MetricAffectingSpan[] spans = - text.getSpans(start, end, MetricAffectingSpan.class); - - ReplacementSpan replacement = null; - workPaint.set(paint); - - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateMeasureState(workPaint); - } - } - - if (replacement == null) { - workPaint.getFontMetricsInt(fmi); - workPaint.getTextWidths(text, start, end, widths); - } else { - int wid = replacement.getSize(workPaint, text, start, end, fmi); - - if (end > start) { - widths[0] = wid; - for (int i = start + 1; i < end; i++) - widths[i - start] = 0; - } - } - return end - start; - } - - /** - * Renders and/or measures a directional run of text on a single line. - * Unlike {@link #drawUniformRun}, this can render runs that cross style - * boundaries. Returns the signed advance width, if requested. - * - * <p>The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - * <p>This optimizes for unstyled text and so workPaint might not be - * modified by this call. - * - * <p>The returned advance width will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawDirectionalRun(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - // XXX: It looks like all calls to this API match dir and runIsRtl, so - // having both parameters is redundant and confusing. - - // fast path for unstyled text - if (!(text instanceof Spanned)) { - float ret = 0; - - if (runIsRtl) { - CharSequence tmp = TextUtils.getReverse(text, start, end); - // XXX: this assumes getReverse doesn't tweak the length of - // the text - int tmpend = end - start; - - if (canvas != null || needWidth) - ret = paint.measureText(tmp, 0, tmpend); - - if (canvas != null) - canvas.drawText(tmp, 0, tmpend, - x - ret, y, paint); - } else { - if (needWidth) - ret = paint.measureText(text, start, end); - - if (canvas != null) - canvas.drawText(text, start, end, x, y, paint); - } - - if (fmi != null) { - paint.getFontMetricsInt(fmi); - } - - return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 - } - - float ox = x; - int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; - - Spanned sp = (Spanned) text; - Class<?> division; - - if (canvas == null) - division = MetricAffectingSpan.class; - else - division = CharacterStyle.class; - - int next; - for (int i = start; i < end; i = next) { - next = sp.nextSpanTransition(i, end, division); - - // XXX: if dir and runIsRtl were not the same, this would draw - // spans in the wrong order, but no one appears to call it this - // way. - x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, - x, top, y, bottom, fmi, paint, workPaint, - needWidth || next != end); - - if (fmi != null) { - if (fmi.ascent < minAscent) - minAscent = fmi.ascent; - if (fmi.descent > maxDescent) - maxDescent = fmi.descent; - - if (fmi.top < minTop) - minTop = fmi.top; - if (fmi.bottom > maxBottom) - maxBottom = fmi.bottom; - } - } - - if (fmi != null) { - if (start == end) { - paint.getFontMetricsInt(fmi); - } else { - fmi.ascent = minAscent; - fmi.descent = maxDescent; - fmi.top = minTop; - fmi.bottom = maxBottom; - } - } - - return x - ox; - } - - /** - * Draws a unidirectional run of text on a single line, and optionally - * returns the signed advance. Unlike drawDirectionalRun, the paragraph - * direction and run direction can be different. - */ - /* package */ static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl - if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || - (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { - // TODO: this needs the real direction - float ch = drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, - workPaint, true); - - ch *= dir; // DIR_RIGHT_TO_LEFT == -1 - drawDirectionalRun(canvas, text, start, end, -dir, - runIsRtl, x + ch, top, y, bottom, null, paint, - workPaint, true); - - return ch; - } - - return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, - x, top, y, bottom, null, paint, workPaint, - needWidth); - } - - /** - * Draws a run of text on a single line, with its - * origin at (x,y), in the specified Paint. The origin is interpreted based - * on the Align setting in the Paint. - * - * This method considers style information in the text (e.g. even when text - * is an instance of {@link android.text.Spanned}, this method correctly - * draws the text). See also - * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, - * float, Paint)} and - * {@link android.graphics.Canvas#drawRect(float, float, float, float, - * Paint)}. - * - * @param canvas The target canvas - * @param text The text to be drawn - * @param start The index of the first character in text to draw - * @param end (end - 1) is the index of the last character in text to draw - * @param direction The direction of the text. This must be - * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or - * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. - * @param x The x-coordinate of origin for where to draw the text - * @param top The top side of the rectangle to be drawn - * @param y The y-coordinate of origin for where to draw the text - * @param bottom The bottom side of the rectangle to be drawn - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal - * workspace. - * @param needWidth If true, this method returns the width of drawn text - * @return Width of the drawn text if needWidth is true - */ - public static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int direction, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // For safety. - direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT - : Layout.DIR_RIGHT_TO_LEFT; - - // Hide runIsRtl parameter since it is meaningless for external - // developers. - // XXX: the runIsRtl probably ought to be the same as direction, then - // this could draw rtl text. - return drawText(canvas, text, start, end, direction, false, - x, top, y, bottom, paint, workPaint, needWidth); - } - - /** - * Returns the width of a run of left-to-right text on a single line, - * considering style information in the text (e.g. even when text is an - * instance of {@link android.text.Spanned}, this method correctly measures - * the width of the text). - * - * @param paint the main {@link TextPaint} object; will not be modified - * @param workPaint the {@link TextPaint} object available for modification; - * will not necessarily be used - * @param text the text to measure - * @param start the index of the first character to start measuring - * @param end 1 beyond the index of the last character to measure - * @param fmi FontMetrics information; can be null - * @return The width of the text - */ - public static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - Paint.FontMetricsInt fmi) { - return drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, - 0, 0, 0, 0, fmi, paint, workPaint, true); - } -} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java new file mode 100644 index 0000000..0e3522e --- /dev/null +++ b/core/java/android/text/TextLine.java @@ -0,0 +1,940 @@ +/* + * 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.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.RectF; +import android.text.Layout.Directions; +import android.text.Layout.TabStops; +import android.text.style.CharacterStyle; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * Represents a line of styled text, for measuring in visual order and + * for rendering. + * + * <p>Get a new instance using obtain(), and when finished with it, return it + * to the pool using recycle(). + * + * <p>Call set to prepare the instance for use, then either draw, measure, + * metrics, or caretToLeftRightOf. + * + * @hide + */ +class TextLine { + private TextPaint mPaint; + private CharSequence mText; + private int mStart; + private int mLen; + private int mDir; + private Directions mDirections; + private boolean mHasTabs; + private TabStops mTabs; + private char[] mChars; + private boolean mCharsValid; + private Spanned mSpanned; + private final TextPaint mWorkPaint = new TextPaint(); + + private static TextLine[] cached = new TextLine[3]; + + /** + * Returns a new TextLine from the shared pool. + * + * @return an uninitialized TextLine + */ + static TextLine obtain() { + TextLine tl; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + tl = cached[i]; + cached[i] = null; + return tl; + } + } + } + tl = new TextLine(); + Log.e("TLINE", "new: " + tl); + return tl; + } + + /** + * Puts a TextLine back into the shared pool. Do not use this TextLine once + * it has been returned. + * @param tl the textLine + * @return null, as a convenience from clearing references to the provided + * TextLine + */ + static TextLine recycle(TextLine tl) { + tl.mText = null; + tl.mPaint = null; + tl.mDirections = null; + if (tl.mLen < 250) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = tl; + break; + } + } + } + } + return null; + } + + /** + * Initializes a TextLine and prepares it for use. + * + * @param paint the base paint for the line + * @param text the text, can be Styled + * @param start the start of the line relative to the text + * @param limit the limit of the line relative to the text + * @param dir the paragraph direction of this line + * @param directions the directions information of this line + * @param hasTabs true if the line might contain tabs or emoji + * @param tabStops the tabStops. Can be null. + */ + void set(TextPaint paint, CharSequence text, int start, int limit, int dir, + Directions directions, boolean hasTabs, TabStops tabStops) { + mPaint = paint; + mText = text; + mStart = start; + mLen = limit - start; + mDir = dir; + mDirections = directions; + mHasTabs = hasTabs; + mSpanned = null; + + boolean hasReplacement = false; + if (text instanceof Spanned) { + mSpanned = (Spanned) text; + hasReplacement = mSpanned.getSpans(start, limit, + ReplacementSpan.class).length > 0; + } + + mCharsValid = hasReplacement || hasTabs || + directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + + if (mCharsValid) { + if (mChars == null || mChars.length < mLen) { + mChars = new char[ArrayUtils.idealCharArraySize(mLen)]; + } + TextUtils.getChars(text, start, limit, mChars, 0); + if (hasReplacement) { + // Handle these all at once so we don't have to do it as we go. + // Replace the first character of each replacement run with the + // object-replacement character and the remainder with zero width + // non-break space aka BOM. Cursor movement code skips these + // zero-width characters. + char[] chars = mChars; + for (int i = start, inext; i < limit; i = inext) { + inext = mSpanned.nextSpanTransition(i, limit, + ReplacementSpan.class); + if (mSpanned.getSpans(i, inext, ReplacementSpan.class) + .length > 0) { // transition into a span + chars[i - start] = '\ufffc'; + for (int j = i - start + 1, e = inext - start; j < e; ++j) { + chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip + } + } + } + } + } + mTabs = tabStops; + } + + /** + * Renders the TextLine. + * + * @param c the canvas to render on + * @param x the leading margin position + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + */ + void draw(Canvas c, float x, int top, int y, int bottom) { + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); + return; + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); + return; + } + } + + float h = 0; + int[] runs = mDirections.mDirections; + RectF emojiRect = null; + + int lastRunIndex = runs.length - 2; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + char[] chars = mChars; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = mChars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(mChars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom, + i != lastRunIndex || j != mLen); + + if (codept == '\t') { + h = mDir * nextTab(h * mDir); + } else if (bm != null) { + float bmAscent = ascent(j); + float bitmapHeight = bm.getHeight(); + float scale = -bmAscent / bitmapHeight; + float width = bm.getWidth() * scale; + + if (emojiRect == null) { + emojiRect = new RectF(); + } + emojiRect.set(x + h, y + bmAscent, + x + h + width, y); + c.drawBitmap(bm, null, emojiRect, mPaint); + h += width; + j++; + } + segstart = j + 1; + } + } + } + } + + /** + * Returns metrics information for the entire line. + * + * @param fmi receives font metrics information, can be null + * @return the signed width of the line + */ + float metrics(FontMetricsInt fmi) { + return measure(mLen, false, fmi); + } + + /** + * Returns information about a position on the line. + * + * @param offset the line-relative character offset, between 0 and the + * line length, inclusive + * @param trailing true to measure the trailing edge of the character + * before offset, false to measure the leading edge of the character + * at offset. + * @param fmi receives metrics information about the requested + * character, can be null. + * @return the signed offset from the leading margin to the requested + * character edge. + */ + float measure(int offset, boolean trailing, FontMetricsInt fmi) { + int target = trailing ? offset - 1 : offset; + if (target < 0) { + return 0; + } + + float h = 0; + + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + return measureRun(0, 0, offset, mLen, false, fmi); + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + return measureRun(0, 0, offset, mLen, true, fmi); + } + } + + char[] chars = mChars; + int[] runs = mDirections.mDirections; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = chars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(chars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + boolean inSegment = target >= segstart && target < j; + + boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; + if (inSegment && advance) { + return h += measureRun(i, segstart, offset, j, runIsRtl, fmi); + } + + float w = measureRun(i, segstart, j, j, runIsRtl, fmi); + h += advance ? w : -w; + + if (inSegment) { + return h += measureRun(i, segstart, offset, j, runIsRtl, null); + } + + if (codept == '\t') { + if (offset == j) { + return h; + } + h = mDir * nextTab(h * mDir); + if (target == j) { + return h; + } + } + + if (bm != null) { + float bmAscent = ascent(j); + float wid = bm.getWidth() * -bmAscent / bm.getHeight(); + h += mDir * wid; + j++; + } + + segstart = j + 1; + } + } + } + + return h; + } + + /** + * Draws a unidirectional (but possibly multi-styled) run of text. + * + * @param c the canvas to draw on + * @param runIndex the index of this directional run + * @param start the line-relative start + * @param limit the line-relative limit + * @param runIsRtl true if the run is right-to-left + * @param x the position of the run that is closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param needWidth true if the width value is required. + * @return the signed width of the run, based on the paragraph direction. + * Only valid if needWidth is true. + */ + private float drawRun(Canvas c, int runIndex, int start, + int limit, boolean runIsRtl, float x, int top, int y, int bottom, + boolean needWidth) { + + if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { + float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null); + handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top, + y, bottom, null, false); + return w; + } + + return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top, + y, bottom, null, needWidth); + } + + /** + * Measures a unidirectional (but possibly multi-styled) run of text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param offset the offset to measure to, between start and limit inclusive + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + * @param fmi receives metrics information about the requested + * run, can be null. + * @return the signed width from the start of the run to the leading edge + * of the character at offset, based on the run (not paragraph) direction + */ + private float measureRun(int runIndex, int start, + int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) { + return handleRun(runIndex, start, offset, limit, runIsRtl, null, + 0, 0, 0, 0, fmi, true); + } + + /** + * Walk the cursor through this line, skipping conjuncts and + * zero-width characters. + * + * <p>This function cannot properly walk the cursor off the ends of the line + * since it does not know about any shaping on the previous/following line + * that might affect the cursor position. Callers must either avoid these + * situations or handle the result specially. + * + * @param cursor the starting position of the cursor, between 0 and the + * length of the line, inclusive + * @param toLeft true if the caret is moving to the left. + * @return the new offset. If it is less than 0 or greater than the length + * of the line, the previous/following line should be examined to get the + * actual offset. + */ + int getOffsetToLeftRightOf(int cursor, boolean toLeft) { + // 1) The caret marks the leading edge of a character. The character + // logically before it might be on a different level, and the active caret + // position is on the character at the lower level. If that character + // was the previous character, the caret is on its trailing edge. + // 2) Take this character/edge and move it in the indicated direction. + // This gives you a new character and a new edge. + // 3) This position is between two visually adjacent characters. One of + // these might be at a lower level. The active position is on the + // character at the lower level. + // 4) If the active position is on the trailing edge of the character, + // the new caret position is the following logical character, else it + // is the character. + + int lineStart = 0; + int lineEnd = mLen; + boolean paraIsRtl = mDir == -1; + int[] runs = mDirections.mDirections; + + int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; + boolean trailing = false; + + if (cursor == lineStart) { + runIndex = -2; + } else if (cursor == lineEnd) { + runIndex = runs.length; + } else { + // First, get information about the run containing the character with + // the active caret. + for (runIndex = 0; runIndex < runs.length; runIndex += 2) { + runStart = lineStart + runs[runIndex]; + if (cursor >= runStart) { + runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > lineEnd) { + runLimit = lineEnd; + } + if (cursor < runLimit) { + runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + if (cursor == runStart) { + // The caret is on a run boundary, see if we should + // use the position on the trailing edge of the previous + // logical character instead. + int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; + int pos = cursor - 1; + for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { + prevRunStart = lineStart + runs[prevRunIndex]; + if (pos >= prevRunStart) { + prevRunLimit = prevRunStart + + (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (prevRunLimit > lineEnd) { + prevRunLimit = lineEnd; + } + if (pos < prevRunLimit) { + prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) + & Layout.RUN_LEVEL_MASK; + if (prevRunLevel < runLevel) { + // Start from logically previous character. + runIndex = prevRunIndex; + runLevel = prevRunLevel; + runStart = prevRunStart; + runLimit = prevRunLimit; + trailing = true; + break; + } + } + } + } + } + break; + } + } + } + + // caret might be == lineEnd. This is generally a space or paragraph + // separator and has an associated run, but might be the end of + // text, in which case it doesn't. If that happens, we ran off the + // end of the run list, and runIndex == runs.length. In this case, + // we are at a run boundary so we skip the below test. + if (runIndex != runs.length) { + boolean runIsRtl = (runLevel & 0x1) != 0; + boolean advance = toLeft == runIsRtl; + if (cursor != (advance ? runLimit : runStart) || advance != trailing) { + // Moving within or into the run, so we can move logically. + newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, + runIsRtl, cursor, advance); + // If the new position is internal to the run, we're at the strong + // position already so we're finished. + if (newCaret != (advance ? runLimit : runStart)) { + return newCaret; + } + } + } + } + + // If newCaret is -1, we're starting at a run boundary and crossing + // into another run. Otherwise we've arrived at a run boundary, and + // need to figure out which character to attach to. Note we might + // need to run this twice, if we cross a run boundary and end up at + // another run boundary. + while (true) { + boolean advance = toLeft == paraIsRtl; + int otherRunIndex = runIndex + (advance ? 2 : -2); + if (otherRunIndex >= 0 && otherRunIndex < runs.length) { + int otherRunStart = lineStart + runs[otherRunIndex]; + int otherRunLimit = otherRunStart + + (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (otherRunLimit > lineEnd) { + otherRunLimit = lineEnd; + } + int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + boolean otherRunIsRtl = (otherRunLevel & 1) != 0; + + advance = toLeft == otherRunIsRtl; + if (newCaret == -1) { + newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, + otherRunLimit, otherRunIsRtl, + advance ? otherRunStart : otherRunLimit, advance); + if (newCaret == (advance ? otherRunLimit : otherRunStart)) { + // Crossed and ended up at a new boundary, + // repeat a second and final time. + runIndex = otherRunIndex; + runLevel = otherRunLevel; + continue; + } + break; + } + + // The new caret is at a boundary. + if (otherRunLevel < runLevel) { + // The strong character is in the other run. + newCaret = advance ? otherRunStart : otherRunLimit; + } + break; + } + + if (newCaret == -1) { + // We're walking off the end of the line. The paragraph + // level is always equal to or lower than any internal level, so + // the boundaries get the strong caret. + newCaret = advance ? mLen + 1 : -1; + break; + } + + // Else we've arrived at the end of the line. That's a strong position. + // We might have arrived here by crossing over a run with no internal + // breaks and dropping out of the above loop before advancing one final + // time, so reset the caret. + // Note, we use '<=' below to handle a situation where the only run + // on the line is a counter-directional run. If we're not advancing, + // we can end up at the 'lineEnd' position but the caret we want is at + // the lineStart. + if (newCaret <= lineEnd) { + newCaret = advance ? lineEnd : lineStart; + } + break; + } + + return newCaret; + } + + /** + * Returns the next valid offset within this directional run, skipping + * conjuncts and zero-width characters. This should not be called to walk + * off the end of the line, since the returned values might not be valid + * on neighboring lines. If the returned offset is less than zero or + * greater than the line length, the offset should be recomputed on the + * preceding or following line, respectively. + * + * @param runIndex the run index + * @param runStart the start of the run + * @param runLimit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param offset the offset + * @param after true if the new offset should logically follow the provided + * offset + * @return the new offset + */ + private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, + boolean runIsRtl, int offset, boolean after) { + + if (runIndex < 0 || offset == (after ? mLen : 0)) { + // Walking off end of line. Since we don't know + // what cursor positions are available on other lines, we can't + // return accurate values. These are a guess. + if (after) { + return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; + } + return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; + } + + TextPaint wp = mWorkPaint; + wp.set(mPaint); + + int spanStart = runStart; + int spanLimit; + if (mSpanned == null) { + spanLimit = runLimit; + } else { + int target = after ? offset + 1 : offset; + int limit = mStart + runLimit; + while (true) { + spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, + MetricAffectingSpan.class) - mStart; + if (spanLimit >= target) { + break; + } + spanStart = spanLimit; + } + + MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, + mStart + spanLimit, MetricAffectingSpan.class); + + if (spans.length > 0) { + ReplacementSpan replacement = null; + for (int j = 0; j < spans.length; j++) { + MetricAffectingSpan span = spans[j]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(wp); + } + } + + if (replacement != null) { + // If we have a replacement span, we're moving either to + // the start or end of this span. + return after ? spanLimit : spanStart; + } + } + } + + int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; + int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; + if (mCharsValid) { + return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, + flags, offset, cursorOpt); + } else { + return wp.getTextRunCursor(mText, mStart + spanStart, + mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart; + } + } + + /** + * Utility function for measuring and rendering text. The text must + * not include a tab or emoji. + * + * @param wp the working paint + * @param start the start of the text + * @param end the end of the text + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if rendering is not needed + * @param x the edge of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the run is needed + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleText(TextPaint wp, int start, int end, + int contextStart, int contextEnd, boolean runIsRtl, + Canvas c, float x, int top, int y, int bottom, + FontMetricsInt fmi, boolean needWidth) { + + float ret = 0; + + int runLen = end - start; + int contextLen = contextEnd - contextStart; + if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) { + int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; + if (mCharsValid) { + ret = wp.getTextRunAdvances(mChars, start, runLen, + contextStart, contextLen, flags, null, 0); + } else { + int delta = mStart; + ret = wp.getTextRunAdvances(mText, delta + start, + delta + end, delta + contextStart, delta + contextEnd, + flags, null, 0); + } + } + + if (fmi != null) { + wp.getFontMetricsInt(fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + + if (wp.bgColor != 0) { + int color = wp.getColor(); + Paint.Style s = wp.getStyle(); + wp.setColor(wp.bgColor); + wp.setStyle(Paint.Style.FILL); + + c.drawRect(x, top, x + ret, bottom, wp); + + wp.setStyle(s); + wp.setColor(color); + } + + drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, + x, y + wp.baselineShift); + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for measuring and rendering a replacement. + * + * @param replacement the replacement + * @param wp the work paint + * @param runIndex the run index + * @param start the start of the run + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if not rendering + * @param x the edge of the replacement closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the replacement is needed + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleReplacement(ReplacementSpan replacement, TextPaint wp, + int runIndex, int start, int limit, boolean runIsRtl, Canvas c, + float x, int top, int y, int bottom, FontMetricsInt fmi, + boolean needWidth) { + + float ret = 0; + + int textStart = mStart + start; + int textLimit = mStart + limit; + + if (needWidth || (c != null && runIsRtl)) { + ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + replacement.draw(c, mText, textStart, textLimit, + x, top, y, bottom, wp); + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for handling a unidirectional run. The run must not + * contain tabs or emoji but can contain styles. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param measureLimit the offset to measure to, between start and limit inclusive + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null + * @param x the end of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width is required + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleRun(int runIndex, int start, int measureLimit, + int limit, boolean runIsRtl, Canvas c, float x, int top, int y, + int bottom, FontMetricsInt fmi, boolean needWidth) { + + // Shaping needs to take into account context up to metric boundaries, + // but rendering needs to take into account character style boundaries. + // So we iterate through metric runs to get metric bounds, + // then within each metric run iterate through character style runs + // for the run bounds. + float ox = x; + for (int i = start, inext; i < measureLimit; i = inext) { + TextPaint wp = mWorkPaint; + wp.set(mPaint); + + int mlimit; + if (mSpanned == null) { + inext = limit; + mlimit = measureLimit; + } else { + inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit, + MetricAffectingSpan.class) - mStart; + + mlimit = inext < measureLimit ? inext : measureLimit; + MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, + mStart + mlimit, MetricAffectingSpan.class); + + if (spans.length > 0) { + ReplacementSpan replacement = null; + for (int j = 0; j < spans.length; j++) { + MetricAffectingSpan span = spans[j]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + // We might have a replacement that uses the draw + // state, otherwise measure state would suffice. + span.updateDrawState(wp); + } + } + + if (replacement != null) { + x += handleReplacement(replacement, wp, runIndex, i, + mlimit, runIsRtl, c, x, top, y, bottom, fmi, + needWidth || mlimit < measureLimit); + continue; + } + } + } + + if (mSpanned == null || c == null) { + x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top, + y, bottom, fmi, needWidth || mlimit < measureLimit); + } else { + for (int j = i, jnext; j < mlimit; j = jnext) { + jnext = mSpanned.nextSpanTransition(mStart + j, + mStart + mlimit, CharacterStyle.class) - mStart; + + CharacterStyle[] spans = mSpanned.getSpans(mStart + j, + mStart + jnext, CharacterStyle.class); + + wp.set(mPaint); + for (int k = 0; k < spans.length; k++) { + CharacterStyle span = spans[k]; + span.updateDrawState(wp); + } + + x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, + top, y, bottom, fmi, needWidth || jnext < measureLimit); + } + } + } + + return x - ox; + } + + /** + * Render a text run with the set-up paint. + * + * @param c the canvas + * @param wp the paint used to render the text + * @param start the start of the run + * @param end the end of the run + * @param contextStart the start of context for the run + * @param contextEnd the end of the context for the run + * @param runIsRtl true if the run is right-to-left + * @param x the x position of the left edge of the run + * @param y the baseline of the run + */ + private void drawTextRun(Canvas c, TextPaint wp, int start, int end, + int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { + + int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; + if (mCharsValid) { + int count = end - start; + int contextCount = contextEnd - contextStart; + c.drawTextRun(mChars, start, count, contextStart, contextCount, + x, y, flags, wp); + } else { + int delta = mStart; + c.drawTextRun(mText, delta + start, delta + end, + delta + contextStart, delta + contextEnd, x, y, flags, wp); + } + } + + /** + * Returns the ascent of the text at start. This is used for scaling + * emoji. + * + * @param pos the line-relative position + * @return the ascent of the text at start + */ + float ascent(int pos) { + if (mSpanned == null) { + return mPaint.ascent(); + } + + pos += mStart; + MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, + MetricAffectingSpan.class); + if (spans.length == 0) { + return mPaint.ascent(); + } + + TextPaint wp = mWorkPaint; + wp.set(mPaint); + for (MetricAffectingSpan span : spans) { + span.updateMeasureState(wp); + } + return wp.ascent(); + } + + /** + * Returns the next tab position. + * + * @param h the (unsigned) offset from the leading margin + * @return the (unsigned) tab position after this offset + */ + float nextTab(float h) { + if (mTabs != null) { + return mTabs.nextTab(h); + } + return TabStops.nextDefaultStop(h, TAB_INCREMENT); + } + + private static final int TAB_INCREMENT = 20; +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 9589bf3..2d6c7b6 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -17,12 +17,11 @@ package android.text; import com.android.internal.R; +import com.android.internal.util.ArrayUtils; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; -import android.text.method.TextKeyListener.Capitalize; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -45,10 +44,8 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; -import com.android.internal.util.ArrayUtils; - -import java.util.regex.Pattern; import java.util.Iterator; +import java.util.regex.Pattern; public class TextUtils { private TextUtils() { /* cannot be instantiated */ } @@ -983,7 +980,7 @@ public class TextUtils { /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, - * or, if it does not fit, a copy with ellipsis character added + * or, if it does not fit, a copy with ellipsis character added * at the specified edge or center. * If <code>preserveLength</code> is specified, the returned copy * will be padded with zero-width spaces to preserve the original @@ -992,7 +989,7 @@ public class TextUtils { * report the start and end of the ellipsized range. */ public static CharSequence ellipsize(CharSequence text, - TextPaint p, + TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback) { @@ -1003,13 +1000,12 @@ public class TextUtils { int len = text.length(); - // Use Paint.breakText() for the non-Spanned case to avoid having - // to allocate memory and accumulate the character widths ourselves. - - if (!(text instanceof Spanned)) { - float wid = p.measureText(text, 0, len); + MeasuredText mt = MeasuredText.obtain(); + try { + float width = setPara(mt, paint, text, 0, text.length(), + Layout.DIR_REQUEST_DEFAULT_LTR); - if (wid <= avail) { + if (width <= avail) { if (callback != null) { callback.ellipsized(0, 0); } @@ -1017,252 +1013,71 @@ public class TextUtils { return text; } - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } - - if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { - buf[i] = '\uFEFF'; - } - String ret = new String(buf, 0, len); - recycle(buf); - return ret; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - int fit = p.breakText(text, 0, len, false, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(0, len - fit); - } - - if (preserveLength) { - return blank(text, 0, len - fit); - } else { - return sEllipsis + text.toString().substring(len - fit, len); - } + // XXX assumes ellipsis string does not require shaping and + // is unaffected by style + float ellipsiswid = paint.measureText(sEllipsis); + avail -= ellipsiswid; + + int left = 0; + int right = len; + if (avail < 0) { + // it all goes + } else if (where == TruncateAt.START) { + right = len - mt.breakText(0, len, false, avail); } else if (where == TruncateAt.END) { - int fit = p.breakText(text, 0, len, true, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(fit, len); - } - - if (preserveLength) { - return blank(text, fit, len); - } else { - return text.toString().substring(0, fit) + sEllipsis; - } - } else /* where == TruncateAt.MIDDLE */ { - int right = p.breakText(text, 0, len, false, - (avail - ellipsiswid) / 2, null); - float used = p.measureText(text, len - right, len); - int left = p.breakText(text, 0, len - right, true, - avail - ellipsiswid - used, null); - - if (callback != null) { - callback.ellipsized(left, len - right); - } - - if (preserveLength) { - return blank(text, left, len - right); - } else { - String s = text.toString(); - return s.substring(0, left) + sEllipsis + - s.substring(len - right, len); - } + left = mt.breakText(0, len, true, avail); + } else { + right = len - mt.breakText(0, len, false, avail / 2); + avail -= mt.measure(right, len); + left = mt.breakText(0, right, true, avail); } - } - - // But do the Spanned cases by hand, because it's such a pain - // to iterate the span transitions backwards and getTextWidths() - // will give us the information we need. - - // getTextWidths() always writes into the start of the array, - // so measure each span into the first half and then copy the - // results into the second half to use later. - - float[] wid = new float[len * 2]; - TextPaint temppaint = new TextPaint(); - Spanned sp = (Spanned) text; - - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); - } - - float sum = 0; - for (int i = 0; i < len; i++) { - sum += wid[len + i]; - } - if (sum <= avail) { if (callback != null) { - callback.ellipsized(0, 0); + callback.ellipsized(left, right); } - return text; - } - - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } + char[] buf = mt.mChars; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int remaining = len - (right - left); if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { + if (remaining > 0) { // else eliminate the ellipsis too + buf[left++] = '\u2026'; + } + for (int i = left; i < right; i++) { buf[i] = '\uFEFF'; } - SpannableString ss = new SpannableString(new String(buf, 0, len)); - recycle(buf); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - sum = 0; - int i; - - for (i = len; i >= 0; i--) { - float w = wid[len + i - 1]; - - if (w + sum + ellipsiswid > avail) { - break; + String s = new String(buf, 0, len); + if (sp == null) { + return s; } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(0, i); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, 0, i)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(1, text, i, len); - - return out; - } - } else if (where == TruncateAt.END) { - sum = 0; - int i; - - for (i = 0; i < len; i++) { - float w = wid[len + i]; - - if (w + sum + ellipsiswid > avail) { - break; - } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(i, len); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, i, len)); + SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, i); - - return out; - } - } else /* where = TruncateAt.MIDDLE */ { - float lsum = 0, rsum = 0; - int left = 0, right = len; - - float ravail = (avail - ellipsiswid) / 2; - for (right = len; right >= 0; right--) { - float w = wid[len + right - 1]; - - if (w + rsum > ravail) { - break; - } - - rsum += w; } - float lavail = avail - ellipsiswid - rsum; - for (left = 0; left < right; left++) { - float w = wid[len + left]; - - if (w + lsum > lavail) { - break; - } - - lsum += w; + if (remaining == 0) { + return ""; } - if (callback != null) { - callback.ellipsized(left, right); + if (sp == null) { + StringBuilder sb = new StringBuilder(remaining + sEllipsis.length()); + sb.append(buf, 0, left); + sb.append(sEllipsis); + sb.append(buf, right, len - right); + return sb.toString(); } - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, left, right)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, left); - out.insert(out.length(), text, right, len); - - return out; - } + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(text, 0, left); + ssb.append(sEllipsis); + ssb.append(text, right, len); + return ssb; + } finally { + MeasuredText.recycle(mt); } } - private static String blank(CharSequence source, int start, int end) { - int len = source.length(); - char[] buf = obtain(len); - - if (start != 0) { - getChars(source, 0, start, buf, 0); - } - if (end != len) { - getChars(source, end, len, buf, end); - } - - if (start != end) { - buf[start] = '\u2026'; - - for (int i = start + 1; i < end; i++) { - buf[i] = '\uFEFF'; - } - } - - String ret = new String(buf, 0, len); - recycle(buf); - - return ret; - } - /** * Converts a CharSequence of the comma-separated form "Andy, Bob, * Charles, David" that is too wide to fit into the specified width @@ -1278,80 +1093,121 @@ public class TextUtils { TextPaint p, float avail, String oneMore, String more) { - int len = text.length(); - char[] buf = new char[len]; - TextUtils.getChars(text, 0, len, buf, 0); - int commaCount = 0; - for (int i = 0; i < len; i++) { - if (buf[i] == ',') { - commaCount++; + MeasuredText mt = MeasuredText.obtain(); + try { + int len = text.length(); + float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR); + if (width <= avail) { + return text; } - } - - float[] wid; - if (text instanceof Spanned) { - Spanned sp = (Spanned) text; - TextPaint temppaint = new TextPaint(); - wid = new float[len * 2]; + char[] buf = mt.mChars; - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); + int commaCount = 0; + for (int i = 0; i < len; i++) { + if (buf[i] == ',') { + commaCount++; + } } - System.arraycopy(wid, len, wid, 0, len); - } else { - wid = new float[len]; - p.getTextWidths(text, 0, len, wid); - } + int remaining = commaCount + 1; - int ok = 0; - int okRemaining = commaCount + 1; - String okFormat = ""; + int ok = 0; + int okRemaining = remaining; + String okFormat = ""; - int w = 0; - int count = 0; + int w = 0; + int count = 0; + float[] widths = mt.mWidths; - for (int i = 0; i < len; i++) { - w += wid[i]; + int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR : + Layout.DIR_REQUEST_RTL; - if (buf[i] == ',') { - count++; + MeasuredText tempMt = MeasuredText.obtain(); + for (int i = 0; i < len; i++) { + w += widths[i]; - int remaining = commaCount - count + 1; - float moreWid; - String format; + if (buf[i] == ',') { + count++; - if (remaining == 1) { - format = " " + oneMore; - } else { - format = " " + String.format(more, remaining); - } + String format; + // XXX should not insert spaces, should be part of string + // XXX should use plural rules and not assume English plurals + if (--remaining == 1) { + format = " " + oneMore; + } else { + format = " " + String.format(more, remaining); + } - moreWid = p.measureText(format); + // XXX this is probably ok, but need to look at it more + tempMt.setPara(format, 0, format.length(), request); + float moreWid = mt.addStyleRun(p, mt.mLen, null); - if (w + moreWid <= avail) { - ok = i + 1; - okRemaining = remaining; - okFormat = format; + if (w + moreWid <= avail) { + ok = i + 1; + okRemaining = remaining; + okFormat = format; + } } } - } + MeasuredText.recycle(tempMt); - if (w <= avail) { - return text; - } else { SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; + } finally { + MeasuredText.recycle(mt); } } + private static float setPara(MeasuredText mt, TextPaint paint, + CharSequence text, int start, int end, int bidiRequest) { + + mt.setPara(text, start, end, bidiRequest); + + float width; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int len = end - start; + if (sp == null) { + width = mt.addStyleRun(paint, len, null); + } else { + width = 0; + int spanEnd; + for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { + spanEnd = sp.nextSpanTransition(spanStart, len, + MetricAffectingSpan.class); + MetricAffectingSpan[] spans = sp.getSpans( + spanStart, spanEnd, MetricAffectingSpan.class); + width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); + } + } + + return width; + } + + private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + + /* package */ + static boolean doesNotNeedBidi(CharSequence s, int start, int end) { + for (int i = start; i < end; i++) { + if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) { + return false; + } + } + return true; + } + + /* package */ + static boolean doesNotNeedBidi(char[] text, int start, int len) { + for (int i = start, e = i + len; i < e; i++) { + if (text[i] >= FIRST_RIGHT_TO_LEFT) { + return false; + } + } + return true; + } + /* package */ static char[] obtain(int len) { char[] buf; @@ -1529,7 +1385,7 @@ public class TextUtils { */ public static final int CAP_MODE_CHARACTERS = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of all words. This value is explicitly defined to be the same as @@ -1537,7 +1393,7 @@ public class TextUtils { */ public static final int CAP_MODE_WORDS = InputType.TYPE_TEXT_FLAG_CAP_WORDS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of each sentence. This value is explicitly defined to be the same as @@ -1545,13 +1401,13 @@ public class TextUtils { */ public static final int CAP_MODE_SENTENCES = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; - + /** * Determine what caps mode should be in effect at the current offset in * the text. Only the mode bits set in <var>reqModes</var> will be * checked. Note that the caps mode flags here are explicitly defined * to match those in {@link InputType}. - * + * * @param cs The text that should be checked for caps modes. * @param off Location in the text at which to check. * @param reqModes The modes to be checked: may be any combination of @@ -1651,7 +1507,7 @@ public class TextUtils { return mode; } - + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 9af42cc..79a0c37 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -16,30 +16,38 @@ package android.text.method; -import android.util.Log; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; import android.view.KeyEvent; -import android.graphics.Rect; -import android.text.*; -import android.widget.TextView; -import android.view.View; -import android.view.ViewConfiguration; import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; +import android.widget.TextView.CursorController; // XXX this doesn't extend MetaKeyKeyListener because the signatures // don't match. Need to figure that out. Meanwhile the meta keys // won't work in fields that don't take input. -public class -ArrowKeyMovementMethod -implements MovementMethod -{ +public class ArrowKeyMovementMethod implements MovementMethod { + /** + * An optional controller for the cursor. + * Use {@link #setCursorController(CursorController)} to set this field. + */ + protected CursorController mCursorController; + + private boolean isCap(Spannable buffer) { + return ((MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0)); + } + + private boolean isAlt(Spannable buffer) { + return MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_ALT_ON) == 1; + } + private boolean up(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -60,12 +68,8 @@ implements MovementMethod } private boolean down(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -86,12 +90,8 @@ implements MovementMethod } private boolean left(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -110,12 +110,8 @@ implements MovementMethod } private boolean right(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -133,35 +129,6 @@ implements MovementMethod } } - private int getOffset(int x, int y, TextView widget){ - // Converts the absolute X,Y coordinates to the character offset for the - // character whose position is closest to the specified - // horizontal position. - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - - // Clamp the position to inside of the view. - if (x < 0) { - x = 0; - } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) { - x = widget.getWidth()-widget.getTotalPaddingRight() - 1; - } - if (y < 0) { - y = 0; - } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) { - y = widget.getHeight()-widget.getTotalPaddingBottom() - 1; - } - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - - int offset = layout.getOffsetForHorizontal(line, x); - return offset; - } - public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { if (executeDown(widget, buffer, keyCode)) { MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); @@ -193,10 +160,9 @@ implements MovementMethod break; case KeyEvent.KEYCODE_DPAD_CENTER: - if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) { - if (widget.showContextMenu()) { + if ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) && + (widget.showContextMenu())) { handled = true; - } } } @@ -214,8 +180,7 @@ implements MovementMethod public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) { int code = event.getKeyCode(); - if (code != KeyEvent.KEYCODE_UNKNOWN - && event.getAction() == KeyEvent.ACTION_MULTIPLE) { + if (code != KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) { int repeat = event.getRepeatCount(); boolean handled = false; while ((--repeat) > 0) { @@ -226,13 +191,22 @@ implements MovementMethod return false; } - public boolean onTrackballEvent(TextView widget, Spannable text, - MotionEvent event) { + public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { + if (mCursorController != null) { + mCursorController.hide(); + } return false; } - public boolean onTouchEvent(TextView widget, Spannable buffer, - MotionEvent event) { + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + if (mCursorController != null) { + return onTouchEventCursor(widget, buffer, event); + } else { + return onTouchEventStandard(widget, buffer, event); + } + } + + private boolean onTouchEventStandard(TextView widget, Spannable buffer, MotionEvent event) { int initialScrollX = -1, initialScrollY = -1; if (event.getAction() == MotionEvent.ACTION_UP) { initialScrollX = Touch.getInitialScrollX(widget, buffer); @@ -243,53 +217,20 @@ implements MovementMethod if (widget.isFocused() && !widget.didTouchFocusSelect()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - int x = (int) event.getX(); - int y = (int) event.getY(); - int offset = getOffset(x, y, widget); - + boolean cap = isCap(buffer); if (cap) { - buffer.setSpan(LAST_TAP_DOWN, offset, offset, - Spannable.SPAN_POINT_POINT); + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); // Disallow intercepting of the touch events, so that // users can scroll and select at the same time. // without this, users would get booted out of select // mode once the view detected it needed to scroll. widget.getParent().requestDisallowInterceptTouchEvent(true); - } else { - OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - - if (tap.length > 0) { - if (event.getEventTime() - tap[0].mWhen <= - ViewConfiguration.getDoubleTapTimeout() && - sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) { - - tap[0].active = true; - MetaKeyKeyListener.startSelecting(widget, buffer); - widget.getParent().requestDisallowInterceptTouchEvent(true); - buffer.setSpan(LAST_TAP_DOWN, offset, offset, - Spannable.SPAN_POINT_POINT); - } - - tap[0].mWhen = event.getEventTime(); - } else { - OnePointFiveTapState newtap = new OnePointFiveTapState(); - newtap.mWhen = event.getEventTime(); - newtap.active = false; - buffer.setSpan(newtap, 0, buffer.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); + boolean cap = isCap(buffer); if (cap && handled) { // Before selecting, make sure we've moved out of the "slop". @@ -297,45 +238,15 @@ implements MovementMethod // OUT of the slop // Turn long press off while we're selecting. User needs to - // re-tap on the selection to enable longpress + // re-tap on the selection to enable long press widget.cancelLongPress(); // Update selection as we're moving the selection area. // Get the current touch position - int x = (int) event.getX(); - int y = (int) event.getY(); - int offset = getOffset(x, y, widget); - - final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - - if (tap.length > 0 && tap[0].active) { - // Get the last down touch position (the position at which the - // user started the selection) - int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); - - // Compute the selection boundaries - int spanstart; - int spanend; - if (offset >= lastDownOffset) { - // Expand from word start of the original tap to new word - // end, since we are selecting "forwards" - spanstart = findWordStart(buffer, lastDownOffset); - spanend = findWordEnd(buffer, offset); - } else { - // Expand to from new word start to word end of the original - // tap since we are selecting "backwards". - // The spanend will always need to be associated with the touch - // up position, so that refining the selection with the - // trackball will work as expected. - spanstart = findWordEnd(buffer, lastDownOffset); - spanend = findWordStart(buffer, offset); - } - Selection.setSelection(buffer, spanstart, spanend); - } else { - Selection.extendSelection(buffer, offset); - } + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + + Selection.extendSelection(buffer, offset); return true; } } else if (event.getAction() == MotionEvent.ACTION_UP) { @@ -344,70 +255,17 @@ implements MovementMethod // the current scroll offset to avoid the scroll jumping later // to show it. if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || - (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { + (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { widget.moveCursorToVisibleOffset(); return true; } - int x = (int) event.getX(); - int y = (int) event.getY(); - int off = getOffset(x, y, widget); - - // XXX should do the same adjust for x as we do for the line. - - OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - if (onepointfivetap.length > 0 && onepointfivetap[0].active && - Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) { - // If we've set select mode, because there was a onepointfivetap, - // but there was no ensuing swipe gesture, undo the select mode - // and remove reference to the last onepointfivetap. - MetaKeyKeyListener.stopSelecting(widget, buffer); - for (int i=0; i < onepointfivetap.length; i++) { - buffer.removeSpan(onepointfivetap[i]); - } + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + if (isCap(buffer)) { buffer.removeSpan(LAST_TAP_DOWN); - } - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - - DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), - DoubleTapState.class); - boolean doubletap = false; - - if (tap.length > 0) { - if (event.getEventTime() - tap[0].mWhen <= - ViewConfiguration.getDoubleTapTimeout() && - sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { - - doubletap = true; - } - - tap[0].mWhen = event.getEventTime(); - } else { - DoubleTapState newtap = new DoubleTapState(); - newtap.mWhen = event.getEventTime(); - buffer.setSpan(newtap, 0, buffer.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } - - if (cap) { - buffer.removeSpan(LAST_TAP_DOWN); - if (onepointfivetap.length > 0 && onepointfivetap[0].active) { - // If we selecting something with the onepointfivetap-and - // swipe gesture, stop it on finger up. - MetaKeyKeyListener.stopSelecting(widget, buffer); - } else { - Selection.extendSelection(buffer, off); - } - } else if (doubletap) { - Selection.setSelection(buffer, - findWordStart(buffer, off), - findWordEnd(buffer, off)); + Selection.extendSelection(buffer, offset); } else { - Selection.setSelection(buffer, off); + Selection.setSelection(buffer, offset); } MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); @@ -420,73 +278,36 @@ implements MovementMethod return handled; } - private static class DoubleTapState implements NoCopySpan { - long mWhen; - } - - /* We check for a onepointfive tap. This is similar to - * doubletap gesture (where a finger goes down, up, down, up, in a short - * time period), except in the onepointfive tap, a users finger only needs - * to go down, up, down in a short time period. We detect this type of tap - * to implement the onepointfivetap-and-swipe selection gesture. - * This gesture allows users to select a segment of text without going - * through the "select text" option in the context menu. - */ - private static class OnePointFiveTapState implements NoCopySpan { - long mWhen; - boolean active; - } - - private static boolean sameWord(CharSequence text, int one, int two) { - int start = findWordStart(text, one); - int end = findWordEnd(text, one); - - if (end == start) { - return false; - } + private boolean onTouchEventCursor(TextView widget, Spannable buffer, MotionEvent event) { + if (widget.isFocused() && !widget.didTouchFocusSelect()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + widget.cancelLongPress(); - return start == findWordStart(text, two) && - end == findWordEnd(text, two); - } + // Offset the current touch position (from controller to cursor) + final float x = event.getX() + mCursorController.getOffsetX(); + final float y = event.getY() + mCursorController.getOffsetY(); + int offset = widget.getOffset((int) x, (int) y); + mCursorController.updatePosition(offset); + return true; - // TODO: Unify with TextView.getWordForDictionary() - private static int findWordStart(CharSequence text, int start) { - for (; start > 0; start--) { - char c = text.charAt(start - 1); - int type = Character.getType(c); - - if (c != '\'' && - type != Character.UPPERCASE_LETTER && - type != Character.LOWERCASE_LETTER && - type != Character.TITLECASE_LETTER && - type != Character.MODIFIER_LETTER && - type != Character.DECIMAL_DIGIT_NUMBER) { - break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mCursorController = null; + return true; } } - - return start; + return false; } - // TODO: Unify with TextView.getWordForDictionary() - private static int findWordEnd(CharSequence text, int end) { - int len = text.length(); - - for (; end < len; end++) { - char c = text.charAt(end); - int type = Character.getType(c); - - if (c != '\'' && - type != Character.UPPERCASE_LETTER && - type != Character.LOWERCASE_LETTER && - type != Character.TITLECASE_LETTER && - type != Character.MODIFIER_LETTER && - type != Character.DECIMAL_DIGIT_NUMBER) { - break; - } - } - - return end; + /** + * Defines the cursor controller. + * + * When set, this object can be used to handle events, that can be translated in cursor updates. + * @param cursorController A cursor controller implementation + */ + public void setCursorController(CursorController cursorController) { + mCursorController = cursorController; } public boolean canSelectArbitrarily() { @@ -525,8 +346,9 @@ implements MovementMethod } public static MovementMethod getInstance() { - if (sInstance == null) + if (sInstance == null) { sInstance = new ArrowKeyMovementMethod(); + } return sInstance; } diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index 42ad10e..3b98fc3 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -17,14 +17,13 @@ package android.text.method; import android.text.Layout; -import android.text.NoCopySpan; import android.text.Layout.Alignment; +import android.text.NoCopySpan; import android.text.Spannable; -import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.widget.TextView; -import android.view.KeyEvent; public class Touch { private Touch() { } @@ -45,6 +44,7 @@ public class Touch { int left = Integer.MAX_VALUE; int right = 0; Alignment a = null; + boolean ltr = true; for (int i = top; i <= bottom; i++) { left = (int) Math.min(left, layout.getLineLeft(i)); @@ -52,6 +52,7 @@ public class Touch { if (a == null) { a = layout.getParagraphAlignment(i); + ltr = layout.getParagraphDirection(i) > 0; } } @@ -59,10 +60,12 @@ public class Touch { int width = widget.getWidth(); int diff = 0; + // align_opposite does NOT mean align_right, we need the paragraph + // direction to resolve it to left or right if (right - left < width - padding) { if (a == Alignment.ALIGN_CENTER) { diff = (width - padding - (right - left)) / 2; - } else if (a == Alignment.ALIGN_OPPOSITE) { + } else if (ltr == (a == Alignment.ALIGN_OPPOSITE)) { diff = width - padding - (right - left); } } @@ -99,7 +102,7 @@ public class Touch { MotionEvent event) { DragState[] ds; - switch (event.getAction()) { + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: ds = buffer.getSpans(0, buffer.length(), DragState.class); diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java index 9d91aca..a763a69 100644 --- a/core/java/android/util/CharsetUtils.java +++ b/core/java/android/util/CharsetUtils.java @@ -17,36 +17,58 @@ package android.util; import android.os.Build; +import android.text.TextUtils; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.Map; /** + * <p> * A class containing utility methods related to character sets. This * class is primarily useful for code that wishes to be vendor-aware - * in its interpretation of Japanese encoding names. - * - * <p>As of this writing, the only vendor that is recognized by this - * class is Docomo (identified case-insensitively as {@code "docomo"}).</p> - * - * <b>Note:</b> This class is hidden in Cupcake, with a plan to - * un-hide in Donut. This was done because the first deployment to use - * this code is based on Cupcake, but the API had to be introduced - * after the public API freeze for that release. The upshot is that - * only system applications can safely use this class until Donut is - * available. - * + * in its interpretation of Japanese charset names (used in DoCoMo, + * KDDI, and SoftBank). + * </p> + * + * <p> + * <b>Note:</b> Developers will need to add an appropriate mapping for + * each vendor-specific charset. You may need to modify the C libraries + * like icu4c in order to let Android support an additional charset. + * </p> + * * @hide */ public final class CharsetUtils { /** - * name of the vendor "Docomo". <b>Note:</b> This isn't a public + * name of the vendor "DoCoMo". <b>Note:</b> This isn't a public * constant, in order to keep this class from becoming a de facto * reference list of vendor names. */ private static final String VENDOR_DOCOMO = "docomo"; - + /** + * Name of the vendor "KDDI". + */ + private static final String VENDOR_KDDI = "kddi"; + /** + * Name of the vendor "SoftBank". + */ + private static final String VENDOR_SOFTBANK = "softbank"; + + /** + * Represents one-to-one mapping from a vendor name to a charset specific to the vendor. + */ + private static final Map<String, String> sVendorShiftJisMap = new HashMap<String, String>(); + + static { + // These variants of Shift_JIS come from icu's mapping data (convrtrs.txt) + sVendorShiftJisMap.put(VENDOR_DOCOMO, "docomo-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_KDDI, "kddi-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_SOFTBANK, "softbank-shift_jis-2007"); + } + /** * This class is uninstantiable. */ @@ -58,20 +80,22 @@ public final class CharsetUtils { * Returns the name of the vendor-specific character set * corresponding to the given original character set name and * vendor. If there is no vendor-specific character set for the - * given name/vendor pair, this returns the original character set - * name. The vendor name is matched case-insensitively. - * + * given name/vendor pair, this returns the original character set name. + * * @param charsetName the base character set name - * @param vendor the vendor to specialize for + * @param vendor the vendor to specialize for. All characters should be lower-cased. * @return the specialized character set name, or {@code charsetName} if * there is no specialized name */ public static String nameForVendor(String charsetName, String vendor) { - // TODO: Eventually, this may want to be table-driven. - - if (vendor.equalsIgnoreCase(VENDOR_DOCOMO) - && isShiftJis(charsetName)) { - return "docomo-shift_jis-2007"; + if (!TextUtils.isEmpty(charsetName) && !TextUtils.isEmpty(vendor)) { + // You can add your own mapping here. + if (isShiftJis(charsetName)) { + final String vendorShiftJis = sVendorShiftJisMap.get(vendor); + if (vendorShiftJis != null) { + return vendorShiftJis; + } + } } return charsetName; diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java index 5cbfd29..3bcd266 100644 --- a/core/java/android/util/Patterns.java +++ b/core/java/android/util/Patterns.java @@ -25,7 +25,7 @@ import java.util.regex.Pattern; public class Patterns { /** * Regular expression to match all IANA top-level domains. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -53,8 +53,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw])"; /** @@ -65,7 +65,7 @@ public class Patterns { /** * Regular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -94,8 +94,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw]))"; /** diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java index 1c8b330..7fc43b9 100644 --- a/core/java/android/util/SparseArray.java +++ b/core/java/android/util/SparseArray.java @@ -90,6 +90,16 @@ public class SparseArray<E> { delete(key); } + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + private void gc() { // Log.e("SparseArray", "gc start with " + mSize); diff --git a/core/java/android/view/AbsSavedState.java b/core/java/android/view/AbsSavedState.java index 840d7c1..6ad33dd 100644 --- a/core/java/android/view/AbsSavedState.java +++ b/core/java/android/view/AbsSavedState.java @@ -54,7 +54,7 @@ public abstract class AbsSavedState implements Parcelable { */ protected AbsSavedState(Parcel source) { // FIXME need class loader - Parcelable superState = (Parcelable) source.readParcelable(null); + Parcelable superState = source.readParcelable(null); mSuperState = superState != null ? superState : EMPTY_STATE; } @@ -75,7 +75,7 @@ public abstract class AbsSavedState implements Parcelable { = new Parcelable.Creator<AbsSavedState>() { public AbsSavedState createFromParcel(Parcel in) { - Parcelable superState = (Parcelable) in.readParcelable(null); + Parcelable superState = in.readParcelable(null); if (superState != null) { throw new IllegalStateException("superState must be null"); } diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java new file mode 100644 index 0000000..0ad3c0b --- /dev/null +++ b/core/java/android/view/GLES20Canvas.java @@ -0,0 +1,610 @@ +/* + * 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; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; + +import javax.microedition.khronos.opengles.GL; + +/** + * An implementation of Canvas on top of OpenGL ES 2.0. + */ +@SuppressWarnings({"deprecation"}) +class GLES20Canvas extends Canvas { + @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) + private final GL mGl; + private final boolean mOpaque; + private final int mRenderer; + + private int mWidth; + private int mHeight; + + private final float[] mPoint = new float[2]; + private final float[] mLine = new float[4]; + + private final Rect mClipBounds = new Rect(); + + private DrawFilter mFilter; + + /////////////////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////////////////// + + GLES20Canvas(GL gl, boolean translucent) { + mGl = gl; + mOpaque = !translucent; + + mRenderer = nCreateRenderer(); + } + + private native int nCreateRenderer(); + + @Override + protected void finalize() throws Throwable { + try { + super.finalize(); + } finally { + nDestroyRenderer(mRenderer); + } + } + + private native void nDestroyRenderer(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Canvas management + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isHardwareAccelerated() { + return true; + } + + @Override + public GL getGL() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBitmap(Bitmap bitmap) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + /////////////////////////////////////////////////////////////////////////// + // Setup + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setViewport(int width, int height) { + mWidth = width; + mHeight = height; + + nSetViewport(mRenderer, width, height); + } + + private native void nSetViewport(int renderer, int width, int height); + + void onPreDraw() { + nPrepare(mRenderer); + } + + private native void nPrepare(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Clipping + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean clipPath(Path path) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipPath(Path path, Region.Op op) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRect(float left, float top, float right, float bottom) { + return nClipRect(mRenderer, left, top, right, bottom); + } + + private native boolean nClipRect(int renderer, float left, float top, float right, float bottom); + + @Override + public boolean clipRect(float left, float top, float right, float bottom, Region.Op op) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRect(int left, int top, int right, int bottom) { + return nClipRect(mRenderer, left, top, right, bottom); + } + + private native boolean nClipRect(int renderer, int left, int top, int right, int bottom); + + @Override + public boolean clipRect(Rect rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + @Override + public boolean clipRect(Rect rect, Region.Op op) { + // TODO: Implement + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRect(RectF rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + @Override + public boolean clipRect(RectF rect, Region.Op op) { + // TODO: Implement + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRegion(Region region) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean clipRegion(Region region, Region.Op op) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getClipBounds(Rect bounds) { + return nGetClipBounds(mRenderer, bounds); + } + + private native boolean nGetClipBounds(int renderer, Rect bounds); + + @Override + public boolean quickReject(float left, float top, float right, float bottom, EdgeType type) { + return nQuickReject(mRenderer, left, top, right, bottom, type.nativeInt); + } + + private native boolean nQuickReject(int renderer, float left, float top, + float right, float bottom, int edge); + + @Override + public boolean quickReject(Path path, EdgeType type) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean quickReject(RectF rect, EdgeType type) { + return quickReject(rect.left, rect.top, rect.right, rect.bottom, type); + } + + /////////////////////////////////////////////////////////////////////////// + // Transformations + /////////////////////////////////////////////////////////////////////////// + + @Override + public void translate(float dx, float dy) { + nTranslate(mRenderer, dx, dy); + } + + private native void nTranslate(int renderer, float dx, float dy); + + @Override + public void skew(float sx, float sy) { + throw new UnsupportedOperationException(); + } + + @Override + public void rotate(float degrees) { + nRotate(mRenderer, degrees); + } + + private native void nRotate(int renderer, float degrees); + + @Override + public void scale(float sx, float sy) { + nScale(mRenderer, sx, sy); + } + + private native void nScale(int renderer, float sx, float sy); + + @Override + public void setMatrix(Matrix matrix) { + nSetMatrix(mRenderer, matrix.native_instance); + } + + private native void nSetMatrix(int renderer, int matrix); + + @Override + public void getMatrix(Matrix matrix) { + nGetMatrix(mRenderer, matrix.native_instance); + } + + private native void nGetMatrix(int renderer, int matrix); + + @Override + public void concat(Matrix matrix) { + nConcatMatrix(mRenderer, matrix.native_instance); + } + + private native void nConcatMatrix(int renderer, int matrix); + + /////////////////////////////////////////////////////////////////////////// + // State management + /////////////////////////////////////////////////////////////////////////// + + @Override + public int save() { + return nSave(mRenderer, 0); + } + + @Override + public int save(int saveFlags) { + return nSave(mRenderer, saveFlags); + } + + private native int nSave(int renderer, int flags); + + @Override + public int saveLayer(RectF bounds, Paint paint, int saveFlags) { + return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags); + } + + @Override + public int saveLayer(float left, float top, float right, float bottom, Paint paint, + int saveFlags) { + int nativePaint = paint == null ? 0 : paint.mNativePaint; + return nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); + } + + private native int nSaveLayer(int renderer, float left, float top, float right, float bottom, + int paint, int saveFlags); + + @Override + public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) { + return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, + alpha, saveFlags); + } + + @Override + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, + int saveFlags) { + return nSaveLayerAlpha(mRenderer, left, top, right, bottom, alpha, saveFlags); + } + + private native int nSaveLayerAlpha(int renderer, float left, float top, float right, + float bottom, int alpha, int saveFlags); + + @Override + public void restore() { + nRestore(mRenderer); + } + + private native void nRestore(int renderer); + + @Override + public void restoreToCount(int saveCount) { + nRestoreToCount(mRenderer, saveCount); + } + + private native void nRestoreToCount(int renderer, int saveCount); + + @Override + public int getSaveCount() { + return nGetSaveCount(mRenderer); + } + + private native int nGetSaveCount(int renderer); + + /////////////////////////////////////////////////////////////////////////// + // Filtering + /////////////////////////////////////////////////////////////////////////// + + @Override + public void setDrawFilter(DrawFilter filter) { + // Don't crash, but ignore the draw filter + // TODO: Implement PaintDrawFilter + mFilter = filter; + } + + @Override + public DrawFilter getDrawFilter() { + return mFilter; + } + + /////////////////////////////////////////////////////////////////////////// + // Drawing + /////////////////////////////////////////////////////////////////////////// + + @Override + public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, + Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawARGB(int a, int r, int g, int b) { + drawColor((a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); + } + + @Override + public void drawPatch(Bitmap bitmap, byte[] chunks, RectF dst, Paint paint) { + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawPatch(mRenderer, bitmap.mNativeBitmap, chunks, dst.left, dst.top, + dst.right, dst.bottom, nativePaint); + } + + private native void nDrawPatch(int renderer, int bitmap, byte[] chunks, float left, float top, + float right, float bottom, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, nativePaint); + } + + private native void nDrawBitmap(int renderer, int bitmap, float left, float top, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, matrix.native_instance, nativePaint); + } + + private native void nDrawBitmap(int renderer, int bitmap, int matrix, int paint); + + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, src.left, src.top, src.right, src.bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint + ); + } + + @Override + public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, src.left, src.top, src.right, src.bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint + ); + } + + private native void nDrawBitmap(int renderer, int bitmap, + float srcLeft, float srcTop, float srcRight, float srcBottom, + float left, float top, float right, float bottom, int paint); + + @Override + public void drawBitmap(int[] colors, int offset, int stride, float x, float y, + int width, int height, boolean hasAlpha, Paint paint) { + final Bitmap.Config config = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + final Bitmap b = Bitmap.createBitmap(colors, offset, stride, width, height, config); + final int nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, b.mNativeBitmap, x, y, nativePaint); + b.recycle(); + } + + @Override + public void drawBitmap(int[] colors, int offset, int stride, int x, int y, + int width, int height, boolean hasAlpha, Paint paint) { + drawBitmap(colors, offset, stride, (float) x, (float) y, width, height, hasAlpha, paint); + } + + @Override + public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, + int vertOffset, int[] colors, int colorOffset, Paint paint) { + // TODO: Implement + } + + @Override + public void drawCircle(float cx, float cy, float radius, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawColor(int color) { + drawColor(color, PorterDuff.Mode.SRC_OVER); + } + + @Override + public void drawColor(int color, PorterDuff.Mode mode) { + nDrawColor(mRenderer, color, mode.nativeInt); + } + + private native void nDrawColor(int renderer, int color, int mode); + + @Override + public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { + mLine[0] = startX; + mLine[1] = startY; + mLine[2] = stopX; + mLine[3] = stopY; + drawLines(mLine, 0, 1, paint); + } + + @Override + public void drawLines(float[] pts, int offset, int count, Paint paint) { + // TODO: Implement + } + + @Override + public void drawLines(float[] pts, Paint paint) { + drawLines(pts, 0, pts.length / 4, paint); + } + + @Override + public void drawOval(RectF oval, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPaint(Paint paint) { + final Rect r = mClipBounds; + nGetClipBounds(mRenderer, r); + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawPath(Path path, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPicture(Picture picture) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPicture(Picture picture, Rect dst) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPicture(Picture picture, RectF dst) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPoint(float x, float y, Paint paint) { + mPoint[0] = x; + mPoint[1] = y; + drawPoints(mPoint, 0, 1, paint); + } + + @Override + public void drawPoints(float[] pts, int offset, int count, Paint paint) { + // TODO: Implement + } + + @Override + public void drawPoints(float[] pts, Paint paint) { + drawPoints(pts, 0, pts.length / 2, paint); + } + + @Override + public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawPosText(String text, float[] pos, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawRect(float left, float top, float right, float bottom, Paint paint) { + nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); + } + + private native void nDrawRect(int renderer, float left, float top, float right, float bottom, + int paint); + + @Override + public void drawRect(Rect r, Paint paint) { + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawRect(RectF r, Paint paint) { + drawRect(r.left, r.top, r.right, r.bottom, paint); + } + + @Override + public void drawRGB(int r, int g, int b) { + drawColor(0xFF000000 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF)); + } + + @Override + public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawText(char[] text, int index, int count, float x, float y, Paint paint) { + // TODO: Implement + } + + @Override + public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { + // TODO: Implement + } + + @Override + public void drawText(String text, int start, int end, float x, float y, Paint paint) { + // TODO: Implement + } + + @Override + public void drawText(String text, float x, float y, Paint paint) { + drawText(text, 0, text.length(), x, y, paint); + } + + @Override + public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, + float vOffset, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, + float x, float y, int dir, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, + float x, float y, int dir, Paint paint) { + throw new UnsupportedOperationException(); + } + + @Override + public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, + float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, + int indexOffset, int indexCount, Paint paint) { + // TODO: Implement + } +} diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java new file mode 100644 index 0000000..090a743 --- /dev/null +++ b/core/java/android/view/HardwareRenderer.java @@ -0,0 +1,562 @@ +/* + * 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; + +import android.graphics.Canvas; +import android.os.SystemClock; +import android.util.Log; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL11; + +import static javax.microedition.khronos.opengles.GL10.GL_COLOR_BUFFER_BIT; +import static javax.microedition.khronos.opengles.GL10.GL_SCISSOR_TEST; + +/** + * Interface for rendering a ViewRoot using hardware acceleration. + * + * @hide + */ +abstract class HardwareRenderer { + private boolean mEnabled; + private boolean mRequested = true; + private static final String LOG_TAG = "HardwareRenderer"; + + /** + * Destroys the hardware rendering context. + */ + abstract void destroy(); + + /** + * Initializes the hardware renderer for the specified surface. + * + * @param holder The holder for the surface to hardware accelerate. + * + * @return True if the initialization was successful, false otherwise. + */ + abstract boolean initialize(SurfaceHolder holder); + + /** + * Setup the hardware renderer for drawing. This is called for every + * frame to draw. + * + * @param width Width of the drawing surface. + * @param height Height of the drawing surface. + * @param attachInfo The AttachInfo used to render the ViewRoot. + */ + abstract void setup(int width, int height, View.AttachInfo attachInfo); + + /** + * Draws the specified view. + * + * @param view The view to draw. + * @param attachInfo AttachInfo tied to the specified view. + */ + abstract void draw(View view, View.AttachInfo attachInfo, int yOffset); + + /** + * Initializes the hardware renderer for the specified surface and setup the + * renderer for drawing, if needed. This is invoked when the ViewRoot has + * 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 + */ + void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo, + SurfaceHolder holder) { + + if (isRequested()) { + // We lost the gl context, so recreate it. + if (!isEnabled()) { + if (initialize(holder)) { + setup(width, height, attachInfo); + } + } + } + } + + /** + * Creates a hardware renderer using OpenGL. + * + * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.) + * @param translucent True if the surface is translucent, false otherwise + * + * @return A hardware renderer backed by OpenGL. + */ + static HardwareRenderer createGlRenderer(int glVersion, boolean translucent) { + switch (glVersion) { + case 1: + return new Gl10Renderer(translucent); + case 2: + return new Gl20Renderer(translucent); + } + throw new IllegalArgumentException("Unknown GL version: " + glVersion); + } + + /** + * Indicates whether hardware acceleration is currently enabled. + * + * @return True if hardware acceleration is in use, false otherwise. + */ + boolean isEnabled() { + return mEnabled; + } + + /** + * Indicates whether hardware acceleration is currently enabled. + * + * @param enabled True if the hardware renderer is in use, false otherwise. + */ + void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + /** + * Indicates whether hardware acceleration is currently request but not + * necessarily enabled yet. + * + * @return True if requested, false otherwise. + */ + boolean isRequested() { + return mRequested; + } + + /** + * Indicates whether hardware acceleration is currently request but not + * necessarily enabled yet. + * + * @return True to request hardware acceleration, false otherwise. + */ + void setRequested(boolean requested) { + mRequested = requested; + } + + @SuppressWarnings({"deprecation"}) + static abstract class GlRenderer extends HardwareRenderer { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLContext mEglContext; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + + GL mGl; + Canvas mCanvas; + + final int mGlVersion; + final boolean mTranslucent; + + GlRenderer(int glVersion, boolean translucent) { + mGlVersion = glVersion; + mTranslucent = translucent; + } + + /** + * Checks for OpenGL errors. If an error has occured, {@link #destroy()} + * is invoked and the requested flag is turned off. The error code is + * also logged as a warning. + */ + void checkErrors() { + if (isEnabled()) { + int error = mEgl.eglGetError(); + if (error != EGL10.EGL_SUCCESS) { + // something bad has happened revert to + // normal rendering. + destroy(); + if (error != EGL11.EGL_CONTEXT_LOST) { + // we'll try again if it was context lost + setRequested(false); + } + Log.w(LOG_TAG, "OpenGL error: " + error); + } + } + } + + @Override + boolean initialize(SurfaceHolder holder) { + if (isRequested() && !isEnabled()) { + initializeEgl(); + mGl = createEglSurface(holder); + + if (mGl != null) { + int err = mEgl.eglGetError(); + if (err != EGL10.EGL_SUCCESS) { + destroy(); + setRequested(false); + } else { + mCanvas = createCanvas(); + if (mCanvas != null) { + setEnabled(true); + } else { + Log.w(LOG_TAG, "Hardware accelerated Canvas could not be created"); + } + } + + return mCanvas != null; + } + } + return false; + } + + abstract Canvas createCanvas(); + + void initializeEgl() { + mEgl = (EGL10) EGLContext.getEGL(); + + // Get to the default display. + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + // We can now initialize EGL for that display + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + mEglConfig = getConfigChooser(mGlVersion).chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = createContext(mEgl, mEglDisplay, mEglConfig); + } + + GL createEglSurface(SurfaceHolder holder) { + // Check preconditions. + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + + /* + * Unbind and destroy the old EGL surface, if + * there is one. + */ + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + mEgl.eglDestroySurface(mEglDisplay, mEglSurface); + } + + // Create an EGL surface we can render into. + mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, holder, null); + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + return null; + } + throw new RuntimeException("createWindowSurface failed"); + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + throw new RuntimeException("eglMakeCurrent failed"); + + } + + return mEglContext.getGL(); + } + + EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, mGlVersion, EGL10.EGL_NONE }; + + return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, + mGlVersion != 0 ? attrib_list : null); + } + + @Override + void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo, + SurfaceHolder holder) { + + if (isRequested()) { + checkErrors(); + super.initializeIfNeeded(width, height, attachInfo, holder); + } + } + + @Override + void destroy() { + if (!isEnabled()) return; + + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + mEgl.eglDestroyContext(mEglDisplay, mEglContext); + mEgl.eglDestroySurface(mEglDisplay, mEglSurface); + mEgl.eglTerminate(mEglDisplay); + + mEglContext = null; + mEglSurface = null; + mEglDisplay = null; + mEgl = null; + mGl = null; + mCanvas = null; + + setEnabled(false); + } + + @Override + void setup(int width, int height, View.AttachInfo attachInfo) { + final float scale = attachInfo.mApplicationScale; + mCanvas.setViewport((int) (width * scale + 0.5f), (int) (height * scale + 0.5f)); + } + + boolean canDraw() { + return mGl != null && mCanvas != null; + } + + void onPreDraw() { + } + + /** + * Defines the EGL configuration for this renderer. The default configuration + * is RGBX, no depth, no stencil. + * + * @return An {@link android.view.HardwareRenderer.GlRenderer.EglConfigChooser}. + * @param glVersion + */ + EglConfigChooser getConfigChooser(int glVersion) { + return new ComponentSizeChooser(glVersion, 8, 8, 8, mTranslucent ? 8 : 0, 0, 0); + } + + @Override + void draw(View view, View.AttachInfo attachInfo, int yOffset) { + if (canDraw()) { + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + attachInfo.mIgnoreDirtyState = true; + view.mPrivateFlags |= View.DRAWN; + + onPreDraw(); + + Canvas canvas = mCanvas; + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.translate(0, -yOffset); + + try { + view.draw(canvas); + } finally { + canvas.restoreToCount(saveCount); + } + + attachInfo.mIgnoreDirtyState = false; + + mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); + checkErrors(); + } + } + + static abstract class EglConfigChooser { + final int[] mConfigSpec; + private final int mGlVersion; + + EglConfigChooser(int glVersion, int[] configSpec) { + mGlVersion = glVersion; + mConfigSpec = filterConfigSpec(configSpec); + } + + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] index = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, index)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = index[0]; + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, index)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); + + private int[] filterConfigSpec(int[] configSpec) { + if (mGlVersion != 2) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); + newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; + newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ + newConfigSpec[len + 1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + static class ComponentSizeChooser extends EglConfigChooser { + private int[] mValue; + + private int mRedSize; + private int mGreenSize; + private int mBlueSize; + private int mAlphaSize; + private int mDepthSize; + private int mStencilSize; + + ComponentSizeChooser(int glVersion, int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(glVersion, new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE }); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + if (d >= mDepthSize && s >= mStencilSize) { + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + if (r == mRedSize && g == mGreenSize && b == mBlueSize && a >= mAlphaSize) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, + int attribute, int defaultValue) { + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + + return defaultValue; + } + } + } + + /** + * Hardware renderer using OpenGL ES 2.0. + */ + static class Gl20Renderer extends GlRenderer { + private GLES20Canvas mGlCanvas; + + Gl20Renderer(boolean translucent) { + super(2, translucent); + } + + @Override + Canvas createCanvas() { + return mGlCanvas = new GLES20Canvas(mGl, mTranslucent); + } + + @Override + void onPreDraw() { + mGlCanvas.onPreDraw(); + } + } + + /** + * Hardware renderer using OpenGL ES 1.0. + */ + @SuppressWarnings({"deprecation"}) + static class Gl10Renderer extends GlRenderer { + Gl10Renderer(boolean translucent) { + super(1, translucent); + } + + @Override + Canvas createCanvas() { + return new Canvas(mGl); + } + + @Override + void destroy() { + if (isEnabled()) { + nativeAbandonGlCaches(); + } + + super.destroy(); + } + + @Override + void onPreDraw() { + GL11 gl = (GL11) mGl; + gl.glDisable(GL_SCISSOR_TEST); + gl.glClearColor(0, 0, 0, 0); + gl.glClear(GL_COLOR_BUFFER_BIT); + gl.glEnable(GL_SCISSOR_TEST); + } + } + + // Inform Skia to just abandon its texture cache IDs doesn't call glDeleteTextures + // Used only by the native Skia OpenGL ES 1.x implementation + private static native void nativeAbandonGlCaches(); +} diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index e5985c1..479e757 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -16,15 +16,15 @@ package android.view; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.lang.reflect.Constructor; import java.util.HashMap; @@ -71,11 +71,11 @@ public abstract class LayoutInflater { private final Object[] mConstructorArgs = new Object[2]; - private static final Class[] mConstructorSignature = new Class[] { + private static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class}; - private static final HashMap<String, Constructor> sConstructorMap = - new HashMap<String, Constructor>(); + private static final HashMap<String, Constructor<? extends View>> sConstructorMap = + new HashMap<String, Constructor<? extends View>>(); private HashMap<String, Boolean> mFilterMap; @@ -97,6 +97,7 @@ public abstract class LayoutInflater { * * @return True if this class is allowed to be inflated, or false otherwise */ + @SuppressWarnings("unchecked") boolean onLoadClass(Class clazz); } @@ -379,7 +380,7 @@ public abstract class LayoutInflater { + "ViewGroup root and attachToRoot=true"); } - rInflate(parser, root, attrs); + rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml View temp = createViewFromTag(name, attrs); @@ -404,7 +405,7 @@ public abstract class LayoutInflater { System.out.println("-----> start inflating children"); } // Inflate all children under temp - rInflate(parser, temp, attrs); + rInflate(parser, temp, attrs, true); if (DEBUG) { System.out.println("-----> done inflating children"); } @@ -453,18 +454,18 @@ public abstract class LayoutInflater { * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * - * @return View The newly instantied view, or null. + * @return View The newly instantiated view, or null. */ public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { - Constructor constructor = sConstructorMap.get(name); - Class clazz = null; + Constructor<? extends View> constructor = sConstructorMap.get(name); + Class<? extends View> clazz = null; try { if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( - prefix != null ? (prefix + name) : name); + prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); @@ -482,7 +483,7 @@ public abstract class LayoutInflater { if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( - prefix != null ? (prefix + name) : name); + prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); @@ -497,7 +498,7 @@ public abstract class LayoutInflater { Object[] args = mConstructorArgs; args[1] = attrs; - return (View) constructor.newInstance(args); + return constructor.newInstance(args); } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() @@ -506,6 +507,13 @@ public abstract class LayoutInflater { ie.initCause(e); throw ie; + } catch (ClassCastException e) { + // If loaded class is not a View subclass + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Class is not a View " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; @@ -519,7 +527,7 @@ public abstract class LayoutInflater { } /** - * Throw an excpetion because the specified class is not allowed to be inflated. + * Throw an exception because the specified class is not allowed to be inflated. */ private void failNotAllowed(String name, String prefix, AttributeSet attrs) { InflateException ie = new InflateException(attrs.getPositionDescription() @@ -590,8 +598,8 @@ public abstract class LayoutInflater { * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). */ - private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) - throws XmlPullParserException, IOException { + private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, + boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; @@ -618,12 +626,12 @@ public abstract class LayoutInflater { final View view = createViewFromTag(name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); - rInflate(parser, view, attrs); + rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } } - parent.onFinishInflate(); + if (finishInflate) parent.onFinishInflate(); } private void parseRequestFocus(XmlPullParser parser, View parent) @@ -674,7 +682,7 @@ public abstract class LayoutInflater { if (TAG_MERGE.equals(childName)) { // Inflate all children. - rInflate(childParser, parent, childAttrs); + rInflate(childParser, parent, childAttrs, false); } else { final View view = createViewFromTag(childName, childAttrs); final ViewGroup group = (ViewGroup) parent; @@ -699,7 +707,7 @@ public abstract class LayoutInflater { } // Inflate all children. - rInflate(childParser, view, childAttrs); + rInflate(childParser, view, childAttrs, true); // Attempt to override the included layout's android:id with the // one set on the <include /> tag itself. diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java index 46c805c..4a966b5 100644 --- a/core/java/android/view/MenuInflater.java +++ b/core/java/android/view/MenuInflater.java @@ -16,9 +16,8 @@ package android.view; -import com.android.internal.view.menu.MenuItemImpl; - import java.io.IOException; +import java.lang.reflect.Method; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -30,6 +29,8 @@ import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; +import com.android.internal.view.menu.MenuItemImpl; + /** * This class is used to instantiate menu XML files into Menu objects. * <p> @@ -166,6 +167,41 @@ public class MenuInflater { } } + private static class InflatedOnMenuItemClickListener + implements MenuItem.OnMenuItemClickListener { + private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; + + private Context mContext; + private Method mMethod; + + public InflatedOnMenuItemClickListener(Context context, String methodName) { + mContext = context; + Class c = context.getClass(); + try { + mMethod = c.getMethod(methodName, PARAM_TYPES); + } catch (Exception e) { + InflateException ex = new InflateException( + "Couldn't resolve menu item onClick handler " + methodName + + " in class " + c.getName()); + ex.initCause(e); + throw ex; + } + } + + public boolean onMenuItemClick(MenuItem item) { + try { + if (mMethod.getReturnType() == Boolean.TYPE) { + return (Boolean) mMethod.invoke(mContext, item); + } else { + mMethod.invoke(mContext, item); + return true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + /** * State for the current menu. * <p> @@ -205,6 +241,16 @@ public class MenuInflater { private boolean itemVisible; private boolean itemEnabled; + /** + * Sync to attrs.xml enum, values in MenuItem: + * - 0: never + * - 1: ifRoom + * - 2: always + */ + private int itemShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; + + private String itemListenerMethodName; + private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; @@ -276,6 +322,8 @@ public class MenuInflater { itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); + itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, 0); + itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); a.recycle(); @@ -298,10 +346,19 @@ public class MenuInflater { .setTitleCondensed(itemTitleCondensed) .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) - .setNumericShortcut(itemNumericShortcut); + .setNumericShortcut(itemNumericShortcut) + .setShowAsAction(itemShowAsAction); + + if (itemListenerMethodName != null) { + item.setOnMenuItemClickListener( + new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); + } - if (itemCheckable >= 2) { - ((MenuItemImpl) item).setExclusiveCheckable(true); + if (item instanceof MenuItemImpl) { + MenuItemImpl impl = (MenuItemImpl) item; + if (itemCheckable >= 2) { + impl.setExclusiveCheckable(true); + } } } diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index fcebec5..bfa349c 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -31,6 +31,21 @@ import android.view.View.OnCreateContextMenuListener; * For a feature set of specific menu types, see {@link Menu}. */ public interface MenuItem { + /* + * These should be kept in sync with attrs.xml enum constants for showAsAction + */ + /** Never show this item as a button in an Action Bar. */ + public static final int SHOW_AS_ACTION_NEVER = 0; + /** Show this item as a button in an Action Bar if the system decides there is room for it. */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + /** + * Always show this item as a button in an Action Bar. + * Use sparingly! If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with smaller screens. + * A good rule of thumb is to have no more than 2 items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + /** * Interface definition for a callback to be invoked when a menu item is * clicked. @@ -381,4 +396,13 @@ public interface MenuItem { * menu item to the menu. This can be null. */ public ContextMenuInfo getMenuInfo(); + + /** + * Sets how this item should display in the presence of an Action Bar. + * + * @param actionEnum How the item should display. One of + * + * @see android.app.ActionBar + */ + public void setShowAsAction(int actionEnum); }
\ No newline at end of file diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index ae8c21d..35e229a 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -722,7 +722,7 @@ public final class MotionEvent implements Parcelable { * * @param pointerId The identifier of the pointer to be found. * @return Returns either the index of the pointer (for use with - * {@link #getX(int) et al.), or -1 if there is no data available for + * {@link #getX(int)} et al.), or -1 if there is no data available for * that pointer identifier. */ public final int findPointerIndex(int pointerId) { diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index c469bcc..54cb4ca 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -543,6 +543,9 @@ public class SurfaceView extends View { } if (creating || formatChanged || sizeChanged || visibleChanged || realSizeChanged) { + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); + } } if (redrawNeeded) { for (SurfaceHolder.Callback c : callbacks) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 329b2e7..0831fb1 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -887,6 +887,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public static final int HAPTIC_FEEDBACK_ENABLED = 0x10000000; /** + * <p>Indicates that the view hierarchy should stop saving state when + * it reaches this view. If state saving is initiated immediately at + * the view, it will be allowed. + * {@hide} + */ + static final int PARENT_SAVE_DISABLED = 0x20000000; + + /** + * <p>Mask for use with setFlags indicating bits used for PARENT_SAVE_DISABLED.</p> + * {@hide} + */ + static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; + + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. */ @@ -3352,6 +3366,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** + * Indicates whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. The default + * is true; if false, these views will not be saved unless + * {@link #saveHierarchyState(SparseArray)} is called directly on this view. + * + * @return Returns true if the view state saving from parent is enabled, else false. + * + * @see #setSaveFromParentEnabled(boolean) + */ + public boolean isSaveFromParentEnabled() { + return (mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED; + } + + /** + * Controls whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. The default + * is true; if false, these views will not be saved unless + * {@link #saveHierarchyState(SparseArray)} is called directly on this view. + * + * @param enabled Set to false to <em>disable</em> state saving, or true + * (the default) to allow it. + * + * @see #isSaveFromParentEnabled() + * @see #setId(int) + * @see #onSaveInstanceState() + */ + public void setSaveFromParentEnabled(boolean enabled) { + setFlags(enabled ? 0 : PARENT_SAVE_DISABLED, PARENT_SAVE_DISABLED_MASK); + } + + + /** * Returns whether this View is able to take focus. * * @return True if this view can take focus, or false otherwise. @@ -6812,16 +6858,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (verticalEdges) { topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); - drawTop = topFadeStrength >= 0.0f; + drawTop = topFadeStrength > 0.0f; bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); - drawBottom = bottomFadeStrength >= 0.0f; + drawBottom = bottomFadeStrength > 0.0f; } if (horizontalEdges) { leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); - drawLeft = leftFadeStrength >= 0.0f; + drawLeft = leftFadeStrength > 0.0f; rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); - drawRight = rightFadeStrength >= 0.0f; + drawRight = rightFadeStrength > 0.0f; } saveCount = canvas.getSaveCount(); @@ -8558,13 +8604,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (mAttachInfo == null) { return false; } - if ((flags&HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0 + if ((flags & HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0 && !isHapticFeedbackEnabled()) { return false; } - return mAttachInfo.mRootCallbacks.performHapticFeedback( - feedbackConstant, - (flags&HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0); + return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, + (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0); } /** @@ -8635,8 +8680,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility ViewConfiguration.getLongPressTimeout() - delayOffset); } - private static int[] stateSetUnion(final int[] stateSet1, - final int[] stateSet2) { + private static int[] stateSetUnion(final int[] stateSet1, final int[] stateSet2) { final int stateSet1Length = stateSet1.length; final int stateSet2Length = stateSet2.length; final int[] newSet = new int[stateSet1Length + stateSet2Length]; @@ -8674,7 +8718,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); } - + /** * A MeasureSpec encapsulates the layout requirements passed from parent to child. * Each MeasureSpec represents a requirement for either the width or the height. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e7b6c50..34777ce 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -86,10 +86,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // The view contained within this ViewGroup that has or contains focus. private View mFocused; - // The current transformation to apply on the child being drawn - private Transformation mChildTransformation; + /** + * A Transformation used when drawing children, to + * apply on the child being drawn. + */ + private final Transformation mChildTransformation = new Transformation(); + + /** + * Used to track the current invalidation region. + */ private RectF mInvalidateRegion; + /** + * A Transformation used to calculate a correct + * invalidation area when the application is autoscaled. + */ + private Transformation mInvalidationTransformation; + // Target of Motion events private View mMotionTarget; private final Rect mTempRect = new Rect(); @@ -1182,7 +1195,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { - children[i].dispatchSaveInstanceState(container); + View c = children[i]; + if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { + c.dispatchSaveInstanceState(container); + } } } @@ -1207,7 +1223,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { - children[i].dispatchRestoreInstanceState(container); + View c = children[i]; + if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { + c.dispatchRestoreInstanceState(container); + } } } @@ -1477,21 +1496,25 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int flags = mGroupFlags; if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) { - if (mChildTransformation != null) { - mChildTransformation.clear(); - } + mChildTransformation.clear(); mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION; } Transformation transformToApply = null; + Transformation invalidationTransform; final Animation a = child.getAnimation(); boolean concatMatrix = false; + boolean scalingRequired = false; + boolean caching = false; + if (!canvas.isHardwareAccelerated() && + (flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE || + (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) { + caching = true; + if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; + } + if (a != null) { - if (mInvalidateRegion == null) { - mInvalidateRegion = new RectF(); - } - final RectF region = mInvalidateRegion; final boolean initialized = a.isInitialized(); if (!initialized) { @@ -1500,10 +1523,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager child.onAnimationStart(); } - if (mChildTransformation == null) { - mChildTransformation = new Transformation(); + more = a.getTransformation(drawingTime, mChildTransformation, + scalingRequired ? mAttachInfo.mApplicationScale : 1f); + if (scalingRequired && mAttachInfo.mApplicationScale != 1f) { + if (mInvalidationTransformation == null) { + mInvalidationTransformation = new Transformation(); + } + invalidationTransform = mInvalidationTransformation; + a.getTransformation(drawingTime, invalidationTransform, 1f); + } else { + invalidationTransform = mChildTransformation; } - more = a.getTransformation(drawingTime, mChildTransformation); transformToApply = mChildTransformation; concatMatrix = a.willChangeTransformationMatrix(); @@ -1520,7 +1550,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager invalidate(cl, ct, cr, cb); } } else { - a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply); + if (mInvalidateRegion == null) { + mInvalidateRegion = new RectF(); + } + final RectF region = mInvalidateRegion; + a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, invalidationTransform); // The child need to draw an animation, potentially offscreen, so // make sure we do not cancel invalidate requests @@ -1533,9 +1567,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == FLAG_SUPPORT_STATIC_TRANSFORMATIONS) { - if (mChildTransformation == null) { - mChildTransformation = new Transformation(); - } final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation); if (hasTransform) { final int transformType = mChildTransformation.getTransformationType(); @@ -1559,12 +1590,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int sx = child.mScrollX; final int sy = child.mScrollY; - boolean scalingRequired = false; Bitmap cache = null; - if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE || - (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) { + if (caching) { cache = child.getDrawingCache(true); - if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; } final boolean hasNoCache = cache == null; @@ -2870,7 +2898,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"), - @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ANIMATION"), + @ViewDebug.IntToString(from = PERSISTENT_ANIMATION_CACHE, to = "ANIMATION"), @ViewDebug.IntToString(from = PERSISTENT_SCROLLING_CACHE, to = "SCROLLING"), @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ALL") }) diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 260bf7bc..a89e7f6 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -16,6 +16,7 @@ package android.view; +import android.content.pm.ApplicationInfo; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.IInputMethodCallback; import com.android.internal.view.IInputMethodSession; @@ -33,7 +34,6 @@ import android.util.Config; import android.util.DisplayMetrics; import android.util.Log; import android.util.EventLog; -import android.util.Slog; import android.util.SparseArray; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; @@ -52,15 +52,10 @@ import android.Manifest; import android.media.AudioManager; import java.lang.ref.WeakReference; -import java.io.FileDescriptor; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; -import javax.microedition.khronos.egl.*; -import javax.microedition.khronos.opengles.*; -import static javax.microedition.khronos.opengles.GL10.*; - /** * The top of a view hierarchy, implementing the needed protocol between View * and the WindowManager. This is for the most part an internal implementation @@ -68,14 +63,12 @@ import static javax.microedition.khronos.opengles.GL10.*; * * {@hide} */ -@SuppressWarnings({"EmptyCatchBlock"}) -public final class ViewRoot extends Handler implements ViewParent, - View.AttachInfo.Callbacks { +@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) +public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { private static final String TAG = "ViewRoot"; private static final boolean DBG = false; private static final boolean SHOW_FPS = false; - @SuppressWarnings({"ConstantConditionalExpression"}) - private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; /** @noinspection PointlessBooleanExpression*/ private static final boolean DEBUG_DRAW = false || LOCAL_LOGV; private static final boolean DEBUG_LAYOUT = false || LOCAL_LOGV; @@ -205,14 +198,7 @@ public final class ViewRoot extends Handler implements ViewParent, int mCurScrollY; Scroller mScroller; - EGL10 mEgl; - EGLDisplay mEglDisplay; - EGLContext mEglContext; - EGLSurface mEglSurface; - GL11 mGL; - Canvas mGlCanvas; - boolean mUseGL; - boolean mGlWanted; + HardwareRenderer mHwRenderer; final ViewConfiguration mViewConfiguration; @@ -242,8 +228,10 @@ public final class ViewRoot extends Handler implements ViewParent, public ViewRoot(Context context) { super(); - if (MEASURE_LATENCY && lt == null) { - lt = new LatencyTimer(100, 1000); + if (MEASURE_LATENCY) { + if (lt == null) { + lt = new LatencyTimer(100, 1000); + } } // For debug only @@ -331,122 +319,18 @@ public final class ViewRoot extends Handler implements ViewParent, return false; } - private void initializeGL() { - initializeGLInner(); - int err = mEgl.eglGetError(); - if (err != EGL10.EGL_SUCCESS) { - // give-up on using GL - destroyGL(); - mGlWanted = false; - } - } - - private void initializeGLInner() { - final EGL10 egl = (EGL10) EGLContext.getEGL(); - mEgl = egl; - - /* - * Get to the default display. - */ - final EGLDisplay eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - mEglDisplay = eglDisplay; - - /* - * We can now initialize EGL for that display - */ - int[] version = new int[2]; - egl.eglInitialize(eglDisplay, version); - - /* - * Specify a configuration for our opengl session - * and grab the first configuration that matches is - */ - final int[] configSpec = { - EGL10.EGL_RED_SIZE, 5, - EGL10.EGL_GREEN_SIZE, 6, - EGL10.EGL_BLUE_SIZE, 5, - EGL10.EGL_DEPTH_SIZE, 0, - EGL10.EGL_NONE - }; - final EGLConfig[] configs = new EGLConfig[1]; - final int[] num_config = new int[1]; - egl.eglChooseConfig(eglDisplay, configSpec, configs, 1, num_config); - final EGLConfig config = configs[0]; - - /* - * Create an OpenGL ES context. This must be done only once, an - * OpenGL context is a somewhat heavy object. - */ - final EGLContext context = egl.eglCreateContext(eglDisplay, config, - EGL10.EGL_NO_CONTEXT, null); - mEglContext = context; - - /* - * Create an EGL surface we can render into. - */ - final EGLSurface surface = egl.eglCreateWindowSurface(eglDisplay, config, mHolder, null); - mEglSurface = surface; - - /* - * Before we can issue GL commands, we need to make sure - * the context is current and bound to a surface. - */ - egl.eglMakeCurrent(eglDisplay, surface, surface, context); - - /* - * Get to the appropriate GL interface. - * This is simply done by casting the GL context to either - * GL10 or GL11. - */ - final GL11 gl = (GL11) context.getGL(); - mGL = gl; - mGlCanvas = new Canvas(gl); - mUseGL = true; - } - - private void destroyGL() { - // inform skia that the context is gone - nativeAbandonGlCaches(); - - mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); - mEgl.eglDestroyContext(mEglDisplay, mEglContext); - mEgl.eglDestroySurface(mEglDisplay, mEglSurface); - mEgl.eglTerminate(mEglDisplay); - mEglContext = null; - mEglSurface = null; - mEglDisplay = null; - mEgl = null; - mGlCanvas = null; - mGL = null; - mUseGL = false; - } - - private void checkEglErrors() { - if (mUseGL) { - int err = mEgl.eglGetError(); - if (err != EGL10.EGL_SUCCESS) { - // something bad has happened revert to - // normal rendering. - destroyGL(); - if (err != EGL11.EGL_CONTEXT_LOST) { - // we'll try again if it was context lost - mGlWanted = false; - } - } - } - } - /** * We have one child */ - public void setView(View view, WindowManager.LayoutParams attrs, - View panelParentView) { + public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; mWindowAttributes.copyFrom(attrs); attrs = mWindowAttributes; + + enableHardwareAcceleration(view, attrs); + if (view instanceof RootViewSurfaceTaker) { mSurfaceHolderCallback = ((RootViewSurfaceTaker)view).willYouTakeTheSurface(); @@ -576,6 +460,20 @@ public final class ViewRoot extends Handler implements ViewParent, } } + private void enableHardwareAcceleration(View view, WindowManager.LayoutParams attrs) { + // Only enable hardware acceleration if we are not in the system process + // The window manager creates ViewRoots to display animated preview windows + // of launching apps and we don't want those to be hardware accelerated + if (Process.myUid() != Process.SYSTEM_UID) { + // Try to enable hardware acceleration if requested + if ((view.getContext().getApplicationInfo().flags & + ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + final boolean translucent = attrs.format != PixelFormat.OPAQUE; + mHwRenderer = HardwareRenderer.createGlRenderer(2, translucent); + } + } + } + public View getView() { return mView; } @@ -732,8 +630,6 @@ public final class ViewRoot extends Handler implements ViewParent, boolean viewVisibilityChanged = mViewVisibility != viewVisibility || mNewSurfaceNeeded; - float appScale = mAttachInfo.mApplicationScale; - WindowManager.LayoutParams params = null; if (mWindowAttributesChanged) { mWindowAttributesChanged = false; @@ -781,8 +677,8 @@ public final class ViewRoot extends Handler implements ViewParent, attachInfo.mWindowVisibility = viewVisibility; host.dispatchWindowVisibilityChanged(viewVisibility); if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) { - if (mUseGL) { - destroyGL(); + if (mHwRenderer != null) { + mHwRenderer.destroy(); } } if (viewVisibility == View.GONE) { @@ -898,10 +794,12 @@ public final class ViewRoot extends Handler implements ViewParent, final boolean computesInternalInsets = attachInfo.mTreeObserver.hasComputeInternalInsetsListeners(); + boolean insetsPending = false; int relayoutResult = 0; - if (mFirst || windowShouldResize || insetsChanged - || viewVisibilityChanged || params != null) { + + if (mFirst || windowShouldResize || insetsChanged || + viewVisibilityChanged || params != null) { if (viewVisibility == View.VISIBLE) { // If this window is giving internal insets to the window @@ -913,26 +811,19 @@ public final class ViewRoot extends Handler implements ViewParent, // window, waiting until we can finish laying out this window // and get back to the window manager with the ultimately // computed insets. - insetsPending = computesInternalInsets - && (mFirst || viewVisibilityChanged); - - if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) { - if (params == null) { - params = mWindowAttributes; - } - mGlWanted = true; - } + insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged); } if (mSurfaceHolder != null) { mSurfaceHolder.mSurfaceLock.lock(); mDrawingAllowed = true; } - - boolean initialized = false; + + boolean hwIntialized = false; boolean contentInsetsChanged = false; boolean visibleInsetsChanged; boolean hadSurface = mSurface.isValid(); + try { int fl = 0; if (params != null) { @@ -992,9 +883,8 @@ public final class ViewRoot extends Handler implements ViewParent, fullRedrawNeeded = true; mPreviousTransparentRegion.setEmpty(); - if (mGlWanted && !mUseGL) { - initializeGL(); - initialized = mGlCanvas != null; + if (mHwRenderer != null) { + hwIntialized = mHwRenderer.initialize(mHolder); } } } else if (!mSurface.isValid()) { @@ -1072,9 +962,8 @@ public final class ViewRoot extends Handler implements ViewParent, } } - if (initialized) { - mGlCanvas.setViewport((int) (mWidth * appScale + 0.5f), - (int) (mHeight * appScale + 0.5f)); + if (hwIntialized) { + mHwRenderer.setup(mWidth, mHeight, mAttachInfo); } boolean focusChangedDueToTouchMode = ensureTouchModeLocally( @@ -1347,7 +1236,8 @@ public final class ViewRoot extends Handler implements ViewParent, if (!sFirstDrawComplete) { synchronized (sFirstDrawHandlers) { sFirstDrawComplete = true; - for (int i=0; i<sFirstDrawHandlers.size(); i++) { + final int count = sFirstDrawHandlers.size(); + for (int i = 0; i< count; i++) { post(sFirstDrawHandlers.get(i)); } } @@ -1381,53 +1271,16 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (mUseGL) { + if (mHwRenderer != null && mHwRenderer.isEnabled()) { if (!dirty.isEmpty()) { - Canvas canvas = mGlCanvas; - if (mGL != null && canvas != null) { - mGL.glDisable(GL_SCISSOR_TEST); - mGL.glClearColor(0, 0, 0, 0); - mGL.glClear(GL_COLOR_BUFFER_BIT); - mGL.glEnable(GL_SCISSOR_TEST); - - mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); - mAttachInfo.mIgnoreDirtyState = true; - mView.mPrivateFlags |= View.DRAWN; - - int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); - try { - canvas.translate(0, -yoff); - if (mTranslator != null) { - mTranslator.translateCanvas(canvas); - } - canvas.setScreenDensity(scalingRequired - ? DisplayMetrics.DENSITY_DEVICE : 0); - mView.draw(canvas); - if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { - mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); - } - } finally { - canvas.restoreToCount(saveCount); - } - - mAttachInfo.mIgnoreDirtyState = false; - - mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); - checkEglErrors(); - - if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) { - int now = (int)SystemClock.elapsedRealtime(); - if (sDrawTime != 0) { - nativeShowFPS(canvas, now - sDrawTime); - } - sDrawTime = now; - } - } + mHwRenderer.draw(mView, mAttachInfo, yoff); } + if (scrolling) { mFullRedrawNeeded = true; scheduleTraversals(); } + return; } @@ -1739,8 +1592,6 @@ public final class ViewRoot extends Handler implements ViewParent, } void dispatchDetachedFromWindow() { - if (Config.LOGV) Log.v("ViewRoot", "Detaching in " + this + " of " + mSurface); - if (mView != null) { mView.dispatchDetachedFromWindow(); } @@ -1749,8 +1600,8 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mRootView = null; mAttachInfo.mSurface = null; - if (mUseGL) { - destroyGL(); + if (mHwRenderer != null) { + mHwRenderer.destroy(); } mSurface.release(); @@ -1934,18 +1785,8 @@ public final class ViewRoot extends Handler implements ViewParent, boolean inTouchMode = msg.arg2 != 0; ensureTouchModeLocally(inTouchMode); - if (mGlWanted) { - checkEglErrors(); - // we lost the gl context, so recreate it. - if (mGlWanted && !mUseGL) { - initializeGL(); - if (mGlCanvas != null) { - float appScale = mAttachInfo.mApplicationScale; - mGlCanvas.setViewport( - (int) (mWidth * appScale + 0.5f), - (int) (mHeight * appScale + 0.5f)); - } - } + if (mHwRenderer != null) { + mHwRenderer.initializeIfNeeded(mWidth, mHeight, mAttachInfo, mHolder); } } @@ -1995,8 +1836,7 @@ public final class ViewRoot extends Handler implements ViewParent, if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) { // The IME is trying to say this event is from the // system! Bad bad bad! - event = KeyEvent.changeFlags(event, - event.getFlags()&~KeyEvent.FLAG_FROM_SYSTEM); + event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); } deliverKeyEventToViewHierarchy((KeyEvent)msg.obj, false); } break; @@ -2479,8 +2319,7 @@ public final class ViewRoot extends Handler implements ViewParent, private void deliverKeyEvent(KeyEvent event, boolean sendDone) { // If mView is null, we just consume the key event because it doesn't // make sense to do anything else with it. - boolean handled = mView != null - ? mView.dispatchKeyEventPreIme(event) : true; + boolean handled = mView == null || mView.dispatchKeyEventPreIme(event); if (handled) { if (sendDone) { if (LOCAL_LOGV) Log.v( @@ -2514,7 +2353,6 @@ public final class ViewRoot extends Handler implements ViewParent, final boolean sendDone = seq >= 0; if (!handled) { deliverKeyEventToViewHierarchy(event, sendDone); - return; } else if (sendDone) { if (LOCAL_LOGV) Log.v( "ViewRoot", "Telling window manager key is finished"); @@ -2715,7 +2553,7 @@ public final class ViewRoot extends Handler implements ViewParent, void doDie() { checkThread(); - if (Config.LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface); + if (LOCAL_LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface); synchronized (this) { if (mAdded && !mFirst) { int viewVisibility = mView.getVisibility(); @@ -2798,13 +2636,12 @@ public final class ViewRoot extends Handler implements ViewParent, if (event.getAction() == KeyEvent.ACTION_DOWN) { //noinspection ConstantConditions if (false && event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) { - if (Config.LOGD) Log.d("keydisp", - "==================================================="); - if (Config.LOGD) Log.d("keydisp", "Focused view Hierarchy is:"); + if (DBG) Log.d("keydisp", "==================================================="); + if (DBG) Log.d("keydisp", "Focused view Hierarchy is:"); + debug(); - if (Config.LOGD) Log.d("keydisp", - "==================================================="); + if (DBG) Log.d("keydisp", "==================================================="); } } @@ -3374,8 +3211,4 @@ public final class ViewRoot extends Handler implements ViewParent, } private static native void nativeShowFPS(Canvas canvas, int durationMillis); - - // inform skia to just abandon its texture cache IDs - // doesn't call glDeleteTextures - private static native void nativeAbandonGlCaches(); } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 11c09c1..be681cc 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -61,6 +61,13 @@ public abstract class Window { @hide */ public static final int FEATURE_OPENGL = 8; + /** + * Flag for enabling the Action Bar. + * This is enabled by default for some devices. The Action Bar + * replaces the title bar and provides an alternate location + * for an on-screen menu button on some devices. + */ + public static final int FEATURE_ACTION_BAR = 9; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; /** Flag for setting the progress bar's visibility to GONE */ @@ -817,6 +824,8 @@ public abstract class Window { public abstract void togglePanel(int featureId, KeyEvent event); + public abstract void invalidatePanelMenu(int featureId); + public abstract boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, @@ -996,6 +1005,16 @@ public abstract class Window { { return mFeatures; } + + /** + * Query for the availability of a certain feature. + * + * @param feature The feature ID to check + * @return true if the feature is enabled, false otherwise. + */ + public boolean hasFeature(int feature) { + return (getFeatures() & (1 << feature)) != 0; + } /** * Return the feature bits that are being implemented by this Window. diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index c22f991..fc61700 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -622,6 +622,7 @@ public final class AccessibilityEvent implements Parcelable { mPackageName = null; mContentDescription = null; mBeforeText = null; + mParcelableData = null; mText.clear(); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 0186270..f406da9 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -94,7 +94,9 @@ public final class AccessibilityManager { public static AccessibilityManager getInstance(Context context) { synchronized (sInstanceSync) { if (sInstance == null) { - sInstance = new AccessibilityManager(context); + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + IAccessibilityManager service = IAccessibilityManager.Stub.asInterface(iBinder); + sInstance = new AccessibilityManager(context, service); } } return sInstance; @@ -104,13 +106,16 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. + * @param service An interface to the backing service. + * + * @hide */ - private AccessibilityManager(Context context) { + public AccessibilityManager(Context context, IAccessibilityManager service) { mHandler = new MyHandler(context.getMainLooper()); - IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); - mService = IAccessibilityManager.Stub.asInterface(iBinder); + mService = service; + try { - mService.addClient(mClient); + mIsEnabled = mService.addClient(mClient); } catch (RemoteException re) { Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); } @@ -128,6 +133,18 @@ public final class AccessibilityManager { } /** + * Returns the client interface this instance registers in + * the centralized accessibility manager service. + * + * @return The client. + * + * @hide + */ + public IAccessibilityManagerClient getClient() { + return (IAccessibilityManagerClient) mClient.asBinder(); + } + + /** * Sends an {@link AccessibilityEvent}. If this {@link AccessibilityManager} is not * enabled the call is a NOOP. * diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 32788be..7633569 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -29,7 +29,7 @@ import android.content.pm.ServiceInfo; */ interface IAccessibilityManager { - void addClient(IAccessibilityManagerClient client); + boolean addClient(IAccessibilityManagerClient client); boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent); diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 349b7e5..f3392d9 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -174,7 +174,13 @@ public abstract class Animation implements Cloneable { * Desired Z order mode during animation. */ private int mZAdjustment; - + + /** + * scalefactor to apply to pivot points, etc. during animation. Subclasses retrieve the + * value via getScaleFactor(). + */ + private float mScaleFactor = 1f; + /** * Don't animate the wallpaper. */ @@ -553,6 +559,19 @@ public abstract class Animation implements Cloneable { } /** + * The scale factor is set by the call to <code>getTransformation</code>. Overrides of + * {@link #getTransformation(long, Transformation, float)} will get this value + * directly. Overrides of {@link #applyTransformation(float, Transformation)} can + * call this method to get the value. + * + * @return float The scale factor that should be applied to pre-scaled values in + * an Animation such as the pivot points in {@link ScaleAnimation} and {@link RotateAnimation}. + */ + protected float getScaleFactor() { + return mScaleFactor; + } + + /** * If detachWallpaper is true, and this is a window animation of a window * that has a wallpaper background, then the window will be detached from * the wallpaper while it runs. That is, the animation will only be applied @@ -735,6 +754,7 @@ public abstract class Animation implements Cloneable { * @return True if the animation is still running */ public boolean getTransformation(long currentTime, Transformation outTransformation) { + if (mStartTime == -1) { mStartTime = currentTime; } @@ -806,6 +826,24 @@ public abstract class Animation implements Cloneable { return mMore; } + + /** + * Gets the transformation to apply at a specified point in time. Implementations of this + * method should always replace the specified Transformation or document they are doing + * otherwise. + * + * @param currentTime Where we are in the animation. This is wall clock time. + * @param outTransformation A tranformation object that is provided by the + * caller and will be filled in by the animation. + * @param scale Scaling factor to apply to any inputs to the transform operation, such + * pivot points being rotated or scaled around. + * @return True if the animation is still running + */ + public boolean getTransformation(long currentTime, Transformation outTransformation, + float scale) { + mScaleFactor = scale; + return getTransformation(currentTime, outTransformation); + } /** * <p>Indicates whether this animation has started or not.</p> diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 1546dcd..873ce53 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -312,7 +312,7 @@ public class AnimationSet extends Animation { final Animation a = animations.get(i); temp.clear(); - more = a.getTransformation(currentTime, temp) || more; + more = a.getTransformation(currentTime, temp, getScaleFactor()) || more; t.compose(temp); started = started || a.hasStarted(); diff --git a/core/java/android/view/animation/RotateAnimation.java b/core/java/android/view/animation/RotateAnimation.java index 284ccce..58bf084 100644 --- a/core/java/android/view/animation/RotateAnimation.java +++ b/core/java/android/view/animation/RotateAnimation.java @@ -148,11 +148,12 @@ public class RotateAnimation extends Animation { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime); - + float scale = getScaleFactor(); + if (mPivotX == 0.0f && mPivotY == 0.0f) { t.getMatrix().setRotate(degrees); } else { - t.getMatrix().setRotate(degrees, mPivotX, mPivotY); + t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale); } } diff --git a/core/java/android/view/animation/ScaleAnimation.java b/core/java/android/view/animation/ScaleAnimation.java index 1a56c8b..8537d42 100644 --- a/core/java/android/view/animation/ScaleAnimation.java +++ b/core/java/android/view/animation/ScaleAnimation.java @@ -161,6 +161,7 @@ public class ScaleAnimation extends Animation { protected void applyTransformation(float interpolatedTime, Transformation t) { float sx = 1.0f; float sy = 1.0f; + float scale = getScaleFactor(); if (mFromX != 1.0f || mToX != 1.0f) { sx = mFromX + ((mToX - mFromX) * interpolatedTime); @@ -172,7 +173,7 @@ public class ScaleAnimation extends Animation { if (mPivotX == 0 && mPivotY == 0) { t.getMatrix().setScale(sx, sy); } else { - t.getMatrix().setScale(sx, sy, mPivotX, mPivotY); + t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY); } } diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java new file mode 100644 index 0000000..49ddc19 --- /dev/null +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -0,0 +1,99 @@ +/* + * 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.webkit; + +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.webkit.WebViewCore.EventHub; + +/** + * This class injects accessibility into WebViews with disabled JavaScript or + * WebViews with enabled JavaScript but for which we have no accessibility + * script to inject. + */ +class AccessibilityInjector { + + // Handle to the WebView this injector is associated with. + private final WebView mWebView; + + /** + * Creates a new injector associated with a given VwebView. + * + * @param webView The associated WebView. + */ + public AccessibilityInjector(WebView webView) { + mWebView = webView; + } + + /** + * Processes a key down <code>event</code>. + * + * @return True if the event was processed. + */ + public boolean onKeyEvent(KeyEvent event) { + + // as a proof of concept let us do the simplest example + + if (event.getAction() != KeyEvent.ACTION_UP) { + return false; + } + + int keyCode = event.getKeyCode(); + + switch (keyCode) { + case KeyEvent.KEYCODE_N: + modifySelection("extend", "forward", "sentence"); + break; + case KeyEvent.KEYCODE_P: + modifySelection("extend", "backward", "sentence"); + break; + } + + return false; + } + + /** + * Called when the <code>selectionString</code> has changed. + */ + public void onSelectionStringChange(String selectionString) { + // put the selection string in an AccessibilityEvent and send it + AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); + event.getText().add(selectionString); + mWebView.sendAccessibilityEventUnchecked(event); + } + + /** + * Modifies the current selection. + * + * @param alter Specifies how to alter the selection. + * @param direction The direction in which to alter the selection. + * @param granularity The granularity of the selection modification. + */ + private void modifySelection(String alter, String direction, String granularity) { + WebViewCore webViewCore = mWebView.getWebViewCore(); + + if (webViewCore == null) { + return; + } + + WebViewCore.ModifySelectionData data = new WebViewCore.ModifySelectionData(); + data.mAlter = alter; + data.mDirection = direction; + data.mGranularity = granularity; + webViewCore.sendMessage(EventHub.MODIFY_SELECTION, data); + } +} diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index 219a469..b021ded 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -72,6 +72,8 @@ class BrowserFrame extends Handler { // queue has been cleared,they are ignored. private boolean mBlockMessages = false; + private static String sDataDirectory = ""; + // Is this frame the main frame? private boolean mIsMainFrame; @@ -224,6 +226,11 @@ class BrowserFrame extends Handler { AssetManager am = context.getAssets(); nativeCreateFrame(w, am, proxy.getBackForwardList()); + if (sDataDirectory.length() == 0) { + String dir = appContext.getFilesDir().getAbsolutePath(); + sDataDirectory = dir.substring(0, dir.lastIndexOf('/')); + } + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); } @@ -294,6 +301,18 @@ class BrowserFrame extends Handler { } /** + * Saves the contents of the frame as a web archive. + * + * @param basename The filename where the archive should be placed. + * @param autoname If false, takes filename to be a file. If true, filename + * is assumed to be a directory in which a filename will be + * chosen according to the url of the current page. + */ + /* package */ String saveWebArchive(String basename, boolean autoname) { + return nativeSaveWebArchive(basename, autoname); + } + + /** * Go back or forward the number of steps given. * @param steps A negative or positive number indicating the direction * and number of steps to move. @@ -510,12 +529,21 @@ class BrowserFrame extends Handler { private native String externalRepresentation(); /** - * Retrieves the visual text of the current frame, puts it as the object for + * Retrieves the visual text of the frames, puts it as the object for * the message and sends the message. * @param callback the message to use to send the visual text */ public void documentAsText(Message callback) { - callback.obj = documentAsText();; + StringBuilder text = new StringBuilder(); + if (callback.arg1 != 0) { + // Dump top frame as text. + text.append(documentAsText()); + } + if (callback.arg2 != 0) { + // Dump child frames as text. + text.append(childFramesAsText()); + } + callback.obj = text.toString(); callback.sendToTarget(); } @@ -524,6 +552,11 @@ class BrowserFrame extends Handler { */ private native String documentAsText(); + /** + * Return the text drawn on the child frames as a string + */ + private native String childFramesAsText(); + /* * This method is called by WebCore to inform the frame that * the Javascript window object has been cleared. @@ -617,6 +650,14 @@ class BrowserFrame extends Handler { } /** + * Called by JNI. Gets the applications data directory + * @return String The applications data directory + */ + private static String getDataDirectory() { + return sDataDirectory; + } + + /** * Start loading a resource. * @param loaderHandle The native ResourceLoader that is the target of the * data. @@ -785,11 +826,7 @@ class BrowserFrame extends Handler { * @return The BrowserFrame object stored in the new WebView. */ private BrowserFrame createWindow(boolean dialog, boolean userGesture) { - WebView w = mCallbackProxy.createWindow(dialog, userGesture); - if (w != null) { - return w.getWebViewCore().getBrowserFrame(); - } - return null; + return mCallbackProxy.createWindow(dialog, userGesture); } /** @@ -846,6 +883,7 @@ class BrowserFrame extends Handler { private static final int FILE_UPLOAD_LABEL = 4; private static final int RESET_LABEL = 5; private static final int SUBMIT_LABEL = 6; + private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7; String getRawResFilename(int id) { int resid; @@ -875,6 +913,10 @@ class BrowserFrame extends Handler { return mContext.getResources().getString( com.android.internal.R.string.submit); + case FILE_UPLOAD_NO_FILE_CHOSEN: + return mContext.getResources().getString( + com.android.internal.R.string.no_file_chosen); + default: Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); return ""; @@ -1010,5 +1052,7 @@ class BrowserFrame extends Handler { */ private native HashMap getFormTextData(); + private native String nativeSaveWebArchive(String basename, boolean autoname); + private native void nativeOrientationChanged(int orientation); } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 0e0e032..1b5651b 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -16,6 +16,7 @@ package android.webkit; +import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; @@ -41,6 +42,7 @@ import com.android.internal.R; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; +import java.util.Map; /** * This class is a proxy class for handling WebCore -> UI thread messaging. All @@ -112,6 +114,7 @@ class CallbackProxy extends Handler { private static final int ADD_HISTORY_ITEM = 135; private static final int HISTORY_INDEX_CHANGED = 136; private static final int AUTH_CREDENTIALS = 137; + private static final int SET_INSTALLABLE_WEBAPP = 138; // Message triggered by the client to resume execution private static final int NOTIFY = 200; @@ -500,18 +503,32 @@ class CallbackProxy extends Handler { String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsAlert(mWebView, url, message, res)) { + // only display the alert dialog if the mContext is + // Activity and its window has the focus. + if (!(mContext instanceof Activity) + || !((Activity) mContext).hasWindowFocus()) { + res.cancel(); + res.setReady(); + break; + } new AlertDialog.Builder(mContext) .setTitle(getJsDialogTitle(url)) .setMessage(message) .setPositiveButton(R.string.ok, - new AlertDialog.OnClickListener() { + new DialogInterface.OnClickListener() { public void onClick( DialogInterface dialog, int which) { res.confirm(); } }) - .setCancelable(false) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel( + DialogInterface dialog) { + res.cancel(); + } + }) .show(); } res.setReady(); @@ -525,6 +542,14 @@ class CallbackProxy extends Handler { String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsConfirm(mWebView, url, message, res)) { + // only display the alert dialog if the mContext is + // Activity and its window has the focus. + if (!(mContext instanceof Activity) + || !((Activity) mContext).hasWindowFocus()) { + res.cancel(); + res.setReady(); + break; + } new AlertDialog.Builder(mContext) .setTitle(getJsDialogTitle(url)) .setMessage(message) @@ -565,6 +590,14 @@ class CallbackProxy extends Handler { String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsPrompt(mWebView, url, message, defaultVal, res)) { + // only display the alert dialog if the mContext is + // Activity and its window has the focus. + if (!(mContext instanceof Activity) + || !((Activity) mContext).hasWindowFocus()) { + res.cancel(); + res.setReady(); + break; + } final LayoutInflater factory = LayoutInflater .from(mContext); final View view = factory.inflate(R.layout.js_prompt, @@ -616,6 +649,14 @@ class CallbackProxy extends Handler { String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res)) { + // only display the alert dialog if the mContext is + // Activity and its window has the focus. + if (!(mContext instanceof Activity) + || !((Activity) mContext).hasWindowFocus()) { + res.cancel(); + res.setReady(); + break; + } final String m = mContext.getString( R.string.js_dialog_before_unload, message); new AlertDialog.Builder(mContext) @@ -725,7 +766,8 @@ class CallbackProxy extends Handler { case OPEN_FILE_CHOOSER: if (mWebChromeClient != null) { - mWebChromeClient.openFileChooser((UploadFile) msg.obj); + UploadFileMessageData data = (UploadFileMessageData)msg.obj; + mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType()); } break; @@ -750,6 +792,9 @@ class CallbackProxy extends Handler { mWebView.setHttpAuthUsernamePassword( host, realm, username, password); break; + case SET_INSTALLABLE_WEBAPP: + mWebChromeClient.setInstallableWebApp(); + break; } } @@ -1087,10 +1132,15 @@ class CallbackProxy extends Handler { public void onProgressChanged(int newProgress) { // Synchronize so that mLatestProgress is up-to-date. synchronized (this) { - if (mWebChromeClient == null || mLatestProgress == newProgress) { + // update mLatestProgress even mWebChromeClient is null as + // WebView.getProgress() needs it + if (mLatestProgress == newProgress) { return; } mLatestProgress = newProgress; + if (mWebChromeClient == null) { + return; + } if (!mProgressUpdatePending) { sendEmptyMessage(PROGRESS); mProgressUpdatePending = true; @@ -1098,7 +1148,7 @@ class CallbackProxy extends Handler { } } - public WebView createWindow(boolean dialog, boolean userGesture) { + public BrowserFrame createWindow(boolean dialog, boolean userGesture) { // Do an unsynchronized quick check to avoid posting if no callback has // been set. if (mWebChromeClient == null) { @@ -1122,9 +1172,15 @@ class CallbackProxy extends Handler { WebView w = transport.getWebView(); if (w != null) { - w.getWebViewCore().initializeSubwindow(); + WebViewCore core = w.getWebViewCore(); + // If WebView.destroy() has been called, core may be null. Skip + // initialization in that case and return null. + if (core != null) { + core.initializeSubwindow(); + return core.getBrowserFrame(); + } } - return w; + return null; } public void onRequestFocus() { @@ -1166,9 +1222,7 @@ class CallbackProxy extends Handler { // for null. WebHistoryItem i = mBackForwardList.getCurrentItem(); if (i != null) { - if (precomposed || i.getTouchIconUrl() == null) { - i.setTouchIconUrl(url); - } + i.setTouchIconUrl(url, precomposed); } // Do an unsynchronized quick check to avoid posting if no callback has // been set. @@ -1426,6 +1480,24 @@ class CallbackProxy extends Handler { sendMessage(msg); } + private static class UploadFileMessageData { + private UploadFile mCallback; + private String mAcceptType; + + public UploadFileMessageData(UploadFile uploadFile, String acceptType) { + mCallback = uploadFile; + mAcceptType = acceptType; + } + + public UploadFile getUploadFile() { + return mCallback; + } + + public String getAcceptType() { + return mAcceptType; + } + } + private class UploadFile implements ValueCallback<Uri> { private Uri mValue; public void onReceiveValue(Uri value) { @@ -1442,13 +1514,14 @@ class CallbackProxy extends Handler { /** * Called by WebViewCore to open a file chooser. */ - /* package */ Uri openFileChooser() { + /* package */ Uri openFileChooser(String acceptType) { if (mWebChromeClient == null) { return null; } Message myMessage = obtainMessage(OPEN_FILE_CHOOSER); UploadFile uploadFile = new UploadFile(); - myMessage.obj = uploadFile; + UploadFileMessageData data = new UploadFileMessageData(uploadFile, acceptType); + myMessage.obj = data; synchronized (this) { sendMessage(myMessage); try { @@ -1477,4 +1550,11 @@ class CallbackProxy extends Handler { Message msg = obtainMessage(HISTORY_INDEX_CHANGED, index, 0, item); sendMessage(msg); } + + void setInstallableWebApp() { + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(SET_INSTALLABLE_WEBAPP)); + } } diff --git a/core/java/android/webkit/GeolocationService.java b/core/java/android/webkit/GeolocationService.java index 24306f4..91de1d8 100755 --- a/core/java/android/webkit/GeolocationService.java +++ b/core/java/android/webkit/GeolocationService.java @@ -45,14 +45,13 @@ final class GeolocationService implements LocationListener { /** * Constructor + * @param context The context from which we obtain the system service. * @param nativeObject The native object to which this object will report position updates and * errors. */ - public GeolocationService(long nativeObject) { + public GeolocationService(Context context, long nativeObject) { mNativeObject = nativeObject; // Register newLocationAvailable with platform service. - ActivityThread thread = ActivityThread.systemMain(); - Context context = thread.getApplication(); mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); if (mLocationManager == null) { Log.e(TAG, "Could not get location manager."); @@ -62,9 +61,10 @@ final class GeolocationService implements LocationListener { /** * Start listening for location updates. */ - public void start() { + public boolean start() { registerForLocationUpdates(); mIsRunning = true; + return mIsNetworkProviderAvailable || mIsGpsProviderAvailable; } /** @@ -87,6 +87,8 @@ final class GeolocationService implements LocationListener { // only unregister from all, then reregister with all but the GPS. unregisterFromLocationUpdates(); registerForLocationUpdates(); + // Check that the providers are still available after we re-register. + maybeReportError("The last location provider is no longer available"); } } } @@ -156,11 +158,16 @@ final class GeolocationService implements LocationListener { */ private void registerForLocationUpdates() { try { - mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); - mIsNetworkProviderAvailable = true; + // Registration may fail if providers are not present on the device. + try { + mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); + mIsNetworkProviderAvailable = true; + } catch(IllegalArgumentException e) { } if (mIsGpsEnabled) { - mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); - mIsGpsProviderAvailable = true; + try { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + mIsGpsProviderAvailable = true; + } catch(IllegalArgumentException e) { } } } catch(SecurityException e) { Log.e(TAG, "Caught security exception registering for location updates from system. " + @@ -173,6 +180,8 @@ final class GeolocationService implements LocationListener { */ private void unregisterFromLocationUpdates() { mLocationManager.removeUpdates(this); + mIsNetworkProviderAvailable = false; + mIsGpsProviderAvailable = false; } /** diff --git a/core/java/android/webkit/HTML5Audio.java b/core/java/android/webkit/HTML5Audio.java new file mode 100644 index 0000000..d292881 --- /dev/null +++ b/core/java/android/webkit/HTML5Audio.java @@ -0,0 +1,224 @@ +/* + * 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.webkit; + +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnBufferingUpdateListener; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaPlayer.OnSeekCompleteListener; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +/** + * <p>HTML5 support class for Audio. + */ +class HTML5Audio extends Handler + implements MediaPlayer.OnBufferingUpdateListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnSeekCompleteListener { + // Logging tag. + private static final String LOGTAG = "HTML5Audio"; + + private MediaPlayer mMediaPlayer; + + // The C++ MediaPlayerPrivateAndroid object. + private int mNativePointer; + + private static int IDLE = 0; + private static int INITIALIZED = 1; + private static int PREPARED = 2; + private static int STARTED = 4; + private static int COMPLETE = 5; + private static int PAUSED = 6; + private static int STOPPED = -2; + private static int ERROR = -1; + + private int mState = IDLE; + + private String mUrl; + private boolean mAskToPlay = false; + + // Timer thread -> UI thread + private static final int TIMEUPDATE = 100; + + // The spec says the timer should fire every 250 ms or less. + private static final int TIMEUPDATE_PERIOD = 250; // ms + // The timer for timeupate events. + // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate + private Timer mTimer; + private final class TimeupdateTask extends TimerTask { + public void run() { + HTML5Audio.this.obtainMessage(TIMEUPDATE).sendToTarget(); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case TIMEUPDATE: { + try { + if (mState != ERROR && mMediaPlayer.isPlaying()) { + int position = mMediaPlayer.getCurrentPosition(); + nativeOnTimeupdate(position, mNativePointer); + } + } catch (IllegalStateException e) { + mState = ERROR; + } + } + } + } + + // event listeners for MediaPlayer + // Those are called from the same thread we created the MediaPlayer + // (i.e. the webviewcore thread here) + + // MediaPlayer.OnBufferingUpdateListener + public void onBufferingUpdate(MediaPlayer mp, int percent) { + nativeOnBuffering(percent, mNativePointer); + } + + // MediaPlayer.OnCompletionListener; + public void onCompletion(MediaPlayer mp) { + resetMediaPlayer(); + mState = IDLE; + nativeOnEnded(mNativePointer); + } + + // MediaPlayer.OnErrorListener + public boolean onError(MediaPlayer mp, int what, int extra) { + mState = ERROR; + resetMediaPlayer(); + mState = IDLE; + return false; + } + + // MediaPlayer.OnPreparedListener + public void onPrepared(MediaPlayer mp) { + mState = PREPARED; + if (mTimer != null) { + mTimer.schedule(new TimeupdateTask(), + TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); + } + nativeOnPrepared(mp.getDuration(), 0, 0, mNativePointer); + if (mAskToPlay) { + mAskToPlay = false; + play(); + } + } + + // MediaPlayer.OnSeekCompleteListener + public void onSeekComplete(MediaPlayer mp) { + nativeOnTimeupdate(mp.getCurrentPosition(), mNativePointer); + } + + + /** + * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. + */ + public HTML5Audio(int nativePtr) { + // Save the native ptr + mNativePointer = nativePtr; + resetMediaPlayer(); + } + + private void resetMediaPlayer() { + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + } else { + mMediaPlayer.reset(); + } + mMediaPlayer.setOnBufferingUpdateListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + + if (mTimer != null) { + mTimer.cancel(); + } + mTimer = new Timer(); + mState = IDLE; + } + + private void setDataSource(String url) { + mUrl = url; + try { + if (mState != IDLE) { + resetMediaPlayer(); + } + mMediaPlayer.setDataSource(url); + mState = INITIALIZED; + mMediaPlayer.prepareAsync(); + } catch (IOException e) { + Log.e(LOGTAG, "couldn't load the resource: " + url + " exc: " + e); + resetMediaPlayer(); + } + } + + private void play() { + if ((mState == ERROR || mState == IDLE) && mUrl != null) { + resetMediaPlayer(); + setDataSource(mUrl); + mAskToPlay = true; + } + + if (mState >= PREPARED) { + mMediaPlayer.start(); + mState = STARTED; + } + } + + private void pause() { + if (mState == STARTED) { + if (mTimer != null) { + mTimer.purge(); + } + mMediaPlayer.pause(); + mState = PAUSED; + } + } + + private void seek(int msec) { + if (mState >= PREPARED) { + mMediaPlayer.seekTo(msec); + } + } + + private void teardown() { + mMediaPlayer.release(); + mState = ERROR; + mNativePointer = 0; + } + + private float getMaxTimeSeekable() { + return mMediaPlayer.getDuration() / 1000.0f; + } + + private native void nativeOnBuffering(int percent, int nativePointer); + private native void nativeOnEnded(int nativePointer); + private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); + private native void nativeOnTimeupdate(int position, int nativePointer); +} diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index e766693..ecad261 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -16,10 +16,12 @@ package android.webkit; +import android.net.Uri; import android.os.Handler; import android.os.Message; import android.util.Log; +import java.util.HashMap; import java.util.Set; final class JWebCoreJavaBridge extends Handler { @@ -49,12 +51,15 @@ final class JWebCoreJavaBridge extends Handler { /* package */ static final int REFRESH_PLUGINS = 100; + private HashMap<String, String> mContentUriToFilePathMap; + /** * Construct a new JWebCoreJavaBridge to interface with * WebCore timers and cookies. */ public JWebCoreJavaBridge() { nativeConstructor(); + } @Override @@ -267,6 +272,28 @@ final class JWebCoreJavaBridge extends Handler { } } + // Called on the WebCore thread through JNI. + private String resolveFilePathForContentUri(String uri) { + if (mContentUriToFilePathMap != null) { + String fileName = mContentUriToFilePathMap.get(uri); + if (fileName != null) { + return fileName; + } + } + + // Failsafe fallback to just use the last path segment. + // (See OpenableColumns documentation in the SDK) + Uri jUri = Uri.parse(uri); + return jUri.getLastPathSegment(); + } + + public void storeFilePathForContentUri(String path, String contentUri) { + if (mContentUriToFilePathMap == null) { + mContentUriToFilePathMap = new HashMap<String, String>(); + } + mContentUriToFilePathMap.put(contentUri, path); + } + private native void nativeConstructor(); private native void nativeFinalize(); private native void sharedTimerFired(); diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index c1ac180..6e9c70a 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -363,6 +363,7 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("application/x-wais-source", "src"); sMimeTypeMap.loadEntry("application/x-wingz", "wz"); sMimeTypeMap.loadEntry("application/x-webarchive", "webarchive"); + sMimeTypeMap.loadEntry("application/x-webarchive-xml", "webarchivexml"); sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt"); sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt"); sMimeTypeMap.loadEntry("application/x-xcf", "xcf"); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index 598f20d..0f03258 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -16,7 +16,12 @@ package android.webkit; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.http.*; import android.os.*; import android.util.Log; @@ -76,6 +81,19 @@ class Network { */ private HttpAuthHandler mHttpAuthHandler; + private Context mContext; + + /** + * True if the currently used network connection is a roaming phone + * connection. + */ + private boolean mRoaming; + + /** + * Tracks if we are roaming. + */ + private RoamingMonitor mRoamingMonitor; + /** * @return The singleton instance of the network. */ @@ -107,6 +125,7 @@ class Network { if (++sPlatformNotificationEnableRefCount == 1) { if (sNetwork != null) { sNetwork.mRequestQueue.enablePlatformNotifications(); + sNetwork.monitorRoaming(); } else { sPlatformNotifications = true; } @@ -121,6 +140,7 @@ class Network { if (--sPlatformNotificationEnableRefCount == 0) { if (sNetwork != null) { sNetwork.mRequestQueue.disablePlatformNotifications(); + sNetwork.stopMonitoringRoaming(); } else { sPlatformNotifications = false; } @@ -136,12 +156,39 @@ class Network { Assert.assertTrue(Thread.currentThread(). getName().equals(WebViewCore.THREAD_NAME)); } + mContext = context; mSslErrorHandler = new SslErrorHandler(); mHttpAuthHandler = new HttpAuthHandler(this); mRequestQueue = new RequestQueue(context); } + private class RoamingMonitor extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) + return; + + NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + if (info != null) + mRoaming = info.isRoaming(); + }; + }; + + private void monitorRoaming() { + mRoamingMonitor = new RoamingMonitor(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(sNetwork.mRoamingMonitor, filter); + } + + private void stopMonitoringRoaming() { + if (mRoamingMonitor != null) { + mContext.unregisterReceiver(mRoamingMonitor); + mRoamingMonitor = null; + } + } + /** * Request a url from either the network or the file system. * @param url The url to load. @@ -170,6 +217,11 @@ class Network { return false; } + // If this is a prefetch, abort it if we're roaming. + if (mRoaming && headers.containsKey("X-Moz") && "prefetch".equals(headers.get("X-Moz"))) { + return false; + } + /* FIXME: this is lame. Pass an InputStream in, rather than making this lame one here */ InputStream bodyProvider = null; diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 1d5aac7..443a3b3 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -314,10 +314,34 @@ public class WebChromeClient { /** * Tell the client to open a file chooser. * @param uploadFile A ValueCallback to set the URI of the file to upload. - * onReceiveValue must be called to wake up the thread. + * onReceiveValue must be called to wake up the thread.a + * @param acceptType The value of the 'accept' attribute of the input tag + * associated with this file picker. * @hide */ - public void openFileChooser(ValueCallback<Uri> uploadFile) { + public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType) { uploadFile.onReceiveValue(null); } + + /** + * Tell the client that the selection has been initiated. + * @hide + */ + public void onSelectionStart() { + } + + /** + * Tell the client that the selection has been copied or canceled. + * @hide + */ + public void onSelectionDone() { + } + + /** + * Tell the client that the page being viewed is web app capable, + * i.e. has specified the fullscreen-web-app-capable meta tag. + * @hide + */ + public void setInstallableWebApp() { } + } diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index 428a59c..7c0e478 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -18,6 +18,9 @@ package android.webkit; import android.graphics.Bitmap; +import java.net.MalformedURLException; +import java.net.URL; + /** * A convenience class for accessing fields in an entry in the back/forward list * of a WebView. Each WebHistoryItem is a snapshot of the requested history @@ -39,8 +42,12 @@ public class WebHistoryItem implements Cloneable { private Bitmap mFavicon; // The pre-flattened data used for saving the state. private byte[] mFlattenedData; - // The apple-touch-icon url for use when adding the site to the home screen - private String mTouchIconUrl; + // The apple-touch-icon url for use when adding the site to the home screen, + // as obtained from a <link> element in the page. + private String mTouchIconUrlFromLink; + // If no <link> is specified, this holds the default location of the + // apple-touch-icon. + private String mTouchIconUrlServerDefault; // Custom client data that is not flattened or read by native code. private Object mCustomData; @@ -132,10 +139,28 @@ public class WebHistoryItem implements Cloneable { /** * Return the touch icon url. + * If no touch icon <link> tag was specified, returns + * <host>/apple-touch-icon.png. The DownloadTouchIcon class that + * attempts to retrieve the touch icon will handle the case where + * that file does not exist. An icon set by a <link> tag is always + * used in preference to an icon saved on the server. * @hide */ public String getTouchIconUrl() { - return mTouchIconUrl; + if (mTouchIconUrlFromLink != null) { + return mTouchIconUrlFromLink; + } else if (mTouchIconUrlServerDefault != null) { + return mTouchIconUrlServerDefault; + } + + try { + URL url = new URL(mOriginalUrl); + mTouchIconUrlServerDefault = new URL(url.getProtocol(), url.getHost(), url.getPort(), + "/apple-touch-icon.png").toString(); + } catch (MalformedURLException e) { + return null; + } + return mTouchIconUrlServerDefault; } /** @@ -171,11 +196,14 @@ public class WebHistoryItem implements Cloneable { } /** - * Set the touch icon url. + * Set the touch icon url. Will not overwrite an icon that has been + * set already from a <link> tag, unless the new icon is precomposed. * @hide */ - /*package*/ void setTouchIconUrl(String url) { - mTouchIconUrl = url; + /*package*/ void setTouchIconUrl(String url, boolean precomposed) { + if (precomposed || mTouchIconUrlFromLink == null) { + mTouchIconUrlFromLink = url; + } } /** diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index b767f11..52e992b 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -19,6 +19,7 @@ package android.webkit; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Handler; import android.os.Message; @@ -107,7 +108,7 @@ public class WebSettings { * Use with {@link #setCacheMode}. */ public static final int LOAD_NO_CACHE = 2; - + /** * Don't use the network, load from cache only. * Use with {@link #setCacheMode}. @@ -178,12 +179,14 @@ public class WebSettings { private boolean mUseWideViewport = false; private boolean mSupportMultipleWindows = false; private boolean mShrinksStandaloneImagesToFit = false; + private long mMaximumDecodedImageSize = 0; // 0 means default // HTML5 API flags private boolean mAppCacheEnabled = false; private boolean mDatabaseEnabled = false; private boolean mDomStorageEnabled = false; private boolean mWorkersEnabled = false; // only affects V8. private boolean mGeolocationEnabled = true; + private boolean mXSSAuditorEnabled = false; // HTML5 configuration parameters private long mAppCacheMaxSize = Long.MAX_VALUE; private String mAppCachePath = ""; @@ -207,6 +210,7 @@ public class WebSettings { private boolean mBuiltInZoomControls = false; private boolean mAllowFileAccess = true; private boolean mLoadWithOverviewMode = false; + private boolean mEnableSmoothTransition = false; // private WebSettings, not accessible by the host activity static private int mDoubleTapToastCount = 3; @@ -296,13 +300,13 @@ public class WebSettings { "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us)" + " AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0" + " Safari/530.17"; - private static final String IPHONE_USERAGENT = + private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us)" + " AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0" + " Mobile/7A341 Safari/528.16"; private static Locale sLocale; private static Object sLockForLocaleSettings; - + /** * Package constructor to prevent clients from creating a new settings * instance. @@ -327,6 +331,8 @@ public class WebSettings { android.os.Process.myUid()) != PackageManager.PERMISSION_GRANTED; } + private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; + /** * Looks at sLocale and returns current AcceptLanguage String. * @return Current AcceptLanguage String. @@ -336,32 +342,53 @@ public class WebSettings { synchronized(sLockForLocaleSettings) { locale = sLocale; } - StringBuffer buffer = new StringBuffer(); - final String language = locale.getLanguage(); - if (language != null) { - buffer.append(language); - final String country = locale.getCountry(); - if (country != null) { - buffer.append("-"); - buffer.append(country); - } - } - if (!locale.equals(Locale.US)) { - buffer.append(", "); - java.util.Locale us = Locale.US; - if (us.getLanguage() != null) { - buffer.append(us.getLanguage()); - final String country = us.getCountry(); - if (country != null) { - buffer.append("-"); - buffer.append(country); - } + StringBuilder buffer = new StringBuilder(); + addLocaleToHttpAcceptLanguage(buffer, locale); + + if (!Locale.US.equals(locale)) { + if (buffer.length() > 0) { + buffer.append(", "); } + buffer.append(ACCEPT_LANG_FOR_US_LOCALE); } return buffer.toString(); } - + + /** + * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, + * to new standard. + */ + private static String convertObsoleteLanguageCodeToNew(String langCode) { + if (langCode == null) { + return null; + } + if ("iw".equals(langCode)) { + // Hebrew + return "he"; + } else if ("in".equals(langCode)) { + // Indonesian + return "id"; + } else if ("ji".equals(langCode)) { + // Yiddish + return "yi"; + } + return langCode; + } + + private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, + Locale locale) { + String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); + if (language != null) { + builder.append(language); + String country = locale.getCountry(); + if (country != null) { + builder.append("-"); + builder.append(country); + } + } + } + /** * Looks at sLocale and mContext and returns current UserAgent String. * @return Current UserAgent String. @@ -379,11 +406,11 @@ public class WebSettings { } else { // default to "1.0" buffer.append("1.0"); - } + } buffer.append("; "); final String language = locale.getLanguage(); if (language != null) { - buffer.append(language.toLowerCase()); + buffer.append(convertObsoleteLanguageCodeToNew(language)); final String country = locale.getCountry(); if (country != null) { buffer.append("-"); @@ -406,11 +433,14 @@ public class WebSettings { buffer.append(" Build/"); buffer.append(id); } + String mobile = ((mContext.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE) ? "" : "Mobile "; final String base = mContext.getResources().getText( com.android.internal.R.string.web_user_agent).toString(); - return String.format(base, buffer); + return String.format(base, buffer, mobile); } - + /** * Enables dumping the pages navigation cache to a text file. */ @@ -426,6 +456,21 @@ 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) { @@ -447,14 +492,14 @@ public class WebSettings { mBuiltInZoomControls = enabled; mWebView.updateMultiTouchSupport(mContext); } - + /** * Returns true if the zoom mechanism built into WebView is being used. */ public boolean getBuiltInZoomControls() { return mBuiltInZoomControls; } - + /** * Enable or disable file access within WebView. File access is enabled by * default. @@ -485,6 +530,25 @@ public class WebSettings { } /** + * Set whether the WebView will enable smooth transition while panning or + * zooming. If it is true, WebView will choose a solution to maximize the + * performance. e.g. the WebView's content may not be updated during the + * transition. If it is false, WebView will keep its fidelity. The default + * value is false. + */ + public void setEnableSmoothTransition(boolean enable) { + mEnableSmoothTransition = enable; + } + + /** + * Returns true if the WebView enables smooth transition while panning or + * zooming. + */ + public boolean enableSmoothTransition() { + return mEnableSmoothTransition; + } + + /** * Store whether the WebView is saving form data. */ public void setSaveFormData(boolean save) { @@ -984,8 +1048,8 @@ public class WebSettings { private void verifyNetworkAccess() { if (!mBlockNetworkLoads) { - if (mContext.checkPermission("android.permission.INTERNET", - android.os.Process.myPid(), android.os.Process.myUid()) != + if (mContext.checkPermission("android.permission.INTERNET", + android.os.Process.myPid(), android.os.Process.myUid()) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException ("Permission denied - " + @@ -1011,6 +1075,7 @@ public class WebSettings { * @deprecated This method has been deprecated in favor of * {@link #setPluginState} */ + @Deprecated public synchronized void setPluginsEnabled(boolean flag) { setPluginState(PluginState.ON); } @@ -1176,6 +1241,18 @@ public class WebSettings { } /** + * Sets whether XSS Auditor is enabled. + * @param flag Whether XSS Auditor should be enabled. + * @hide Only used by LayoutTestController. + */ + public synchronized void setXSSAuditorEnabled(boolean flag) { + if (mXSSAuditorEnabled != flag) { + mXSSAuditorEnabled = flag; + postSync(); + } + } + + /** * Return true if javascript is enabled. <b>Note: The default is false.</b> * @return True if javascript is enabled. */ @@ -1188,6 +1265,7 @@ public class WebSettings { * @return True if plugins are enabled. * @deprecated This method has been replaced by {@link #getPluginState} */ + @Deprecated public synchronized boolean getPluginsEnabled() { return mPluginState == PluginState.ON; } @@ -1256,7 +1334,7 @@ public class WebSettings { public synchronized void setUserAgentString(String ua) { if (ua == null || ua.length() == 0) { synchronized(sLockForLocaleSettings) { - Locale currentLocale = Locale.getDefault(); + Locale currentLocale = Locale.getDefault(); if (!sLocale.equals(currentLocale)) { sLocale = currentLocale; mAcceptLanguage = getCurrentAcceptLanguage(); @@ -1311,11 +1389,11 @@ public class WebSettings { } return mAcceptLanguage; } - + /** * Tell the WebView whether it needs to set a node to have focus when * {@link WebView#requestFocus(int, android.graphics.Rect)} is called. - * + * * @param flag */ public void setNeedInitialFocus(boolean flag) { @@ -1342,7 +1420,7 @@ public class WebSettings { EventHandler.PRIORITY)); } } - + /** * Override the way the cache is used. The way the cache is used is based * on the navigation option. For a normal page load, the cache is checked @@ -1356,7 +1434,7 @@ public class WebSettings { mOverrideCacheMode = mode; } } - + /** * Return the current setting for overriding the cache mode. For a full * description, see the {@link #setCacheMode(int)} function. @@ -1364,7 +1442,7 @@ public class WebSettings { public int getCacheMode() { return mOverrideCacheMode; } - + /** * If set, webkit alternately shrinks and expands images viewed outside * of an HTML page to fit the screen. This conflicts with attempts by @@ -1379,6 +1457,19 @@ 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 + */ + public void setMaximumDecodedImageSize(long size) { + if (mMaximumDecodedImageSize != size) { + mMaximumDecodedImageSize = size; + postSync(); + } + } + int getDoubleTapToastCount() { return mDoubleTapToastCount; } diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index 19abec1..eb36b5d 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -28,6 +28,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.InputFilter; +import android.text.Layout; import android.text.Selection; import android.text.Spannable; import android.text.TextPaint; @@ -300,6 +301,33 @@ import java.util.ArrayList; return connection; } + /** + * In general, TextView makes a call to InputMethodManager.updateSelection + * in onDraw. However, in the general case of WebTextView, we do not draw. + * This method is called by WebView.onDraw to take care of the part that + * needs to be called. + */ + /* package */ void onDrawSubstitute() { + if (!willNotDraw()) { + // If the WebTextView is set to draw, such as in the case of a + // password, onDraw calls updateSelection(), so this code path is + // unnecessary. + return; + } + // This code is copied from TextView.onDraw(). That code does not get + // executed, however, because the WebTextView does not draw, allowing + // webkit's drawing to show through. + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imm.isActive(this)) { + Spannable sp = (Spannable) getText(); + int selStart = Selection.getSelectionStart(sp); + int selEnd = Selection.getSelectionEnd(sp); + int candStart = EditableInputConnection.getComposingSpanStart(sp); + int candEnd = EditableInputConnection.getComposingSpanEnd(sp); + imm.updateSelection(this, selStart, selEnd, candStart, candEnd); + } + } + @Override protected void onDraw(Canvas canvas) { // onDraw should only be called for password fields. If WebTextView is @@ -360,19 +388,8 @@ import java.util.ArrayList; @Override protected void onSelectionChanged(int selStart, int selEnd) { - if (mInSetTextAndKeepSelection) return; - // This code is copied from TextView.onDraw(). That code does not get - // executed, however, because the WebTextView does not draw, allowing - // webkit's drawing to show through. - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null && imm.isActive(this)) { - Spannable sp = (Spannable) getText(); - int candStart = EditableInputConnection.getComposingSpanStart(sp); - int candEnd = EditableInputConnection.getComposingSpanEnd(sp); - imm.updateSelection(this, selStart, selEnd, candStart, candEnd); - } if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType - && mWebView != null) { + && mWebView != null && !mInSetTextAndKeepSelection) { if (DebugFlags.WEB_TEXT_VIEW) { Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart + " selEnd=" + selEnd); @@ -481,9 +498,10 @@ import java.util.ArrayList; // to big for the case of a small textfield. int smallerSlop = slop/2; if (dx > smallerSlop || dy > smallerSlop) { - if (mWebView != null) { - float maxScrollX = (float) Touch.getMaxScrollX(this, - getLayout(), mScrollY); + Layout layout = getLayout(); + if (mWebView != null && layout != null) { + float maxScrollX = (float) Touch.getMaxScrollX(this, layout, + mScrollY); if (DebugFlags.WEB_TEXT_VIEW) { Log.v(LOGTAG, "onTouchEvent x=" + mScrollX + " y=" + mScrollY + " maxX=" + maxScrollX); @@ -667,6 +685,7 @@ import java.util.ArrayList; } else { Selection.setSelection(text, selection, selection); } + if (mWebView != null) mWebView.incrementTextGeneration(); } /** @@ -919,14 +938,4 @@ import java.util.ArrayList; /* package */ void updateCachedTextfield() { mWebView.updateCachedTextfield(getText().toString()); } - - @Override - public boolean requestRectangleOnScreen(Rect rectangle) { - // don't scroll while in zoom animation. When it is done, we will adjust - // the WebTextView if it is in editing mode. - if (!mWebView.inAnimateZoom()) { - return super.requestRectangleOnScreen(rectangle); - } - return false; - } } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 4ca210f..0c8fc79 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -22,22 +22,20 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.DialogInterface.OnCancelListener; -import android.content.pm.PackageManager; import android.database.DataSetObserver; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.CornerPathEffect; +import android.graphics.DrawFilter; import android.graphics.Interpolator; -import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; -import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; @@ -46,6 +44,7 @@ import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; +import android.speech.tts.TextToSpeech; import android.text.IClipboard; import android.text.Selection; import android.text.Spannable; @@ -64,32 +63,29 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.animation.AlphaAnimation; +import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.webkit.WebTextView.AutoCompleteAdapter; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TouchEventData; +import android.webkit.WebViewCore.TouchHighlightData; import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; -import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Scroller; import android.widget.Toast; -import android.widget.ZoomButtonsController; -import android.widget.ZoomControls; import android.widget.AdapterView.OnItemClickListener; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; @@ -310,49 +306,7 @@ public class WebView extends AbsoluteLayout static final String LOGTAG = "webview"; - private static class ExtendedZoomControls extends FrameLayout { - public ExtendedZoomControls(Context context, AttributeSet attrs) { - super(context, attrs); - LayoutInflater inflater = (LayoutInflater) - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); - mPlusMinusZoomControls = (ZoomControls) findViewById( - com.android.internal.R.id.zoomControls); - findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( - View.GONE); - } - - public void show(boolean showZoom, boolean canZoomOut) { - mPlusMinusZoomControls.setVisibility( - showZoom ? View.VISIBLE : View.GONE); - fade(View.VISIBLE, 0.0f, 1.0f); - } - - public void hide() { - fade(View.GONE, 1.0f, 0.0f); - } - - private void fade(int visibility, float startAlpha, float endAlpha) { - AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); - anim.setDuration(500); - startAnimation(anim); - setVisibility(visibility); - } - - public boolean hasFocus() { - return mPlusMinusZoomControls.hasFocus(); - } - - public void setOnZoomInClickListener(OnClickListener listener) { - mPlusMinusZoomControls.setOnZoomInClickListener(listener); - } - - public void setOnZoomOutClickListener(OnClickListener listener) { - mPlusMinusZoomControls.setOnZoomOutClickListener(listener); - } - - ZoomControls mPlusMinusZoomControls; - } + private ZoomManager mZoomManager; /** * Transportation object for returning WebView across thread boundaries. @@ -398,6 +352,8 @@ public class WebView extends AbsoluteLayout // more key events. private int mTextGeneration; + /* package */ void incrementTextGeneration() { mTextGeneration++; } + // Used by WebViewCore to create child views. /* package */ final ViewManager mViewManager; @@ -445,6 +401,10 @@ public class WebView extends AbsoluteLayout private float mLastVelX; private float mLastVelY; + // only trigger accelerated fling if the new velocity is at least + // MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION times of the previous velocity + private static final float MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION = 0.2f; + /** * Touch mode */ @@ -456,8 +416,7 @@ public class WebView extends AbsoluteLayout private static final int TOUCH_SHORTPRESS_MODE = 5; private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; - private static final int TOUCH_SELECT_MODE = 8; - private static final int TOUCH_PINCH_DRAG = 9; + private static final int TOUCH_PINCH_DRAG = 8; // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; @@ -496,10 +455,6 @@ public class WebView extends AbsoluteLayout // true if onPause has been called (and not onResume) private boolean mIsPaused; - // true if, during a transition to a new page, we're delaying - // deleting a root layer until there's something to draw of the new page. - private boolean mDelayedDeleteRootLayer; - /** * Customizable constant */ @@ -521,9 +476,6 @@ public class WebView extends AbsoluteLayout private static final int MIN_FLING_TIME = 250; // draw unfiltered after drag is held without movement private static final int MOTIONLESS_TIME = 100; - // The time that the Zoom Controls are visible before fading away - private static final long ZOOM_CONTROLS_TIMEOUT = - ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. private static final int PAGE_SCROLL_OVERLAP = 24; @@ -564,15 +516,24 @@ public class WebView extends AbsoluteLayout private static final int MOTIONLESS_IGNORE = 3; private int mHeldMotionless; - // whether support multi-touch - private boolean mSupportMultiTouch; - // use the framework's ScaleGestureDetector to handle multi-touch - private ScaleGestureDetector mScaleDetector; - - // the anchor point in the document space where VIEW_SIZE_CHANGED should - // apply to - private int mAnchorX; - private int mAnchorY; + // An instance for injecting accessibility in WebViews with disabled + // JavaScript or ones for which no accessibility script exists + private AccessibilityInjector mAccessibilityInjector; + + // the color used to highlight the touch rectangles + private static final int mHightlightColor = 0x33000000; + // the round corner for the highlight path + private static final float TOUCH_HIGHLIGHT_ARC = 5.0f; + // the region indicating where the user touched on the screen + private Region mTouchHighlightRegion = new Region(); + // the paint for the touch highlight + private Paint mTouchHightlightPaint; + // debug only + private static final boolean DEBUG_TOUCH_HIGHLIGHT = true; + private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000; + private Paint mTouchCrossHairColor; + private int mTouchHighlightX; + private int mTouchHighlightY; /* * Private message ids @@ -606,7 +567,7 @@ public class WebView extends AbsoluteLayout static final int WEBCORE_INITIALIZED_MSG_ID = 107; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 108; static final int UPDATE_ZOOM_RANGE = 109; - static final int MOVE_OUT_OF_PLUGIN = 110; + static final int UNHANDLED_NAV_KEY = 110; static final int CLEAR_TEXT_ENTRY = 111; static final int UPDATE_TEXT_SELECTION_MSG_ID = 112; static final int SHOW_RECT_MSG_ID = 113; @@ -620,16 +581,19 @@ public class WebView extends AbsoluteLayout static final int SHOW_FULLSCREEN = 120; static final int HIDE_FULLSCREEN = 121; static final int DOM_FOCUS_CHANGED = 122; - static final int IMMEDIATE_REPAINT_MSG_ID = 123; - static final int SET_ROOT_LAYER_MSG_ID = 124; + static final int REPLACE_BASE_CONTENT = 123; + // 124; static final int RETURN_LABEL = 125; static final int FIND_AGAIN = 126; static final int CENTER_FIT_RECT = 127; 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 SAVE_WEBARCHIVE_FINISHED = 132; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; - private static final int LAST_PACKAGE_MSG_ID = SET_SCROLLBAR_MODES; + private static final int LAST_PACKAGE_MSG_ID = SET_TOUCH_HIGHLIGHT_RECTS; static final String[] HandlerPrivateDebugString = { "REMEMBER_PASSWORD", // = 1; @@ -654,7 +618,7 @@ public class WebView extends AbsoluteLayout "WEBCORE_INITIALIZED_MSG_ID", // = 107; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 108; "UPDATE_ZOOM_RANGE", // = 109; - "MOVE_OUT_OF_PLUGIN", // = 110; + "UNHANDLED_NAV_KEY", // = 110; "CLEAR_TEXT_ENTRY", // = 111; "UPDATE_TEXT_SELECTION_MSG_ID", // = 112; "SHOW_RECT_MSG_ID", // = 113; @@ -667,13 +631,16 @@ public class WebView extends AbsoluteLayout "SHOW_FULLSCREEN", // = 120; "HIDE_FULLSCREEN", // = 121; "DOM_FOCUS_CHANGED", // = 122; - "IMMEDIATE_REPAINT_MSG_ID", // = 123; - "SET_ROOT_LAYER_MSG_ID", // = 124; + "REPLACE_BASE_CONTENT", // = 123; + "124", // = 124; "RETURN_LABEL", // = 125; "FIND_AGAIN", // = 126; "CENTER_FIT_RECT", // = 127; "REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID", // = 128; - "SET_SCROLLBAR_MODES" // = 129; + "SET_SCROLLBAR_MODES", // = 129; + "SELECTION_STRING_CHANGED", // = 130; + "SET_TOUCH_HIGHLIGHT_RECTS", // = 131; + "SAVE_WEBARCHIVE_FINISHED" // = 132; }; // If the site doesn't use the viewport meta tag to specify the viewport, @@ -686,49 +653,9 @@ public class WebView extends AbsoluteLayout // the minimum preferred width is huge, an upper limit is needed. static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH; - // default scale limit. Depending on the display density - private static float DEFAULT_MAX_ZOOM_SCALE; - private static float DEFAULT_MIN_ZOOM_SCALE; - // scale limit, which can be set through viewport meta tag in the web page - private float mMaxZoomScale; - private float mMinZoomScale; - private boolean mMinZoomScaleFixed = true; - // initial scale in percent. 0 means using default. private int mInitialScaleInPercent = 0; - // while in the zoom overview mode, the page's width is fully fit to the - // current window. The page is alive, in another words, you can click to - // follow the links. Double tap will toggle between zoom overview mode and - // the last zoom scale. - boolean mInZoomOverview = false; - - // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, - // engadget always have wider mContentWidth no matter what viewport size is. - int mZoomOverviewWidth = DEFAULT_VIEWPORT_WIDTH; - float mTextWrapScale; - - // default scale. Depending on the display density. - static int DEFAULT_SCALE_PERCENT; - private float mDefaultScale; - - private static float MINIMUM_SCALE_INCREMENT = 0.01f; - - // set to true temporarily during ScaleGesture triggered zoom - private boolean mPreviewZoomOnly = false; - - // computed scale and inverse, from mZoomWidth. - private float mActualScale; - private float mInvActualScale; - // if this is non-zero, it is used on drawing rather than mActualScale - private float mZoomScale; - private float mInvInitialZoomScale; - private float mInvFinalZoomScale; - private int mInitialScrollX; - private int mInitialScrollY; - private long mZoomStart; - private static final int ZOOM_ANIMATION_LENGTH = 500; - private boolean mUserScroll = false; private int mSnapScrollMode = SNAP_NONE; @@ -752,6 +679,19 @@ public class WebView extends AbsoluteLayout private int mHorizontalScrollBarMode = SCROLLBAR_AUTO; private int mVerticalScrollBarMode = SCROLLBAR_AUTO; + // 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 = + "javascript:(function() {" + + " var chooser = document.createElement('script');" + + " chooser.type = 'text/javascript';" + + " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" + + " document.getElementsByTagName('head')[0].appendChild(chooser);" + + " })();"; + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -856,43 +796,6 @@ public class WebView extends AbsoluteLayout } } - // The View containing the zoom controls - private ExtendedZoomControls mZoomControls; - private Runnable mZoomControlRunnable; - - // mZoomButtonsController will be lazy initialized in - // getZoomButtonsController() to get better performance. - private ZoomButtonsController mZoomButtonsController; - - // These keep track of the center point of the zoom. They are used to - // determine the point around which we should zoom. - private float mZoomCenterX; - private float mZoomCenterY; - - private ZoomButtonsController.OnZoomListener mZoomListener = - new ZoomButtonsController.OnZoomListener() { - - public void onVisibilityChanged(boolean visible) { - if (visible) { - switchOutDrawHistory(); - // Bring back the hidden zoom controls. - mZoomButtonsController.getZoomControls().setVisibility( - View.VISIBLE); - updateZoomButtonsEnabled(); - } - } - - public void onZoom(boolean zoomIn) { - if (zoomIn) { - zoomIn(); - } else { - zoomOut(); - } - - updateZoomButtonsEnabled(); - } - }; - /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. @@ -928,51 +831,37 @@ 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 javascriptInterfaces is a Map of intareface names, as keys, and + * @param javascriptInterfaces is a Map of interface names, as keys, and * object implementing those interfaces, as values. * @hide pending API council approval. */ protected WebView(Context context, AttributeSet attrs, int defStyle, Map<String, Object> javascriptInterfaces) { super(context, attrs, defStyle); - init(); + + if (AccessibilityManager.getInstance(context).isEnabled()) { + if (javascriptInterfaces == null) { + javascriptInterfaces = new HashMap<String, Object>(); + } + exposeAccessibilityJavaScriptApi(javascriptInterfaces); + } mCallbackProxy = new CallbackProxy(context, this); mViewManager = new ViewManager(this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); mScroller = new Scroller(context); + mZoomManager = new ZoomManager(this, mCallbackProxy); + /* The init method must follow the creation of certain member variables, + * such as the mZoomManager. + */ + init(); updateMultiTouchSupport(context); } void updateMultiTouchSupport(Context context) { - WebSettings settings = getSettings(); - mSupportMultiTouch = context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) - && settings.supportZoom() && settings.getBuiltInZoomControls(); - if (mSupportMultiTouch && (mScaleDetector == null)) { - mScaleDetector = new ScaleGestureDetector(context, - new ScaleDetectorListener()); - } else if (!mSupportMultiTouch && (mScaleDetector != null)) { - mScaleDetector = null; - } - } - - private void updateZoomButtonsEnabled() { - if (mZoomButtonsController == null) return; - boolean canZoomIn = mActualScale < mMaxZoomScale; - boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview; - if (!canZoomIn && !canZoomOut) { - // Hide the zoom in and out buttons, as well as the fit to page - // button, if the page cannot zoom - mZoomButtonsController.getZoomControls().setVisibility(View.GONE); - } else { - // Set each one individually, as a page may be able to zoom in - // or out. - mZoomButtonsController.setZoomInEnabled(canZoomIn); - mZoomButtonsController.setZoomOutEnabled(canZoomOut); - } + mZoomManager.updateMultiTouchSupport(context); } private void init() { @@ -992,34 +881,35 @@ public class WebView extends AbsoluteLayout // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link mNavSlop = (int) (16 * density); - // density adjusted scale factors - DEFAULT_SCALE_PERCENT = (int) (100 * density); - mDefaultScale = density; - mActualScale = density; - mInvActualScale = 1 / density; - mTextWrapScale = density; - DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; - DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + mZoomManager.init(density); mMaximumFling = configuration.getScaledMaximumFlingVelocity(); } + /** + * Exposes accessibility APIs to JavaScript by appending them to the JavaScript + * interfaces map provided by the WebView client. In case of conflicting + * alias with the one of the accessibility API the user specified one wins. + * + * @param javascriptInterfaces A map with interfaces to be exposed to JavaScript. + */ + private void exposeAccessibilityJavaScriptApi(Map<String, Object> javascriptInterfaces) { + if (javascriptInterfaces.containsKey(ALIAS_ACCESSIBILITY_JS_INTERFACE)) { + Log.w(LOGTAG, "JavaScript interface mapped to \"" + ALIAS_ACCESSIBILITY_JS_INTERFACE + + "\" overrides the accessibility API JavaScript interface. No accessibility" + + "API will be exposed to JavaScript!"); + return; + } + + // expose the TTS for now ... + javascriptInterfaces.put(ALIAS_ACCESSIBILITY_JS_INTERFACE, + new TextToSpeech(getContext(), null)); + } + /* package */void updateDefaultZoomDensity(int zoomDensity) { - final float density = getContext().getResources().getDisplayMetrics().density + final float density = mContext.getResources().getDisplayMetrics().density * 100 / zoomDensity; - if (Math.abs(density - mDefaultScale) > 0.01) { - float scaleFactor = density / mDefaultScale; - // adjust the limits - mNavSlop = (int) (16 * density); - DEFAULT_SCALE_PERCENT = (int) (100 * density); - DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; - DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; - mDefaultScale = density; - mMaxZoomScale *= scaleFactor; - mMinZoomScale *= scaleFactor; - setNewZoomScale(mActualScale * scaleFactor, true, false); - } + mNavSlop = (int) (16 * density); + mZoomManager.updateDefaultZoomDensity(density); } /* package */ boolean onSavePassword(String schemePlusHost, String username, @@ -1136,14 +1026,14 @@ public class WebView extends AbsoluteLayout * returns the height of the titlebarview (if any). Does not care about * scrolling */ - private int getTitleHeight() { + int getTitleHeight() { return mTitleBar != null ? mTitleBar.getHeight() : 0; } /* * Return the amount of the titlebarview (if any) that is visible */ - private int getVisibleTitleHeight() { + int getVisibleTitleHeight() { return Math.max(getTitleHeight() - mScrollY, 0); } @@ -1404,29 +1294,23 @@ public class WebView extends AbsoluteLayout // now update the bundle b.putInt("scrollX", mScrollX); b.putInt("scrollY", mScrollY); - b.putFloat("scale", mActualScale); - b.putFloat("textwrapScale", mTextWrapScale); - b.putBoolean("overview", mInZoomOverview); + mZoomManager.saveZoomState(b); return true; } private void restoreHistoryPictureFields(Picture p, Bundle b) { int sx = b.getInt("scrollX", 0); int sy = b.getInt("scrollY", 0); - float scale = b.getFloat("scale", 1.0f); + mDrawHistory = true; mHistoryPicture = p; mScrollX = sx; mScrollY = sy; + mZoomManager.restoreZoomState(b); + final float scale = mZoomManager.getScale(); mHistoryWidth = Math.round(p.getWidth() * scale); mHistoryHeight = Math.round(p.getHeight() * scale); - // as getWidth() / getHeight() of the view are not available yet, set up - // mActualScale, so that when onSizeChanged() is called, the rest will - // be set correctly - mActualScale = scale; - mInvActualScale = 1 / scale; - mTextWrapScale = b.getFloat("textwrapScale", scale); - mInZoomOverview = b.getBoolean("overview"); + invalidate(); } @@ -1638,6 +1522,45 @@ public class WebView extends AbsoluteLayout } /** + * Saves the current view as a web archive. + * + * @param filename The filename where the archive should be placed. + */ + public void saveWebArchive(String filename) { + saveWebArchive(filename, false, null); + } + + /* package */ static class SaveWebArchiveMessage { + SaveWebArchiveMessage (String basename, boolean autoname, ValueCallback<String> callback) { + mBasename = basename; + mAutoname = autoname; + mCallback = callback; + } + + /* package */ final String mBasename; + /* package */ final boolean mAutoname; + /* package */ final ValueCallback<String> mCallback; + /* package */ String mResultFile; + } + + /** + * Saves the current view as a web archive. + * + * @param basename The filename where the archive should be placed. + * @param autoname If false, takes basename to be a file. If true, basename + * is assumed to be a directory in which a filename will be + * chosen according to the url of the current page. + * @param callback Called after the web archive has been saved. The + * parameter for onReceiveValue will either be the filename + * under which the file was saved, or null if saving the + * file failed. + */ + public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { + mWebViewCore.sendMessage(EventHub.SAVE_WEBARCHIVE, + new SaveWebArchiveMessage(basename, autoname, callback)); + } + + /** * Stop the current load. */ public void stopLoading() { @@ -1806,6 +1729,7 @@ public class WebView extends AbsoluteLayout public void clearView() { mContentWidth = 0; mContentHeight = 0; + nativeSetBaseLayer(0); mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } @@ -1819,8 +1743,9 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { - if (null == mWebViewCore) return null; // check for out of memory tab - return mWebViewCore.copyContentPicture(); + Picture result = new Picture(); + nativeCopyBaseContentToPicture(result); + return result; } /** @@ -1849,7 +1774,7 @@ public class WebView extends AbsoluteLayout * @return The current scale. */ public float getScale() { - return mActualScale; + return mZoomManager.getScale(); } /** @@ -1861,7 +1786,7 @@ public class WebView extends AbsoluteLayout * @param scaleInPercent The initial scale in percent. */ public void setInitialScale(int scaleInPercent) { - mInitialScaleInPercent = scaleInPercent; + mZoomManager.setInitialScaleInPercent(scaleInPercent); } /** @@ -1875,13 +1800,7 @@ public class WebView extends AbsoluteLayout return; } clearTextEntry(false); - if (getSettings().getBuiltInZoomControls()) { - getZoomButtonsController().setVisible(true); - } else { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } + mZoomManager.invokeZoomPicker(); } /** @@ -1996,7 +1915,7 @@ public class WebView extends AbsoluteLayout msg.sendToTarget(); } - private static int pinLoc(int x, int viewMax, int docMax) { + static int pinLoc(int x, int viewMax, int docMax) { // Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax); if (docMax < viewMax) { // the doc has room on the sides for "blank" // pin the short document to the top/left of the screen @@ -2013,12 +1932,12 @@ public class WebView extends AbsoluteLayout } // Expects x in view coordinates - private int pinLocX(int x) { + int pinLocX(int x) { return pinLoc(x, getViewWidth(), computeHorizontalScrollRange()); } // Expects y in view coordinates - private int pinLocY(int y) { + int pinLocY(int y) { return pinLoc(y, getViewHeightWithTitle(), computeVerticalScrollRange() + getTitleHeight()); } @@ -2068,7 +1987,7 @@ public class WebView extends AbsoluteLayout * height. */ private int viewToContentDimension(int d) { - return Math.round(d * mInvActualScale); + return Math.round(d * mZoomManager.getInvScale()); } /** @@ -2094,7 +2013,7 @@ public class WebView extends AbsoluteLayout * Returns the result as a float. */ private float viewToContentXf(int x) { - return x * mInvActualScale; + return x * mZoomManager.getInvScale(); } /** @@ -2103,7 +2022,7 @@ public class WebView extends AbsoluteLayout * embedded into the WebView. Returns the result as a float. */ private float viewToContentYf(int y) { - return (y - getTitleHeight()) * mInvActualScale; + return (y - getTitleHeight()) * mZoomManager.getInvScale(); } /** @@ -2113,7 +2032,7 @@ public class WebView extends AbsoluteLayout * height. */ /*package*/ int contentToViewDimension(int d) { - return Math.round(d * mActualScale); + return Math.round(d * mZoomManager.getScale()); } /** @@ -2154,7 +2073,7 @@ public class WebView extends AbsoluteLayout // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { - final float scale = mActualScale; + final float scale = mZoomManager.getScale(); final int dy = getTitleHeight(); invalidate((int)Math.floor(l * scale), (int)Math.floor(t * scale) + dy, @@ -2165,7 +2084,7 @@ public class WebView extends AbsoluteLayout // Called by JNI to invalidate the View after a delay, given rectangle // coordinates in content space private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { - final float scale = mActualScale; + final float scale = mZoomManager.getScale(); final int dy = getTitleHeight(); postInvalidateDelayed(delay, (int)Math.floor(l * scale), @@ -2203,13 +2122,7 @@ public class WebView extends AbsoluteLayout // updated when we get out of that mode. if (!mDrawHistory) { // repin our scroll, taking into account the new content size - int oldX = mScrollX; - int oldY = mScrollY; - mScrollX = pinLocX(mScrollX); - mScrollY = pinLocY(mScrollY); - if (oldX != mScrollX || oldY != mScrollY) { - onScrollChanged(mScrollX, mScrollY, oldX, oldY); - } + updateScrollCoordinates(pinLocX(mScrollX), pinLocY(mScrollY)); if (!mScroller.isFinished()) { // We are in the middle of a scroll. Repin the final scroll // position. @@ -2221,77 +2134,12 @@ public class WebView extends AbsoluteLayout contentSizeChanged(updateLayout); } - private void setNewZoomScale(float scale, boolean updateTextWrapScale, - boolean force) { - if (scale < mMinZoomScale) { - scale = mMinZoomScale; - // set mInZoomOverview for non mobile sites - if (scale < mDefaultScale) mInZoomOverview = true; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; - } - if (updateTextWrapScale) { - mTextWrapScale = scale; - // reset mLastHeightSent to force VIEW_SIZE_CHANGED sent to WebKit - mLastHeightSent = 0; - } - if (scale != mActualScale || force) { - if (mDrawHistory) { - // If history Picture is drawn, don't update scroll. They will - // be updated when we get out of that mode. - if (scale != mActualScale && !mPreviewZoomOnly) { - mCallbackProxy.onScaleChanged(mActualScale, scale); - } - mActualScale = scale; - mInvActualScale = 1 / scale; - sendViewSizeZoom(); - } else { - // update our scroll so we don't appear to jump - // i.e. keep the center of the doc in the center of the view - - int oldX = mScrollX; - int oldY = mScrollY; - float ratio = scale * mInvActualScale; // old inverse - float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; - float sy = ratio * oldY + (ratio - 1) - * (mZoomCenterY - getTitleHeight()); - - // now update our new scale and inverse - if (scale != mActualScale && !mPreviewZoomOnly) { - mCallbackProxy.onScaleChanged(mActualScale, scale); - } - mActualScale = scale; - mInvActualScale = 1 / scale; - - // Scale all the child views - mViewManager.scaleAll(); - - // as we don't have animation for scaling, don't do animation - // for scrolling, as it causes weird intermediate state - // pinScrollTo(Math.round(sx), Math.round(sy)); - mScrollX = pinLocX(Math.round(sx)); - mScrollY = pinLocY(Math.round(sy)); - - // update webkit - if (oldX != mScrollX || oldY != mScrollY) { - onScrollChanged(mScrollX, mScrollY, oldX, oldY); - } else { - // the scroll position is adjusted at the beginning of the - // zoom animation. But we want to update the WebKit at the - // end of the zoom animation. See comments in onScaleEnd(). - sendOurVisibleRect(); - } - sendViewSizeZoom(); - } - } - } - // Used to avoid sending many visible rect messages. private Rect mLastVisibleRectSent; private Rect mLastGlobalRect; - private Rect sendOurVisibleRect() { - if (mPreviewZoomOnly) return mLastVisibleRectSent; + Rect sendOurVisibleRect() { + if (mZoomManager.isPreventingWebkitUpdates()) return mLastVisibleRectSent; Rect rect = new Rect(); calcOurContentVisibleRect(rect); @@ -2324,9 +2172,6 @@ public class WebView extends AbsoluteLayout Point p = new Point(); getGlobalVisibleRect(r, p); r.offset(-p.x, -p.y); - if (mFindIsUp) { - r.bottom -= mFindHeight; - } } // Sets r to be our visible rectangle in content coordinates @@ -2373,16 +2218,19 @@ public class WebView extends AbsoluteLayout /** * Compute unzoomed width and height, and if they differ from the last - * values we sent, send them to webkit (to be used has new viewport) + * values we sent, send them to webkit (to be used as new viewport) + * + * @param force ensures that the message is sent to webkit even if the width + * or height has not changed since the last message * * @return true if new values were sent */ - private boolean sendViewSizeZoom() { - if (mPreviewZoomOnly) return false; + boolean sendViewSizeZoom(boolean force) { + if (mZoomManager.isPreventingWebkitUpdates()) return false; int viewWidth = getViewWidth(); - int newWidth = Math.round(viewWidth * mInvActualScale); - int newHeight = Math.round(getViewHeight() * mInvActualScale); + int newWidth = Math.round(viewWidth * mZoomManager.getInvScale()); + int newHeight = Math.round(getViewHeight() * mZoomManager.getInvScale()); /* * Because the native side may have already done a layout before the * View system was able to measure us, we have to send a height of 0 to @@ -2395,19 +2243,20 @@ public class WebView extends AbsoluteLayout newHeight = 0; } // Avoid sending another message if the dimensions have not changed. - if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) { + if (newWidth != mLastWidthSent || newHeight != mLastHeightSent || force) { ViewSizeData data = new ViewSizeData(); data.mWidth = newWidth; data.mHeight = newHeight; - data.mTextWrapWidth = Math.round(viewWidth / mTextWrapScale);; - data.mScale = mActualScale; - data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure; - data.mAnchorX = mAnchorX; - data.mAnchorY = mAnchorY; + data.mTextWrapWidth = Math.round(viewWidth / mZoomManager.getTextWrapScale()); + data.mScale = mZoomManager.getScale(); + data.mIgnoreHeight = mZoomManager.isFixedLengthAnimationInProgress() + && !mHeightCanMeasure; + data.mAnchorX = mZoomManager.getDocumentAnchorX(); + data.mAnchorY = mZoomManager.getDocumentAnchorY(); mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; - mAnchorX = mAnchorY = 0; + mZoomManager.clearDocumentAnchor(); return true; } return false; @@ -2418,12 +2267,12 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryWidth; } else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF - && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { + && !mZoomManager.canZoomOut()) { // only honor the scrollbar mode when it is at minimum zoom level return computeHorizontalScrollExtent(); } else { // to avoid rounding error caused unnecessary scrollbar, use floor - return (int) Math.floor(mContentWidth * mActualScale); + return (int) Math.floor(mContentWidth * mZoomManager.getScale()); } } @@ -2432,12 +2281,12 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryHeight; } else if (mVerticalScrollBarMode == SCROLLBAR_ALWAYSOFF - && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { + && !mZoomManager.canZoomOut()) { // only honor the scrollbar mode when it is at minimum zoom level return computeVerticalScrollExtent(); } else { // to avoid rounding error caused unnecessary scrollbar, use floor - return (int) Math.floor(mContentHeight * mActualScale); + return (int) Math.floor(mContentHeight * mZoomManager.getScale()); } } @@ -2505,7 +2354,9 @@ public class WebView extends AbsoluteLayout } /** - * Get the touch icon url for the apple-touch-icon <link> element. + * Get the touch icon url for the apple-touch-icon <link> element, or + * a URL on this site's server pointing to the standard location of a + * touch icon. * @hide */ public String getTouchIconUrl() { @@ -2682,19 +2533,22 @@ public class WebView extends AbsoluteLayout */ public void setFindIsUp(boolean isUp) { mFindIsUp = isUp; - if (isUp) { - recordNewContentSize(mContentWidth, mContentHeight + mFindHeight, - false); - } if (0 == mNativeClass) return; // client isn't initialized nativeSetFindIsUp(isUp); } + /** + * @hide + */ + public int findIndex() { + if (0 == mNativeClass) return -1; + return nativeFindIndex(); + } + // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; - private int mFindHeight; // Keep track of the last string sent, so we can search again after an // orientation change or the dismissal of the soft keyboard. private String mLastFind; @@ -2769,8 +2623,6 @@ public class WebView extends AbsoluteLayout } clearMatches(); setFindIsUp(false); - recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, - false); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. pinScrollTo(mScrollX, mScrollY, false, 0); @@ -2778,16 +2630,6 @@ public class WebView extends AbsoluteLayout } /** - * @hide - */ - public void setFindDialogHeight(int height) { - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "setFindDialogHeight height=" + height); - } - mFindHeight = height; - } - - /** * Query the document to see if it contains any image references. The * message object will be dispatched with arg1 being set to 1 if images * were found and 0 if the document does not reference any images. @@ -2810,6 +2652,11 @@ public class WebView extends AbsoluteLayout postInvalidate(); // So we draw again if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); + } else { + abortAnimation(); + mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); + WebViewCore.resumePriority(); + WebViewCore.resumeUpdatePicture(mWebViewCore); } } else { super.computeScroll(); @@ -2898,6 +2745,29 @@ public class WebView extends AbsoluteLayout } mPageThatNeedsToSlideTitleBarOffScreen = null; } + + injectAccessibilityForUrl(url); + } + + /** + * This method injects accessibility in the loaded document if accessibility + * is enabled. If JavaScript is enabled we try to inject a URL specific script. + * If no URL specific script is found or JavaScript is disabled we fallback to + * the default {@link AccessibilityInjector} implementation. + * + * @param url The URL loaded by this {@link WebView}. + */ + private void injectAccessibilityForUrl(String url) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + if (getSettings().getJavaScriptEnabled()) { + loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + } else if (mAccessibilityInjector == null) { + mAccessibilityInjector = new AccessibilityInjector(this); + } + } else { + // it is possible that accessibility was turned off between reloads + mAccessibilityInjector = null; + } } /** @@ -3014,7 +2884,7 @@ public class WebView extends AbsoluteLayout } else { // If we don't request a layout, try to send our view size to the // native side to ensure that WebCore has the correct dimensions. - sendViewSizeZoom(); + sendViewSizeZoom(false); } } @@ -3144,7 +3014,7 @@ public class WebView extends AbsoluteLayout * settings. */ public WebSettings getSettings() { - return mWebViewCore.getSettings(); + return (mWebViewCore != null) ? mWebViewCore.getSettings() : null; } /** @@ -3285,7 +3155,47 @@ public class WebView extends AbsoluteLayout if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } + if (inEditingMode()) mWebTextView.onDrawSubstitute(); mWebViewCore.signalRepaintDone(); + + // paint the highlight in the end + if (!mTouchHighlightRegion.isEmpty()) { + if (mTouchHightlightPaint == null) { + mTouchHightlightPaint = new Paint(); + mTouchHightlightPaint.setColor(mHightlightColor); + mTouchHightlightPaint.setAntiAlias(true); + mTouchHightlightPaint.setPathEffect(new CornerPathEffect( + TOUCH_HIGHLIGHT_ARC)); + } + canvas.drawPath(mTouchHighlightRegion.getBoundaryPath(), + mTouchHightlightPaint); + } + if (DEBUG_TOUCH_HIGHLIGHT) { + if (getSettings().getNavDump()) { + if ((mTouchHighlightX | mTouchHighlightY) != 0) { + if (mTouchCrossHairColor == null) { + mTouchCrossHairColor = new Paint(); + mTouchCrossHairColor.setColor(Color.RED); + } + canvas.drawLine(mTouchHighlightX - mNavSlop, + mTouchHighlightY - mNavSlop, mTouchHighlightX + + mNavSlop + 1, mTouchHighlightY + mNavSlop + + 1, mTouchCrossHairColor); + canvas.drawLine(mTouchHighlightX + mNavSlop + 1, + mTouchHighlightY - mNavSlop, mTouchHighlightX + - mNavSlop, + mTouchHighlightY + mNavSlop + 1, + mTouchCrossHairColor); + } + } + } + } + + private void removeTouchHighlight(boolean removePendingMessage) { + if (removePendingMessage) { + mWebViewCore.removeMessages(EventHub.GET_TOUCH_HIGHLIGHT_RECTS); + } + mWebViewCore.sendMessage(EventHub.REMOVE_TOUCH_HIGHLIGHT_RECTS); } @Override @@ -3306,27 +3216,35 @@ public class WebView extends AbsoluteLayout // Send the click so that the textfield is in focus centerKeyPressOnTextField(); rebuildWebTextView(); + } else { + clearTextEntry(true); } if (inEditingMode()) { return mWebTextView.performLongClick(); - } else { - return super.performLongClick(); } + /* if long click brings up a context menu, the super function + * returns true and we're done. Otherwise, nothing happened when + * the user clicked. */ + if (super.performLongClick()) { + return true; + } + /* In the case where the application hasn't already handled the long + * click action, look for a word under the click. If one is found, + * animate the text selection into view. + * FIXME: no animation code yet */ + if (mSelectingText) return false; // long click does nothing on selection + int x = viewToContentX((int) mLastTouchX + mScrollX); + int y = viewToContentY((int) mLastTouchY + mScrollY); + setUpSelect(); + if (mNativeClass != 0 && nativeWordSelection(x, y)) { + nativeSetExtendSelection(); + getWebChromeClient().onSelectionStart(); + return true; + } + notifySelectDialogDismissed(); + return false; } - boolean inAnimateZoom() { - return mZoomScale != 0; - } - - /** - * Need to adjust the WebTextView after a change in zoom, since mActualScale - * has changed. This is especially important for password fields, which are - * drawn by the WebTextView, since it conveys more information than what - * webkit draws. Thus we need to reposition it to show in the correct - * place. - */ - private boolean mNeedToAdjustWebTextView; - private boolean didUpdateTextViewBounds(boolean allowIntersect) { Rect contentBounds = nativeFocusCandidateNodeBounds(); Rect vBox = contentToViewRect(contentBounds); @@ -3354,25 +3272,54 @@ public class WebView extends AbsoluteLayout } } - private void drawExtras(Canvas canvas, int extras, boolean animationsRunning) { - // If mNativeClass is 0, we should not reach here, so we do not - // need to check it again. - if (animationsRunning) { - canvas.setDrawFilter(mWebViewCore.mZoomFilter); + private void onZoomAnimationStart() { + // If it is in password mode, turn it off so it does not draw misplaced. + if (inEditingMode() && nativeFocusCandidateIsPassword()) { + mWebTextView.setInPassword(false); } - nativeDrawExtras(canvas, extras); - canvas.setDrawFilter(null); } + private void onZoomAnimationEnd() { + // adjust the edit text view if needed + if (inEditingMode() && didUpdateTextViewBounds(false) && nativeFocusCandidateIsPassword()) { + // If it is a password field, start drawing the WebTextView once + // again. + mWebTextView.setInPassword(true); + } + } + + void onFixedLengthZoomAnimationStart() { + WebViewCore.pauseUpdatePicture(getWebViewCore()); + onZoomAnimationStart(); + } + + void onFixedLengthZoomAnimationEnd() { + WebViewCore.resumeUpdatePicture(mWebViewCore); + onZoomAnimationEnd(); + } + + private static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG | + Paint.DITHER_FLAG | + Paint.SUBPIXEL_TEXT_FLAG; + private static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG | + Paint.DITHER_FLAG; + + private final DrawFilter mZoomFilter = + new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); + // If we need to trade better quality for speed, set mScrollFilter to null + private final DrawFilter mScrollFilter = + new PaintFlagsDrawFilter(SCROLL_BITS, 0); + private void drawCoreAndCursorRing(Canvas canvas, int color, boolean drawCursorRing) { if (mDrawHistory) { - canvas.scale(mActualScale, mActualScale); + canvas.scale(mZoomManager.getScale(), mZoomManager.getScale()); canvas.drawPicture(mHistoryPicture); return; } + if (mNativeClass == 0) return; - boolean animateZoom = mZoomScale != 0; + boolean animateZoom = mZoomManager.isFixedLengthAnimationInProgress(); boolean animateScroll = ((!mScroller.isFinished() || mVelocityTracker != null) && (mTouchMode != TOUCH_DRAG_MODE || @@ -3391,59 +3338,9 @@ public class WebView extends AbsoluteLayout } } if (animateZoom) { - float zoomScale; - int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); - if (interval < ZOOM_ANIMATION_LENGTH) { - float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; - zoomScale = 1.0f / (mInvInitialZoomScale - + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); - invalidate(); - } else { - zoomScale = mZoomScale; - // set mZoomScale to be 0 as we have done animation - mZoomScale = 0; - WebViewCore.resumeUpdatePicture(mWebViewCore); - // call invalidate() again to draw with the final filters - invalidate(); - if (mNeedToAdjustWebTextView) { - mNeedToAdjustWebTextView = false; - if (didUpdateTextViewBounds(false) - && nativeFocusCandidateIsPassword()) { - // If it is a password field, start drawing the - // WebTextView once again. - mWebTextView.setInPassword(true); - } - } - } - // calculate the intermediate scroll position. As we need to use - // zoomScale, we can't use pinLocX/Y directly. Copy the logic here. - float scale = zoomScale * mInvInitialZoomScale; - int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - - mZoomCenterX); - tx = -pinLoc(tx, getViewWidth(), Math.round(mContentWidth - * zoomScale)) + mScrollX; - int titleHeight = getTitleHeight(); - int ty = Math.round(scale - * (mInitialScrollY + mZoomCenterY - titleHeight) - - (mZoomCenterY - titleHeight)); - ty = -(ty <= titleHeight ? Math.max(ty, 0) : pinLoc(ty - - titleHeight, getViewHeight(), Math.round(mContentHeight - * zoomScale)) + titleHeight) + mScrollY; - canvas.translate(tx, ty); - canvas.scale(zoomScale, zoomScale); - if (inEditingMode() && !mNeedToAdjustWebTextView - && mZoomScale != 0) { - // The WebTextView is up. Keep track of this so we can adjust - // its size and placement when we finish zooming - mNeedToAdjustWebTextView = true; - // If it is in password mode, turn it off so it does not draw - // misplaced. - if (nativeFocusCandidateIsPassword()) { - mWebTextView.setInPassword(false); - } - } + mZoomManager.animateZoom(canvas); } else { - canvas.scale(mActualScale, mActualScale); + canvas.scale(mZoomManager.getScale(), mZoomManager.getScale()); } boolean UIAnimationsRunning = false; @@ -3455,39 +3352,42 @@ public class WebView extends AbsoluteLayout // we ask for a repaint. invalidate(); } - mWebViewCore.drawContentPicture(canvas, color, - (animateZoom || mPreviewZoomOnly || UIAnimationsRunning), - animateScroll); - if (mNativeClass == 0) return; + // decide which adornments to draw int extras = DRAW_EXTRAS_NONE; + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "mFindIsUp=" + mFindIsUp + + " mSelectingText=" + mSelectingText + + " nativePageShouldHandleShiftAndArrows()=" + + nativePageShouldHandleShiftAndArrows() + + " animateZoom=" + animateZoom); + } if (mFindIsUp) { - // When the FindDialog is up, only draw the matches if we are not in - // the process of scrolling them into view. - if (!animateScroll) { - extras = DRAW_EXTRAS_FIND; - } - } else if (mShiftIsPressed && !nativeFocusIsPlugin()) { - if (!animateZoom && !mPreviewZoomOnly) { - extras = DRAW_EXTRAS_SELECTION; - nativeSetSelectionRegion(mTouchSelection || mExtendSelection); - nativeSetSelectionPointer(!mTouchSelection, mInvActualScale, - mSelectX, mSelectY - getTitleHeight(), - mExtendSelection); - } + extras = DRAW_EXTRAS_FIND; + } else if (mSelectingText) { + extras = DRAW_EXTRAS_SELECTION; + nativeSetSelectionPointer(mDrawSelectionPointer, + mZoomManager.getInvScale(), + mSelectX, mSelectY - getTitleHeight()); } else if (drawCursorRing) { extras = DRAW_EXTRAS_CURSOR_RING; } - drawExtras(canvas, extras, UIAnimationsRunning); + DrawFilter df = null; + if (mZoomManager.isZoomAnimating() || UIAnimationsRunning) { + df = mZoomFilter; + } else if (animateScroll) { + df = mScrollFilter; + } + canvas.setDrawFilter(df); + int content = nativeDraw(canvas, color, extras, true); + canvas.setDrawFilter(null); + if (content != 0) { + mWebViewCore.sendMessage(EventHub.SPLIT_PICTURE_SET, content, 0); + } if (extras == DRAW_EXTRAS_CURSOR_RING) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; - HitTestResult hitTest = getHitTestResult(); - if (hitTest == null - || hitTest.mType == HitTestResult.UNKNOWN_TYPE) { - mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); - } } } if (mFocusSizeChanged) { @@ -3512,10 +3412,14 @@ public class WebView extends AbsoluteLayout return mDrawHistory; } + int getHistoryPictureWidth() { + return (mHistoryPicture != null) ? mHistoryPicture.getWidth() : 0; + } + // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this - if (mDrawHistory && mWebViewCore.pictureReady()) { + if (mDrawHistory && (getProgress() == 100 || nativeHasContent())) { mDrawHistory = false; mHistoryPicture = null; invalidate(); @@ -3566,7 +3470,9 @@ public class WebView extends AbsoluteLayout * @param end End of selection. */ /* package */ void setSelection(int start, int end) { - mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); + if (mWebViewCore != null) { + mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); + } } @Override @@ -3585,13 +3491,10 @@ public class WebView extends AbsoluteLayout getContext().getSystemService(Context.INPUT_METHOD_SERVICE); // bring it back to the default scale so that user can enter text - boolean zoom = mActualScale < mDefaultScale; + boolean zoom = mZoomManager.getScale() < mZoomManager.getDefaultScale(); if (zoom) { - mInZoomOverview = false; - mZoomCenterX = mLastTouchX; - mZoomCenterY = mLastTouchY; - // do not change text wrap scale so that there is no reflow - setNewZoomScale(mDefaultScale, false, false); + mZoomManager.setZoomCenter(mLastTouchX, mLastTouchY); + mZoomManager.setZoomScale(mZoomManager.getDefaultScale(), false); } if (isTextView) { rebuildWebTextView(); @@ -3817,17 +3720,19 @@ public class WebView extends AbsoluteLayout // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; + // 3. the accessibility injector is present and wants to handle it; if (event.isSystem() - || mCallbackProxy.uiOverrideKeyEvent(event)) { + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) { return false; } if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativeFocusIsPlugin()) { + if (nativePageShouldHandleShiftAndArrows()) { mShiftIsPressed = true; - } else if (!nativeCursorWantsKeyEvents() && !mShiftIsPressed) { - setUpSelectXY(); + } else if (!nativeCursorWantsKeyEvents() && !mSelectingText) { + setUpSelect(); } } @@ -3844,11 +3749,11 @@ public class WebView extends AbsoluteLayout if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { switchOutDrawHistory(); - if (nativeFocusIsPlugin()) { - letPluginHandleNavKey(keyCode, event.getEventTime(), true); + if (nativePageShouldHandleShiftAndArrows()) { + letPageHandleNavKey(keyCode, event.getEventTime(), true); return true; } - if (mShiftIsPressed) { + if (mSelectingText) { int xRate = keyCode == KeyEvent.KEYCODE_DPAD_LEFT ? -1 : keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ? 1 : 0; int yRate = keyCode == KeyEvent.KEYCODE_DPAD_UP ? @@ -3868,7 +3773,7 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { - if (mShiftIsPressed && !nativeFocusIsPlugin()) { + if (mSelectingText) { return true; // discard press if copy in progress } mGotCenterDown = true; @@ -3886,10 +3791,8 @@ public class WebView extends AbsoluteLayout if (keyCode != KeyEvent.KEYCODE_SHIFT_LEFT && keyCode != KeyEvent.KEYCODE_SHIFT_RIGHT) { // turn off copy select if a shift-key combo is pressed - mExtendSelection = mShiftIsPressed = false; - if (mTouchMode == TOUCH_SELECT_MODE) { - mTouchMode = TOUCH_INIT_MODE; - } + selectionDone(); + mShiftIsPressed = false; } if (getSettings().getNavDump()) { @@ -3971,23 +3874,27 @@ public class WebView extends AbsoluteLayout // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; - if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { + // 3. the accessibility injector is present and wants to handle it; + if (event.isSystem() + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) { return false; } if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativeFocusIsPlugin()) { + if (nativePageShouldHandleShiftAndArrows()) { mShiftIsPressed = false; - } else if (commitCopy()) { + } else if (copySelection()) { + selectionDone(); return true; } } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { - if (nativeFocusIsPlugin()) { - letPluginHandleNavKey(keyCode, event.getEventTime(), false); + if (nativePageShouldHandleShiftAndArrows()) { + letPageHandleNavKey(keyCode, event.getEventTime(), false); return true; } // always handle the navigation keys in the UI thread @@ -4000,11 +3907,13 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mGotCenterDown = false; - if (mShiftIsPressed && !nativeFocusIsPlugin()) { + if (mSelectingText) { if (mExtendSelection) { - commitCopy(); + copySelection(); + selectionDone(); } else { mExtendSelection = true; + nativeSetExtendSelection(); invalidate(); // draw the i-beam instead of the arrow } return true; // discard press if copy in progress @@ -4049,9 +3958,18 @@ public class WebView extends AbsoluteLayout return false; } - private void setUpSelectXY() { + /** + * @hide pending API council approval. + */ + public void setUpSelect() { + if (0 == mNativeClass) return; // client isn't initialized + if (inFullScreenMode()) return; + if (mSelectingText) return; mExtendSelection = false; - mShiftIsPressed = true; + mSelectingText = mDrawSelectionPointer = true; + // don't let the picture change during text selection + WebViewCore.pauseUpdatePicture(mWebViewCore); + nativeResetSelection(); if (nativeHasCursorNode()) { Rect rect = nativeCursorNodeBounds(); mSelectX = contentToViewX(rect.left); @@ -4071,40 +3989,82 @@ public class WebView extends AbsoluteLayout * Do not rely on this functionality; it will be deprecated in the future. */ public void emulateShiftHeld() { + setUpSelect(); + } + + /** + * @hide pending API council approval. + */ + public void selectAll() { if (0 == mNativeClass) return; // client isn't initialized - setUpSelectXY(); + if (inFullScreenMode()) return; + if (!mSelectingText) setUpSelect(); + nativeSelectAll(); + mDrawSelectionPointer = false; + mExtendSelection = true; + invalidate(); } - private boolean commitCopy() { + /** + * @hide pending API council approval. + */ + public boolean selectDialogIsUp() { + return mSelectingText; + } + + /** + * @hide pending API council approval. + */ + public void notifySelectDialogDismissed() { + mSelectingText = false; + WebViewCore.resumeUpdatePicture(mWebViewCore); + } + + /** + * @hide pending API council approval. + */ + public void selectionDone() { + if (mSelectingText) { + getWebChromeClient().onSelectionDone(); + invalidate(); // redraw without selection + notifySelectDialogDismissed(); + } + } + + /** + * @hide pending API council approval. + */ + public boolean copySelection() { boolean copiedSomething = false; - if (mExtendSelection) { - String selection = nativeGetSelection(); - if (selection != "") { - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "commitCopy \"" + selection + "\""); - } - Toast.makeText(mContext - , com.android.internal.R.string.text_copied - , Toast.LENGTH_SHORT).show(); - copiedSomething = true; - try { - IClipboard clip = IClipboard.Stub.asInterface( - ServiceManager.getService("clipboard")); - clip.setClipboardText(selection); - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Clipboard failed", e); - } + String selection = getSelection(); + if (selection != "") { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "copySelection \"" + selection + "\""); + } + Toast.makeText(mContext + , com.android.internal.R.string.text_copied + , Toast.LENGTH_SHORT).show(); + copiedSomething = true; + try { + IClipboard clip = IClipboard.Stub.asInterface( + ServiceManager.getService("clipboard")); + clip.setClipboardText(selection); + } catch (android.os.RemoteException e) { + Log.e(LOGTAG, "Clipboard failed", e); } - mExtendSelection = false; } - mShiftIsPressed = false; invalidate(); // remove selection region and pointer - if (mTouchMode == TOUCH_SELECT_MODE) { - mTouchMode = TOUCH_INIT_MODE; - } return copiedSomething; } + /** + * @hide pending API council approval. + */ + public String getSelection() { + if (mNativeClass == 0) return ""; + return nativeGetSelection(); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -4114,11 +4074,21 @@ public class WebView extends AbsoluteLayout @Override protected void onDetachedFromWindow() { clearTextEntry(false); - dismissZoomControl(); + mZoomManager.dismissZoomPicker(); if (hasWindowFocus()) setActive(false); super.onDetachedFromWindow(); } + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // The zoomManager may be null if the webview is created from XML that + // specifies the view's visibility param as not visible (see http://b/2794841) + if (visibility != View.VISIBLE && mZoomManager != null) { + mZoomManager.dismissZoomPicker(); + } + } + /** * @deprecated WebView no longer needs to implement * ViewGroup.OnHierarchyChangeListener. This method does nothing now. @@ -4163,17 +4133,14 @@ public class WebView extends AbsoluteLayout // false for the first parameter } } else { - if (mWebViewCore != null && getSettings().getBuiltInZoomControls() - && (mZoomButtonsController == null || - !mZoomButtonsController.isVisible())) { + if (!mZoomManager.isZoomPickerVisible()) { /* - * The zoom controls come in their own window, so our window - * loses focus. Our policy is to not draw the cursor ring if - * our window is not focused, but this is an exception since + * The external zoom controls come in their own window, so our + * window loses focus. Our policy is to not draw the cursor ring + * if our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ - // If our window has lost focus, stop drawing the cursor ring mDrawCursorRing = false; } mGotKeyDown = false; @@ -4262,81 +4229,24 @@ public class WebView extends AbsoluteLayout // system won't call onSizeChanged if the dimension is not changed. // In this case, we need to call sendViewSizeZoom() explicitly to // notify the WebKit about the new dimensions. - sendViewSizeZoom(); + sendViewSizeZoom(false); } return changed; } - private static class PostScale implements Runnable { - final WebView mWebView; - final boolean mUpdateTextWrap; - - public PostScale(WebView webView, boolean updateTextWrap) { - mWebView = webView; - mUpdateTextWrap = updateTextWrap; - } - - public void run() { - if (mWebView.mWebViewCore != null) { - // we always force, in case our height changed, in which case we - // still want to send the notification over to webkit. - mWebView.setNewZoomScale(mWebView.mActualScale, - mUpdateTextWrap, true); - // update the zoom buttons as the scale can be changed - if (mWebView.getSettings().getBuiltInZoomControls()) { - mWebView.updateZoomButtonsEnabled(); - } - } - } - } - @Override protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); - // Center zooming to the center of the screen. - if (mZoomScale == 0) { // unless we're already zooming - // To anchor at top left corner. - mZoomCenterX = 0; - mZoomCenterY = getVisibleTitleHeight(); - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - } // adjust the max viewport width depending on the view dimensions. This // is to ensure the scaling is not going insane. So do not shrink it if // the view size is temporarily smaller, e.g. when soft keyboard is up. - int newMaxViewportWidth = (int) (Math.max(w, h) / DEFAULT_MIN_ZOOM_SCALE); + int newMaxViewportWidth = (int) (Math.max(w, h) / mZoomManager.getDefaultMinZoomScale()); if (newMaxViewportWidth > sMaxViewportWidth) { sMaxViewportWidth = newMaxViewportWidth; } - // update mMinZoomScale if the minimum zoom scale is not fixed - if (!mMinZoomScaleFixed) { - // when change from narrow screen to wide screen, the new viewWidth - // can be wider than the old content width. We limit the minimum - // scale to 1.0f. The proper minimum scale will be calculated when - // the new picture shows up. - mMinZoomScale = Math.min(1.0f, (float) getViewWidth() - / (mDrawHistory ? mHistoryPicture.getWidth() - : mZoomOverviewWidth)); - if (mInitialScaleInPercent > 0) { - // limit the minZoomScale to the initialScale if it is set - float initialScale = mInitialScaleInPercent / 100.0f; - if (mMinZoomScale > initialScale) { - mMinZoomScale = initialScale; - } - } - } - - dismissZoomControl(); - - // onSizeChanged() is called during WebView layout. And any - // requestLayout() is blocked during layout. As setNewZoomScale() will - // call its child View to reposition itself through ViewManager's - // scaleAll(), we need to post a Runnable to ensure requestLayout(). - // <b/> - // only update the text wrap scale if width changed. - post(new PostScale(this, w != ow)); + mZoomManager.onSizeChanged(w, h, ow, oh); } @Override @@ -4347,7 +4257,7 @@ public class WebView extends AbsoluteLayout // as getVisibleTitleHeight. int titleHeight = getTitleHeight(); if (Math.max(titleHeight - t, 0) != Math.max(titleHeight - oldt, 0)) { - sendViewSizeZoom(); + sendViewSizeZoom(false); } } @@ -4355,9 +4265,11 @@ public class WebView extends AbsoluteLayout public boolean dispatchKeyEvent(KeyEvent event) { boolean dispatch = true; - // Textfields and plugins need to receive the shift up key even if - // another key was released while the shift key was held down. - if (!inEditingMode() && (mNativeClass == 0 || !nativeFocusIsPlugin())) { + // Textfields, plugins, and contentEditable nodes need to receive the + // shift up key even if another key was released while the shift key + // was held down. + if (!inEditingMode() && (mNativeClass == 0 + || !nativePageShouldHandleShiftAndArrows())) { if (event.getAction() == KeyEvent.ACTION_DOWN) { mGotKeyDown = true; } else { @@ -4591,81 +4503,6 @@ public class WebView extends AbsoluteLayout private DragTracker mDragTracker; private DragTrackerHandler mDragTrackerHandler; - private class ScaleDetectorListener implements - ScaleGestureDetector.OnScaleGestureListener { - - public boolean onScaleBegin(ScaleGestureDetector detector) { - // cancel the single touch handling - cancelTouch(); - dismissZoomControl(); - // reset the zoom overview mode so that the page won't auto grow - mInZoomOverview = false; - // If it is in password mode, turn it off so it does not draw - // misplaced. - if (inEditingMode() && nativeFocusCandidateIsPassword()) { - mWebTextView.setInPassword(false); - } - - mViewManager.startZoom(); - - return true; - } - - public void onScaleEnd(ScaleGestureDetector detector) { - if (mPreviewZoomOnly) { - mPreviewZoomOnly = false; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - // don't reflow when zoom in; when zoom out, do reflow if the - // new scale is almost minimum scale; - boolean reflowNow = (mActualScale - mMinZoomScale - <= MINIMUM_SCALE_INCREMENT) - || ((mActualScale <= 0.8 * mTextWrapScale)); - // force zoom after mPreviewZoomOnly is set to false so that the - // new view size will be passed to the WebKit - setNewZoomScale(mActualScale, reflowNow, true); - // call invalidate() to draw without zoom filter - invalidate(); - } - // adjust the edit text view if needed - if (inEditingMode() && didUpdateTextViewBounds(false) - && nativeFocusCandidateIsPassword()) { - // If it is a password field, start drawing the - // WebTextView once again. - mWebTextView.setInPassword(true); - } - // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as it - // may trigger the unwanted click, can't use TOUCH_DRAG_MODE as it - // may trigger the unwanted fling. - mTouchMode = TOUCH_PINCH_DRAG; - mConfirmMove = true; - startTouch(detector.getFocusX(), detector.getFocusY(), - mLastTouchTime); - - mViewManager.endZoom(); - } - - public boolean onScale(ScaleGestureDetector detector) { - float scale = (float) (Math.round(detector.getScaleFactor() - * mActualScale * 100) / 100.0); - if (Math.abs(scale - mActualScale) >= MINIMUM_SCALE_INCREMENT) { - mPreviewZoomOnly = true; - // limit the scale change per step - if (scale > mActualScale) { - scale = Math.min(scale, mActualScale * 1.25f); - } else { - scale = Math.max(scale, mActualScale * 0.8f); - } - mZoomCenterX = detector.getFocusX(); - mZoomCenterY = detector.getFocusY(); - setNewZoomScale(scale, false, false); - invalidate(); - return true; - } - return false; - } - } - private boolean hitFocusedPlugin(int contentX, int contentY) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "nativeFocusIsPlugin()=" + nativeFocusIsPlugin()); @@ -4679,7 +4516,7 @@ public class WebView extends AbsoluteLayout private boolean shouldForwardTouchEvent() { return mFullScreenHolder != null || (mForwardTouchEvents - && mTouchMode != TOUCH_SELECT_MODE + && !mSelectingText && mPreventDefault != PREVENT_DEFAULT_IGNORE); } @@ -4687,6 +4524,22 @@ public class WebView extends AbsoluteLayout return mFullScreenHolder != null; } + void onPinchToZoomAnimationStart() { + // cancel the single touch handling + cancelTouch(); + onZoomAnimationStart(); + } + + void onPinchToZoomAnimationEnd(ScaleGestureDetector detector) { + onZoomAnimationEnd(); + // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as + // it may trigger the unwanted click, can't use TOUCH_DRAG_MODE + // as it may trigger the unwanted fling. + mTouchMode = TOUCH_PINCH_DRAG; + mConfirmMove = true; + startTouch(detector.getFocusX(), detector.getFocusY(), mLastTouchTime); + } + @Override public boolean onTouchEvent(MotionEvent ev) { if (mNativeClass == 0 || !isClickable() || !isLongClickable()) { @@ -4704,32 +4557,36 @@ public class WebView extends AbsoluteLayout // FIXME: we may consider to give WebKit an option to handle multi-touch // events later. - if (mSupportMultiTouch && ev.getPointerCount() > 1) { - if (mMinZoomScale < mMaxZoomScale) { - mScaleDetector.onTouchEvent(ev); - if (mScaleDetector.isInProgress()) { - mLastTouchTime = eventTime; + if (mZoomManager.supportsMultiTouchZoom() && ev.getPointerCount() > 1) { + + // if the page disallows zoom, then skip multi-pointer action + if (mZoomManager.isZoomScaleFixed()) { + return true; + } + + ScaleGestureDetector detector = mZoomManager.getMultiTouchGestureDetector(); + detector.onTouchEvent(ev); + + if (detector.isInProgress()) { + mLastTouchTime = eventTime; + return true; + } + + x = detector.getFocusX(); + y = detector.getFocusY(); + action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action == MotionEvent.ACTION_POINTER_DOWN) { + cancelTouch(); + action = MotionEvent.ACTION_DOWN; + } else if (action == MotionEvent.ACTION_POINTER_UP) { + // set mLastTouchX/Y to the remaining point + mLastTouchX = x; + mLastTouchY = y; + } else if (action == MotionEvent.ACTION_MOVE) { + // negative x or y indicate it is on the edge, skip it. + if (x < 0 || y < 0) { return true; } - x = mScaleDetector.getFocusX(); - y = mScaleDetector.getFocusY(); - action = ev.getAction() & MotionEvent.ACTION_MASK; - if (action == MotionEvent.ACTION_POINTER_DOWN) { - cancelTouch(); - action = MotionEvent.ACTION_DOWN; - } else if (action == MotionEvent.ACTION_POINTER_UP) { - // set mLastTouchX/Y to the remaining point - mLastTouchX = x; - mLastTouchY = y; - } else if (action == MotionEvent.ACTION_MOVE) { - // negative x or y indicate it is on the edge, skip it. - if (x < 0 || y < 0) { - return true; - } - } - } else { - // if the page disallow zoom, skip multi-pointer action - return true; } } else { action = ev.getAction(); @@ -4767,18 +4624,11 @@ public class WebView extends AbsoluteLayout mTouchMode = TOUCH_DRAG_START_MODE; mConfirmMove = true; mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); - } else if (!inFullScreenMode() && mShiftIsPressed) { - mSelectX = mScrollX + (int) x; - mSelectY = mScrollY + (int) y; - mTouchMode = TOUCH_SELECT_MODE; - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); - } - nativeMoveSelection(contentX, contentY, false); - mTouchSelection = mExtendSelection = true; - invalidate(); // draw the i-beam instead of the arrow } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); + if (getSettings().supportTouchOnly()) { + removeTouchHighlight(true); + } if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { mTouchMode = TOUCH_DOUBLE_TAP_MODE; } else { @@ -4790,17 +4640,45 @@ public class WebView extends AbsoluteLayout contentX, contentY) : false; } } else { // the normal case - mPreviewZoomOnly = false; mTouchMode = TOUCH_INIT_MODE; mDeferTouchProcess = (!inFullScreenMode() && mForwardTouchEvents) ? hitFocusedPlugin( contentX, contentY) : false; mWebViewCore.sendMessage( EventHub.UPDATE_FRAME_CACHE_IF_LOADING); + if (getSettings().supportTouchOnly()) { + TouchHighlightData data = new TouchHighlightData(); + data.mX = contentX; + data.mY = contentY; + data.mSlop = viewToContentDimension(mNavSlop); + mWebViewCore.sendMessageDelayed( + EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data, + ViewConfiguration.getTapTimeout()); + if (DEBUG_TOUCH_HIGHLIGHT) { + if (getSettings().getNavDump()) { + mTouchHighlightX = (int) x + mScrollX; + mTouchHighlightY = (int) y + mScrollY; + mPrivateHandler.postDelayed(new Runnable() { + public void run() { + mTouchHighlightX = mTouchHighlightY = 0; + invalidate(); + } + }, TOUCH_HIGHLIGHT_ELAPSE_TIME); + } + } + } if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { EventLog.writeEvent(EventLogTags.BROWSER_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } + if (mSelectingText) { + mDrawSelectionPointer = false; + mSelectionStarted = nativeStartSelection(contentX, contentY); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "select=" + contentX + "," + contentY); + } + invalidate(); + } } // Trigger the link if (mTouchMode == TOUCH_INIT_MODE @@ -4824,17 +4702,15 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (mDeferTouchProcess) { // still needs to set them for compute deltaX/Y mLastTouchX = x; mLastTouchY = y; - ted.mViewX = x; - ted.mViewY = y; - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); break; } - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (!inFullScreenMode()) { + mPrivateHandler.removeMessages(PREVENT_DEFAULT_TIMEOUT); mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(PREVENT_DEFAULT_TIMEOUT, action, 0), TAP_TIMEOUT); @@ -4855,24 +4731,24 @@ public class WebView extends AbsoluteLayout if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mTouchMode = TOUCH_INIT_MODE; } + if (getSettings().supportTouchOnly()) { + removeTouchHighlight(true); + } } // pass the touch events from UI thread to WebCore thread if (shouldForwardTouchEvent() && mConfirmMove && (firstMove || eventTime - mLastSentTouchTime > mCurrentTouchInterval)) { - mLastSentTouchTime = eventTime; TouchEventData ted = new TouchEventData(); ted.mAction = action; ted.mX = contentX; ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + mLastSentTouchTime = eventTime; if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); break; } - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (firstMove && !inFullScreenMode()) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(PREVENT_DEFAULT_TIMEOUT, @@ -4892,20 +4768,20 @@ public class WebView extends AbsoluteLayout + " mTouchMode = " + mTouchMode); } mVelocityTracker.addMovement(ev); - if (mTouchMode != TOUCH_DRAG_MODE) { - if (mTouchMode == TOUCH_SELECT_MODE) { - mSelectX = mScrollX + (int) x; - mSelectY = mScrollY + (int) y; - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); - } - nativeMoveSelection(contentX, contentY, true); - invalidate(); - break; + if (mSelectingText && mSelectionStarted) { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "extend=" + contentX + "," + contentY); } + nativeExtendSelection(contentX, contentY); + invalidate(); + break; + } + if (mTouchMode != TOUCH_DRAG_MODE) { + if (!mConfirmMove) { break; } + if (mPreventDefault == PREVENT_DEFAULT_MAYBE_YES || mPreventDefault == PREVENT_DEFAULT_NO_FROM_TOUCH_DOWN) { // track mLastTouchTime as we may need to do fling at @@ -5033,6 +4909,7 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_UP: { + if (!isFocused()) requestFocus(); // pass the touch events from UI thread to WebCore thread if (shouldForwardTouchEvent()) { TouchEventData ted = new TouchEventData(); @@ -5041,10 +4918,6 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } mLastTouchUpTime = eventTime; @@ -5059,20 +4932,12 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mPreventDefault != PREVENT_DEFAULT_YES){ - doDoubleTap(); + mZoomManager.handleDoubleTap(mLastTouchX, mLastTouchY); mTouchMode = TOUCH_DONE_MODE; } break; - case TOUCH_SELECT_MODE: - commitCopy(); - mTouchSelection = false; - break; case TOUCH_INIT_MODE: // tap case TOUCH_SHORTPRESS_START_MODE: case TOUCH_SHORTPRESS_MODE: @@ -5084,9 +4949,17 @@ public class WebView extends AbsoluteLayout if (mPreventDefault != PREVENT_DEFAULT_YES && (computeMaxScrollX() > 0 || computeMaxScrollY() > 0)) { - // UI takes control back, cancel WebCore touch - cancelWebCoreTouchEvent(contentX, contentY, - true); + // If the user has performed a very quick touch + // sequence it is possible that we may get here + // before WebCore has had a chance to process the events. + // In this case, any call to preventDefault in the + // JS touch handler will not have been executed yet. + // Hence we will see both the UI (now) and WebCore + // (when context switches) handling the event, + // regardless of whether the web developer actually + // doeses preventDefault in their touch handler. This + // is the nature of our asynchronous touch model. + // we will not rewrite drag code here, but we // will try fling if it applies. WebViewCore.reducePriority(); @@ -5103,7 +4976,17 @@ public class WebView extends AbsoluteLayout break; } } else { - if (mTouchMode == TOUCH_INIT_MODE) { + if (mSelectingText) { + // tapping on selection or controls does nothing + if (!nativeHitSelection(contentX, contentY)) { + selectionDone(); + } + break; + } + // only trigger double tap if the WebView is + // scalable + if (mTouchMode == TOUCH_INIT_MODE + && (canZoomIn() || canZoomOut())) { mPrivateHandler.sendEmptyMessageDelayed( RELEASE_SINGLE_TAP, ViewConfiguration .getDoubleTapTimeout()); @@ -5194,21 +5077,10 @@ public class WebView extends AbsoluteLayout if (!mDragFromTextInput) { nativeHideCursor(); } - WebSettings settings = getSettings(); - if (settings.supportZoom() - && settings.getBuiltInZoomControls() - && !getZoomButtonsController().isVisible() - && mMinZoomScale < mMaxZoomScale - && (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF - || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF)) { - mZoomButtonsController.setVisible(true); - int count = settings.getDoubleTapToastCount(); - if (mInZoomOverview && count > 0) { - settings.setDoubleTapToastCount(--count); - Toast.makeText(mContext, - com.android.internal.R.string.double_tap_toast, - Toast.LENGTH_LONG).show(); - } + + if (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF + || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF) { + mZoomManager.invokeZoomPicker(); } } @@ -5216,18 +5088,7 @@ public class WebView extends AbsoluteLayout if ((deltaX | deltaY) != 0) { scrollBy(deltaX, deltaY); } - if (!getSettings().getBuiltInZoomControls()) { - boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; - if (mZoomControls != null && showPlusMinus) { - if (mZoomControls.getVisibility() == View.VISIBLE) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } else { - mZoomControls.show(showPlusMinus, false); - } - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } - } + mZoomManager.keepZoomPickerVisible(); } private void stopTouch() { @@ -5262,6 +5123,9 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + if (getSettings().supportTouchOnly()) { + removeTouchHighlight(true); + } mHeldMotionless = MOTIONLESS_TRUE; mTouchMode = TOUCH_DONE_MODE; nativeHideCursor(); @@ -5273,8 +5137,10 @@ public class WebView extends AbsoluteLayout private float mTrackballRemainsY = 0.0f; private int mTrackballXMove = 0; private int mTrackballYMove = 0; + private boolean mSelectingText = false; + private boolean mSelectionStarted = false; private boolean mExtendSelection = false; - private boolean mTouchSelection = false; + private boolean mDrawSelectionPointer = false; private static final int TRACKBALL_KEY_TIMEOUT = 1000; private static final int TRACKBALL_TIMEOUT = 200; private static final int TRACKBALL_WAIT = 100; @@ -5313,10 +5179,8 @@ public class WebView extends AbsoluteLayout if (ev.getY() < 0) pageUp(true); return true; } - boolean shiftPressed = mShiftIsPressed && (mNativeClass == 0 - || !nativeFocusIsPlugin()); if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (shiftPressed) { + if (mSelectingText) { return true; // discard press if copy in progress } mTrackballDown = true; @@ -5341,11 +5205,13 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mTrackballDown = false; mTrackballUpTime = time; - if (shiftPressed) { + if (mSelectingText) { if (mExtendSelection) { - commitCopy(); + copySelection(); + selectionDone(); } else { mExtendSelection = true; + nativeSetExtendSelection(); invalidate(); // draw the i-beam instead of the arrow } return true; // discard press if copy in progress @@ -5412,8 +5278,7 @@ public class WebView extends AbsoluteLayout + " yRate=" + yRate ); } - nativeMoveSelection(viewToContentX(mSelectX), - viewToContentY(mSelectY), mExtendSelection); + nativeMoveSelection(viewToContentX(mSelectX), viewToContentY(mSelectY)); int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; @@ -5479,7 +5344,16 @@ public class WebView extends AbsoluteLayout float yRate = mTrackballRemainsY * 1000 / elapsed; int viewWidth = getViewWidth(); int viewHeight = getViewHeight(); - if (mShiftIsPressed && (mNativeClass == 0 || !nativeFocusIsPlugin())) { + if (mSelectingText) { + if (!mDrawSelectionPointer) { + // The last selection was made by touch, disabling drawing the + // selection pointer. Allow the trackball to adjust the + // position of the touch control. + mSelectX = contentToViewX(nativeSelectionX()); + mSelectY = contentToViewY(nativeSelectionY()); + mDrawSelectionPointer = mExtendSelection = true; + nativeSetExtendSelection(); + } moveSelection(scaleTrackballX(xRate, viewWidth), scaleTrackballY(yRate, viewHeight)); mTrackballRemainsX = mTrackballRemainsY = 0; @@ -5517,11 +5391,11 @@ public class WebView extends AbsoluteLayout + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - if (mNativeClass != 0 && nativeFocusIsPlugin()) { + if (mNativeClass != 0 && nativePageShouldHandleShiftAndArrows()) { for (int i = 0; i < count; i++) { - letPluginHandleNavKey(selectKeyCode, time, true); + letPageHandleNavKey(selectKeyCode, time, true); } - letPluginHandleNavKey(selectKeyCode, time, false); + letPageHandleNavKey(selectKeyCode, time, false); } else if (navHandledKey(selectKeyCode, count, false, time)) { playSoundEffect(keyCodeToSoundsEffect(selectKeyCode)); } @@ -5560,6 +5434,19 @@ public class WebView extends AbsoluteLayout - getViewHeightWithTitle(), 0); } + boolean updateScrollCoordinates(int x, int y) { + int oldX = mScrollX; + int oldY = mScrollY; + mScrollX = x; + mScrollY = y; + if (oldX != mScrollX || oldY != mScrollY) { + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + return true; + } else { + return false; + } + } + public void flingScroll(int vx, int vy) { mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0, computeMaxScrollY()); @@ -5595,13 +5482,16 @@ public class WebView extends AbsoluteLayout return; } float currentVelocity = mScroller.getCurrVelocity(); - if (mLastVelocity > 0 && currentVelocity > 0) { + float velocity = (float) Math.hypot(vx, vy); + if (mLastVelocity > 0 && currentVelocity > 0 && velocity + > mLastVelocity * MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION) { float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX) - Math.atan2(vy, vx))); final float circle = (float) (Math.PI) * 2.0f; if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) { vx += currentVelocity * mLastVelX / mLastVelocity; vy += currentVelocity * mLastVelY / mLastVelocity; + velocity = (float) Math.hypot(vx, vy); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy); } @@ -5617,45 +5507,15 @@ public class WebView extends AbsoluteLayout } mLastVelX = vx; mLastVelY = vy; - mLastVelocity = (float) Math.hypot(vx, vy); + mLastVelocity = velocity; mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); - // TODO: duration is calculated based on velocity, if the range is - // small, the animation will stop before duration is up. We may - // want to calculate how long the animation is going to run to precisely - // resume the webcore update. final int time = mScroller.getDuration(); mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_PRIORITY, time); awakenScrollBars(time); invalidate(); } - private boolean zoomWithPreview(float scale, boolean updateTextWrapScale) { - float oldScale = mActualScale; - mInitialScrollX = mScrollX; - mInitialScrollY = mScrollY; - - // snap to DEFAULT_SCALE if it is close - if (Math.abs(scale - mDefaultScale) < MINIMUM_SCALE_INCREMENT) { - scale = mDefaultScale; - } - - setNewZoomScale(scale, updateTextWrapScale, false); - - if (oldScale != mActualScale) { - // use mZoomPickerScale to see zoom preview first - mZoomStart = SystemClock.uptimeMillis(); - mInvInitialZoomScale = 1.0f / oldScale; - mInvFinalZoomScale = 1.0f / mActualScale; - mZoomScale = mActualScale; - WebViewCore.pauseUpdatePicture(mWebViewCore); - invalidate(); - return true; - } else { - return false; - } - } - /** * Returns a view containing zoom controls i.e. +/- buttons. The caller is * in charge of installing this view to the view hierarchy. This view will @@ -5675,81 +5535,29 @@ public class WebView extends AbsoluteLayout Log.w(LOGTAG, "This WebView doesn't support zoom."); return null; } - if (mZoomControls == null) { - mZoomControls = createZoomControls(); + return mZoomManager.getExternalZoomPicker(); + } - /* - * need to be set to VISIBLE first so that getMeasuredHeight() in - * {@link #onSizeChanged()} can return the measured value for proper - * layout. - */ - mZoomControls.setVisibility(View.VISIBLE); - mZoomControlRunnable = new Runnable() { - public void run() { + void dismissZoomControl() { + mZoomManager.dismissZoomPicker(); + } - /* Don't dismiss the controls if the user has - * focus on them. Wait and check again later. - */ - if (!mZoomControls.hasFocus()) { - mZoomControls.hide(); - } else { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } - } - }; - } - return mZoomControls; + float getDefaultZoomScale() { + return mZoomManager.getDefaultScale(); } - private ExtendedZoomControls createZoomControls() { - ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext - , null); - zoomControls.setOnZoomInClickListener(new OnClickListener() { - public void onClick(View v) { - // reset time out - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomIn(); - } - }); - zoomControls.setOnZoomOutClickListener(new OnClickListener() { - public void onClick(View v) { - // reset time out - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomOut(); - } - }); - return zoomControls; + /** + * @return TRUE if the WebView can be zoomed in. + */ + public boolean canZoomIn() { + return mZoomManager.canZoomIn(); } /** - * Gets the {@link ZoomButtonsController} which can be used to add - * additional buttons to the zoom controls window. - * - * @return The instance of {@link ZoomButtonsController} used by this class, - * or null if it is unavailable. - * @hide + * @return TRUE if the WebView can be zoomed out. */ - public ZoomButtonsController getZoomButtonsController() { - if (mZoomButtonsController == null) { - mZoomButtonsController = new ZoomButtonsController(this); - mZoomButtonsController.setOnZoomListener(mZoomListener); - // ZoomButtonsController positions the buttons at the bottom, but in - // the middle. Change their layout parameters so they appear on the - // right. - View controls = mZoomButtonsController.getZoomControls(); - ViewGroup.LayoutParams params = controls.getLayoutParams(); - if (params instanceof FrameLayout.LayoutParams) { - FrameLayout.LayoutParams frameParams = (FrameLayout.LayoutParams) params; - frameParams.gravity = Gravity.RIGHT; - } - } - return mZoomButtonsController; + public boolean canZoomOut() { + return mZoomManager.canZoomOut(); } /** @@ -5757,15 +5565,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom in succeeds. FALSE if no zoom changes. */ public boolean zoomIn() { - // TODO: alternatively we can disallow this during draw history mode - switchOutDrawHistory(); - mInZoomOverview = false; - // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - return zoomWithPreview(mActualScale * 1.25f, true); + return mZoomManager.zoomIn(); } /** @@ -5773,14 +5573,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom out succeeds. FALSE if no zoom changes. */ public boolean zoomOut() { - // TODO: alternatively we can disallow this during draw history mode - switchOutDrawHistory(); - // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - return zoomWithPreview(mActualScale * 0.8f, true); + return mZoomManager.zoomOut(); } private void updateSelection() { @@ -5881,7 +5674,14 @@ public class WebView extends AbsoluteLayout // mLastTouchX and mLastTouchY are the point in the current viewport int contentX = viewToContentX((int) mLastTouchX + mScrollX); int contentY = viewToContentY((int) mLastTouchY + mScrollY); - if (nativePointInNavCache(contentX, contentY, mNavSlop)) { + if (getSettings().supportTouchOnly()) { + removeTouchHighlight(false); + 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 + touchUpData.mMoveGeneration = 0; + mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData); + } else if (nativePointInNavCache(contentX, contentY, mNavSlop)) { WebViewCore.MotionUpData motionUpData = new WebViewCore .MotionUpData(); motionUpData.mFrame = nativeCacheHitFramePointer(); @@ -5909,27 +5709,16 @@ public class WebView extends AbsoluteLayout * Return true if the view (Plugin) is fully visible and maximized inside * the WebView. */ - private boolean isPluginFitOnScreen(ViewManager.ChildView view) { - int viewWidth = getViewWidth(); - int viewHeight = getViewHeightWithTitle(); - float scale = Math.min((float) viewWidth / view.width, - (float) viewHeight / view.height); - if (scale < mMinZoomScale) { - scale = mMinZoomScale; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; - } - if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) { - if (contentToViewX(view.x) >= mScrollX - && contentToViewX(view.x + view.width) <= mScrollX - + viewWidth - && contentToViewY(view.y) >= mScrollY - && contentToViewY(view.y + view.height) <= mScrollY - + viewHeight) { - return true; - } - } - return false; + boolean isPluginFitOnScreen(ViewManager.ChildView view) { + final int viewWidth = getViewWidth(); + final int viewHeight = getViewHeightWithTitle(); + float scale = Math.min((float) viewWidth / view.width, (float) viewHeight / view.height); + scale = mZoomManager.computeScaleWithLimits(scale); + return !mZoomManager.willScaleTriggerZoom(scale) + && contentToViewX(view.x) >= mScrollX + && contentToViewX(view.x + view.width) <= mScrollX + viewWidth + && contentToViewY(view.y) >= mScrollY + && contentToViewY(view.y + view.height) <= mScrollY + viewHeight; } /* @@ -5938,22 +5727,19 @@ public class WebView extends AbsoluteLayout * animated scroll to center it. If the zoom needs to be changed, find the * zoom center and do a smooth zoom transition. */ - private void centerFitRect(int docX, int docY, int docWidth, int docHeight) { + void centerFitRect(int docX, int docY, int docWidth, int docHeight) { int viewWidth = getViewWidth(); int viewHeight = getViewHeightWithTitle(); float scale = Math.min((float) viewWidth / docWidth, (float) viewHeight / docHeight); - if (scale < mMinZoomScale) { - scale = mMinZoomScale; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; - } - if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) { + scale = mZoomManager.computeScaleWithLimits(scale); + if (!mZoomManager.willScaleTriggerZoom(scale)) { pinScrollTo(contentToViewX(docX + docWidth / 2) - viewWidth / 2, contentToViewY(docY + docHeight / 2) - viewHeight / 2, true, 0); } else { - float oldScreenX = docX * mActualScale - mScrollX; + float actualScale = mZoomManager.getScale(); + float oldScreenX = docX * actualScale - mScrollX; float rectViewX = docX * scale; float rectViewWidth = docWidth * scale; float newMaxWidth = mContentWidth * scale; @@ -5964,9 +5750,9 @@ public class WebView extends AbsoluteLayout } else if (newScreenX > (newMaxWidth - rectViewX - rectViewWidth)) { newScreenX = viewWidth - (newMaxWidth - rectViewX); } - mZoomCenterX = (oldScreenX * scale - newScreenX * mActualScale) - / (scale - mActualScale); - float oldScreenY = docY * mActualScale + getTitleHeight() + float zoomCenterX = (oldScreenX * scale - newScreenX * actualScale) + / (scale - actualScale); + float oldScreenY = docY * actualScale + getTitleHeight() - mScrollY; float rectViewY = docY * scale + getTitleHeight(); float rectViewHeight = docHeight * scale; @@ -5978,109 +5764,10 @@ public class WebView extends AbsoluteLayout } else if (newScreenY > (newMaxHeight - rectViewY - rectViewHeight)) { newScreenY = viewHeight - (newMaxHeight - rectViewY); } - mZoomCenterY = (oldScreenY * scale - newScreenY * mActualScale) - / (scale - mActualScale); - zoomWithPreview(scale, false); - } - } - - void dismissZoomControl() { - if (mWebViewCore == null) { - // maybe called after WebView's destroy(). As we can't get settings, - // just hide zoom control for both styles. - if (mZoomButtonsController != null) { - mZoomButtonsController.setVisible(false); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - return; - } - WebSettings settings = getSettings(); - if (settings.getBuiltInZoomControls()) { - if (mZoomButtonsController != null) { - mZoomButtonsController.setVisible(false); - } - } else { - if (mZoomControlRunnable != null) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - } - } - - // Rule for double tap: - // 1. if the current scale is not same as the text wrap scale and layout - // algorithm is NARROW_COLUMNS, fit to column; - // 2. if the current state is not overview mode, change to overview mode; - // 3. if the current state is overview mode, change to default scale. - private void doDoubleTap() { - if (mWebViewCore.getSettings().getUseWideViewPort() == false) { - return; - } - mZoomCenterX = mLastTouchX; - mZoomCenterY = mLastTouchY; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - WebSettings settings = getSettings(); - settings.setDoubleTapToastCount(0); - // remove the zoom control after double tap - dismissZoomControl(); - ViewManager.ChildView plugin = mViewManager.hitTest(mAnchorX, mAnchorY); - if (plugin != null) { - if (isPluginFitOnScreen(plugin)) { - mInZoomOverview = true; - // Force the titlebar fully reveal in overview mode - if (mScrollY < getTitleHeight()) mScrollY = 0; - zoomWithPreview((float) getViewWidth() / mZoomOverviewWidth, - true); - } else { - mInZoomOverview = false; - centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height); - } - return; - } - boolean zoomToDefault = false; - if ((settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS) - && (Math.abs(mActualScale - mTextWrapScale) >= MINIMUM_SCALE_INCREMENT)) { - setNewZoomScale(mActualScale, true, true); - float overviewScale = (float) getViewWidth() / mZoomOverviewWidth; - if (Math.abs(mActualScale - overviewScale) < MINIMUM_SCALE_INCREMENT) { - mInZoomOverview = true; - } - } else if (!mInZoomOverview) { - float newScale = (float) getViewWidth() / mZoomOverviewWidth; - if (Math.abs(mActualScale - newScale) >= MINIMUM_SCALE_INCREMENT) { - mInZoomOverview = true; - // Force the titlebar fully reveal in overview mode - if (mScrollY < getTitleHeight()) mScrollY = 0; - zoomWithPreview(newScale, true); - } else if (Math.abs(mActualScale - mDefaultScale) >= MINIMUM_SCALE_INCREMENT) { - zoomToDefault = true; - } - } else { - zoomToDefault = true; - } - if (zoomToDefault) { - mInZoomOverview = false; - int left = nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); - if (left != NO_LEFTEDGE) { - // add a 5pt padding to the left edge. - int viewLeft = contentToViewX(left < 5 ? 0 : (left - 5)) - - mScrollX; - // Re-calculate the zoom center so that the new scroll x will be - // on the left edge. - if (viewLeft > 0) { - mZoomCenterX = viewLeft * mDefaultScale - / (mDefaultScale - mActualScale); - } else { - scrollBy(viewLeft, 0); - mZoomCenterX = 0; - } - } - zoomWithPreview(mDefaultScale, true); + float zoomCenterY = (oldScreenY * scale - newScreenY * actualScale) + / (scale - actualScale); + mZoomManager.setZoomCenter(zoomCenterX, zoomCenterY); + mZoomManager.startZoomAnimation(scale, false); } } @@ -6092,6 +5779,9 @@ public class WebView extends AbsoluteLayout @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + // FIXME: If a subwindow is showing find, and the user touches the + // background window, it can steal focus. + if (mFindIsUp) return false; boolean result = false; if (inEditingMode()) { result = mWebTextView.requestFocus(direction, @@ -6179,6 +5869,12 @@ public class WebView extends AbsoluteLayout public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + // don't scroll while in zoom animation. When it is done, we will adjust + // the necessary components (e.g., WebTextView if it is in editing mode) + if (mZoomManager.isFixedLengthAnimationInProgress()) { + return false; + } + rect.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); @@ -6321,7 +6017,8 @@ public class WebView extends AbsoluteLayout } case SWITCH_TO_SHORTPRESS: { if (mTouchMode == TOUCH_INIT_MODE) { - if (mPreventDefault != PREVENT_DEFAULT_YES) { + if (!getSettings().supportTouchOnly() + && mPreventDefault != PREVENT_DEFAULT_YES) { mTouchMode = TOUCH_SHORTPRESS_START_MODE; updateSelection(); } else { @@ -6335,6 +6032,9 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_LONGPRESS: { + if (getSettings().supportTouchOnly()) { + removeTouchHighlight(false); + } if (inFullScreenMode() || mDeferTouchProcess) { TouchEventData ted = new TouchEventData(); ted.mAction = WebViewCore.ACTION_LONGPRESS; @@ -6346,15 +6046,10 @@ public class WebView extends AbsoluteLayout // simplicity for now, we don't set it. ted.mMetaState = 0; ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = mLastTouchX; - ted.mViewY = mLastTouchY; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mPreventDefault != PREVENT_DEFAULT_YES) { mTouchMode = TOUCH_DONE_MODE; performLongClick(); - rebuildWebTextView(); } break; } @@ -6387,70 +6082,36 @@ public class WebView extends AbsoluteLayout spawnContentScrollTo(msg.arg1, msg.arg2); break; case UPDATE_ZOOM_RANGE: { - WebViewCore.RestoreState restoreState - = (WebViewCore.RestoreState) msg.obj; + WebViewCore.ViewState viewState = (WebViewCore.ViewState) msg.obj; // mScrollX contains the new minPrefWidth - updateZoomRange(restoreState, getViewWidth(), - restoreState.mScrollX, false); + mZoomManager.updateZoomRange(viewState, getViewWidth(), viewState.mScrollX); + break; + } + case REPLACE_BASE_CONTENT: { + nativeReplaceBaseContent(msg.arg1); break; } case NEW_PICTURE_MSG_ID: { - // If we've previously delayed deleting a root - // layer, do it now. - if (mDelayedDeleteRootLayer) { - mDelayedDeleteRootLayer = false; - nativeSetRootLayer(0); - } - WebSettings settings = mWebViewCore.getSettings(); // called for new content - final int viewWidth = getViewWidth(); - final WebViewCore.DrawData draw = - (WebViewCore.DrawData) msg.obj; + final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; + nativeSetBaseLayer(draw.mBaseLayer); final Point viewSize = draw.mViewPoint; - boolean useWideViewport = settings.getUseWideViewPort(); - WebViewCore.RestoreState restoreState = draw.mRestoreState; - boolean hasRestoreState = restoreState != null; - if (hasRestoreState) { - updateZoomRange(restoreState, viewSize.x, - draw.mMinPrefWidth, true); + WebViewCore.ViewState viewState = draw.mViewState; + boolean isPictureAfterFirstLayout = viewState != null; + if (isPictureAfterFirstLayout) { + // Reset the last sent data here since dealing with new page. + mLastWidthSent = 0; + mZoomManager.onFirstLayout(draw); if (!mDrawHistory) { - mInZoomOverview = false; - - if (mInitialScaleInPercent > 0) { - setNewZoomScale(mInitialScaleInPercent / 100.0f, - mInitialScaleInPercent != mTextWrapScale * 100, - false); - } else if (restoreState.mViewScale > 0) { - mTextWrapScale = restoreState.mTextWrapScale; - setNewZoomScale(restoreState.mViewScale, false, - false); - } else { - mInZoomOverview = useWideViewport - && settings.getLoadWithOverviewMode(); - float scale; - if (mInZoomOverview) { - scale = (float) viewWidth - / DEFAULT_VIEWPORT_WIDTH; - } else { - scale = restoreState.mTextWrapScale; - } - setNewZoomScale(scale, Math.abs(scale - - mTextWrapScale) >= MINIMUM_SCALE_INCREMENT, - false); - } - setContentScrollTo(restoreState.mScrollX, - restoreState.mScrollY); + setContentScrollTo(viewState.mScrollX, viewState.mScrollY); // As we are on a new page, remove the WebTextView. This // is necessary for page loads driven by webkit, and in // particular when the user was on a password field, so // the WebTextView was visible. clearTextEntry(false); - // update the zoom buttons as the scale can be changed - if (getSettings().getBuiltInZoomControls()) { - updateZoomButtonsEnabled(); - } } } + // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to // WebCore matches the view size of the picture we just @@ -6458,45 +6119,25 @@ public class WebView extends AbsoluteLayout final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; recordNewContentSize(draw.mWidthHeight.x, - draw.mWidthHeight.y - + (mFindIsUp ? mFindHeight : 0), updateLayout); + draw.mWidthHeight.y, updateLayout); if (DebugFlags.WEB_VIEW) { Rect b = draw.mInvalRegion.getBounds(); Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + b.left+","+b.top+","+b.right+","+b.bottom+"}"); } invalidateContentRect(draw.mInvalRegion.getBounds()); + if (mPictureListener != null) { mPictureListener.onNewPicture(WebView.this, capturePicture()); } - if (useWideViewport) { - // limit mZoomOverviewWidth upper bound to - // sMaxViewportWidth so that if the page doesn't behave - // well, the WebView won't go insane. limit the lower - // bound to match the default scale for mobile sites. - mZoomOverviewWidth = Math.min(sMaxViewportWidth, Math - .max((int) (viewWidth / mDefaultScale), Math - .max(draw.mMinPrefWidth, - draw.mViewPoint.x))); - } - if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; - } - if (!mDrawHistory && mInZoomOverview) { - // fit the content width to the current view. Ignore - // the rounding error case. - if (Math.abs((viewWidth * mInvActualScale) - - mZoomOverviewWidth) > 1) { - setNewZoomScale((float) viewWidth - / mZoomOverviewWidth, Math.abs(mActualScale - - mTextWrapScale) < MINIMUM_SCALE_INCREMENT, - false); - } - } + + // update the zoom information based on the new picture + mZoomManager.onNewPicture(draw); + if (draw.mFocusSizeChanged && inEditingMode()) { mFocusSizeChanged = true; } - if (hasRestoreState) { + if (isPictureAfterFirstLayout) { mViewManager.postReadyToDrawAll(); } break; @@ -6530,14 +6171,8 @@ public class WebView extends AbsoluteLayout break; case REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID: displaySoftKeyboard(true); - updateTextSelectionFromMessage(msg.arg1, msg.arg2, - (WebViewCore.TextSelectionData) msg.obj); - break; + // fall through to UPDATE_TEXT_SELECTION_MSG_ID case UPDATE_TEXT_SELECTION_MSG_ID: - // If no textfield was in focus, and the user touched one, - // causing it to send this message, then WebTextView has not - // been set up yet. Rebuild it so it can set its selection. - rebuildWebTextView(); updateTextSelectionFromMessage(msg.arg1, msg.arg2, (WebViewCore.TextSelectionData) msg.obj); break; @@ -6555,7 +6190,7 @@ public class WebView extends AbsoluteLayout } } break; - case MOVE_OUT_OF_PLUGIN: + case UNHANDLED_NAV_KEY: navHandledKey(msg.arg1, 1, false, 0); break; case UPDATE_TEXT_ENTRY_MSG_ID: @@ -6580,23 +6215,6 @@ public class WebView extends AbsoluteLayout } break; } - case IMMEDIATE_REPAINT_MSG_ID: { - invalidate(); - break; - } - case SET_ROOT_LAYER_MSG_ID: { - if (0 == msg.arg1) { - // Null indicates deleting the old layer, but - // don't actually do so until we've got the - // new page to display. - mDelayedDeleteRootLayer = true; - } else { - mDelayedDeleteRootLayer = false; - nativeSetRootLayer(msg.arg1); - invalidate(); - } - break; - } case REQUEST_FORM_DATA: AutoCompleteAdapter adapter = (AutoCompleteAdapter) msg.obj; if (mWebTextView.isSameTextField(msg.arg1)) { @@ -6640,33 +6258,40 @@ public class WebView extends AbsoluteLayout mPreventDefault = msg.arg2 == 1 ? PREVENT_DEFAULT_YES : PREVENT_DEFAULT_NO; } + if (mPreventDefault == PREVENT_DEFAULT_YES) { + mTouchHighlightRegion.setEmpty(); + } } else if (msg.arg2 == 0) { // prevent default is not called in WebCore, so the // message needs to be reprocessed in UI TouchEventData ted = (TouchEventData) msg.obj; switch (ted.mAction) { case MotionEvent.ACTION_DOWN: - mLastDeferTouchX = ted.mViewX; - mLastDeferTouchY = ted.mViewY; + mLastDeferTouchX = contentToViewX(ted.mX) + - mScrollX; + mLastDeferTouchY = contentToViewY(ted.mY) + - mScrollY; mDeferTouchMode = TOUCH_INIT_MODE; break; case MotionEvent.ACTION_MOVE: { // no snapping in defer process + int x = contentToViewX(ted.mX) - mScrollX; + int y = contentToViewY(ted.mY) - mScrollY; if (mDeferTouchMode != TOUCH_DRAG_MODE) { mDeferTouchMode = TOUCH_DRAG_MODE; - mLastDeferTouchX = ted.mViewX; - mLastDeferTouchY = ted.mViewY; + mLastDeferTouchX = x; + mLastDeferTouchY = y; startDrag(); } int deltaX = pinLocX((int) (mScrollX - + mLastDeferTouchX - ted.mViewX)) + + mLastDeferTouchX - x)) - mScrollX; int deltaY = pinLocY((int) (mScrollY - + mLastDeferTouchY - ted.mViewY)) + + mLastDeferTouchY - y)) - mScrollY; doDrag(deltaX, deltaY); - if (deltaX != 0) mLastDeferTouchX = ted.mViewX; - if (deltaY != 0) mLastDeferTouchY = ted.mViewY; + if (deltaX != 0) mLastDeferTouchX = x; + if (deltaY != 0) mLastDeferTouchY = y; break; } case MotionEvent.ACTION_UP: @@ -6680,9 +6305,9 @@ public class WebView extends AbsoluteLayout break; case WebViewCore.ACTION_DOUBLETAP: // doDoubleTap() needs mLastTouchX/Y as anchor - mLastTouchX = ted.mViewX; - mLastTouchY = ted.mViewY; - doDoubleTap(); + mLastTouchX = contentToViewX(ted.mX) - mScrollX; + mLastTouchY = contentToViewY(ted.mY) - mScrollY; + mZoomManager.handleDoubleTap(mLastTouchX, mLastTouchY); mDeferTouchMode = TOUCH_DONE_MODE; break; case WebViewCore.ACTION_LONGPRESS: @@ -6690,7 +6315,6 @@ public class WebView extends AbsoluteLayout if (hitTest != null && hitTest.mType != HitTestResult.UNKNOWN_TYPE) { performLongClick(); - rebuildWebTextView(); } mDeferTouchMode = TOUCH_DONE_MODE; break; @@ -6814,7 +6438,6 @@ public class WebView extends AbsoluteLayout case CENTER_FIT_RECT: Rect r = (Rect)msg.obj; - mInZoomOverview = false; centerFitRect(r.left, r.top, r.width(), r.height()); break; @@ -6823,6 +6446,43 @@ public class WebView extends AbsoluteLayout mVerticalScrollBarMode = msg.arg2; break; + case SELECTION_STRING_CHANGED: + if (mAccessibilityInjector != null) { + String selectionString = (String) msg.obj; + mAccessibilityInjector.onSelectionStringChange(selectionString); + } + break; + + case SET_TOUCH_HIGHLIGHT_RECTS: + invalidate(mTouchHighlightRegion.getBounds()); + mTouchHighlightRegion.setEmpty(); + if (msg.obj != null) { + ArrayList<Rect> rects = (ArrayList<Rect>) msg.obj; + for (Rect rect : rects) { + Rect viewRect = contentToViewRect(rect); + // some sites, like stories in nytimes.com, set + // mouse event handler in the top div. It is not + // user friendly to highlight the div if it covers + // more than half of the screen. + if (viewRect.width() < getWidth() >> 1 + || viewRect.height() < getHeight() >> 1) { + mTouchHighlightRegion.union(viewRect); + invalidate(viewRect); + } else { + Log.w(LOGTAG, "Skip the huge selection rect:" + + viewRect); + } + } + } + break; + + case SAVE_WEBARCHIVE_FINISHED: + SaveWebArchiveMessage saveMessage = (SaveWebArchiveMessage)msg.obj; + if (saveMessage.mCallback != null) { + saveMessage.mCallback.onReceiveValue(saveMessage.mResultFile); + } + break; + default: super.handleMessage(msg); break; @@ -7130,37 +6790,6 @@ public class WebView extends AbsoluteLayout new InvokeListBox(array, enabledArray, selectedArray)); } - private void updateZoomRange(WebViewCore.RestoreState restoreState, - int viewWidth, int minPrefWidth, boolean updateZoomOverview) { - if (restoreState.mMinScale == 0) { - if (restoreState.mMobileSite) { - if (minPrefWidth > Math.max(0, viewWidth)) { - mMinZoomScale = (float) viewWidth / minPrefWidth; - mMinZoomScaleFixed = false; - if (updateZoomOverview) { - WebSettings settings = getSettings(); - mInZoomOverview = settings.getUseWideViewPort() && - settings.getLoadWithOverviewMode(); - } - } else { - mMinZoomScale = restoreState.mDefaultScale; - mMinZoomScaleFixed = true; - } - } else { - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; - mMinZoomScaleFixed = false; - } - } else { - mMinZoomScale = restoreState.mMinScale; - mMinZoomScaleFixed = true; - } - if (restoreState.mMaxScale == 0) { - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - } else { - mMaxZoomScale = restoreState.mMaxScale; - } - } - /* * Request a dropdown menu for a listbox with single selection or a single * <select> element. @@ -7243,7 +6872,7 @@ public class WebView extends AbsoluteLayout // FIXME the divisor should be retrieved from somewhere // the closest thing today is hard-coded into ScrollView.java // (from ScrollView.java, line 363) int maxJump = height/2; - return Math.round(height * mInvActualScale); + return Math.round(height * mZoomManager.getInvScale()); } /** @@ -7254,10 +6883,10 @@ public class WebView extends AbsoluteLayout } /** - * Pass the key to the plugin. This assumes that nativeFocusIsPlugin() - * returned true. + * Pass the key directly to the page. This assumes that + * nativePageShouldHandleShiftAndArrows() returned true. */ - private void letPluginHandleNavKey(int keyCode, long time, boolean down) { + private void letPageHandleNavKey(int keyCode, long time, boolean down) { int keyEventAction; int eventHubAction; if (down) { @@ -7354,7 +6983,7 @@ public class WebView extends AbsoluteLayout * @hide only needs to be accessible to Browser and testing */ public void drawPage(Canvas canvas) { - mWebViewCore.drawContentPicture(canvas, 0, false, false); + nativeDraw(canvas, 0, 0, false); } /** @@ -7398,9 +7027,18 @@ public class WebView extends AbsoluteLayout private native boolean nativeCursorWantsKeyEvents(); private native void nativeDebugDump(); private native void nativeDestroy(); - private native boolean nativeEvaluateLayersAnimations(); - private native void nativeDrawExtras(Canvas canvas, int extra); + + /** + * Draw the picture set with a background color and extra. If + * "splitIfNeeded" is true and the return value is not 0, the return value + * MUST be passed to WebViewCore with SPLIT_PICTURE_SET message so that the + * native allocation can be freed. + */ + private native int nativeDraw(Canvas canvas, int color, int extra, + boolean splitIfNeeded); private native void nativeDumpDisplayTree(String urlOrNull); + private native boolean nativeEvaluateLayersAnimations(); + private native void nativeExtendSelection(int x, int y); private native int nativeFindAll(String findLower, String findUpper); private native void nativeFindNext(boolean forward); /* package */ native int nativeFocusCandidateFramePointer(); @@ -7427,6 +7065,7 @@ public class WebView extends AbsoluteLayout private native boolean nativeHasCursorNode(); private native boolean nativeHasFocusNode(); private native void nativeHideCursor(); + private native boolean nativeHitSelection(int x, int y); private native String nativeImageURI(int x, int y); private native void nativeInstrumentReport(); /* package */ native boolean nativeMoveCursorToNextTextInput(); @@ -7436,29 +7075,46 @@ public class WebView extends AbsoluteLayout private native boolean nativeMoveCursor(int keyCode, int count, boolean noScroll); private native int nativeMoveGeneration(); - private native void nativeMoveSelection(int x, int y, - boolean extendSelection); + private native void nativeMoveSelection(int x, int y); + /** + * @return true if the page should get the shift and arrow keys, rather + * than select text/navigation. + * + * If the focus is a plugin, or if the focus and cursor match and are + * a contentEditable element, then the page should handle these keys. + */ + private native boolean nativePageShouldHandleShiftAndArrows(); private native boolean nativePointInNavCache(int x, int y, int slop); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, boolean pressed, boolean invalidate); + private native void nativeResetSelection(); + private native void nativeSelectAll(); private native void nativeSelectBestAt(Rect rect); + private native int nativeSelectionX(); + private native int nativeSelectionY(); + private native int nativeFindIndex(); + private native void nativeSetExtendSelection(); private native void nativeSetFindIsEmpty(); private native void nativeSetFindIsUp(boolean isUp); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); - private native void nativeSetRootLayer(int layer); + private native void nativeSetBaseLayer(int layer); + private native void nativeReplaceBaseContent(int content); + private native void nativeCopyBaseContentToPicture(Picture pict); + private native boolean nativeHasContent(); private native void nativeSetSelectionPointer(boolean set, - float scale, int x, int y, boolean extendSelection); - private native void nativeSetSelectionRegion(boolean set); + float scale, int x, int y); + private native boolean nativeStartSelection(int x, int y); private native Rect nativeSubtractLayers(Rect content); private native int nativeTextGeneration(); // Never call this version except by updateCachedTextfield(String) - // we always want to pass in our generation number. private native void nativeUpdateCachedTextfield(String updatedText, int generation); + private native boolean nativeWordSelection(int x, int y); // return NO_LEFTEDGE means failure. - private static final int NO_LEFTEDGE = -1; - private native int nativeGetBlockLeftEdge(int x, int y, float scale); + static final int NO_LEFTEDGE = -1; + native int nativeGetBlockLeftEdge(int x, int y, float scale); } diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 4118119..21af570 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -17,14 +17,8 @@ package android.webkit; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; -import android.graphics.Canvas; -import android.graphics.DrawFilter; -import android.graphics.Paint; -import android.graphics.PaintFlagsDrawFilter; -import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; @@ -33,12 +27,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; -import android.provider.Browser; -import android.provider.OpenableColumns; +import android.provider.MediaStore; import android.util.Log; import android.util.SparseBooleanArray; import android.view.KeyEvent; -import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; @@ -117,7 +109,7 @@ final class WebViewCore { private int mViewportDensityDpi = -1; private int mRestoredScale = 0; - private int mRestoredScreenWidthScale = 0; + private int mRestoredTextWrapScale = 0; private int mRestoredX = 0; private int mRestoredY = 0; @@ -279,34 +271,36 @@ final class WebViewCore { /** * Called by JNI. Open a file chooser to upload a file. - * @return String version of the URI plus the name of the file. - * FIXME: Just return the URI here, and in FileSystem::pathGetFileName, call - * into Java to get the filename. + * @param acceptType The value of the 'accept' attribute of the + * input tag associated with this file picker. + * @return String version of the URI. */ - private String openFileChooser() { - Uri uri = mCallbackProxy.openFileChooser(); - if (uri == null) return ""; - // Find out the name, and append it to the URI. - // Webkit will treat the name as the filename, and - // the URI as the path. The URI will be used - // in BrowserFrame to get the actual data. - Cursor cursor = mContext.getContentResolver().query( - uri, - new String[] { OpenableColumns.DISPLAY_NAME }, - null, - null, - null); - String name = ""; - if (cursor != null) { - try { - if (cursor.moveToNext()) { - name = cursor.getString(0); + private String openFileChooser(String acceptType) { + Uri uri = mCallbackProxy.openFileChooser(acceptType); + if (uri != null) { + String filePath = ""; + // Note - querying for MediaStore.Images.Media.DATA + // seems to work for all content URIs, not just images + Cursor cursor = mContext.getContentResolver().query( + uri, + new String[] { MediaStore.Images.Media.DATA }, + null, null, null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + filePath = cursor.getString(0); + } + } finally { + cursor.close(); } - } finally { - cursor.close(); + } else { + filePath = uri.getLastPathSegment(); } + String uriString = uri.toString(); + BrowserFrame.sJavaBridge.storeFilePathForContentUri(filePath, uriString); + return uriString; } - return uri.toString() + "/" + name; + return ""; } /** @@ -424,6 +418,13 @@ final class WebViewCore { return mCallbackProxy.onJsTimeout(); } + /** + * Notify the webview that this is an installable web app. + */ + protected void setInstallableWebApp() { + mCallbackProxy.setInstallableWebApp(); + } + //------------------------------------------------------------------------- // JNI methods //------------------------------------------------------------------------- @@ -436,35 +437,18 @@ final class WebViewCore { private native void nativeClearContent(); /** - * Create a flat picture from the set of pictures. - */ - private native void nativeCopyContentToPicture(Picture picture); - - /** - * Draw the picture set with a background color. Returns true - * if some individual picture took too long to draw and can be - * split into parts. Called from the UI thread. - */ - private native boolean nativeDrawContent(Canvas canvas, int color); - - /** - * check to see if picture is blank and in progress - */ - private native boolean nativePictureReady(); - - /** * Redraw a portion of the picture set. The Point wh returns the * width and height of the overall picture. */ - private native boolean nativeRecordContent(Region invalRegion, Point wh); + private native int nativeRecordContent(Region invalRegion, Point wh); private native boolean nativeFocusBoundsChanged(); /** - * Splits slow parts of the picture set. Called from the webkit - * thread after nativeDrawContent returns true. + * Splits slow parts of the picture set. Called from the webkit thread after + * WebView.nativeDraw() returns content to be split. */ - private native void nativeSplitContent(); + private native void nativeSplitContent(int content); private native boolean nativeKey(int keyCode, int unichar, int repeatCount, boolean isShift, boolean isAlt, boolean isSym, @@ -480,12 +464,12 @@ final class WebViewCore { of layout/line-breaking. These coordinates are in document space, which is the same as View coords unless we have zoomed the document (see nativeSetZoom). - screenWidth is used by layout to wrap column around. If viewport uses - fixed size, screenWidth can be different from width with zooming. + textWrapWidth is used by layout to wrap column around. If viewport uses + fixed size, textWrapWidth can be different from width with zooming. should this be called nativeSetViewPortSize? */ - private native void nativeSetSize(int width, int height, int screenWidth, - float scale, int realScreenWidth, int screenHeight, int anchorX, + private native void nativeSetSize(int width, int height, int textWrapWidth, + float scale, int screenWidth, int screenHeight, int anchorX, int anchorY, boolean ignoreHeight); private native int nativeGetContentMinPrefWidth(); @@ -576,7 +560,18 @@ final class WebViewCore { /** * Provide WebCore with the previously visted links from the history database */ - private native void nativeProvideVisitedHistory(String[] history); + private native void nativeProvideVisitedHistory(String[] history); + + /** + * Modifies the current selection. + * + * @param alter Specifies how to alter the selection. + * @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(String alter, String direction, String granularity); // EventHub for processing messages private final EventHub mEventHub; @@ -697,6 +692,12 @@ final class WebViewCore { int mY; } + static class TouchHighlightData { + int mX; + int mY; + int mSlop; + } + // mAction of TouchEventData can be MotionEvent.getAction() which uses the // last two bytes or one of the following values static final int ACTION_LONGPRESS = 0x100; @@ -708,8 +709,6 @@ final class WebViewCore { int mY; int mMetaState; boolean mReprocess; - float mViewX; - float mViewY; } static class GeolocationPermissionsData { @@ -718,7 +717,11 @@ final class WebViewCore { boolean mRemember; } - + static class ModifySelectionData { + String mAlter; + String mDirection; + String mGranularity; + } static final String[] HandlerDebugString = { "REQUEST_LABEL", // 97 @@ -771,6 +774,7 @@ final class WebViewCore { "ON_RESUME", // = 144 "FREE_MEMORY", // = 145 "VALID_NODE_BOUNDS", // = 146 + "SAVE_WEBARCHIVE", // = 147 }; class EventHub { @@ -837,6 +841,9 @@ final class WebViewCore { static final int FREE_MEMORY = 145; static final int VALID_NODE_BOUNDS = 146; + // Load and save web archives + static final int SAVE_WEBARCHIVE = 147; + // Network-based messaging static final int CLEAR_SSL_PREF_TABLE = 150; @@ -865,6 +872,12 @@ 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 REMOVE_TOUCH_HIGHLIGHT_RECTS = 188; + + // accessibility support + static final int MODIFY_SELECTION = 190; + // private message ids private static final int DESTROY = 200; @@ -1238,6 +1251,19 @@ final class WebViewCore { nativeSetSelection(msg.arg1, msg.arg2); break; + case MODIFY_SELECTION: + ModifySelectionData modifySelectionData = + (ModifySelectionData) msg.obj; + String selectionString = nativeModifySelection( + modifySelectionData.mAlter, + modifySelectionData.mDirection, + modifySelectionData.mGranularity); + + mWebView.mPrivateHandler.obtainMessage( + WebView.SELECTION_STRING_CHANGED, selectionString) + .sendToTarget(); + break; + case LISTBOX_CHOICES: SparseBooleanArray choices = (SparseBooleanArray) msg.obj; @@ -1278,6 +1304,15 @@ final class WebViewCore { nativeSetJsFlags((String)msg.obj); break; + case SAVE_WEBARCHIVE: + WebView.SaveWebArchiveMessage saveMessage = + (WebView.SaveWebArchiveMessage)msg.obj; + saveMessage.mResultFile = + saveWebArchive(saveMessage.mBasename, saveMessage.mAutoname); + mWebView.mPrivateHandler.obtainMessage( + WebView.SAVE_WEBARCHIVE_FINISHED, saveMessage).sendToTarget(); + break; + case GEOLOCATION_PERMISSIONS_PROVIDE: GeolocationPermissionsData data = (GeolocationPermissionsData) msg.obj; @@ -1291,7 +1326,9 @@ final class WebViewCore { break; case SPLIT_PICTURE_SET: - nativeSplitContent(); + nativeSplitContent(msg.arg1); + mWebView.mPrivateHandler.obtainMessage( + WebView.REPLACE_BASE_CONTENT, msg.arg1, 0); mSplitPictureIsScheduled = false; break; @@ -1357,6 +1394,21 @@ final class WebViewCore { BrowserFrame.sJavaBridge.removePackageName( (String) msg.obj); break; + + case GET_TOUCH_HIGHLIGHT_RECTS: + TouchHighlightData d = (TouchHighlightData) msg.obj; + ArrayList<Rect> rects = nativeGetTouchHighlightRects + (d.mX, d.mY, d.mSlop); + mWebView.mPrivateHandler.obtainMessage( + WebView.SET_TOUCH_HIGHLIGHT_RECTS, rects) + .sendToTarget(); + break; + + case REMOVE_TOUCH_HIGHLIGHT_RECTS: + mWebView.mPrivateHandler.obtainMessage( + WebView.SET_TOUCH_HIGHLIGHT_RECTS, null) + .sendToTarget(); + break; } } }; @@ -1562,6 +1614,13 @@ final class WebViewCore { mBrowserFrame.loadUrl(url, extraHeaders); } + private String saveWebArchive(String filename, boolean autoname) { + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, " CORE saveWebArchive " + filename + " " + autoname); + } + return mBrowserFrame.saveWebArchive(filename, autoname); + } + private void key(KeyEvent evt, boolean isDown) { if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, "CORE key at " + System.currentTimeMillis() + ", " @@ -1575,11 +1634,12 @@ final class WebViewCore { if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { if (DebugFlags.WEB_VIEW_CORE) { - Log.v(LOGTAG, "key: arrow unused by plugin: " + keyCode); + Log.v(LOGTAG, "key: arrow unused by page: " + keyCode); } if (mWebView != null && evt.isDown()) { Message.obtain(mWebView.mPrivateHandler, - WebView.MOVE_OUT_OF_PLUGIN, keyCode).sendToTarget(); + WebView.UNHANDLED_NAV_KEY, keyCode, + 0).sendToTarget(); } return; } @@ -1675,6 +1735,14 @@ final class WebViewCore { return usedQuota; } + // called from UI thread + void splitContent(int content) { + if (!mSplitPictureIsScheduled) { + mSplitPictureIsScheduled = true; + sendMessage(EventHub.SPLIT_PICTURE_SET, content, 0); + } + } + // Used to avoid posting more than one draw message. private boolean mDrawIsScheduled; @@ -1684,11 +1752,11 @@ final class WebViewCore { // Used to suspend drawing. private boolean mDrawIsPaused; - // mRestoreState is set in didFirstLayout(), and reset in the next - // webkitDraw after passing it to the UI thread. - private RestoreState mRestoreState = null; + // mInitialViewState is set by didFirstLayout() and then reset in the + // next webkitDraw after passing the state to the UI thread. + private ViewState mInitialViewState = null; - static class RestoreState { + static class ViewState { float mMinScale; float mMaxScale; float mViewScale; @@ -1701,15 +1769,17 @@ final class WebViewCore { static class DrawData { DrawData() { + mBaseLayer = 0; mInvalRegion = new Region(); mWidthHeight = new Point(); } + int mBaseLayer; Region mInvalRegion; Point mViewPoint; Point mWidthHeight; int mMinPrefWidth; - RestoreState mRestoreState; // only non-null if it is for the first - // picture set after the first layout + // only non-null if it is for the first picture set after the first layout + ViewState mViewState; boolean mFocusSizeChanged; } @@ -1717,8 +1787,8 @@ final class WebViewCore { mDrawIsScheduled = false; DrawData draw = new DrawData(); if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start"); - if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) - == false) { + draw.mBaseLayer = nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight); + if (draw.mBaseLayer == 0) { if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort"); return; } @@ -1734,9 +1804,9 @@ final class WebViewCore { : mViewportWidth), nativeGetContentMinPrefWidth()); } - if (mRestoreState != null) { - draw.mRestoreState = mRestoreState; - mRestoreState = null; + if (mInitialViewState != null) { + draw.mViewState = mInitialViewState; + mInitialViewState = null; } if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); Message.obtain(mWebView.mPrivateHandler, @@ -1751,51 +1821,6 @@ final class WebViewCore { } } - /////////////////////////////////////////////////////////////////////////// - // These are called from the UI thread, not our thread - - static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG | - Paint.DITHER_FLAG | - Paint.SUBPIXEL_TEXT_FLAG; - static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG | - Paint.DITHER_FLAG; - - final DrawFilter mZoomFilter = - new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); - // If we need to trade better quality for speed, set mScrollFilter to null - final DrawFilter mScrollFilter = - new PaintFlagsDrawFilter(SCROLL_BITS, 0); - - /* package */ void drawContentPicture(Canvas canvas, int color, - boolean animatingZoom, - boolean animatingScroll) { - DrawFilter df = null; - if (animatingZoom) { - df = mZoomFilter; - } else if (animatingScroll) { - df = mScrollFilter; - } - canvas.setDrawFilter(df); - boolean tookTooLong = nativeDrawContent(canvas, color); - canvas.setDrawFilter(null); - if (tookTooLong && mSplitPictureIsScheduled == false) { - mSplitPictureIsScheduled = true; - sendMessage(EventHub.SPLIT_PICTURE_SET); - } - } - - /* package */ synchronized boolean pictureReady() { - return 0 != mNativeClass ? nativePictureReady() : false; - } - - /*package*/ synchronized Picture copyContentPicture() { - Picture result = new Picture(); - if (0 != mNativeClass) { - nativeCopyContentToPicture(result); - } - return result; - } - static void reducePriority() { // remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY); @@ -1817,6 +1842,8 @@ final class WebViewCore { // called from UI thread while WEBKIT_DRAW is just pulled out of the // queue in WebCore thread to be executed. Then update won't be blocked. if (core != null) { + if (!core.getSettings().enableSmoothTransition()) return; + synchronized (core) { core.mDrawIsPaused = true; if (core.mDrawIsScheduled) { @@ -1829,6 +1856,10 @@ final class WebViewCore { static void resumeUpdatePicture(WebViewCore core) { if (core != null) { + // if mDrawIsPaused is true, ignore the setting, continue to resume + if (!core.mDrawIsPaused + && !core.getSettings().enableSmoothTransition()) return; + synchronized (core) { core.mDrawIsPaused = false; if (core.mDrawIsScheduled) { @@ -1971,24 +2002,6 @@ final class WebViewCore { mRepaintScheduled = false; } - // called by JNI - private void sendImmediateRepaint() { - if (mWebView != null && !mRepaintScheduled) { - mRepaintScheduled = true; - Message.obtain(mWebView.mPrivateHandler, - WebView.IMMEDIATE_REPAINT_MSG_ID).sendToTarget(); - } - } - - // called by JNI - private void setRootLayer(int layer) { - if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SET_ROOT_LAYER_MSG_ID, - layer, 0).sendToTarget(); - } - } - /* package */ WebView getWebView() { return mWebView; } @@ -2005,18 +2018,24 @@ final class WebViewCore { if (mWebView == null) return; - boolean updateRestoreState = standardLoad || mRestoredScale > 0; - setupViewport(updateRestoreState); + boolean updateViewState = standardLoad || mRestoredScale > 0; + setupViewport(updateViewState); // if updateRestoreState is true, ViewManager.postReadyToDrawAll() will - // be called after the WebView restore the state. If updateRestoreState + // be called after the WebView updates its state. If updateRestoreState // is false, start to draw now as it is ready. - if (!updateRestoreState) { + if (!updateViewState) { mWebView.mViewManager.postReadyToDrawAll(); } + // remove the touch highlight when moving to a new page + if (getSettings().supportTouchOnly()) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.REMOVE_TOUCH_HIGHLIGHT_RECTS)); + } + // reset the scroll position, the restored offset and scales mWebkitScrollX = mWebkitScrollY = mRestoredX = mRestoredY - = mRestoredScale = mRestoredScreenWidthScale = 0; + = mRestoredScale = mRestoredTextWrapScale = 0; } // called by JNI @@ -2030,15 +2049,17 @@ final class WebViewCore { } } - private void setupViewport(boolean updateRestoreState) { + private void setupViewport(boolean updateViewState) { // set the viewport settings from WebKit setViewportSettingsFromNative(); // adjust the default scale to match the densityDpi float adjust = 1.0f; if (mViewportDensityDpi == -1) { - if (WebView.DEFAULT_SCALE_PERCENT != 100) { - adjust = WebView.DEFAULT_SCALE_PERCENT / 100.0f; + // 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(); } } else if (mViewportDensityDpi > 0) { adjust = (float) mContext.getResources().getDisplayMetrics().densityDpi @@ -2080,17 +2101,17 @@ final class WebViewCore { } // if mViewportWidth is 0, it means device-width, always update. - if (mViewportWidth != 0 && !updateRestoreState) { - RestoreState restoreState = new RestoreState(); - restoreState.mMinScale = mViewportMinimumScale / 100.0f; - restoreState.mMaxScale = mViewportMaximumScale / 100.0f; - restoreState.mDefaultScale = adjust; + if (mViewportWidth != 0 && !updateViewState) { + ViewState viewState = new ViewState(); + viewState.mMinScale = mViewportMinimumScale / 100.0f; + viewState.mMaxScale = mViewportMaximumScale / 100.0f; + viewState.mDefaultScale = adjust; // as mViewportWidth is not 0, it is not mobile site. - restoreState.mMobileSite = false; + viewState.mMobileSite = false; // for non-mobile site, we don't need minPrefWidth, set it as 0 - restoreState.mScrollX = 0; + viewState.mScrollX = 0; Message.obtain(mWebView.mPrivateHandler, - WebView.UPDATE_ZOOM_RANGE, restoreState).sendToTarget(); + WebView.UPDATE_ZOOM_RANGE, viewState).sendToTarget(); return; } @@ -2111,32 +2132,31 @@ final class WebViewCore { } else { webViewWidth = Math.round(viewportWidth * mCurrentViewScale); } - mRestoreState = new RestoreState(); - mRestoreState.mMinScale = mViewportMinimumScale / 100.0f; - mRestoreState.mMaxScale = mViewportMaximumScale / 100.0f; - mRestoreState.mDefaultScale = adjust; - mRestoreState.mScrollX = mRestoredX; - mRestoreState.mScrollY = mRestoredY; - mRestoreState.mMobileSite = (0 == mViewportWidth); + mInitialViewState = new ViewState(); + mInitialViewState.mMinScale = mViewportMinimumScale / 100.0f; + mInitialViewState.mMaxScale = mViewportMaximumScale / 100.0f; + mInitialViewState.mDefaultScale = adjust; + mInitialViewState.mScrollX = mRestoredX; + mInitialViewState.mScrollY = mRestoredY; + mInitialViewState.mMobileSite = (0 == mViewportWidth); if (mRestoredScale > 0) { - mRestoreState.mViewScale = mRestoredScale / 100.0f; - if (mRestoredScreenWidthScale > 0) { - mRestoreState.mTextWrapScale = - mRestoredScreenWidthScale / 100.0f; + mInitialViewState.mViewScale = mRestoredScale / 100.0f; + if (mRestoredTextWrapScale > 0) { + mInitialViewState.mTextWrapScale = mRestoredTextWrapScale / 100.0f; } else { - mRestoreState.mTextWrapScale = mRestoreState.mViewScale; + mInitialViewState.mTextWrapScale = mInitialViewState.mViewScale; } } else { if (mViewportInitialScale > 0) { - mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + mInitialViewState.mViewScale = mInitialViewState.mTextWrapScale = mViewportInitialScale / 100.0f; } else if (mViewportWidth > 0 && mViewportWidth < webViewWidth) { - mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + mInitialViewState.mViewScale = mInitialViewState.mTextWrapScale = (float) webViewWidth / mViewportWidth; } else { - mRestoreState.mTextWrapScale = adjust; + mInitialViewState.mTextWrapScale = adjust; // 0 will trigger WebView to turn on zoom overview mode - mRestoreState.mViewScale = 0; + mInitialViewState.mViewScale = 0; } } @@ -2177,15 +2197,15 @@ final class WebViewCore { // mViewScale as 0 means it is in zoom overview mode. So we don't // know the exact scale. If mRestoredScale is non-zero, use it; // otherwise just use mTextWrapScale as the initial scale. - data.mScale = mRestoreState.mViewScale == 0 + data.mScale = mInitialViewState.mViewScale == 0 ? (mRestoredScale > 0 ? mRestoredScale / 100.0f - : mRestoreState.mTextWrapScale) - : mRestoreState.mViewScale; + : mInitialViewState.mTextWrapScale) + : mInitialViewState.mViewScale; if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, "setupViewport" + " mRestoredScale=" + mRestoredScale - + " mViewScale=" + mRestoreState.mViewScale - + " mTextWrapScale=" + mRestoreState.mTextWrapScale + + " mViewScale=" + mInitialViewState.mViewScale + + " mTextWrapScale=" + mInitialViewState.mTextWrapScale ); } data.mWidth = Math.round(webViewWidth / data.mScale); @@ -2198,7 +2218,7 @@ final class WebViewCore { Math.round(mWebView.getViewHeight() / data.mScale) : mCurrentViewHeight * data.mWidth / viewportWidth; data.mTextWrapWidth = Math.round(webViewWidth - / mRestoreState.mTextWrapScale); + / mInitialViewState.mTextWrapScale); data.mIgnoreHeight = false; data.mAnchorX = data.mAnchorY = 0; // send VIEW_SIZE_CHANGED to the front of the queue so that we @@ -2211,20 +2231,12 @@ final class WebViewCore { } // called by JNI - private void restoreScale(int scale) { + private void restoreScale(int scale, int textWrapScale) { if (mBrowserFrame.firstLayoutDone() == false) { mRestoredScale = scale; - } - } - - // called by JNI - private void restoreScreenWidthScale(int scale) { - if (!mSettings.getUseWideViewPort()) { - return; - } - - if (mBrowserFrame.firstLayoutDone() == false) { - mRestoredScreenWidthScale = scale; + if (mSettings.getUseWideViewPort()) { + mRestoredTextWrapScale = textWrapScale; + } } } @@ -2469,4 +2481,6 @@ final class WebViewCore { private native boolean nativeValidNodeAndBounds(int frame, int node, Rect bounds); + private native ArrayList<Rect> nativeGetTouchHighlightRects(int x, int y, + int slop); } diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index b18419d..d75d421 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -173,112 +173,140 @@ public class WebViewDatabase { private static int mCacheTransactionRefcount; - private WebViewDatabase() { + // Initially true until the background thread completes. + private boolean mInitialized = false; + + private WebViewDatabase(final Context context) { + new Thread() { + @Override + public void run() { + init(context); + } + }.start(); + // Singleton only, use getInstance() } public static synchronized WebViewDatabase getInstance(Context context) { if (mInstance == null) { - mInstance = new WebViewDatabase(); - try { - mDatabase = context - .openOrCreateDatabase(DATABASE_FILE, 0, null); - } catch (SQLiteException e) { - // try again by deleting the old db and create a new one - if (context.deleteDatabase(DATABASE_FILE)) { - mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, - null); - } - } + mInstance = new WebViewDatabase(context); + } + return mInstance; + } - // mDatabase should not be null, - // the only case is RequestAPI test has problem to create db - if (mDatabase != null && mDatabase.getVersion() != DATABASE_VERSION) { - mDatabase.beginTransaction(); - try { - upgradeDatabase(); - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - } + private synchronized void init(Context context) { + if (mInitialized) { + return; + } - if (mDatabase != null) { - // use per table Mutex lock, turn off database lock, this - // improves performance as database's ReentrantLock is expansive - mDatabase.setLockingEnabled(false); + try { + mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(DATABASE_FILE)) { + mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, + null); } + } + + // mDatabase should not be null, + // the only case is RequestAPI test has problem to create db + if (mDatabase == null) { + mInitialized = true; + notify(); + return; + } + if (mDatabase.getVersion() != DATABASE_VERSION) { + mDatabase.beginTransaction(); try { - mCacheDatabase = context.openOrCreateDatabase( - CACHE_DATABASE_FILE, 0, null); - } catch (SQLiteException e) { - // try again by deleting the old db and create a new one - if (context.deleteDatabase(CACHE_DATABASE_FILE)) { - mCacheDatabase = context.openOrCreateDatabase( - CACHE_DATABASE_FILE, 0, null); - } + upgradeDatabase(); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); } + } - // mCacheDatabase should not be null, - // the only case is RequestAPI test has problem to create db - if (mCacheDatabase != null - && mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) { - mCacheDatabase.beginTransaction(); - try { - upgradeCacheDatabase(); - bootstrapCacheDatabase(); - mCacheDatabase.setTransactionSuccessful(); - } finally { - mCacheDatabase.endTransaction(); - } - // Erase the files from the file system in the - // case that the database was updated and the - // there were existing cache content - CacheManager.removeAllCacheFiles(); + // use per table Mutex lock, turn off database lock, this + // improves performance as database's ReentrantLock is + // expansive + mDatabase.setLockingEnabled(false); + + try { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(CACHE_DATABASE_FILE)) { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); } + } - if (mCacheDatabase != null) { - // use read_uncommitted to speed up READ - mCacheDatabase.execSQL("PRAGMA read_uncommitted = true;"); - // as only READ can be called in the non-WebViewWorkerThread, - // and read_uncommitted is used, we can turn off database lock - // to use transaction. - mCacheDatabase.setLockingEnabled(false); + // mCacheDatabase should not be null, + // the only case is RequestAPI test has problem to create db + if (mCacheDatabase == null) { + mInitialized = true; + notify(); + return; + } - // use InsertHelper for faster insertion - mCacheInserter = new DatabaseUtils.InsertHelper(mCacheDatabase, - "cache"); - mCacheUrlColIndex = mCacheInserter - .getColumnIndex(CACHE_URL_COL); - mCacheFilePathColIndex = mCacheInserter - .getColumnIndex(CACHE_FILE_PATH_COL); - mCacheLastModifyColIndex = mCacheInserter - .getColumnIndex(CACHE_LAST_MODIFY_COL); - mCacheETagColIndex = mCacheInserter - .getColumnIndex(CACHE_ETAG_COL); - mCacheExpiresColIndex = mCacheInserter - .getColumnIndex(CACHE_EXPIRES_COL); - mCacheExpiresStringColIndex = mCacheInserter - .getColumnIndex(CACHE_EXPIRES_STRING_COL); - mCacheMimeTypeColIndex = mCacheInserter - .getColumnIndex(CACHE_MIMETYPE_COL); - mCacheEncodingColIndex = mCacheInserter - .getColumnIndex(CACHE_ENCODING_COL); - mCacheHttpStatusColIndex = mCacheInserter - .getColumnIndex(CACHE_HTTP_STATUS_COL); - mCacheLocationColIndex = mCacheInserter - .getColumnIndex(CACHE_LOCATION_COL); - mCacheContentLengthColIndex = mCacheInserter - .getColumnIndex(CACHE_CONTENTLENGTH_COL); - mCacheContentDispositionColIndex = mCacheInserter - .getColumnIndex(CACHE_CONTENTDISPOSITION_COL); - mCacheCrossDomainColIndex = mCacheInserter - .getColumnIndex(CACHE_CROSSDOMAIN_COL); + if (mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) { + mCacheDatabase.beginTransaction(); + try { + upgradeCacheDatabase(); + bootstrapCacheDatabase(); + mCacheDatabase.setTransactionSuccessful(); + } finally { + mCacheDatabase.endTransaction(); } + // Erase the files from the file system in the + // case that the database was updated and the + // there were existing cache content + CacheManager.removeAllCacheFiles(); } - return mInstance; + // use read_uncommitted to speed up READ + mCacheDatabase.execSQL("PRAGMA read_uncommitted = true;"); + // as only READ can be called in the + // non-WebViewWorkerThread, and read_uncommitted is used, + // we can turn off database lock to use transaction. + mCacheDatabase.setLockingEnabled(false); + + // use InsertHelper for faster insertion + mCacheInserter = + new DatabaseUtils.InsertHelper(mCacheDatabase, + "cache"); + mCacheUrlColIndex = mCacheInserter + .getColumnIndex(CACHE_URL_COL); + mCacheFilePathColIndex = mCacheInserter + .getColumnIndex(CACHE_FILE_PATH_COL); + mCacheLastModifyColIndex = mCacheInserter + .getColumnIndex(CACHE_LAST_MODIFY_COL); + mCacheETagColIndex = mCacheInserter + .getColumnIndex(CACHE_ETAG_COL); + mCacheExpiresColIndex = mCacheInserter + .getColumnIndex(CACHE_EXPIRES_COL); + mCacheExpiresStringColIndex = mCacheInserter + .getColumnIndex(CACHE_EXPIRES_STRING_COL); + mCacheMimeTypeColIndex = mCacheInserter + .getColumnIndex(CACHE_MIMETYPE_COL); + mCacheEncodingColIndex = mCacheInserter + .getColumnIndex(CACHE_ENCODING_COL); + mCacheHttpStatusColIndex = mCacheInserter + .getColumnIndex(CACHE_HTTP_STATUS_COL); + mCacheLocationColIndex = mCacheInserter + .getColumnIndex(CACHE_LOCATION_COL); + mCacheContentLengthColIndex = mCacheInserter + .getColumnIndex(CACHE_CONTENTLENGTH_COL); + mCacheContentDispositionColIndex = mCacheInserter + .getColumnIndex(CACHE_CONTENTDISPOSITION_COL); + mCacheCrossDomainColIndex = mCacheInserter + .getColumnIndex(CACHE_CROSSDOMAIN_COL); + + // Thread done, notify. + mInitialized = true; + notify(); } private static void upgradeDatabase() { @@ -391,8 +419,25 @@ public class WebViewDatabase { } } + // Wait for the background initialization thread to complete and check the + // database creation status. + private boolean checkInitialized() { + synchronized (this) { + while (!mInitialized) { + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while checking " + + "initialization"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + } + return mDatabase != null; + } + private boolean hasEntries(int tableId) { - if (mDatabase == null) { + if (!checkInitialized()) { return false; } @@ -422,7 +467,7 @@ public class WebViewDatabase { */ ArrayList<Cookie> getCookiesForDomain(String domain) { ArrayList<Cookie> list = new ArrayList<Cookie>(); - if (domain == null || mDatabase == null) { + if (domain == null || !checkInitialized()) { return list; } @@ -481,7 +526,7 @@ public class WebViewDatabase { * deleted. */ void deleteCookies(String domain, String path, String name) { - if (domain == null || mDatabase == null) { + if (domain == null || !checkInitialized()) { return; } @@ -501,7 +546,7 @@ public class WebViewDatabase { */ void addCookie(Cookie cookie) { if (cookie.domain == null || cookie.path == null || cookie.name == null - || mDatabase == null) { + || !checkInitialized()) { return; } @@ -534,7 +579,7 @@ public class WebViewDatabase { * Clear cookie database */ void clearCookies() { - if (mDatabase == null) { + if (!checkInitialized()) { return; } @@ -547,7 +592,7 @@ public class WebViewDatabase { * Clear session cookies, which means cookie doesn't have EXPIRES. */ void clearSessionCookies() { - if (mDatabase == null) { + if (!checkInitialized()) { return; } @@ -564,7 +609,7 @@ public class WebViewDatabase { * @param now Time for now */ void clearExpiredCookies(long now) { - if (mDatabase == null) { + if (!checkInitialized()) { return; } @@ -620,7 +665,7 @@ public class WebViewDatabase { * @return CacheResult The CacheManager.CacheResult */ CacheResult getCache(String url) { - if (url == null || mCacheDatabase == null) { + if (url == null || !checkInitialized()) { return null; } @@ -660,7 +705,7 @@ public class WebViewDatabase { * @param url The url */ void removeCache(String url) { - if (url == null || mCacheDatabase == null) { + if (url == null || !checkInitialized()) { return; } @@ -674,7 +719,7 @@ public class WebViewDatabase { * @param c The CacheManager.CacheResult */ void addCache(String url, CacheResult c) { - if (url == null || mCacheDatabase == null) { + if (url == null || !checkInitialized()) { return; } @@ -700,7 +745,7 @@ public class WebViewDatabase { * Clear cache database */ void clearCache() { - if (mCacheDatabase == null) { + if (!checkInitialized()) { return; } @@ -708,7 +753,7 @@ public class WebViewDatabase { } boolean hasCache() { - if (mCacheDatabase == null) { + if (!checkInitialized()) { return false; } @@ -831,7 +876,7 @@ public class WebViewDatabase { */ void setUsernamePassword(String schemePlusHost, String username, String password) { - if (schemePlusHost == null || mDatabase == null) { + if (schemePlusHost == null || !checkInitialized()) { return; } @@ -853,7 +898,7 @@ public class WebViewDatabase { * String[1] is password. Return null if it can't find anything. */ String[] getUsernamePassword(String schemePlusHost) { - if (schemePlusHost == null || mDatabase == null) { + if (schemePlusHost == null || !checkInitialized()) { return null; } @@ -899,7 +944,7 @@ public class WebViewDatabase { * Clear password database */ public void clearUsernamePassword() { - if (mDatabase == null) { + if (!checkInitialized()) { return; } @@ -924,7 +969,7 @@ public class WebViewDatabase { */ void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { - if (host == null || realm == null || mDatabase == null) { + if (host == null || realm == null || !checkInitialized()) { return; } @@ -949,7 +994,7 @@ public class WebViewDatabase { * String[1] is password. Return null if it can't find anything. */ String[] getHttpAuthUsernamePassword(String host, String realm) { - if (host == null || realm == null || mDatabase == null){ + if (host == null || realm == null || !checkInitialized()){ return null; } @@ -996,7 +1041,7 @@ public class WebViewDatabase { * Clear HTTP authentication password database */ public void clearHttpAuthUsernamePassword() { - if (mDatabase == null) { + if (!checkInitialized()) { return; } @@ -1017,7 +1062,7 @@ public class WebViewDatabase { * @param formdata The form data in HashMap */ void setFormData(String url, HashMap<String, String> formdata) { - if (url == null || formdata == null || mDatabase == null) { + if (url == null || formdata == null || !checkInitialized()) { return; } @@ -1066,7 +1111,7 @@ public class WebViewDatabase { */ ArrayList<String> getFormData(String url, String name) { ArrayList<String> values = new ArrayList<String>(); - if (url == null || name == null || mDatabase == null) { + if (url == null || name == null || !checkInitialized()) { return values; } @@ -1126,7 +1171,7 @@ public class WebViewDatabase { * Clear form database */ public void clearFormData() { - if (mDatabase == null) { + if (!checkInitialized()) { return; } diff --git a/core/java/android/webkit/ZoomControlBase.java b/core/java/android/webkit/ZoomControlBase.java new file mode 100644 index 0000000..be9e8f3 --- /dev/null +++ b/core/java/android/webkit/ZoomControlBase.java @@ -0,0 +1,41 @@ +/* + * 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.webkit; + +interface ZoomControlBase { + + /** + * Causes the on-screen zoom control to be made visible + */ + public void show(); + + /** + * Causes the on-screen zoom control to disappear + */ + public void hide(); + + /** + * Enables the control to update its state if necessary in response to a + * change in the pages zoom level. For example, if the max zoom level is + * reached then the control can disable the button for zooming in. + */ + public void update(); + + /** + * Checks to see if the control is currently visible to the user. + */ + public boolean isVisible(); +} diff --git a/core/java/android/webkit/ZoomControlEmbedded.java b/core/java/android/webkit/ZoomControlEmbedded.java new file mode 100644 index 0000000..c29e72b --- /dev/null +++ b/core/java/android/webkit/ZoomControlEmbedded.java @@ -0,0 +1,117 @@ +/* + * 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.webkit; + +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.Toast; +import android.widget.ZoomButtonsController; + +class ZoomControlEmbedded implements ZoomControlBase { + + private final ZoomManager mZoomManager; + private final WebView mWebView; + + // The controller is lazily initialized in getControls() for performance. + private ZoomButtonsController mZoomButtonsController; + + public ZoomControlEmbedded(ZoomManager zoomManager, WebView webView) { + mZoomManager = zoomManager; + mWebView = webView; + } + + public void show() { + if (!getControls().isVisible() && !mZoomManager.isZoomScaleFixed()) { + + mZoomButtonsController.setVisible(true); + + WebSettings settings = mWebView.getSettings(); + int count = settings.getDoubleTapToastCount(); + if (mZoomManager.isInZoomOverview() && count > 0) { + settings.setDoubleTapToastCount(--count); + Toast.makeText(mWebView.getContext(), + com.android.internal.R.string.double_tap_toast, + Toast.LENGTH_LONG).show(); + } + } + } + + public void hide() { + if (mZoomButtonsController != null) { + mZoomButtonsController.setVisible(false); + } + } + + public boolean isVisible() { + return mZoomButtonsController != null && mZoomButtonsController.isVisible(); + } + + public void update() { + if (mZoomButtonsController == null) { + return; + } + + boolean canZoomIn = mZoomManager.canZoomIn(); + boolean canZoomOut = mZoomManager.canZoomOut() && !mZoomManager.isInZoomOverview(); + if (!canZoomIn && !canZoomOut) { + // Hide the zoom in and out buttons if the page cannot zoom + mZoomButtonsController.getZoomControls().setVisibility(View.GONE); + } else { + // Set each one individually, as a page may be able to zoom in or out + mZoomButtonsController.setZoomInEnabled(canZoomIn); + mZoomButtonsController.setZoomOutEnabled(canZoomOut); + } + } + + private ZoomButtonsController getControls() { + if (mZoomButtonsController == null) { + mZoomButtonsController = new ZoomButtonsController(mWebView); + mZoomButtonsController.setOnZoomListener(new ZoomListener()); + // ZoomButtonsController positions the buttons at the bottom, but in + // the middle. Change their layout parameters so they appear on the + // right. + View controls = mZoomButtonsController.getZoomControls(); + ViewGroup.LayoutParams params = controls.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) params).gravity = Gravity.RIGHT; + } + } + return mZoomButtonsController; + } + + private class ZoomListener implements ZoomButtonsController.OnZoomListener { + + public void onVisibilityChanged(boolean visible) { + if (visible) { + mWebView.switchOutDrawHistory(); + // Bring back the hidden zoom controls. + mZoomButtonsController.getZoomControls().setVisibility(View.VISIBLE); + update(); + } + } + + public void onZoom(boolean zoomIn) { + if (zoomIn) { + mWebView.zoomIn(); + } else { + mWebView.zoomOut(); + } + update(); + } + } +} diff --git a/core/java/android/webkit/ZoomControlExternal.java b/core/java/android/webkit/ZoomControlExternal.java new file mode 100644 index 0000000..d75313e --- /dev/null +++ b/core/java/android/webkit/ZoomControlExternal.java @@ -0,0 +1,159 @@ +/* + * 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.webkit; + +import android.content.Context; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.View.OnClickListener; +import android.view.animation.AlphaAnimation; +import android.widget.FrameLayout; + +@Deprecated +class ZoomControlExternal implements ZoomControlBase { + + // The time that the external controls are visible before fading away + private static final long ZOOM_CONTROLS_TIMEOUT = + ViewConfiguration.getZoomControlsTimeout(); + // The view containing the external zoom controls + private ExtendedZoomControls mZoomControls; + private Runnable mZoomControlRunnable; + private final Handler mPrivateHandler = new Handler(); + + private final WebView mWebView; + + public ZoomControlExternal(WebView webView) { + mWebView = webView; + } + + public void show() { + if(mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + getControls().show(true); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + } + + public void hide() { + if (mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + if (mZoomControls != null) { + mZoomControls.hide(); + } + } + + public boolean isVisible() { + return mZoomControls != null && mZoomControls.isShown(); + } + + public void update() { } + + public ExtendedZoomControls getControls() { + if (mZoomControls == null) { + mZoomControls = createZoomControls(); + + /* + * need to be set to VISIBLE first so that getMeasuredHeight() in + * {@link #onSizeChanged()} can return the measured value for proper + * layout. + */ + mZoomControls.setVisibility(View.VISIBLE); + mZoomControlRunnable = new Runnable() { + public void run() { + /* Don't dismiss the controls if the user has + * focus on them. Wait and check again later. + */ + if (!mZoomControls.hasFocus()) { + mZoomControls.hide(); + } else { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + } + } + }; + } + return mZoomControls; + } + + private ExtendedZoomControls createZoomControls() { + ExtendedZoomControls zoomControls = new ExtendedZoomControls(mWebView.getContext()); + zoomControls.setOnZoomInClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + mWebView.zoomIn(); + } + }); + zoomControls.setOnZoomOutClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + mWebView.zoomOut(); + } + }); + return zoomControls; + } + + private static class ExtendedZoomControls extends FrameLayout { + + private android.widget.ZoomControls mPlusMinusZoomControls; + + public ExtendedZoomControls(Context context) { + super(context, null); + LayoutInflater inflater = (LayoutInflater) + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); + mPlusMinusZoomControls = (android.widget.ZoomControls) findViewById( + com.android.internal.R.id.zoomControls); + findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( + View.GONE); + } + + public void show(boolean showZoom) { + mPlusMinusZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); + fade(View.VISIBLE, 0.0f, 1.0f); + } + + public void hide() { + fade(View.GONE, 1.0f, 0.0f); + } + + private void fade(int visibility, float startAlpha, float endAlpha) { + AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); + anim.setDuration(500); + startAnimation(anim); + setVisibility(visibility); + } + + public boolean hasFocus() { + return mPlusMinusZoomControls.hasFocus(); + } + + public void setOnZoomInClickListener(OnClickListener listener) { + mPlusMinusZoomControls.setOnZoomInClickListener(listener); + } + + public void setOnZoomOutClickListener(OnClickListener listener) { + mPlusMinusZoomControls.setOnZoomOutClickListener(listener); + } + } +} diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java new file mode 100644 index 0000000..7f7f46e --- /dev/null +++ b/core/java/android/webkit/ZoomManager.java @@ -0,0 +1,902 @@ +/* + * 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.webkit; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Canvas; +import android.graphics.Point; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; +import android.view.ScaleGestureDetector; +import android.view.View; + +/** + * The ZoomManager is responsible for maintaining the WebView's current zoom + * level state. It is also responsible for managing the on-screen zoom controls + * as well as any animation of the WebView due to zooming. + * + * Currently, there are two methods for animating the zoom of a WebView. + * + * (1) The first method is triggered by startZoomAnimation(...) and is a fixed + * length animation where the final zoom scale is known at startup. This type of + * animation notifies webkit of the final scale BEFORE it animates. The animation + * is then done by scaling the CANVAS incrementally based on a stepping function. + * + * (2) The second method is triggered by a multi-touch pinch and the new scale + * is determined dynamically based on the user's gesture. This type of animation + * only notifies webkit of new scale AFTER the gesture is complete. The animation + * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView) + * to the new scale in response to events related to the user's gesture. + */ +class ZoomManager { + + static final String LOGTAG = "webviewZoom"; + + private final WebView mWebView; + private final CallbackProxy mCallbackProxy; + + // Widgets responsible for the on-screen zoom functions of the WebView. + private ZoomControlEmbedded mEmbeddedZoomControl; + private ZoomControlExternal mExternalZoomControl; + + /* + * The scale factors that determine the upper and lower bounds for the + * default zoom scale. + */ + protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f; + protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f; + + // The default scale limits, which are dependent on the display density. + private float mDefaultMaxZoomScale; + private float mDefaultMinZoomScale; + + // The actual scale limits, which can be set through a webpage's viewport + // meta-tag. + private float mMaxZoomScale; + private float mMinZoomScale; + + // Locks the minimum ZoomScale to the value currently set in mMinZoomScale. + private boolean mMinZoomScaleFixed = true; + + /* + * When loading a new page the WebView does not initially know the final + * width of the page. Therefore, when a new page is loaded in overview mode + * the overview scale is initialized to a default value. This flag is then + * set and used to notify the ZoomManager to take the width of the next + * picture from webkit and use that width to enter into zoom overview mode. + */ + private boolean mInitialZoomOverview = false; + + /* + * When in the zoom overview mode, the page's width is fully fit to the + * current window. Additionally while the page is in this state it is + * active, in other words, you can click to follow the links. We cache a + * boolean to enable us to quickly check whether or not we are in overview + * mode, but this value should only be modified by changes to the zoom + * scale. + */ + private boolean mInZoomOverview = false; + private int mZoomOverviewWidth; + private float mInvZoomOverviewWidth; + + /* + * These variables track the center point of the zoom and they are used to + * determine the point around which we should zoom. They are stored in view + * coordinates. + */ + private float mZoomCenterX; + private float mZoomCenterY; + + /* + * These values represent the point around which the screen should be + * centered after zooming. In other words it is used to determine the center + * point of the visible document after the page has finished zooming. This + * is important because the zoom may have potentially reflowed the text and + * we need to ensure the proper portion of the document remains on the + * screen. + */ + private int mAnchorX; + private int mAnchorY; + + // The scale factor that is used to determine the column width for text + private float mTextWrapScale; + + /* + * The default zoom scale is the scale factor used when the user triggers a + * zoom in by double tapping on the WebView. The value is initially set + * based on the display density, but can be changed at any time via the + * WebSettings. + */ + private float mDefaultScale; + private float mInvDefaultScale; + + // the current computed zoom scale and its inverse. + private float mActualScale; + private float mInvActualScale; + + /* + * The initial scale for the WebView. 0 means default. If initial scale is + * greater than 0 the WebView starts with this value as its initial scale. The + * value is converted from an integer percentage so it is guarenteed to have + * no more than 2 significant digits after the decimal. This restriction + * allows us to convert the scale back to the original percentage by simply + * multiplying the value by 100. + */ + private float mInitialScale; + + private static float MINIMUM_SCALE_INCREMENT = 0.01f; + + /* + * The following member variables are only to be used for animating zoom. If + * mZoomScale is non-zero then we are in the middle of a zoom animation. The + * other variables are used as a cache (e.g. inverse) or as a way to store + * the state of the view prior to animating (e.g. initial scroll coords). + */ + private float mZoomScale; + private float mInvInitialZoomScale; + private float mInvFinalZoomScale; + private int mInitialScrollX; + private int mInitialScrollY; + private long mZoomStart; + + private static final int ZOOM_ANIMATION_LENGTH = 500; + + // whether support multi-touch + private boolean mSupportMultiTouch; + + // use the framework's ScaleGestureDetector to handle multi-touch + private ScaleGestureDetector mScaleDetector; + private boolean mPinchToZoomAnimating = false; + + public ZoomManager(WebView webView, CallbackProxy callbackProxy) { + mWebView = webView; + mCallbackProxy = callbackProxy; + + /* + * Ideally mZoomOverviewWidth should be mContentWidth. But sites like + * ESPN and Engadget always have wider mContentWidth no matter what the + * viewport size is. + */ + setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH); + } + + /** + * Initialize both the default and actual zoom scale to the given density. + * + * @param density The logical density of the display. This is a scaling factor + * for the Density Independent Pixel unit, where one DIP is one pixel on an + * approximately 160 dpi screen (see android.util.DisplayMetrics.density). + */ + public void init(float density) { + assert density > 0; + + setDefaultZoomScale(density); + mActualScale = density; + mInvActualScale = 1 / density; + mTextWrapScale = density; + } + + /** + * Update the default zoom scale using the given density. It will also reset + * the current min and max zoom scales to the default boundaries as well as + * ensure that the actual scale falls within those boundaries. + * + * @param density The logical density of the display. This is a scaling factor + * for the Density Independent Pixel unit, where one DIP is one pixel on an + * approximately 160 dpi screen (see android.util.DisplayMetrics.density). + */ + public void updateDefaultZoomDensity(float density) { + assert density > 0; + + if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) { + // set the new default density + setDefaultZoomScale(density); + // adjust the scale if it falls outside the new zoom bounds + setZoomScale(mActualScale, true); + } + } + + private void setDefaultZoomScale(float defaultScale) { + mDefaultScale = defaultScale; + mInvDefaultScale = 1 / defaultScale; + mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR; + mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR; + mMaxZoomScale = mDefaultMaxZoomScale; + mMinZoomScale = mDefaultMinZoomScale; + } + + public final float getScale() { + return mActualScale; + } + + public final float getInvScale() { + return mInvActualScale; + } + + public final float getTextWrapScale() { + return mTextWrapScale; + } + + public final float getMaxZoomScale() { + return mMaxZoomScale; + } + + public final float getMinZoomScale() { + return mMinZoomScale; + } + + public final float getDefaultScale() { + return mDefaultScale; + } + + public final float getInvDefaultScale() { + return mInvDefaultScale; + } + + public final float getDefaultMaxZoomScale() { + return mDefaultMaxZoomScale; + } + + public final float getDefaultMinZoomScale() { + return mDefaultMinZoomScale; + } + + public final int getDocumentAnchorX() { + return mAnchorX; + } + + public final int getDocumentAnchorY() { + return mAnchorY; + } + + public final void clearDocumentAnchor() { + mAnchorX = mAnchorY = 0; + } + + public final void setZoomCenter(float x, float y) { + mZoomCenterX = x; + mZoomCenterY = y; + } + + public final void setInitialScaleInPercent(int scaleInPercent) { + mInitialScale = scaleInPercent * 0.01f; + } + + public final float computeScaleWithLimits(float scale) { + if (scale < mMinZoomScale) { + scale = mMinZoomScale; + } else if (scale > mMaxZoomScale) { + scale = mMaxZoomScale; + } + return scale; + } + + public final boolean isZoomScaleFixed() { + return mMinZoomScale >= mMaxZoomScale; + } + + public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) { + return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT; + } + + public boolean willScaleTriggerZoom(float scale) { + return exceedsMinScaleIncrement(scale, mActualScale); + } + + public final boolean canZoomIn() { + return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT; + } + + public final boolean canZoomOut() { + return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT; + } + + public boolean zoomIn() { + return zoom(1.25f); + } + + public boolean zoomOut() { + return zoom(0.8f); + } + + // returns TRUE if zoom out succeeds and FALSE if no zoom changes. + private boolean zoom(float zoomMultiplier) { + // TODO: alternatively we can disallow this during draw history mode + mWebView.switchOutDrawHistory(); + // Center zooming to the center of the screen. + mZoomCenterX = mWebView.getViewWidth() * .5f; + mZoomCenterY = mWebView.getViewHeight() * .5f; + mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); + mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); + return startZoomAnimation(mActualScale * zoomMultiplier, true); + } + + /** + * Initiates an animated zoom of the WebView. + * + * @return true if the new scale triggered an animation and false otherwise. + */ + public boolean startZoomAnimation(float scale, boolean reflowText) { + float oldScale = mActualScale; + mInitialScrollX = mWebView.getScrollX(); + mInitialScrollY = mWebView.getScrollY(); + + // snap to DEFAULT_SCALE if it is close + if (!exceedsMinScaleIncrement(scale, mDefaultScale)) { + scale = mDefaultScale; + } + + setZoomScale(scale, reflowText); + + if (oldScale != mActualScale) { + // use mZoomPickerScale to see zoom preview first + mZoomStart = SystemClock.uptimeMillis(); + mInvInitialZoomScale = 1.0f / oldScale; + mInvFinalZoomScale = 1.0f / mActualScale; + mZoomScale = mActualScale; + mWebView.onFixedLengthZoomAnimationStart(); + mWebView.invalidate(); + return true; + } else { + return false; + } + } + + /** + * This method is called by the WebView's drawing code when a fixed length zoom + * animation is occurring. Its purpose is to animate the zooming of the canvas + * to the desired scale which was specified in startZoomAnimation(...). + * + * A fixed length animation begins when startZoomAnimation(...) is called and + * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that + * interval each time the WebView draws it calls this function which is + * responsible for generating the animation. + * + * Additionally, the WebView can check to see if such an animation is currently + * in progress by calling isFixedLengthAnimationInProgress(). + */ + public void animateZoom(Canvas canvas) { + if (mZoomScale == 0) { + Log.w(LOGTAG, "A WebView is attempting to perform a fixed length " + + "zoom animation when no zoom is in progress"); + return; + } + + float zoomScale; + int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); + if (interval < ZOOM_ANIMATION_LENGTH) { + float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; + zoomScale = 1.0f / (mInvInitialZoomScale + + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); + mWebView.invalidate(); + } else { + zoomScale = mZoomScale; + // set mZoomScale to be 0 as we have finished animating + mZoomScale = 0; + mWebView.onFixedLengthZoomAnimationEnd(); + } + // calculate the intermediate scroll position. Since we need to use + // zoomScale, we can't use the WebView's pinLocX/Y functions directly. + float scale = zoomScale * mInvInitialZoomScale; + int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX); + tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth() + * zoomScale)) + mWebView.getScrollX(); + int titleHeight = mWebView.getTitleHeight(); + int ty = Math.round(scale + * (mInitialScrollY + mZoomCenterY - titleHeight) + - (mZoomCenterY - titleHeight)); + ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty + - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight() + * zoomScale)) + titleHeight) + mWebView.getScrollY(); + + canvas.translate(tx, ty); + canvas.scale(zoomScale, zoomScale); + } + + public boolean isZoomAnimating() { + return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating; + } + + public boolean isFixedLengthAnimationInProgress() { + return mZoomScale != 0; + } + + public void refreshZoomScale(boolean reflowText) { + setZoomScale(mActualScale, reflowText, true); + } + + public void setZoomScale(float scale, boolean reflowText) { + setZoomScale(scale, reflowText, false); + } + + private void setZoomScale(float scale, boolean reflowText, boolean force) { + final boolean isScaleLessThanMinZoom = scale < mMinZoomScale; + scale = computeScaleWithLimits(scale); + + // determine whether or not we are in the zoom overview mode + if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) { + mInZoomOverview = true; + } else { + mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale()); + } + + if (reflowText) { + mTextWrapScale = scale; + } + + if (scale != mActualScale || force) { + float oldScale = mActualScale; + float oldInvScale = mInvActualScale; + + if (scale != mActualScale && !mPinchToZoomAnimating) { + mCallbackProxy.onScaleChanged(mActualScale, scale); + } + + mActualScale = scale; + mInvActualScale = 1 / scale; + + if (!mWebView.drawHistory()) { + + // If history Picture is drawn, don't update scroll. They will + // be updated when we get out of that mode. + // update our scroll so we don't appear to jump + // i.e. keep the center of the doc in the center of the view + int oldX = mWebView.getScrollX(); + int oldY = mWebView.getScrollY(); + float ratio = scale * oldInvScale; + float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; + float sy = ratio * oldY + (ratio - 1) + * (mZoomCenterY - mWebView.getTitleHeight()); + + // Scale all the child views + mWebView.mViewManager.scaleAll(); + + // as we don't have animation for scaling, don't do animation + // for scrolling, as it causes weird intermediate state + int scrollX = mWebView.pinLocX(Math.round(sx)); + int scrollY = mWebView.pinLocY(Math.round(sy)); + if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) { + // the scroll position is adjusted at the beginning of the + // zoom animation. But we want to update the WebKit at the + // end of the zoom animation. See comments in onScaleEnd(). + mWebView.sendOurVisibleRect(); + } + } + + // if the we need to reflow the text then force the VIEW_SIZE_CHANGED + // event to be sent to WebKit + mWebView.sendViewSizeZoom(reflowText); + } + } + + /** + * The double tap gesture can result in different behaviors depending on the + * content that is tapped. + * + * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on + * the screen. If the plugin is already maximized then zoom the user into + * overview mode. + * + * (2) HTML/OTHER: If the taps occur outside a plugin then the following + * heuristic is used. + * A. If the current scale is not the same as the text wrap scale and the + * layout algorithm specifies the use of NARROW_COLUMNS, then fit to + * column by reflowing the text. + * B. If the page is not in overview mode then change to overview mode. + * C. If the page is in overmode then change to the default scale. + */ + public void handleDoubleTap(float lastTouchX, float lastTouchY) { + WebSettings settings = mWebView.getSettings(); + if (settings == null || settings.getUseWideViewPort() == false) { + return; + } + + setZoomCenter(lastTouchX, lastTouchY); + mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX()); + mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY()); + settings.setDoubleTapToastCount(0); + + // remove the zoom control after double tap + dismissZoomPicker(); + + /* + * If the double tap was on a plugin then either zoom to maximize the + * plugin on the screen or scale to overview mode. + */ + ViewManager.ChildView plugin = mWebView.mViewManager.hitTest(mAnchorX, mAnchorY); + if (plugin != null) { + if (mWebView.isPluginFitOnScreen(plugin)) { + zoomToOverview(); + } else { + mWebView.centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height); + } + return; + } + + if (settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS + && willScaleTriggerZoom(mTextWrapScale)) { + refreshZoomScale(true); + } else if (!mInZoomOverview) { + zoomToOverview(); + } else { + zoomToDefaultLevel(); + } + } + + private void setZoomOverviewWidth(int width) { + mZoomOverviewWidth = width; + mInvZoomOverviewWidth = 1.0f / width; + } + + private float getZoomOverviewScale() { + return mWebView.getViewWidth() * mInvZoomOverviewWidth; + } + + public boolean isInZoomOverview() { + return mInZoomOverview; + } + + private void zoomToOverview() { + if (!willScaleTriggerZoom(getZoomOverviewScale())) return; + + // Force the titlebar fully reveal in overview mode + int scrollY = mWebView.getScrollY(); + if (scrollY < mWebView.getTitleHeight()) { + mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0); + } + startZoomAnimation(getZoomOverviewScale(), true); + } + + private void zoomToDefaultLevel() { + int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); + if (left != WebView.NO_LEFTEDGE) { + // add a 5pt padding to the left edge. + int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5)) + - mWebView.getScrollX(); + // Re-calculate the zoom center so that the new scroll x will be + // on the left edge. + if (viewLeft > 0) { + mZoomCenterX = viewLeft * mDefaultScale / (mDefaultScale - mActualScale); + } else { + mWebView.scrollBy(viewLeft, 0); + mZoomCenterX = 0; + } + } + startZoomAnimation(mDefaultScale, true); + } + + public void updateMultiTouchSupport(Context context) { + // check the preconditions + assert mWebView.getSettings() != null; + + WebSettings settings = mWebView.getSettings(); + mSupportMultiTouch = context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) + && settings.supportZoom() && settings.getBuiltInZoomControls(); + if (mSupportMultiTouch && (mScaleDetector == null)) { + mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener()); + } else if (!mSupportMultiTouch && (mScaleDetector != null)) { + mScaleDetector = null; + } + } + + public boolean supportsMultiTouchZoom() { + return mSupportMultiTouch; + } + + /** + * Notifies the caller that the ZoomManager is requesting that scale related + * updates should not be sent to webkit. This can occur in cases where the + * ZoomManager is performing an animation and does not want webkit to update + * until the animation is complete. + * + * @return true if scale related updates should not be sent to webkit and + * false otherwise. + */ + public boolean isPreventingWebkitUpdates() { + // currently only animating a multi-touch zoom prevents updates, but + // others can add their own conditions to this method if necessary. + return mPinchToZoomAnimating; + } + + public ScaleGestureDetector getMultiTouchGestureDetector() { + return mScaleDetector; + } + + private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener { + + public boolean onScaleBegin(ScaleGestureDetector detector) { + dismissZoomPicker(); + mWebView.mViewManager.startZoom(); + mWebView.onPinchToZoomAnimationStart(); + return true; + } + + public boolean onScale(ScaleGestureDetector detector) { + float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f; + if (willScaleTriggerZoom(scale)) { + mPinchToZoomAnimating = true; + // limit the scale change per step + if (scale > mActualScale) { + scale = Math.min(scale, mActualScale * 1.25f); + } else { + scale = Math.max(scale, mActualScale * 0.8f); + } + setZoomCenter(detector.getFocusX(), detector.getFocusY()); + setZoomScale(scale, false); + mWebView.invalidate(); + return true; + } + return false; + } + + public void onScaleEnd(ScaleGestureDetector detector) { + if (mPinchToZoomAnimating) { + mPinchToZoomAnimating = false; + mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX()); + mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY()); + // don't reflow when zoom in; when zoom out, do reflow if the + // new scale is almost minimum scale; + boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale); + // force zoom after mPreviewZoomOnly is set to false so that the + // new view size will be passed to the WebKit + refreshZoomScale(reflowNow); + // call invalidate() to draw without zoom filter + mWebView.invalidate(); + } + + mWebView.mViewManager.endZoom(); + mWebView.onPinchToZoomAnimationEnd(detector); + } + } + + public void onSizeChanged(int w, int h, int ow, int oh) { + // reset zoom and anchor to the top left corner of the screen + // unless we are already zooming + if (!isFixedLengthAnimationInProgress()) { + int visibleTitleHeight = mWebView.getVisibleTitleHeight(); + mZoomCenterX = 0; + mZoomCenterY = visibleTitleHeight; + mAnchorX = mWebView.viewToContentX(mWebView.getScrollX()); + mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY()); + } + + // update mMinZoomScale if the minimum zoom scale is not fixed + if (!mMinZoomScaleFixed) { + // when change from narrow screen to wide screen, the new viewWidth + // can be wider than the old content width. We limit the minimum + // scale to 1.0f. The proper minimum scale will be calculated when + // the new picture shows up. + mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth() + / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth() + : mZoomOverviewWidth)); + // limit the minZoomScale to the initialScale if it is set + if (mInitialScale > 0 && mInitialScale < mMinZoomScale) { + mMinZoomScale = mInitialScale; + } + } + + dismissZoomPicker(); + + // onSizeChanged() is called during WebView layout. And any + // requestLayout() is blocked during layout. As refreshZoomScale() will + // cause its child View to reposition itself through ViewManager's + // scaleAll(), we need to post a Runnable to ensure requestLayout(). + // Additionally, only update the text wrap scale if the width changed. + mWebView.post(new PostScale(w != ow)); + } + + private class PostScale implements Runnable { + final boolean mUpdateTextWrap; + + public PostScale(boolean updateTextWrap) { + mUpdateTextWrap = updateTextWrap; + } + + public void run() { + if (mWebView.getWebViewCore() != null) { + // we always force, in case our height changed, in which case we + // still want to send the notification over to webkit. + refreshZoomScale(mUpdateTextWrap); + // update the zoom buttons as the scale can be changed + updateZoomPicker(); + } + } + } + + public void updateZoomRange(WebViewCore.ViewState viewState, + int viewWidth, int minPrefWidth) { + if (viewState.mMinScale == 0) { + if (viewState.mMobileSite) { + if (minPrefWidth > Math.max(0, viewWidth)) { + mMinZoomScale = (float) viewWidth / minPrefWidth; + mMinZoomScaleFixed = false; + } else { + mMinZoomScale = viewState.mDefaultScale; + mMinZoomScaleFixed = true; + } + } else { + mMinZoomScale = mDefaultMinZoomScale; + mMinZoomScaleFixed = false; + } + } else { + mMinZoomScale = viewState.mMinScale; + mMinZoomScaleFixed = true; + } + if (viewState.mMaxScale == 0) { + mMaxZoomScale = mDefaultMaxZoomScale; + } else { + mMaxZoomScale = viewState.mMaxScale; + } + } + + /** + * Updates zoom values when Webkit produces a new picture. This method + * should only be called from the UI thread's message handler. + */ + public void onNewPicture(WebViewCore.DrawData drawData) { + final int viewWidth = mWebView.getViewWidth(); + + if (mWebView.getSettings().getUseWideViewPort()) { + // limit mZoomOverviewWidth upper bound to + // sMaxViewportWidth so that if the page doesn't behave + // well, the WebView won't go insane. limit the lower + // bound to match the default scale for mobile sites. + setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth, + Math.max((int) (viewWidth * mInvDefaultScale), + Math.max(drawData.mMinPrefWidth, drawData.mViewPoint.x)))); + } + + final float zoomOverviewScale = getZoomOverviewScale(); + if (!mMinZoomScaleFixed) { + mMinZoomScale = zoomOverviewScale; + } + // fit the content width to the current view. Ignore the rounding error case. + if (!mWebView.drawHistory() && (mInitialZoomOverview || (mInZoomOverview + && Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1))) { + mInitialZoomOverview = false; + setZoomScale(zoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale)); + } + } + + /** + * Updates zoom values after Webkit completes the initial page layout. It + * is called when visiting a page for the first time as well as when the + * user navigates back to a page (in which case we may need to restore the + * zoom levels to the state they were when you left the page). This method + * should only be called from the UI thread's message handler. + */ + public void onFirstLayout(WebViewCore.DrawData drawData) { + // precondition check + assert drawData != null; + assert drawData.mViewState != null; + assert mWebView.getSettings() != null; + + WebViewCore.ViewState viewState = drawData.mViewState; + final Point viewSize = drawData.mViewPoint; + updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth); + + if (!mWebView.drawHistory()) { + final float scale; + final boolean reflowText; + + if (mInitialScale > 0) { + scale = mInitialScale; + reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); + } else if (viewState.mViewScale > 0) { + mTextWrapScale = viewState.mTextWrapScale; + scale = viewState.mViewScale; + reflowText = false; + } else { + WebSettings settings = mWebView.getSettings(); + if (settings.getUseWideViewPort() && settings.getLoadWithOverviewMode()) { + mInitialZoomOverview = true; + scale = (float) mWebView.getViewWidth() / WebView.DEFAULT_VIEWPORT_WIDTH; + } else { + scale = viewState.mTextWrapScale; + } + reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale); + } + setZoomScale(scale, reflowText); + + // update the zoom buttons as the scale can be changed + updateZoomPicker(); + } + } + + public void saveZoomState(Bundle b) { + b.putFloat("scale", mActualScale); + b.putFloat("textwrapScale", mTextWrapScale); + b.putBoolean("overview", mInZoomOverview); + } + + public void restoreZoomState(Bundle b) { + // as getWidth() / getHeight() of the view are not available yet, set up + // mActualScale, so that when onSizeChanged() is called, the rest will + // be set correctly + mActualScale = b.getFloat("scale", 1.0f); + mInvActualScale = 1 / mActualScale; + mTextWrapScale = b.getFloat("textwrapScale", mActualScale); + mInZoomOverview = b.getBoolean("overview"); + } + + private ZoomControlBase getCurrentZoomControl() { + if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) { + if (mWebView.getSettings().getBuiltInZoomControls()) { + if (mEmbeddedZoomControl == null) { + mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView); + } + return mEmbeddedZoomControl; + } else { + if (mExternalZoomControl == null) { + mExternalZoomControl = new ZoomControlExternal(mWebView); + } + return mExternalZoomControl; + } + } + return null; + } + + public void invokeZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.show(); + } + } + + public void dismissZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.hide(); + } + } + + public boolean isZoomPickerVisible() { + ZoomControlBase control = getCurrentZoomControl(); + return (control != null) ? control.isVisible() : false; + } + + public void updateZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.update(); + } + } + + /** + * The embedded zoom control intercepts touch events and automatically stays + * visible. The external control needs to constantly refresh its internal + * timer to stay visible. + */ + public void keepZoomPickerVisible() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null && control == mExternalZoomControl) { + control.show(); + } + } + + public View getExternalZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null && control == mExternalZoomControl) { + return mExternalZoomControl.getControls(); + } else { + return null; + } + } +} diff --git a/core/java/android/webruntime/WebRuntimeActivity.java b/core/java/android/webruntime/WebRuntimeActivity.java new file mode 100644 index 0000000..ec8c60c --- /dev/null +++ b/core/java/android/webruntime/WebRuntimeActivity.java @@ -0,0 +1,204 @@ +/* + * 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.webruntime; + +import android.app.Activity; +import android.content.Intent; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.View; +import android.view.Window; +import android.webkit.GeolocationPermissions; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.internal.R; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * The runtime used to display installed web applications. + * @hide + */ +public class WebRuntimeActivity extends Activity +{ + private final static String LOGTAG = "WebRuntimeActivity"; + + private WebView mWebView; + private URL mBaseUrl; + private ImageView mSplashScreen; + + public static class SensitiveFeatures { + // All of the sensitive features + private boolean mGeolocation; + // On Android, the Browser doesn't prompt for database access, so we don't require an + // explicit permission here in the WebRuntimeActivity, and there's no Android system + // permission required for it either. + //private boolean mDatabase; + + public boolean getGeolocation() { + return mGeolocation; + } + public void setGeolocation(boolean geolocation) { + mGeolocation = geolocation; + } + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + // Can't get meta data using getApplicationInfo() as it doesn't pass GET_META_DATA + PackageManager packageManager = getPackageManager(); + ComponentName componentName = new ComponentName(this, getClass()); + ActivityInfo activityInfo = null; + try { + activityInfo = packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + Log.d(LOGTAG, "Failed to find component"); + return; + } + if (activityInfo == null) { + Log.d(LOGTAG, "Failed to get activity info"); + return; + } + + Bundle metaData = activityInfo.metaData; + if (metaData == null) { + Log.d(LOGTAG, "No meta data"); + return; + } + + String url = metaData.getString("android.webruntime.url"); + if (url == null) { + Log.d(LOGTAG, "No URL"); + return; + } + + try { + mBaseUrl = new URL(url); + } catch (MalformedURLException e) { + Log.d(LOGTAG, "Invalid URL"); + } + + // All false by default, and reading non-existent bundle properties gives false too. + final SensitiveFeatures sensitiveFeatures = new SensitiveFeatures(); + sensitiveFeatures.setGeolocation(metaData.getBoolean("android.webruntime.SensitiveFeaturesGeolocation")); + + getWindow().requestFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.web_runtime); + mWebView = (WebView) findViewById(R.id.webview); + mSplashScreen = (ImageView) findViewById(R.id.splashscreen); + mSplashScreen.setImageResource( + getResources().getIdentifier("splash_screen", "drawable", getPackageName())); + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + try { + URL newOrigin = new URL(url); + if (areSameOrigin(mBaseUrl, newOrigin)) { + // If simple same origin test passes, load in the webview. + return false; + } + } catch(MalformedURLException e) { + // Don't load anything if this wasn't a proper URL. + return true; + } + + // Otherwise this is a URL that is not same origin so pass it to the + // Browser to load. + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + if (mSplashScreen != null && mSplashScreen.getVisibility() == View.VISIBLE) { + mSplashScreen.setVisibility(View.GONE); + mSplashScreen = null; + } + } + }); + + // Use a custom WebChromeClient with geolocation permissions handling. + mWebView.setWebChromeClient(new WebChromeClient() { + public void onGeolocationPermissionsShowPrompt( + String origin, GeolocationPermissions.Callback callback) { + // Allow this origin if it has Geolocation permissions, otherwise deny. + boolean allowed = false; + if (sensitiveFeatures.getGeolocation()) { + try { + URL originUrl = new URL(origin); + allowed = areSameOrigin(mBaseUrl, originUrl); + } catch(MalformedURLException e) { + } + } + callback.invoke(origin, allowed, false); + } + }); + + // Set the DB location. Optional. Geolocation works without DBs. + mWebView.getSettings().setGeolocationDatabasePath( + getDir("geolocation", MODE_PRIVATE).getPath()); + + String title = metaData.getString("android.webruntime.title"); + // We turned off the title bar to go full screen so display the + // webapp's title as a toast. + if (title != null) { + Toast.makeText(this, title, Toast.LENGTH_SHORT).show(); + } + + // Load the webapp's base URL. + mWebView.loadUrl(url); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) { + mWebView.goBack(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, 0, 0, "Menu item 1"); + menu.add(0, 1, 0, "Menu item 2"); + return true; + } + + private static boolean areSameOrigin(URL a, URL b) { + int aPort = a.getPort() == -1 ? a.getDefaultPort() : a.getPort(); + int bPort = b.getPort() == -1 ? b.getDefaultPort() : b.getPort(); + return a.getProtocol().equals(b.getProtocol()) && aPort == bPort && a.getHost().equals(b.getHost()); + } +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 6cfeb68..70c1e15 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -559,6 +559,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); + + final int adapterId = a.getResourceId(R.styleable.AbsListView_adapter, 0); + if (adapterId != 0) { + final Context c = context; + post(new Runnable() { + public void run() { + setAdapter(Adapters.loadAdapter(c, adapterId)); + } + }); + } a.recycle(); } @@ -1575,6 +1585,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te treeObserver.addOnGlobalLayoutListener(this); } } + + if (mAdapter != null && mDataSetObserver == null) { + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + } } @Override @@ -1595,6 +1610,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mGlobalLayoutListenerAddedFilter = false; } } + + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + mDataSetObserver = null; + } } @Override @@ -2521,6 +2541,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private static final int MOVE_UP_POS = 2; private static final int MOVE_DOWN_BOUND = 3; private static final int MOVE_UP_BOUND = 4; + private static final int MOVE_OFFSET = 5; private int mMode; private int mTargetPos; @@ -2528,6 +2549,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mLastSeenPos; private int mScrollDuration; private final int mExtraScroll; + + private int mOffsetFromTop; PositionScroller() { mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); @@ -2619,12 +2642,46 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te post(this); } - + + void startWithOffset(int position, int offset) { + mTargetPos = position; + mOffsetFromTop = offset; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + mMode = MOVE_OFFSET; + + final int firstPos = mFirstPosition; + final int childCount = getChildCount(); + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount = 0; + if (position < firstPos) { + viewTravelCount = firstPos - position; + } else if (position > lastPos) { + viewTravelCount = position - lastPos; + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(position - firstPos).getTop(); + smoothScrollBy(targetTop - offset, SCROLL_DURATION); + return; + } + + // Estimate how many screens we should travel + final float screenTravelCount = viewTravelCount / childCount; + mScrollDuration = (int) (SCROLL_DURATION / screenTravelCount); + mLastSeenPos = INVALID_POSITION; + post(this); + } + void stop() { removeCallbacks(this); } - + public void run() { + if (mTouchMode != TOUCH_MODE_FLING && mLastSeenPos != INVALID_POSITION) { + return; + } + final int listHeight = getHeight(); final int firstPos = mFirstPosition; @@ -2749,6 +2806,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } + case MOVE_OFFSET: { + final int childCount = getChildCount(); + + mLastSeenPos = firstPos; + final int position = mTargetPos; + final int lastPos = firstPos + childCount - 1; + + if (position < firstPos) { + smoothScrollBy(-getHeight(), mScrollDuration); + post(this); + } else if (position > lastPos) { + smoothScrollBy(getHeight(), mScrollDuration); + post(this); + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(position - firstPos).getTop(); + smoothScrollBy(targetTop - mOffsetFromTop, mScrollDuration); + } + break; + } + default: break; } @@ -2768,6 +2846,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * Smoothly scroll to the specified adapter position. The view will scroll + * such that the indicated position is displayed <code>offset</code> pixels from + * the top edge of the view. If this is impossible, (e.g. the offset would scroll + * the first or last item beyond the boundaries of the list) it will get as close + * as possible. + * + * @param position Position to scroll to + * @param offset Desired distance in pixels of <code>position</code> from the top + * of the view when scrolling is finished + */ + public void smoothScrollToPositionFromTop(int position, int offset) { + if (mPositionScroller == null) { + mPositionScroller = new PositionScroller(); + } + mPositionScroller.startWithOffset(position, offset); + } + + /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed, but it will * stop early if scrolling further would scroll boundPosition out of @@ -3155,6 +3251,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mResurrectToPosition = INVALID_POSITION; removeCallbacks(mFlingRunnable); + removeCallbacks(mPositionScroller); mTouchMode = TOUCH_MODE_REST; clearScrollingCache(); mSpecificTop = selectedTop; @@ -4190,7 +4287,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { - scrap.get(i).setDrawingCacheBackgroundColor(color); + scrap.get(j).setDrawingCacheBackgroundColor(color); } } } diff --git a/core/java/android/widget/Adapters.java b/core/java/android/widget/Adapters.java new file mode 100644 index 0000000..7fd7fb5 --- /dev/null +++ b/core/java/android/widget/Adapters.java @@ -0,0 +1,1232 @@ +/* + * 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.widget; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.View; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * <p>This class can be used to load {@link android.widget.Adapter adapters} defined in + * XML resources. XML-defined adapters can be used to easily create adapters in your + * own application or to pass adapters to other processes.</p> + * + * <h2>Types of adapters</h2> + * <p>Adapters defined using XML resources can only be one of the following supported + * types. Arbitrary adapters are not supported to guarantee the safety of the loaded + * code when adapters are loaded across packages.</p> + * <ul> + * <li><a href="#xml-cursor-adapter">Cursor adapter</a>: a cursor adapter can be used + * to display the content of a cursor, most often coming from a content provider</li> + * </ul> + * <p>The complete XML format definition of each adapter type is available below.</p> + * + * <a name="xml-cursor-adapter" /> + * <h2>Cursor adapter</h2> + * <p>A cursor adapter XML definition starts with the + * <a href="#xml-cursor-adapter-tag"><code><cursor-adapter /></code></a> + * tag and may contain one or more instances of the following tags:</p> + * <ul> + * <li><a href="#xml-cursor-adapter-select-tag"><code><select /></code></a></li> + * <li><a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a></li> + * </ul> + * + * <a name="xml-cursor-adapter-tag" /> + * <h3><cursor-adapter /></h3> + * <p>The <code><cursor-adapter /></code> element defines the beginning of the + * document and supports the following attributes:</p> + * <ul> + * <li><code>android:layout</code>: Reference to the XML layout to be inflated for + * each item of the adapter. This attribute is mandatory.</li> + * <li><code>android:selection</code>: Selection expression, used when the + * <code>android:uri</code> attribute is defined or when the adapter is loaded with + * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * This attribute is optional.</li> + * <li><code>android:sortOrder</code>: Sort expression, used when the + * <code>android:uri</code> attribute is defined or when the adapter is loaded with + * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * This attribute is optional.</li> + * <li><code>android:uri</code>: URI of the content provider to query to retrieve a cursor. + * Specifying this attribute is equivalent to calling + * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * If you call this method, the value of the XML attribute is ignored. This attribute is + * optional.</li> + * </ul> + * <p>In addition, you can specify one or more instances of + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code></a> and + * <a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a> tags as children + * of <code><cursor-adapter /></code>.</p> + * + * <a name="xml-cursor-adapter-select-tag" /> + * <h3><select /></h3> + * <p>The <code><select /></code> tag is used to select columns from the cursor + * when doing the query. This can be very useful when using transformations in the + * <code><bind /></code> elements. It can also be very useful if you are providing + * your own <a href="#xml-cursor-adapter-bind-data-types">binder</a> or + * <a href="#xml-cursor-adapter-bind-data-types">transformation</a> classes. + * <code><select /></code> elements are ignored if you supply the cursor yourself.</p> + * <p>The <code><select /></code> supports the following attributes:</p> + * <ul> + * <li><code>android:column</code>: Name of the column to select in the cursor during the + * query operation</li> + * </ul> + * <p><strong>Note:</strong> The column named <code>_id</code> is always implicitly + * selected.</p> + * + * <a name="xml-cursor-adapter-bind-tag" /> + * <h3><bind /></h3> + * <p>The <code><bind /></code> tag is used to bind a column from the cursor to + * a {@link android.view.View}. A column bound using this tag is automatically selected + * during the query and a matching + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag is therefore + * not required.</p> + * + * <p>Each binding is declared as a one to one matching but + * custom binder classes or special + * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> can + * allow you to bind several columns to a single view. In this case you must use the + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag to make + * sure any required column is part of the query.</p> + * + * <p>The <code><bind /></code> tag supports the following attributes:</p> + * <ul> + * <li><code>android:from</code>: The name of the column to bind from. + * This attribute is mandatory. Note that <code>@</code> which are not used to reference resources + * should be backslash protected as in <code>\@</code>.</li> + * <li><code>android:to</code>: The id of the view to bind to. This attribute is mandatory.</li> + * <li><code>android:as</code>: The <a href="#xml-cursor-adapter-bind-data-types">data type</a> + * of the binding. This attribute is mandatory.</li> + * </ul> + * + * <p>In addition, a <code><bind /></code> can contain zero or more instances of + * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> children + * tags.</p> + * + * <a name="xml-cursor-adapter-bind-data-types" /> + * <h4>Binding data types</h4> + * <p>For a binding to occur the data type of the bound column/view pair must be specified. + * The following data types are currently supported:</p> + * <ul> + * <li><code>string</code>: The content of the column is interpreted as a string and must be + * bound to a {@link android.widget.TextView}</li> + * <li><code>image</code>: The content of the column is interpreted as a blob describing an + * image and must be bound to an {@link android.widget.ImageView}</li> + * <li><code>image-uri</code>: The content of the column is interpreted as a URI to an image + * and must be bound to an {@link android.widget.ImageView}</li> + * <li><code>drawable</code>: The content of the column is interpreted as a resource id to a + * drawable and must be bound to an {@link android.widget.ImageView}</li> + * <li><code>tag</code>: The content of the column is interpreted as a string and will be set as + * the tag (using {@link View#setTag(Object)} of the associated View. This can be used to + * associate meta-data to your view, that can be used for instance by a listener.</li> + * <li>A fully qualified class name: The name of a class corresponding to an implementation of + * {@link android.widget.Adapters.CursorBinder}. Cursor binders can be used to provide + * bindings not supported by default. Custom binders cannot be used with + * {@link android.content.Context#isRestricted() restricted contexts}, for instance in an + * application widget</li> + * </ul> + * + * <a name="xml-cursor-adapter-bind-transformation" /> + * <h4>Binding transformations</h4> + * <p>When defining a data binding you can specify an optional transformation by using one + * of the following tags as a child of a <code><bind /></code> elements:</p> + * <ul> + * <li><code><map /></code>: Maps a constant string to a string or a resource. Use + * one instance of this tag per value you want to map</li> + * <li><code><transform /></code>: Transforms a column's value using an expression + * or an instance of {@link android.widget.Adapters.CursorTransformation}</li> + * </ul> + * <p>While several <code><map /></code> tags can be used at the same time, you cannot + * mix <code><map /></code> and <code><transform /></code> tags. If several + * <code><transform /></code> tags are specified, only the last one is retained.</p> + * + * <a name="xml-cursor-adapter-bind-transformation-map" /> + * <p><strong><map /></strong></p> + * <p>A map element simply specifies a value to match from and a value to match to. When + * a column's value equals the value to match from, it is replaced with the value to match + * to. The following attributes are supported:</p> + * <ul> + * <li><code>android:fromValue</code>: The value to match from. This attribute is mandatory</li> + * <li><code>android:toValue</code>: The value to match to. This value can be either a string + * or a resource identifier. This value is interpreted as a resource identifier when the + * data binding is of type <code>drawable</code>. This attribute is mandatory</li> + * </ul> + * + * <a name="xml-cursor-adapter-bind-transformation-transform" /> + * <p><strong><transform /></strong></p> + * <p>A simple transform that occurs either by calling a specified class or by performing + * simple text substitution. The following attributes are supported:</p> + * <ul> + * <li><code>android:withExpression</code>: The transformation expression. The expression is + * a string containing column names surrounded with curly braces { and }. During the + * transformation each column name is replaced by its value. All columns must have been + * selected in the query. An example of expression is <code>"First name: {first_name}, + * last name: {last_name}"</code>. This attribute is mandatory + * if <code>android:withClass</code> is not specified and ignored if <code>android:withClass</code> + * is specified</li> + * <li><code>android:withClass</code>: A fully qualified class name corresponding to an + * implementation of {@link android.widget.Adapters.CursorTransformation}. Custom + * transformations cannot be used with + * {@link android.content.Context#isRestricted() restricted contexts}, for instance in + * an app widget This attribute is mandatory if <code>android:withExpression</code> is + * not specified</li> + * </ul> + * + * <h3>Example</h3> + * <p>The following example defines a cursor adapter that queries all the contacts with + * a phone number using the contacts content provider. Each contact is displayed with + * its display name, its favorite status and its photo. To display photos, a custom data + * binder is declared:</p> + * + * <pre class="prettyprint"> + * <cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android" + * android:uri="content://com.android.contacts/contacts" + * android:selection="has_phone_number=1" + * android:layout="@layout/contact_item"> + * + * <bind android:from="display_name" android:to="@id/name" android:as="string" /> + * <bind android:from="starred" android:to="@id/star" android:as="drawable"> + * <map android:fromValue="0" android:toValue="@android:drawable/star_big_off" /> + * <map android:fromValue="1" android:toValue="@android:drawable/star_big_on" /> + * </bind> + * <bind android:from="_id" android:to="@id/name" + * android:as="com.google.android.test.adapters.ContactPhotoBinder" /> + * + * </cursor-adapter> + * </pre> + * + * <h3>Related APIs</h3> + * <ul> + * <li>{@link android.widget.Adapters#loadAdapter(android.content.Context, int, Object[])}</li> + * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}</li> + * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}</li> + * <li>{@link android.widget.Adapters.CursorBinder}</li> + * <li>{@link android.widget.Adapters.CursorTransformation}</li> + * <li>{@link android.widget.CursorAdapter}</li> + * </ul> + * + * @see android.widget.Adapter + * @see android.content.ContentProvider + * + * @attr ref android.R.styleable#CursorAdapter_layout + * @attr ref android.R.styleable#CursorAdapter_selection + * @attr ref android.R.styleable#CursorAdapter_sortOrder + * @attr ref android.R.styleable#CursorAdapter_uri + * @attr ref android.R.styleable#CursorAdapter_BindItem_as + * @attr ref android.R.styleable#CursorAdapter_BindItem_from + * @attr ref android.R.styleable#CursorAdapter_BindItem_to + * @attr ref android.R.styleable#CursorAdapter_MapItem_fromValue + * @attr ref android.R.styleable#CursorAdapter_MapItem_toValue + * @attr ref android.R.styleable#CursorAdapter_SelectItem_column + * @attr ref android.R.styleable#CursorAdapter_TransformItem_withClass + * @attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression + */ +@SuppressWarnings({"JavadocReference"}) +public class Adapters { + private static final String ADAPTER_CURSOR = "cursor-adapter"; + + /** + * <p>Interface used to bind a {@link android.database.Cursor} column to a View. This + * interface can be used to provide bindings for data types not supported by the + * standard implementation of {@link android.widget.Adapters}.</p> + * + * <p>A binder is provided with a cursor transformation which may or may not be used + * to transform the value retrieved from the cursor. The transformation is guaranteed + * to never be null so it's always safe to apply the transformation.</p> + * + * <p>The binder is associated with a Context but can be re-used with multiple cursors. + * As such, the implementation should make no assumption about the Cursor in use.</p> + * + * @see android.view.View + * @see android.database.Cursor + * @see android.widget.Adapters.CursorTransformation + */ + public static abstract class CursorBinder { + /** + * <p>The context associated with this binder.</p> + */ + protected final Context mContext; + + /** + * <p>The transformation associated with this binder. This transformation is never + * null and may or may not be applied to the Cursor data during the + * {@link #bind(android.view.View, android.database.Cursor, int)} operation.</p> + * + * @see #bind(android.view.View, android.database.Cursor, int) + */ + protected final CursorTransformation mTransformation; + + /** + * <p>Creates a new Cursor binder.</p> + * + * @param context The context associated with this binder. + * @param transformation The transformation associated with this binder. This + * transformation may or may not be applied by the binder and is guaranteed + * to not be null. + */ + public CursorBinder(Context context, CursorTransformation transformation) { + mContext = context; + mTransformation = transformation; + } + + /** + * <p>Binds the specified Cursor column to the supplied View. The binding operation + * can query other Cursor columns as needed. During the binding operation, values + * retrieved from the Cursor may or may not be transformed using this binder's + * cursor transformation.</p> + * + * @param view The view to bind data to. + * @param cursor The cursor to bind data from. + * @param columnIndex The column index in the cursor where the data to bind resides. + * + * @see #mTransformation + * + * @return True if the column was successfully bound to the View, false otherwise. + */ + public abstract boolean bind(View view, Cursor cursor, int columnIndex); + } + + /** + * <p>Interface used to transform data coming out of a {@link android.database.Cursor} + * before it is bound to a {@link android.view.View}.</p> + * + * <p>Transformations are used to transform text-based data (in the form of a String), + * or to transform data into a resource identifier. A default implementation is provided + * to generate resource identifiers.</p> + * + * @see android.database.Cursor + * @see android.widget.Adapters.CursorBinder + */ + public static abstract class CursorTransformation { + /** + * <p>The context associated with this transformation.</p> + */ + protected final Context mContext; + + /** + * <p>Creates a new Cursor transformation.</p> + * + * @param context The context associated with this transformation. + */ + public CursorTransformation(Context context) { + mContext = context; + } + + /** + * <p>Transforms the specified Cursor column into a String. The transformation + * can simply return the content of the column as a String (this is known + * as the identity transformation) or manipulate the content. For instance, + * a transformation can perform text substitutions or concatenate other + * columns with the specified column.</p> + * + * @param cursor The cursor that contains the data to transform. + * @param columnIndex The index of the column to transform. + * + * @return A String containing the transformed value of the column. + */ + public abstract String transform(Cursor cursor, int columnIndex); + + /** + * <p>Transforms the specified Cursor column into a resource identifier. + * The default implementation simply interprets the content of the column + * as an integer.</p> + * + * @param cursor The cursor that contains the data to transform. + * @param columnIndex The index of the column to transform. + * + * @return A resource identifier. + */ + public int transformToResource(Cursor cursor, int columnIndex) { + return cursor.getInt(columnIndex); + } + } + + /** + * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified + * XML resource. The content of the adapter is loaded from the content provider + * identified by the supplied URI.</p> + * + * <p><strong>Note:</strong> If the supplied {@link android.content.Context} is + * an {@link android.app.Activity}, the cursor returned by the content provider + * will be automatically managed. Otherwise, you are responsible for managing the + * cursor yourself.</p> + * + * <p>The format of the XML definition of the cursor adapter is documented at + * the top of this page.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param uri The URI of the content provider. + * @param parameters Optional parameters to pass to the CursorAdapter, used + * to substitute values in the selection expression. + * + * @return A {@link android.widget.CursorAdapter} + * + * @throws IllegalArgumentException If the XML resource does not contain + * a valid <cursor-adapter /> definition. + * + * @see android.content.ContentProvider + * @see android.widget.CursorAdapter + * @see #loadAdapter(android.content.Context, int, Object[]) + */ + public static CursorAdapter loadCursorAdapter(Context context, int id, String uri, + Object... parameters) { + + XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, + parameters); + + if (uri != null) { + adapter.setUri(uri); + } + adapter.load(); + + return adapter; + } + + /** + * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified + * XML resource. The content of the adapter is loaded from the specified cursor. + * You are responsible for managing the supplied cursor.</p> + * + * <p>The format of the XML definition of the cursor adapter is documented at + * the top of this page.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param cursor The cursor containing the data for the adapter. + * @param parameters Optional parameters to pass to the CursorAdapter, used + * to substitute values in the selection expression. + * + * @return A {@link android.widget.CursorAdapter} + * + * @throws IllegalArgumentException If the XML resource does not contain + * a valid <cursor-adapter /> definition. + * + * @see android.content.ContentProvider + * @see android.widget.CursorAdapter + * @see android.database.Cursor + * @see #loadAdapter(android.content.Context, int, Object[]) + */ + public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor, + Object... parameters) { + + XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, + parameters); + + if (cursor != null) { + adapter.changeCursor(cursor); + } + + return adapter; + } + + /** + * <p>Loads the adapter defined in the specified XML resource. The XML definition of + * the adapter must follow the format definition of one of the supported adapter + * types described at the top of this page.</p> + * + * <p><strong>Note:</strong> If the loaded adapter is a {@link android.widget.CursorAdapter} + * and the supplied {@link android.content.Context} is an {@link android.app.Activity}, + * the cursor returned by the content provider will be automatically managed. Otherwise, + * you are responsible for managing the cursor yourself.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param parameters Optional parameters to pass to the adapter. + * + * @return An adapter instance. + * + * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[]) + * @see #loadCursorAdapter(android.content.Context, int, String, Object[]) + */ + public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) { + final BaseAdapter adapter = loadAdapter(context, id, null, parameters); + if (adapter instanceof ManagedAdapter) { + ((ManagedAdapter) adapter).load(); + } + return adapter; + } + + /** + * Loads an adapter from the specified XML resource. The optional assertName can + * be used to exit early if the adapter defined in the XML resource is not of the + * expected type. + * + * @param context The context to associate with the adapter. + * @param id The resource id of the XML document defining the adapter. + * @param assertName The mandatory name of the adapter in the XML document. + * Ignored if null. + * @param parameters Optional parameters passed to the adapter. + * + * @return An instance of {@link android.widget.BaseAdapter}. + */ + private static BaseAdapter loadAdapter(Context context, int id, String assertName, + Object... parameters) { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getXml(id); + return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser), + id, parameters, assertName); + } catch (XmlPullParserException ex) { + Resources.NotFoundException rnf = new Resources.NotFoundException( + "Can't load adapter resource ID " + + context.getResources().getResourceEntryName(id)); + rnf.initCause(ex); + throw rnf; + } catch (IOException ex) { + Resources.NotFoundException rnf = new Resources.NotFoundException( + "Can't load adapter resource ID " + + context.getResources().getResourceEntryName(id)); + rnf.initCause(ex); + throw rnf; + } finally { + if (parser != null) parser.close(); + } + } + + /** + * Generates an adapter using the specified XML parser. This method is responsible + * for choosing the type of the adapter to create based on the content of the + * XML parser. + * + * This method will generate an {@link IllegalArgumentException} if + * <code>assertName</code> is not null and does not match the root tag of the XML + * document. + */ + private static BaseAdapter createAdapterFromXml(Context c, + XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters, + String assertName) throws XmlPullParserException, IOException { + + BaseAdapter adapter = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if (assertName != null && !assertName.equals(name)) { + throw new IllegalArgumentException("The adapter defined in " + + c.getResources().getResourceEntryName(id) + " must be a <" + name + " />"); + } + + if (ADAPTER_CURSOR.equals(name)) { + adapter = createCursorAdapter(c, parser, attrs, id, parameters); + } else { + throw new IllegalArgumentException("Unknown adapter name " + parser.getName() + + " in " + c.getResources().getResourceEntryName(id)); + } + } + + return adapter; + + } + + /** + * Creates an XmlCursorAdapter using an XmlCursorAdapterParser. + */ + private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser, + AttributeSet attrs, int id, Object[] parameters) + throws IOException, XmlPullParserException { + + return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters); + } + + /** + * Parser that can generate XmlCursorAdapter instances. This parser is responsible for + * handling all the attributes and child nodes for a <cursor-adapter />. + */ + private static class XmlCursorAdapterParser { + private static final String ADAPTER_CURSOR_BIND = "bind"; + private static final String ADAPTER_CURSOR_SELECT = "select"; + private static final String ADAPTER_CURSOR_AS_STRING = "string"; + private static final String ADAPTER_CURSOR_AS_IMAGE = "image"; + private static final String ADAPTER_CURSOR_AS_TAG = "tag"; + private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri"; + private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable"; + private static final String ADAPTER_CURSOR_MAP = "map"; + private static final String ADAPTER_CURSOR_TRANSFORM = "transform"; + + private final Context mContext; + private final XmlPullParser mParser; + private final AttributeSet mAttrs; + private final int mId; + + private final HashMap<String, CursorBinder> mBinders; + private final ArrayList<String> mFrom; + private final ArrayList<Integer> mTo; + private final CursorTransformation mIdentity; + private final Resources mResources; + + public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) { + mContext = c; + mParser = parser; + mAttrs = attrs; + mId = id; + + mResources = mContext.getResources(); + mBinders = new HashMap<String, CursorBinder>(); + mFrom = new ArrayList<String>(); + mTo = new ArrayList<Integer>(); + mIdentity = new IdentityTransformation(mContext); + } + + public XmlCursorAdapter parse(Object[] parameters) + throws IOException, XmlPullParserException { + + Resources resources = mResources; + TypedArray a = resources.obtainAttributes(mAttrs, android.R.styleable.CursorAdapter); + + String uri = a.getString(android.R.styleable.CursorAdapter_uri); + String selection = a.getString(android.R.styleable.CursorAdapter_selection); + String sortOrder = a.getString(android.R.styleable.CursorAdapter_sortOrder); + int layout = a.getResourceId(android.R.styleable.CursorAdapter_layout, 0); + if (layout == 0) { + throw new IllegalArgumentException("The layout specified in " + + resources.getResourceEntryName(mId) + " does not exist"); + } + + a.recycle(); + + XmlPullParser parser = mParser; + int type; + int depth = parser.getDepth(); + + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (ADAPTER_CURSOR_BIND.equals(name)) { + parseBindTag(); + } else if (ADAPTER_CURSOR_SELECT.equals(name)) { + parseSelectTag(); + } else { + throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + + resources.getResourceEntryName(mId)); + } + } + + String[] fromArray = mFrom.toArray(new String[mFrom.size()]); + int[] toArray = new int[mTo.size()]; + for (int i = 0; i < toArray.length; i++) { + toArray[i] = mTo.get(i); + } + + String[] selectionArgs = null; + if (parameters != null) { + selectionArgs = new String[parameters.length]; + for (int i = 0; i < selectionArgs.length; i++) { + selectionArgs[i] = (String) parameters[i]; + } + } + + return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection, + selectionArgs, sortOrder, mBinders); + } + + private void parseSelectTag() { + TypedArray a = mResources.obtainAttributes(mAttrs, + android.R.styleable.CursorAdapter_SelectItem); + + String fromName = a.getString(android.R.styleable.CursorAdapter_SelectItem_column); + if (fromName == null) { + throw new IllegalArgumentException("A select item in " + + mResources.getResourceEntryName(mId) + + " does not have a 'column' attribute"); + } + + a.recycle(); + + mFrom.add(fromName); + mTo.add(View.NO_ID); + } + + private void parseBindTag() throws IOException, XmlPullParserException { + Resources resources = mResources; + TypedArray a = resources.obtainAttributes(mAttrs, + android.R.styleable.CursorAdapter_BindItem); + + String fromName = a.getString(android.R.styleable.CursorAdapter_BindItem_from); + if (fromName == null) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have a 'from' attribute"); + } + + int toName = a.getResourceId(android.R.styleable.CursorAdapter_BindItem_to, 0); + if (toName == 0) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have a 'to' attribute"); + } + + String asType = a.getString(android.R.styleable.CursorAdapter_BindItem_as); + if (asType == null) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have an 'as' attribute"); + } + + mFrom.add(fromName); + mTo.add(toName); + mBinders.put(fromName, findBinder(asType)); + + a.recycle(); + } + + private CursorBinder findBinder(String type) throws IOException, XmlPullParserException { + final XmlPullParser parser = mParser; + final Context context = mContext; + CursorTransformation transformation = mIdentity; + + int tagType; + int depth = parser.getDepth(); + + final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type); + + while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && tagType != XmlPullParser.END_DOCUMENT) { + + if (tagType != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (ADAPTER_CURSOR_TRANSFORM.equals(name)) { + transformation = findTransformation(); + } else if (ADAPTER_CURSOR_MAP.equals(name)) { + if (!(transformation instanceof MapTransformation)) { + transformation = new MapTransformation(context); + } + findMap(((MapTransformation) transformation), isDrawable); + } else { + throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + + context.getResources().getResourceEntryName(mId)); + } + } + + if (ADAPTER_CURSOR_AS_STRING.equals(type)) { + return new StringBinder(context, transformation); + } else if (ADAPTER_CURSOR_AS_TAG.equals(type)) { + return new TagBinder(context, transformation); + } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) { + return new ImageBinder(context, transformation); + } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) { + return new ImageUriBinder(context, transformation); + } else if (isDrawable) { + return new DrawableBinder(context, transformation); + } else { + return createBinder(type, transformation); + } + } + + private CursorBinder createBinder(String type, CursorTransformation transformation) { + if (mContext.isRestricted()) return null; + + try { + final Class<?> klass = Class.forName(type, true, mContext.getClassLoader()); + if (CursorBinder.class.isAssignableFrom(klass)) { + final Constructor<?> c = klass.getDeclaredConstructor( + Context.class, CursorTransformation.class); + return (CursorBinder) c.newInstance(mContext, transformation); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } + + return null; + } + + private void findMap(MapTransformation transformation, boolean drawable) { + Resources resources = mResources; + + TypedArray a = resources.obtainAttributes(mAttrs, + android.R.styleable.CursorAdapter_MapItem); + + String from = a.getString(android.R.styleable.CursorAdapter_MapItem_fromValue); + if (from == null) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + + " does not have a 'fromValue' attribute"); + } + + if (!drawable) { + String to = a.getString(android.R.styleable.CursorAdapter_MapItem_toValue); + if (to == null) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + + " does not have a 'toValue' attribute"); + } + transformation.addStringMapping(from, to); + } else { + int to = a.getResourceId(android.R.styleable.CursorAdapter_MapItem_toValue, 0); + if (to == 0) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + + " does not have a 'toValue' attribute"); + } + transformation.addResourceMapping(from, to); + } + + a.recycle(); + } + + private CursorTransformation findTransformation() { + Resources resources = mResources; + CursorTransformation transformation = null; + TypedArray a = resources.obtainAttributes(mAttrs, + android.R.styleable.CursorAdapter_TransformItem); + + String className = a.getString(android.R.styleable.CursorAdapter_TransformItem_withClass); + if (className == null) { + String expression = a.getString( + android.R.styleable.CursorAdapter_TransformItem_withExpression); + transformation = createExpressionTransformation(expression); + } else if (!mContext.isRestricted()) { + try { + final Class<?> klas = Class.forName(className, true, mContext.getClassLoader()); + if (CursorTransformation.class.isAssignableFrom(klas)) { + final Constructor<?> c = klas.getDeclaredConstructor(Context.class); + transformation = (CursorTransformation) c.newInstance(mContext); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } + } + + a.recycle(); + + if (transformation == null) { + throw new IllegalArgumentException("A transform item in " + + resources.getResourceEntryName(mId) + " must have a 'withClass' or " + + "'withExpression' attribute"); + } + + return transformation; + } + + private CursorTransformation createExpressionTransformation(String expression) { + return new ExpressionTransformation(mContext, expression); + } + } + + /** + * Interface used by adapters that require to be loaded after creation. + */ + private static interface ManagedAdapter { + /** + * Loads the content of the adapter, asynchronously. + */ + void load(); + } + + /** + * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper + * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders. + */ + private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter { + private String mUri; + private final String mSelection; + private final String[] mSelectionArgs; + private final String mSortOrder; + private final String[] mColumns; + private final CursorBinder[] mBinders; + private AsyncTask<Void,Void,Cursor> mLoadTask; + + XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to, + String selection, String[] selectionArgs, String sortOrder, + HashMap<String, CursorBinder> binders) { + + super(context, layout, null, from, to); + mContext = context; + mUri = uri; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + mColumns = new String[from.length + 1]; + // This is mandatory in CursorAdapter + mColumns[0] = "_id"; + System.arraycopy(from, 0, mColumns, 1, from.length); + + CursorBinder basic = new StringBinder(context, new IdentityTransformation(context)); + final int count = from.length; + mBinders = new CursorBinder[count]; + + for (int i = 0; i < count; i++) { + CursorBinder binder = binders.get(from[i]); + if (binder == null) binder = basic; + mBinders[i] = binder; + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + final CursorBinder[] binders = mBinders; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + binders[i].bind(v, cursor, from[i]); + } + } + } + + public void load() { + if (mUri != null) { + mLoadTask = new QueryTask().execute(); + } + } + + void setUri(String uri) { + mUri = uri; + } + + @Override + public void changeCursor(Cursor c) { + if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) { + mLoadTask.cancel(true); + mLoadTask = null; + } + super.changeCursor(c); + } + + class QueryTask extends AsyncTask<Void, Void, Cursor> { + @Override + protected Cursor doInBackground(Void... params) { + if (mContext instanceof Activity) { + return ((Activity) mContext).managedQuery( + Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); + } else { + return mContext.getContentResolver().query( + Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); + } + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (!isCancelled()) { + XmlCursorAdapter.super.changeCursor(cursor); + } + } + } + } + + /** + * Identity transformation, returns the content of the specified column as a String, + * without performing any manipulation. This is used when no transformation is specified. + */ + private static class IdentityTransformation extends CursorTransformation { + public IdentityTransformation(Context context) { + super(context); + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + return cursor.getString(columnIndex); + } + } + + /** + * An expression transformation is a simple template based replacement utility. + * In an expression, each segment of the form <code>{([^}]+)}</code> is replaced + * with the value of the column of name $1. + */ + private static class ExpressionTransformation extends CursorTransformation { + private final ExpressionNode mFirstNode = new ConstantExpressionNode(""); + private final StringBuilder mBuilder = new StringBuilder(); + + public ExpressionTransformation(Context context, String expression) { + super(context); + + parse(expression); + } + + private void parse(String expression) { + ExpressionNode node = mFirstNode; + int segmentStart; + int count = expression.length(); + + for (int i = 0; i < count; i++) { + char c = expression.charAt(i); + // Start a column name segment + segmentStart = i; + if (c == '{') { + while (i < count && (c = expression.charAt(i)) != '}') { + i++; + } + // We've reached the end, but the expression didn't close + if (c != '}') { + throw new IllegalStateException("The transform expression contains a " + + "non-closed column name: " + + expression.substring(segmentStart + 1, i)); + } + node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i)); + } else { + while (i < count && (c = expression.charAt(i)) != '{') { + i++; + } + node.next = new ConstantExpressionNode(expression.substring(segmentStart, i)); + // Rewind if we've reached a column expression + if (c == '{') i--; + } + node = node.next; + } + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + final StringBuilder builder = mBuilder; + builder.delete(0, builder.length()); + + ExpressionNode node = mFirstNode; + // Skip the first node + while ((node = node.next) != null) { + builder.append(node.asString(cursor)); + } + + return builder.toString(); + } + + static abstract class ExpressionNode { + public ExpressionNode next; + + public abstract String asString(Cursor cursor); + } + + static class ConstantExpressionNode extends ExpressionNode { + private final String mConstant; + + ConstantExpressionNode(String constant) { + mConstant = constant; + } + + @Override + public String asString(Cursor cursor) { + return mConstant; + } + } + + static class ColumnExpressionNode extends ExpressionNode { + private final String mColumnName; + private Cursor mSignature; + private int mColumnIndex = -1; + + ColumnExpressionNode(String columnName) { + mColumnName = columnName; + } + + @Override + public String asString(Cursor cursor) { + if (cursor != mSignature || mColumnIndex == -1) { + mColumnIndex = cursor.getColumnIndex(mColumnName); + mSignature = cursor; + } + + return cursor.getString(mColumnIndex); + } + } + } + + /** + * A map transformation offers a simple mapping between specified String values + * to Strings or integers. + */ + private static class MapTransformation extends CursorTransformation { + private final HashMap<String, String> mStringMappings; + private final HashMap<String, Integer> mResourceMappings; + + public MapTransformation(Context context) { + super(context); + mStringMappings = new HashMap<String, String>(); + mResourceMappings = new HashMap<String, Integer>(); + } + + void addStringMapping(String from, String to) { + mStringMappings.put(from, to); + } + + void addResourceMapping(String from, int to) { + mResourceMappings.put(from, to); + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + final String value = cursor.getString(columnIndex); + final String transformed = mStringMappings.get(value); + return transformed == null ? value : transformed; + } + + @Override + public int transformToResource(Cursor cursor, int columnIndex) { + final String value = cursor.getString(columnIndex); + final Integer transformed = mResourceMappings.get(value); + try { + return transformed == null ? Integer.parseInt(value) : transformed; + } catch (NumberFormatException e) { + return 0; + } + } + } + + /** + * Binds a String to a TextView. + */ + private static class StringBinder extends CursorBinder { + public StringBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + if (view instanceof TextView) { + final String text = mTransformation.transform(cursor, columnIndex); + ((TextView) view).setText(text); + return true; + } + return false; + } + } + + /** + * Binds an image blob to an ImageView. + */ + private static class ImageBinder extends CursorBinder { + public ImageBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + if (view instanceof ImageView) { + final byte[] data = cursor.getBlob(columnIndex); + ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0, + data.length)); + return true; + } + return false; + } + } + + private static class TagBinder extends CursorBinder { + public TagBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + final String text = mTransformation.transform(cursor, columnIndex); + view.setTag(text); + return true; + } + } + + /** + * Binds an image URI to an ImageView. + */ + private static class ImageUriBinder extends CursorBinder { + public ImageUriBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + if (view instanceof ImageView) { + ((ImageView) view).setImageURI(Uri.parse( + mTransformation.transform(cursor, columnIndex))); + return true; + } + return false; + } + } + + /** + * Binds a drawable resource identifier to an ImageView. + */ + private static class DrawableBinder extends CursorBinder { + public DrawableBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + if (view instanceof ImageView) { + final int resource = mTransformation.transformToResource(cursor, columnIndex); + if (resource == 0) return false; + + ((ImageView) view).setImageResource(resource); + return true; + } + return false; + } + } +} diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index e15a520..34aef99 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -29,13 +29,12 @@ import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import com.android.internal.R; @@ -90,45 +89,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe static final boolean DEBUG = false; static final String TAG = "AutoCompleteTextView"; - private static final int HINT_VIEW_ID = 0x17; - - /** - * This value controls the length of time that the user - * must leave a pointer down without scrolling to expand - * the autocomplete dropdown list to cover the IME. - */ - private static final int EXPAND_LIST_TIMEOUT = 250; - private CharSequence mHintText; + private TextView mHintView; private int mHintResource; private ListAdapter mAdapter; private Filter mFilter; private int mThreshold; - private PopupWindow mPopup; - private DropDownListView mDropDownList; - private int mDropDownVerticalOffset; - private int mDropDownHorizontalOffset; + private ListPopupWindow mPopup; private int mDropDownAnchorId; - private View mDropDownAnchorView; // view is retrieved lazily from id once needed - private int mDropDownWidth; - private int mDropDownHeight; - private final Rect mTempRect = new Rect(); - - private Drawable mDropDownListHighlight; private AdapterView.OnItemClickListener mItemClickListener; private AdapterView.OnItemSelectedListener mItemSelectedListener; - private final DropDownItemClickListener mDropDownItemClickListener = - new DropDownItemClickListener(); - - private boolean mDropDownAlwaysVisible = false; - private boolean mDropDownDismissedOnCompletion = true; - - private boolean mForceIgnoreOutsideTouch = false; private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; private boolean mOpenBefore; @@ -137,10 +112,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private boolean mBlockCompletion; - private ListSelectorHider mHideSelector; - private Runnable mShowDropDownRunnable; - private Runnable mResizePopupRunnable = new ResizePopupRunnable(); - private PassThroughClickListener mPassThroughClickListener; private PopupDataSetObserver mObserver; @@ -155,9 +126,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mPopup = new PopupWindow(context, attrs, + mPopup = new ListPopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); TypedArray a = context.obtainStyledAttributes( @@ -166,14 +138,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mThreshold = a.getInt( R.styleable.AutoCompleteTextView_completionThreshold, 2); - mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint); - - mDropDownListHighlight = a.getDrawable( - R.styleable.AutoCompleteTextView_dropDownSelector); - mDropDownVerticalOffset = (int) - a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); - mDropDownHorizontalOffset = (int) - a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); + mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector)); + mPopup.setVerticalOffset((int) + a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f)); + mPopup.setHorizontalOffset((int) + a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f)); // Get the anchor's id now, but the view won't be ready, so wait to actually get the // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. @@ -184,13 +153,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe // For dropdown width, the developer can specify a specific width, or MATCH_PARENT // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). - mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, - ViewGroup.LayoutParams.WRAP_CONTENT); - mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight, - ViewGroup.LayoutParams.WRAP_CONTENT); + mPopup.setWidth(a.getLayoutDimension( + R.styleable.AutoCompleteTextView_dropDownWidth, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mPopup.setHeight(a.getLayoutDimension( + R.styleable.AutoCompleteTextView_dropDownHeight, + ViewGroup.LayoutParams.WRAP_CONTENT)); mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); + + mPopup.setOnItemClickListener(new DropDownItemClickListener()); + setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint)); // Always turn on the auto complete input type flag, since it // makes no sense to use this widget without it. @@ -238,6 +212,20 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ public void setCompletionHint(CharSequence hint) { mHintText = hint; + if (hint != null) { + if (mHintView == null) { + final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate( + mHintResource, null).findViewById(com.android.internal.R.id.text1); + hintView.setText(mHintText); + mHintView = hintView; + mPopup.setPromptView(hintView); + } else { + mHintView.setText(hint); + } + } else { + mPopup.setPromptView(null); + mHintView = null; + } } /** @@ -250,7 +238,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth */ public int getDropDownWidth() { - return mDropDownWidth; + return mPopup.getWidth(); } /** @@ -263,7 +251,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth */ public void setDropDownWidth(int width) { - mDropDownWidth = width; + mPopup.setWidth(width); } /** @@ -277,7 +265,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight */ public int getDropDownHeight() { - return mDropDownHeight; + return mPopup.getHeight(); } /** @@ -291,7 +279,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight */ public void setDropDownHeight(int height) { - mDropDownHeight = height; + mPopup.setHeight(height); } /** @@ -316,7 +304,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ public void setDropDownAnchor(int id) { mDropDownAnchorId = id; - mDropDownAnchorView = null; + mPopup.setAnchorView(null); } /** @@ -358,7 +346,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param offset the vertical offset */ public void setDropDownVerticalOffset(int offset) { - mDropDownVerticalOffset = offset; + mPopup.setVerticalOffset(offset); } /** @@ -367,7 +355,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @return the vertical offset */ public int getDropDownVerticalOffset() { - return mDropDownVerticalOffset; + return mPopup.getVerticalOffset(); } /** @@ -376,7 +364,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param offset the horizontal offset */ public void setDropDownHorizontalOffset(int offset) { - mDropDownHorizontalOffset = offset; + mPopup.setHorizontalOffset(offset); } /** @@ -385,7 +373,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @return the horizontal offset */ public int getDropDownHorizontalOffset() { - return mDropDownHorizontalOffset; + return mPopup.getHorizontalOffset(); } /** @@ -422,7 +410,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide Pending API council approval */ public boolean isDropDownAlwaysVisible() { - return mDropDownAlwaysVisible; + return mPopup.isDropDownAlwaysVisible(); } /** @@ -439,7 +427,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide Pending API council approval */ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { - mDropDownAlwaysVisible = dropDownAlwaysVisible; + mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible); } /** @@ -606,15 +594,13 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mFilter = null; } - if (mDropDownList != null) { - mDropDownList.setAdapter(mAdapter); - } + mPopup.setAdapter(mAdapter); } @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing() - && !mDropDownAlwaysVisible) { + && !mPopup.isDropDownAlwaysVisible()) { // special case for the back key, we do not even try to send it // to the drop down list but instead, consume it immediately if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { @@ -633,18 +619,16 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) { - boolean consumed = mDropDownList.onKeyUp(keyCode, event); - if (consumed) { - switch (keyCode) { - // if the list accepts the key events and the key event - // was a click, the text view gets the selected item - // from the drop down as its content - case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_DPAD_CENTER: - performCompletion(); - return true; - } + boolean consumed = mPopup.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + performCompletion(); + return true; } } return super.onKeyUp(keyCode, event); @@ -652,87 +636,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - // when the drop down is shown, we drive it directly - if (isPopupShowing()) { - // the key events are forwarded to the list in the drop down view - // note that ListView handles space but we don't want that to happen - // also if selection is not currently in the drop down, then don't - // let center or enter presses go there since that would cause it - // to select one of its items - if (keyCode != KeyEvent.KEYCODE_SPACE - && (mDropDownList.getSelectedItemPosition() >= 0 - || (keyCode != KeyEvent.KEYCODE_ENTER - && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { - int curIndex = mDropDownList.getSelectedItemPosition(); - boolean consumed; - - final boolean below = !mPopup.isAboveAnchor(); - - final ListAdapter adapter = mAdapter; - - boolean allEnabled; - int firstItem = Integer.MAX_VALUE; - int lastItem = Integer.MIN_VALUE; - - if (adapter != null) { - allEnabled = adapter.areAllItemsEnabled(); - firstItem = allEnabled ? 0 : - mDropDownList.lookForSelectablePosition(0, true); - lastItem = allEnabled ? adapter.getCount() - 1 : - mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); - } - - if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || - (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { - // When the selection is at the top, we block the key - // event to prevent focus from moving. - clearListSelection(); - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - showDropDown(); - return true; - } else { - // WARNING: Please read the comment where mListSelectionHidden - // is declared - mDropDownList.mListSelectionHidden = false; - } - - consumed = mDropDownList.onKeyDown(keyCode, event); - if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); - - if (consumed) { - // If it handled the key event, then the user is - // navigating in the list, so we should put it in front. - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - // Here's a little trick we need to do to make sure that - // the list view is actually showing its focus indicator, - // by ensuring it has focus and getting its window out - // of touch mode. - mDropDownList.requestFocusFromTouch(); - showDropDown(); - - switch (keyCode) { - // avoid passing the focus from the text view to the - // next component - case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - return true; - } - } else { - if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - // when the selection is at the bottom, we block the - // event to avoid going to the next focusable widget - if (curIndex == lastItem) { - return true; - } - } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && - curIndex == firstItem) { - return true; - } - } - } - } else { + if (mPopup.onKeyDown(keyCode, event)) { + return true; + } + + if (!isPopupShowing()) { switch(keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: performValidation(); @@ -743,7 +651,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe boolean handled = super.onKeyDown(keyCode, event); mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; - if (handled && isPopupShowing() && mDropDownList != null) { + if (handled && isPopupShowing()) { clearListSelection(); } @@ -804,11 +712,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (enoughToFilter()) { if (mFilter != null) { performFiltering(getText(), mLastKeyCode); + buildImeCompletions(); } } else { // drop down is automatically dismissed when enough characters // are deleted from the text view - if (!mDropDownAlwaysVisible) dismissDropDown(); + if (!mPopup.isDropDownAlwaysVisible()) dismissDropDown(); if (mFilter != null) { mFilter.filter(null); } @@ -841,13 +750,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * it back. */ public void clearListSelection() { - final DropDownListView list = mDropDownList; - if (list != null) { - // WARNING: Please read the comment where mListSelectionHidden is declared - list.mListSelectionHidden = true; - list.hideSelector(); - list.requestLayout(); - } + mPopup.clearListSelection(); } /** @@ -856,11 +759,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param position The position to move the selector to. */ public void setListSelection(int position) { - if (mPopup.isShowing() && (mDropDownList != null)) { - mDropDownList.mListSelectionHidden = false; - mDropDownList.setSelection(position); - // ListView.setSelection() will call requestLayout() - } + mPopup.setSelection(position); } /** @@ -874,10 +773,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @see ListView#getSelectedItemPosition() */ public int getListSelection() { - if (mPopup.isShowing() && (mDropDownList != null)) { - return mDropDownList.getSelectedItemPosition(); - } - return ListView.INVALID_POSITION; + return mPopup.getSelectedItemPosition(); } /** @@ -911,13 +807,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe replaceText(completion.getText()); mBlockCompletion = false; - if (mItemClickListener != null) { - final DropDownListView list = mDropDownList; - // Note that we don't have a View here, so we will need to - // supply null. Hopefully no existing apps crash... - mItemClickListener.onItemClick(list, null, completion.getPosition(), - completion.getId()); - } + mPopup.performItemClick(completion.getPosition()); } } @@ -925,7 +815,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (isPopupShowing()) { Object selectedItem; if (position < 0) { - selectedItem = mDropDownList.getSelectedItem(); + selectedItem = mPopup.getSelectedItem(); } else { selectedItem = mAdapter.getItem(position); } @@ -939,18 +829,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mBlockCompletion = false; if (mItemClickListener != null) { - final DropDownListView list = mDropDownList; + final ListPopupWindow list = mPopup; if (selectedView == null || position < 0) { selectedView = list.getSelectedView(); position = list.getSelectedItemPosition(); id = list.getSelectedItemId(); } - mItemClickListener.onItemClick(list, selectedView, position, id); + mItemClickListener.onItemClick(list.getListView(), selectedView, position, id); } } - if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) { + if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1000,7 +890,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** {@inheritDoc} */ public void onFilterComplete(int count) { updateDropDownForFilter(count); - } private void updateDropDownForFilter(int count) { @@ -1014,11 +903,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * to filter. */ - if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) { + final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); + if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter()) { if (hasFocus() && hasWindowFocus()) { showDropDown(); } - } else if (!mDropDownAlwaysVisible) { + } else if (!dropDownAlwaysVisible) { dismissDropDown(); } } @@ -1026,7 +916,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus && !mDropDownAlwaysVisible) { + if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1036,7 +926,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe super.onDisplayHint(hint); switch (hint) { case INVISIBLE: - if (!mDropDownAlwaysVisible) { + if (!mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } break; @@ -1050,7 +940,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (!focused) { performValidation(); } - if (!focused && !mDropDownAlwaysVisible) { + if (!focused && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1075,8 +965,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe imm.displayCompletions(this, null); } mPopup.dismiss(); - mPopup.setContentView(null); - mDropDownList = null; } @Override @@ -1089,18 +977,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return result; } - - /** - * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of - * the id is NO_ID or we can't find a view for the given id, we return this TextView as - * the default anchoring point.</p> - */ - private View getDropDownAnchorView() { - if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { - mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); - } - return mDropDownAnchorView == null ? this : mDropDownAnchorView; - } /** * Issues a runnable to show the dropdown as soon as possible. @@ -1108,7 +984,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide internal used only by SearchDialog */ public void showDropDownAfterLayout() { - post(mShowDropDownRunnable); + mPopup.postShow(); } /** @@ -1119,7 +995,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ public void ensureImeVisible(boolean visible) { mPopup.setInputMethodMode(visible - ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED); + ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED); showDropDown(); } @@ -1127,89 +1003,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide internal used only here and SearchDialog */ public boolean isInputMethodNotNeeded() { - return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED; } /** * <p>Displays the drop down on screen.</p> */ public void showDropDown() { - int height = buildDropDown(); - - int widthSpec = 0; - int heightSpec = 0; - - boolean noInputMethod = isInputMethodNotNeeded(); - - if (mPopup.isShowing()) { - if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { - // The call to PopupWindow's update method below can accept -1 for any - // value you do not want to update. - widthSpec = -1; - } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { - widthSpec = getDropDownAnchorView().getWidth(); - } else { - widthSpec = mDropDownWidth; - } - - if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - // The call to PopupWindow's update method below can accept -1 for any - // value you do not want to update. - heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; - if (noInputMethod) { - mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? - ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); - } else { - mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? - ViewGroup.LayoutParams.MATCH_PARENT : 0, - ViewGroup.LayoutParams.MATCH_PARENT); - } - } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { - heightSpec = height; + if (mPopup.getAnchorView() == null) { + if (mDropDownAnchorId != View.NO_ID) { + mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId)); } else { - heightSpec = mDropDownHeight; + mPopup.setAnchorView(this); } - - mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); - - mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, - mDropDownVerticalOffset, widthSpec, heightSpec); - } else { - if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { - widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; - } else { - if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { - mPopup.setWidth(getDropDownAnchorView().getWidth()); - } else { - mPopup.setWidth(mDropDownWidth); - } - } - - if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; - } else { - if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { - mPopup.setHeight(height); - } else { - mPopup.setHeight(mDropDownHeight); - } - } - - mPopup.setWindowLayoutMode(widthSpec, heightSpec); - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - - // use outside touchable to dismiss drop down when touching outside of it, so - // only set this if the dropdown is not always visible - mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); - mPopup.setTouchInterceptor(new PopupTouchInterceptor()); - mPopup.showAsDropDown(getDropDownAnchorView(), - mDropDownHorizontalOffset, mDropDownVerticalOffset); - mDropDownList.setSelection(ListView.INVALID_POSITION); - clearListSelection(); - post(mHideSelector); } + mPopup.show(); } /** @@ -1220,19 +1028,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide used only by SearchDialog */ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { - mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch); } - - /** - * <p>Builds the popup window's content and returns the height the popup - * should have. Returns -1 when the content already exists.</p> - * - * @return the content's height or -1 if content already exists - */ - private int buildDropDown() { - ViewGroup dropDownView; - int otherHeights = 0; - + + private void buildImeCompletions() { final ListAdapter adapter = mAdapter; if (adapter != null) { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -1260,135 +1059,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe imm.displayCompletions(this, completions); } } - - if (mDropDownList == null) { - Context context = getContext(); - - mHideSelector = new ListSelectorHider(); - - /** - * This Runnable exists for the sole purpose of checking if the view layout has got - * completed and if so call showDropDown to display the drop down. This is used to show - * the drop down as soon as possible after user opens up the search dialog, without - * waiting for the normal UI pipeline to do it's job which is slower than this method. - */ - mShowDropDownRunnable = new Runnable() { - public void run() { - // View layout should be all done before displaying the drop down. - View view = getDropDownAnchorView(); - if (view != null && view.getWindowToken() != null) { - showDropDown(); - } - } - }; - - mDropDownList = new DropDownListView(context); - mDropDownList.setSelector(mDropDownListHighlight); - mDropDownList.setAdapter(adapter); - mDropDownList.setVerticalFadingEdgeEnabled(true); - mDropDownList.setOnItemClickListener(mDropDownItemClickListener); - mDropDownList.setFocusable(true); - mDropDownList.setFocusableInTouchMode(true); - mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - public void onItemSelected(AdapterView<?> parent, View view, - int position, long id) { - - if (position != -1) { - DropDownListView dropDownList = mDropDownList; - - if (dropDownList != null) { - dropDownList.mListSelectionHidden = false; - } - } - } - - public void onNothingSelected(AdapterView<?> parent) { - } - }); - mDropDownList.setOnScrollListener(new PopupScrollListener()); - - if (mItemSelectedListener != null) { - mDropDownList.setOnItemSelectedListener(mItemSelectedListener); - } - - dropDownView = mDropDownList; - - View hintView = getHintView(context); - if (hintView != null) { - // if an hint has been specified, we accomodate more space for it and - // add a text view in the drop down menu, at the bottom of the list - LinearLayout hintContainer = new LinearLayout(context); - hintContainer.setOrientation(LinearLayout.VERTICAL); - - LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f - ); - hintContainer.addView(dropDownView, hintParams); - hintContainer.addView(hintView); - - // measure the hint's height to find how much more vertical space - // we need to add to the drop down's height - int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.UNSPECIFIED; - hintView.measure(widthSpec, heightSpec); - - hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); - otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin - + hintParams.bottomMargin; - - dropDownView = hintContainer; - } - - mPopup.setContentView(dropDownView); - } else { - dropDownView = (ViewGroup) mPopup.getContentView(); - final View view = dropDownView.findViewById(HINT_VIEW_ID); - if (view != null) { - LinearLayout.LayoutParams hintParams = - (LinearLayout.LayoutParams) view.getLayoutParams(); - otherHeights = view.getMeasuredHeight() + hintParams.topMargin - + hintParams.bottomMargin; - } - } - - // Max height available on the screen for a popup. - boolean ignoreBottomDecorations = - mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; - final int maxHeight = mPopup.getMaxAvailableHeight( - getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); - - // getMaxAvailableHeight() subtracts the padding, so we put it back, - // to get the available height for the whole window - int padding = 0; - Drawable background = mPopup.getBackground(); - if (background != null) { - background.getPadding(mTempRect); - padding = mTempRect.top + mTempRect.bottom; - } - - if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - return maxHeight + padding; - } - - final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, - 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); - // add padding only if the list has items in it, that way we don't show - // the popup if it is not needed - if (listContent > 0) otherHeights += padding; - - return listContent + otherHeights; - } - - private View getHintView(Context context) { - if (mHintText != null && mHintText.length() > 0) { - final TextView hintView = (TextView) LayoutInflater.from(context).inflate( - mHintResource, null).findViewById(com.android.internal.R.id.text1); - hintView.setText(mHintText); - hintView.setId(HINT_VIEW_ID); - return hintView; - } else { - return null; - } } /** @@ -1440,47 +1110,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return mFilter; } - private class ListSelectorHider implements Runnable { - public void run() { - clearListSelection(); - } - } - - private class ResizePopupRunnable implements Runnable { - public void run() { - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - showDropDown(); - } - } - - private class PopupTouchInterceptor implements OnTouchListener { - public boolean onTouch(View v, MotionEvent event) { - final int action = event.getAction(); - if (action == MotionEvent.ACTION_DOWN && - mPopup != null && mPopup.isShowing()) { - postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); - } else if (action == MotionEvent.ACTION_UP) { - removeCallbacks(mResizePopupRunnable); - } - return false; - } - } - - private class PopupScrollListener implements ListView.OnScrollListener { - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - - } - - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (scrollState == SCROLL_STATE_TOUCH_SCROLL && - !isInputMethodNotNeeded() && mPopup.getContentView() != null) { - removeCallbacks(mResizePopupRunnable); - mResizePopupRunnable.run(); - } - } - } - private class DropDownItemClickListener implements AdapterView.OnItemClickListener { public void onItemClick(AdapterView parent, View v, int position, long id) { performCompletion(v, position, id); @@ -1488,123 +1117,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } /** - * <p>Wrapper class for a ListView. This wrapper hijacks the focus to - * make sure the list uses the appropriate drawables and states when - * displayed on screen within a drop down. The focus is never actually - * passed to the drop down; the list only looks focused.</p> - */ - private static class DropDownListView extends ListView { - /* - * WARNING: This is a workaround for a touch mode issue. - * - * Touch mode is propagated lazily to windows. This causes problems in - * the following scenario: - * - Type something in the AutoCompleteTextView and get some results - * - Move down with the d-pad to select an item in the list - * - Move up with the d-pad until the selection disappears - * - Type more text in the AutoCompleteTextView *using the soft keyboard* - * and get new results; you are now in touch mode - * - The selection comes back on the first item in the list, even though - * the list is supposed to be in touch mode - * - * Using the soft keyboard triggers the touch mode change but that change - * is propagated to our window only after the first list layout, therefore - * after the list attempts to resurrect the selection. - * - * The trick to work around this issue is to pretend the list is in touch - * mode when we know that the selection should not appear, that is when - * we know the user moved the selection away from the list. - * - * This boolean is set to true whenever we explicitely hide the list's - * selection and reset to false whenver we know the user moved the - * selection back to the list. - * - * When this boolean is true, isInTouchMode() returns true, otherwise it - * returns super.isInTouchMode(). - */ - private boolean mListSelectionHidden; - - /** - * <p>Creates a new list view wrapper.</p> - * - * @param context this view's context - */ - public DropDownListView(Context context) { - super(context, null, com.android.internal.R.attr.dropDownListViewStyle); - } - - /** - * <p>Avoids jarring scrolling effect by ensuring that list elements - * made of a text view fit on a single line.</p> - * - * @param position the item index in the list to get a view for - * @return the view for the specified item - */ - @Override - View obtainView(int position, boolean[] isScrap) { - View view = super.obtainView(position, isScrap); - - if (view instanceof TextView) { - ((TextView) view).setHorizontallyScrolling(true); - } - - return view; - } - - @Override - public boolean isInTouchMode() { - // WARNING: Please read the comment where mListSelectionHidden is declared - return mListSelectionHidden || super.isInTouchMode(); - } - - /** - * <p>Returns the focus state in the drop down.</p> - * - * @return true always - */ - @Override - public boolean hasWindowFocus() { - return true; - } - - /** - * <p>Returns the focus state in the drop down.</p> - * - * @return true always - */ - @Override - public boolean isFocused() { - return true; - } - - /** - * <p>Returns the focus state in the drop down.</p> - * - * @return true always - */ - @Override - public boolean hasFocus() { - return true; - } - - protected int[] onCreateDrawableState(int extraSpace) { - int[] res = super.onCreateDrawableState(extraSpace); - //noinspection ConstantIfStatement - if (false) { - StringBuilder sb = new StringBuilder("Created drawable state: ["); - for (int i=0; i<res.length; i++) { - if (i > 0) sb.append(", "); - sb.append("0x"); - sb.append(Integer.toHexString(res[i])); - } - sb.append("]"); - Log.i(TAG, sb.toString()); - } - return res; - } - } - - /** * This interface is used to make sure that the text entered in this TextView complies to * a certain format. Since there is no foolproof way to prevent the user from leaving * this View with an incorrect value in it, all we can do is try to fix it ourselves @@ -1652,10 +1164,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private class PopupDataSetObserver extends DataSetObserver { @Override public void onChanged() { - if (isPopupShowing()) { - // This will resize the popup to fit the new adapter's content - showDropDown(); - } else if (mAdapter != null) { + if (mAdapter != null) { // If the popup is not showing already, showing it will cause // the list of data set observers attached to the adapter to // change. We can't do it from here, because we are in the middle @@ -1670,14 +1179,5 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe }); } } - - @Override - public void onInvalidated() { - if (!mDropDownAlwaysVisible) { - // There's no data to display so make sure we're not showing - // the drop down and its list - dismissDropDown(); - } - } } } diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index baa6833..4cf8785 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -80,6 +80,18 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, protected FilterQueryProvider mFilterQueryProvider; /** + * If set the adapter will call requery() on the cursor whenever a content change + * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER} + */ + public static final int FLAG_AUTO_REQUERY = 0x01; + + /** + * If set the adapter will register a content observer on the cursor and will call + * {@link #onContentChanged()} when a notification comes in. + */ + public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; + + /** * Constructor. The adapter will call requery() on the cursor whenever * it changes so that the most recent data is always displayed. * @@ -87,7 +99,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, * @param context The context */ public CursorAdapter(Context context, Cursor c) { - init(context, c, true); + init(context, c, FLAG_AUTO_REQUERY); } /** @@ -99,19 +111,43 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, * data is always displayed. */ public CursorAdapter(Context context, Cursor c, boolean autoRequery) { - init(context, c, autoRequery); + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + /** + * Constructor + * @param c The cursor from which to get the data. + * @param context The context + * @param flags flags used to determine the behavior of the adapter + */ + public CursorAdapter(Context context, Cursor c, int flags) { + init(context, c, flags); } protected void init(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + protected void init(Context context, Cursor c, int flags) { + if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { + flags |= FLAG_REGISTER_CONTENT_OBSERVER; + mAutoRequery = true; + } else { + mAutoRequery = false; + } boolean cursorPresent = c != null; - mAutoRequery = autoRequery; mCursor = c; mDataValid = cursorPresent; mContext = context; mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; - mChangeObserver = new ChangeObserver(); + if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { + mChangeObserver = new ChangeObserver(); + } else { + mChangeObserver = null; + } + if (cursorPresent) { - c.registerContentObserver(mChangeObserver); + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); c.registerDataSetObserver(mDataSetObserver); } } @@ -246,13 +282,13 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, return; } if (mCursor != null) { - mCursor.unregisterContentObserver(mChangeObserver); + if (mChangeObserver != null) mCursor.unregisterContentObserver(mChangeObserver); mCursor.unregisterDataSetObserver(mDataSetObserver); mCursor.close(); } mCursor = cursor; if (cursor != null) { - cursor.registerContentObserver(mChangeObserver); + if (mChangeObserver != null) cursor.registerContentObserver(mChangeObserver); cursor.registerDataSetObserver(mDataSetObserver); mRowIDColumn = cursor.getColumnIndexOrThrow("_id"); mDataValid = true; diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index 1ed6b16..c47292f 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -1207,7 +1207,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList // We unfocus the old child down here so the above hasFocus check // returns true - if (oldSelectedChild != null) { + if (oldSelectedChild != null && oldSelectedChild != child) { // Make sure its drawable state doesn't contain 'selected' oldSelectedChild.setSelected(false); @@ -1263,6 +1263,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList */ if (gainFocus && mSelectedChild != null) { mSelectedChild.requestFocus(direction); + mSelectedChild.setSelected(true); } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index d2829db..fe69a13 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -23,6 +23,7 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; +import android.view.ViewDebug; import android.view.ViewGroup; import android.view.SoundEffectConstants; import android.view.animation.GridLayoutAnimationController; @@ -112,7 +113,7 @@ public class GridView extends AbsListView { */ @Override public void setAdapter(ListAdapter adapter) { - if (null != mAdapter) { + if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } @@ -1774,6 +1775,19 @@ public class GridView extends AbsListView { requestLayoutIfNecessary(); } } + + /** + * Get the number of columns in the grid. + * Returns {@link #AUTO_FIT} if the Grid has never been laid out. + * + * @attr ref android.R.styleable#GridView_numColumns + * + * @see #setNumColumns(int) + */ + @ViewDebug.ExportedProperty + public int getNumColumns() { + return mNumColumns; + } /** * Make sure views are touching the top or bottom edge, as appropriate for diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index bd07e1f..7254c3c 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -40,6 +40,13 @@ import android.widget.RemoteViews.RemoteView; * <p> * Also see {@link LinearLayout.LayoutParams android.widget.LinearLayout.LayoutParams} * for layout attributes </p> + * + * @attr ref android.R.styleable#LinearLayout_baselineAligned + * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex + * @attr ref android.R.styleable#LinearLayout_gravity + * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild + * @attr ref android.R.styleable#LinearLayout_orientation + * @attr ref android.R.styleable#LinearLayout_weightSum */ @RemoteView public class LinearLayout extends ViewGroup { @@ -112,7 +119,11 @@ public class LinearLayout extends ViewGroup { } public LinearLayout(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public LinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout); @@ -137,8 +148,7 @@ public class LinearLayout extends ViewGroup { mBaselineAlignedChildIndex = a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1); - // TODO: Better name, add Java APIs, make it public - mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_useLargestChild, false); + mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false); a.recycle(); } @@ -167,6 +177,33 @@ public class LinearLayout extends ViewGroup { mBaselineAligned = baselineAligned; } + /** + * When true, all children with a weight will be considered having + * the minimum size of the largest child. If false, all children are + * measured normally. + * + * @return True to measure children with a weight using the minimum + * size of the largest child, false otherwise. + */ + public boolean isMeasureWithLargestChildEnabled() { + return mUseLargestChild; + } + + /** + * When set to true, all children with a weight will be considered having + * the minimum size of the largest child. If false, all children are + * measured normally. + * + * Disabled by default. + * + * @param enabled True to measure children with a weight using the + * minimum size of the largest child, false otherwise. + */ + @android.view.RemotableViewMethod + public void setMeasureWithLargestChildEnabled(boolean enabled) { + mUseLargestChild = enabled; + } + @Override public int getBaseline() { if (mBaselineAlignedChildIndex < 0) { diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java new file mode 100644 index 0000000..5c34c2c --- /dev/null +++ b/core/java/android/widget/ListPopupWindow.java @@ -0,0 +1,1228 @@ +/* + * 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.widget; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.View.MeasureSpec; +import android.view.View.OnTouchListener; + +/** + * A ListPopupWindow anchors itself to a host view and displays a + * list of choices. When one is selected, the popup is dismissed. + * + * <p>ListPopupWindow contains a number of tricky behaviors surrounding + * positioning, scrolling parents to fit the dropdown, interacting + * sanely with the IME if present, and others. + * + * @see android.widget.AutoCompleteTextView + * @see android.widget.Spinner + */ +public class ListPopupWindow { + private static final String TAG = "ListPopupWindow"; + private static final boolean DEBUG = false; + + /** + * This value controls the length of time that the user + * must leave a pointer down without scrolling to expand + * the autocomplete dropdown list to cover the IME. + */ + private static final int EXPAND_LIST_TIMEOUT = 250; + + private Context mContext; + private PopupWindow mPopup; + private ListAdapter mAdapter; + private DropDownListView mDropDownList; + + private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownHorizontalOffset; + private int mDropDownVerticalOffset; + + private boolean mDropDownAlwaysVisible = false; + private boolean mForceIgnoreOutsideTouch = false; + + private View mPromptView; + private int mPromptPosition = POSITION_PROMPT_ABOVE; + + private DataSetObserver mObserver; + + private View mDropDownAnchorView; + + private Drawable mDropDownListHighlight; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); + private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); + private final PopupScrollListener mScrollListener = new PopupScrollListener(); + private final ListSelectorHider mHideSelector = new ListSelectorHider(); + private Runnable mShowDropDownRunnable; + + private Handler mHandler = new Handler(); + + private Rect mTempRect = new Rect(); + + private boolean mModal; + + /** + * The provided prompt view should appear above list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_ABOVE = 0; + + /** + * The provided prompt view should appear below list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_BELOW = 1; + + /** + * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * If used to specify a popup width, the popup will match the width of the anchor view. + * If used to specify a popup height, the popup will fill available space. + */ + public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; + + /** + * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. + * If used to specify a popup width, the popup will use the width of its content. + */ + public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** + * Mode for {@link #setInputMethodMode(int)}: the requirements for the + * input method should be based on the focusability of the popup. That is + * if it is focusable than it needs to work with the input method, else + * it doesn't. + */ + public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup always needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed so that the user can also operate + * the input method while it is shown. + */ + public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup never needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed to use as much space on the + * screen as needed, regardless of whether this covers the input method. + */ + public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + public ListPopupWindow(Context context) { + this(context, null, 0, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + */ + public ListPopupWindow(Context context, AttributeSet attrs) { + this(context, attrs, 0, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Default style attribute to use for popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Style attribute to read for default styling of popup content. + * @param defStyleRes Style resource ID to use for default styling of popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mContext = context; + mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Sets the adapter that provides the data and the views to represent the data + * in this popup window. + * + * @param adapter The adapter to use to create this window's content. + */ + public void setAdapter(ListAdapter adapter) { + if (mObserver == null) { + mObserver = new PopupDataSetObserver(); + } else if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + mAdapter = adapter; + if (mAdapter != null) { + adapter.registerDataSetObserver(mObserver); + } + + if (mDropDownList != null) { + mDropDownList.setAdapter(mAdapter); + } + } + + /** + * Set where the optional prompt view should appear. The default is + * {@link #POSITION_PROMPT_ABOVE}. + * + * @param position A position constant declaring where the prompt should be displayed. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public void setPromptPosition(int position) { + mPromptPosition = position; + } + + /** + * @return Where the optional prompt view should appear. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public int getPromptPosition() { + return mPromptPosition; + } + + /** + * Set whether this window should be modal when shown. + * + * <p>If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + public void setModal(boolean modal) { + mModal = true; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + public boolean isModal() { + return mModal; + } + + /** + * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is + * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we + * ignore outside touch even when the drop down is not set to always visible. + * + * @hide Used only by AutoCompleteTextView to handle some internal special cases. + */ + public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { + mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mDropDownAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public boolean isDropDownAlwaysVisible() { + return mDropDownAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + public void setSoftInputMode(int mode) { + mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + public int getSoftInputMode() { + return mPopup.getSoftInputMode(); + } + + /** + * Sets a drawable to use as the list item selector. + * + * @param selector List selector drawable to use in the popup. + */ + public void setListSelector(Drawable selector) { + mDropDownListHighlight = selector; + } + + /** + * @return The background drawable for the popup window. + */ + public Drawable getBackground() { + return mPopup.getBackground(); + } + + /** + * Sets a drawable to be the background for the popup window. + * + * @param d A drawable to set as the background. + */ + public void setBackgroundDrawable(Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * + * @param animationStyle Animation style to use. + */ + public void setAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is + * shown or dismissed. + * + * @return Animation style that will be used. + */ + public int getAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * + * @return The popup's anchor view + */ + public View getAnchorView() { + return mDropDownAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to + * the anchor view when shown. + * + * @param anchor The view to use as an anchor. + */ + public void setAnchorView(View anchor) { + mDropDownAnchorView = anchor; + } + + /** + * @return The horizontal offset of the popup from its anchor in pixels. + */ + public int getHorizontalOffset() { + return mDropDownHorizontalOffset; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * + * @param offset The horizontal offset of the popup from its anchor. + */ + public void setHorizontalOffset(int offset) { + mDropDownHorizontalOffset = offset; + } + + /** + * @return The vertical offset of the popup from its anchor in pixels. + */ + public int getVerticalOffset() { + return mDropDownVerticalOffset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * + * @param offset The vertical offset of the popup from its anchor. + */ + public void setVerticalOffset(int offset) { + mDropDownVerticalOffset = offset; + } + + /** + * @return The width of the popup window in pixels. + */ + public int getWidth() { + return mDropDownWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} + * or {@link #WRAP_CONTENT}. + * + * @param width Width of the popup window. + */ + public void setWidth(int width) { + mDropDownWidth = width; + } + + /** + * Sets the width of the popup window by the size of its content. The final width may be + * larger to accommodate styled window dressing. + * + * @param width Desired width of content in pixels. + */ + public void setContentWidth(int width) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + mDropDownWidth = popupBackground.getIntrinsicWidth() + width; + } + } + + /** + * @return The height of the popup window in pixels. + */ + public int getHeight() { + return mDropDownHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. + * + * @param height Height of the popup window. + */ + public void setHeight(int height) { + mDropDownHeight = height; + } + + /** + * Sets a listener to receive events when a list item is clicked. + * + * @param clickListener Listener to register + * + * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { + mItemClickListener = clickListener; + } + + /** + * Sets a listener to receive events when a list item is selected. + * + * @param selectedListener Listener to register. + * + * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { + mItemSelectedListener = selectedListener; + } + + /** + * Set a view to act as a user prompt for this popup window. Where the prompt view will appear + * is controlled by {@link #setPromptPosition(int)}. + * + * @param prompt View to use as an informational prompt. + */ + public void setPromptView(View prompt) { + boolean showing = isShowing(); + if (showing) { + removePromptView(); + } + mPromptView = prompt; + if (showing) { + show(); + } + } + + /** + * Post a {@link #show()} call to the UI thread. + */ + public void postShow() { + mHandler.post(mShowDropDownRunnable); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + public void show() { + int height = buildDropDown(); + + int widthSpec = 0; + int heightSpec = 0; + + boolean noInputMethod = isInputMethodNotNeeded(); + + if (mPopup.isShowing()) { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + if (noInputMethod) { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); + } else { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, + ViewGroup.LayoutParams.MATCH_PARENT); + } + } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + + mPopup.update(getAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, widthSpec, heightSpec); + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setWidth(getAnchorView().getWidth()); + } else { + mPopup.setWidth(mDropDownWidth); + } + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setHeight(height); + } else { + mPopup.setHeight(mDropDownHeight); + } + } + + mPopup.setWindowLayoutMode(widthSpec, heightSpec); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + mPopup.setTouchInterceptor(mTouchInterceptor); + mPopup.showAsDropDown(getAnchorView(), + mDropDownHorizontalOffset, mDropDownVerticalOffset); + mDropDownList.setSelection(ListView.INVALID_POSITION); + + if (!mModal || mDropDownList.isInTouchMode()) { + clearListSelection(); + } + if (!mModal) { + mHandler.post(mHideSelector); + } + } + } + + /** + * Dismiss the popup window. + */ + public void dismiss() { + mPopup.dismiss(); + removePromptView(); + mPopup.setContentView(null); + mDropDownList = null; + } + + private void removePromptView() { + if (mPromptView != null) { + final ViewParent parent = mPromptView.getParent(); + if (parent instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) parent; + group.removeView(mPromptView); + } + } + } + + /** + * Control how the popup operates with an input method: one of + * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, + * or {@link #INPUT_METHOD_NOT_NEEDED}. + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.</p> + * + * @see #getInputMethodMode() + * @see #show() + */ + public void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + /** + * Return the current value in {@link #setInputMethodMode(int)}. + * + * @see #setInputMethodMode(int) + */ + public int getInputMethodMode() { + return mPopup.getInputMethodMode(); + } + + /** + * Set the selected position of the list. + * Only valid when {@link #isShowing()} == {@code true}. + * + * @param position List position to set as selected. + */ + public void setSelection(int position) { + DropDownListView list = mDropDownList; + if (isShowing() && list != null) { + list.mListSelectionHidden = false; + list.setSelection(position); + if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { + list.setItemChecked(position, true); + } + } + } + + /** + * Clear any current list selection. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public void clearListSelection() { + final DropDownListView list = mDropDownList; + if (list != null) { + // WARNING: Please read the comment where mListSelectionHidden is declared + list.mListSelectionHidden = true; + list.hideSelector(); + list.requestLayout(); + } + } + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + public boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + public boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; + } + + /** + * Perform an item click operation on the specified list adapter position. + * + * @param position Adapter position for performing the click + * @return true if the click action could be performed, false if not. + * (e.g. if the popup was not showing, this method would return false.) + */ + public boolean performItemClick(int position) { + if (isShowing()) { + if (mItemClickListener != null) { + final DropDownListView list = mDropDownList; + final View child = list.getChildAt(position - list.getFirstVisiblePosition()); + mItemClickListener.onItemClick(list, child, position, child.getId()); + } + return true; + } + return false; + } + + /** + * @return The currently selected item or null if the popup is not showing. + */ + public Object getSelectedItem() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedItem(); + } + + /** + * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemPosition() + */ + public int getSelectedItemPosition() { + if (!isShowing()) { + return ListView.INVALID_POSITION; + } + return mDropDownList.getSelectedItemPosition(); + } + + /** + * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemId() + */ + public long getSelectedItemId() { + if (!isShowing()) { + return ListView.INVALID_ROW_ID; + } + return mDropDownList.getSelectedItemId(); + } + + /** + * @return The View for the currently selected item or null if + * {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedView() + */ + public View getSelectedView() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedView(); + } + + /** + * @return The {@link ListView} displayed within the popup window. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public ListView getListView() { + return mDropDownList; + } + + /** + * Filter key down events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyDown + * @param event event param passed to the host view's onKeyDown + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + // when the drop down is shown, we drive it directly + if (isShowing()) { + // the key events are forwarded to the list in the drop down view + // note that ListView handles space but we don't want that to happen + // also if selection is not currently in the drop down, then don't + // let center or enter presses go there since that would cause it + // to select one of its items + if (keyCode != KeyEvent.KEYCODE_SPACE + && (mDropDownList.getSelectedItemPosition() >= 0 + || (keyCode != KeyEvent.KEYCODE_ENTER + && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { + int curIndex = mDropDownList.getSelectedItemPosition(); + boolean consumed; + + final boolean below = !mPopup.isAboveAnchor(); + + final ListAdapter adapter = mAdapter; + + boolean allEnabled; + int firstItem = Integer.MAX_VALUE; + int lastItem = Integer.MIN_VALUE; + + if (adapter != null) { + allEnabled = adapter.areAllItemsEnabled(); + firstItem = allEnabled ? 0 : + mDropDownList.lookForSelectablePosition(0, true); + lastItem = allEnabled ? adapter.getCount() - 1 : + mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); + } + + if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || + (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { + // When the selection is at the top, we block the key + // event to prevent focus from moving. + clearListSelection(); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + show(); + return true; + } else { + // WARNING: Please read the comment where mListSelectionHidden + // is declared + mDropDownList.mListSelectionHidden = false; + } + + consumed = mDropDownList.onKeyDown(keyCode, event); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); + + if (consumed) { + // If it handled the key event, then the user is + // navigating in the list, so we should put it in front. + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + // Here's a little trick we need to do to make sure that + // the list view is actually showing its focus indicator, + // by ensuring it has focus and getting its window out + // of touch mode. + mDropDownList.requestFocusFromTouch(); + show(); + + switch (keyCode) { + // avoid passing the focus from the text view to the + // next component + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + return true; + } + } else { + if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // when the selection is at the bottom, we block the + // event to avoid going to the next focusable widget + if (curIndex == lastItem) { + return true; + } + } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && + curIndex == firstItem) { + return true; + } + } + } + } + + return false; + } + + /** + * Filter key down events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyUp + * @param event event param passed to the host view's onKeyUp + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { + boolean consumed = mDropDownList.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + dismiss(); + break; + } + } + return consumed; + } + return false; + } + + /** + * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} + * events to this function, views using ListPopupWindow can have it dismiss the popup + * when the back key is pressed. + * + * @param keyCode keyCode param passed to the host view's onKeyPreIme + * @param event event param passed to the host view's onKeyPreIme + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + final View anchorView = mDropDownAnchorView; + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + anchorView.getKeyDispatcherState().startTracking(event, this); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + anchorView.getKeyDispatcherState().handleUpEvent(event); + if (event.isTracking() && !event.isCanceled()) { + dismiss(); + return true; + } + } + } + return false; + } + + /** + * <p>Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.</p> + * + * @return the content's height or -1 if content already exists + */ + private int buildDropDown() { + ViewGroup dropDownView; + int otherHeights = 0; + + if (mDropDownList == null) { + Context context = mContext; + + /** + * This Runnable exists for the sole purpose of checking if the view layout has got + * completed and if so call showDropDown to display the drop down. This is used to show + * the drop down as soon as possible after user opens up the search dialog, without + * waiting for the normal UI pipeline to do it's job which is slower than this method. + */ + mShowDropDownRunnable = new Runnable() { + public void run() { + // View layout should be all done before displaying the drop down. + View view = getAnchorView(); + if (view != null && view.getWindowToken() != null) { + show(); + } + } + }; + + mDropDownList = new DropDownListView(context, !mModal); + if (mDropDownListHighlight != null) { + mDropDownList.setSelector(mDropDownListHighlight); + } + mDropDownList.setAdapter(mAdapter); + mDropDownList.setVerticalFadingEdgeEnabled(true); + mDropDownList.setOnItemClickListener(mItemClickListener); + mDropDownList.setFocusable(true); + mDropDownList.setFocusableInTouchMode(true); + mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView<?> parent, View view, + int position, long id) { + + if (position != -1) { + DropDownListView dropDownList = mDropDownList; + + if (dropDownList != null) { + dropDownList.mListSelectionHidden = false; + } + } + } + + public void onNothingSelected(AdapterView<?> parent) { + } + }); + mDropDownList.setOnScrollListener(mScrollListener); + + if (mItemSelectedListener != null) { + mDropDownList.setOnItemSelectedListener(mItemSelectedListener); + } + + dropDownView = mDropDownList; + + View hintView = mPromptView; + if (hintView != null) { + // if an hint has been specified, we accomodate more space for it and + // add a text view in the drop down menu, at the bottom of the list + LinearLayout hintContainer = new LinearLayout(context); + hintContainer.setOrientation(LinearLayout.VERTICAL); + + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f + ); + + switch (mPromptPosition) { + case POSITION_PROMPT_BELOW: + hintContainer.addView(dropDownView, hintParams); + hintContainer.addView(hintView); + break; + + case POSITION_PROMPT_ABOVE: + hintContainer.addView(hintView); + hintContainer.addView(dropDownView, hintParams); + break; + + default: + Log.e(TAG, "Invalid hint position " + mPromptPosition); + break; + } + + // measure the hint's height to find how much more vertical space + // we need to add to the drop down's height + int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.UNSPECIFIED; + hintView.measure(widthSpec, heightSpec); + + hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); + otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + + dropDownView = hintContainer; + } + + mPopup.setContentView(dropDownView); + } else { + dropDownView = (ViewGroup) mPopup.getContentView(); + final View view = mPromptView; + if (view != null) { + LinearLayout.LayoutParams hintParams = + (LinearLayout.LayoutParams) view.getLayoutParams(); + otherHeights = view.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + } + } + + // Max height available on the screen for a popup. + boolean ignoreBottomDecorations = + mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + final int maxHeight = mPopup.getMaxAvailableHeight( + getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); + + // getMaxAvailableHeight() subtracts the padding, so we put it back, + // to get the available height for the whole window + int padding = 0; + Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + padding = mTempRect.top + mTempRect.bottom; + } + + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return maxHeight + padding; + } + + final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, + 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); + // add padding only if the list has items in it, that way we don't show + // the popup if it is not needed + if (listContent > 0) otherHeights += padding; + + return listContent + otherHeights; + } + + /** + * <p>Wrapper class for a ListView. This wrapper can hijack the focus to + * make sure the list uses the appropriate drawables and states when + * displayed on screen within a drop down. The focus is never actually + * passed to the drop down in this mode; the list only looks focused.</p> + */ + private static class DropDownListView extends ListView { + private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; + /* + * WARNING: This is a workaround for a touch mode issue. + * + * Touch mode is propagated lazily to windows. This causes problems in + * the following scenario: + * - Type something in the AutoCompleteTextView and get some results + * - Move down with the d-pad to select an item in the list + * - Move up with the d-pad until the selection disappears + * - Type more text in the AutoCompleteTextView *using the soft keyboard* + * and get new results; you are now in touch mode + * - The selection comes back on the first item in the list, even though + * the list is supposed to be in touch mode + * + * Using the soft keyboard triggers the touch mode change but that change + * is propagated to our window only after the first list layout, therefore + * after the list attempts to resurrect the selection. + * + * The trick to work around this issue is to pretend the list is in touch + * mode when we know that the selection should not appear, that is when + * we know the user moved the selection away from the list. + * + * This boolean is set to true whenever we explicitly hide the list's + * selection and reset to false whenever we know the user moved the + * selection back to the list. + * + * When this boolean is true, isInTouchMode() returns true, otherwise it + * returns super.isInTouchMode(). + */ + private boolean mListSelectionHidden; + + /** + * True if this wrapper should fake focus. + */ + private boolean mHijackFocus; + + /** + * <p>Creates a new list view wrapper.</p> + * + * @param context this view's context + */ + public DropDownListView(Context context, boolean hijackFocus) { + super(context, null, com.android.internal.R.attr.dropDownListViewStyle); + mHijackFocus = hijackFocus; + } + + /** + * <p>Avoids jarring scrolling effect by ensuring that list elements + * made of a text view fit on a single line.</p> + * + * @param position the item index in the list to get a view for + * @return the view for the specified item + */ + @Override + View obtainView(int position, boolean[] isScrap) { + View view = super.obtainView(position, isScrap); + + if (view instanceof TextView) { + ((TextView) view).setHorizontallyScrolling(true); + } + + return view; + } + + @Override + public boolean isInTouchMode() { + // WARNING: Please read the comment where mListSelectionHidden is declared + return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always if hijacking focus + */ + @Override + public boolean hasWindowFocus() { + return mHijackFocus || super.hasWindowFocus(); + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always if hijacking focus + */ + @Override + public boolean isFocused() { + return mHijackFocus || super.isFocused(); + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always if hijacking focus + */ + @Override + public boolean hasFocus() { + return mHijackFocus || super.hasFocus(); + } + } + + private class PopupDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + if (isShowing()) { + // Resize the popup to fit new content + show(); + } + } + + @Override + public void onInvalidated() { + dismiss(); + } + } + + private class ListSelectorHider implements Runnable { + public void run() { + clearListSelection(); + } + } + + private class ResizePopupRunnable implements Runnable { + public void run() { + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + show(); + } + } + + private class PopupTouchInterceptor implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + final int action = event.getAction(); + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + if (action == MotionEvent.ACTION_DOWN && + mPopup != null && mPopup.isShowing() && + (x >= 0 && x < getWidth() && y >= 0 && y < getHeight())) { + mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); + } else if (action == MotionEvent.ACTION_UP) { + mHandler.removeCallbacks(mResizePopupRunnable); + } + return false; + } + } + + private class PopupScrollListener implements ListView.OnScrollListener { + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + + } + + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL && + !isInputMethodNotNeeded() && mPopup.getContentView() != null) { + mHandler.removeCallbacks(mResizePopupRunnable); + mResizePopupRunnable.run(); + } + } + } +} diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 892c44a..86913ae 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -415,7 +415,7 @@ public class ListView extends AbsListView { */ @Override public void setAdapter(ListAdapter adapter) { - if (null != mAdapter) { + if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } @@ -2968,7 +2968,7 @@ public class ListView extends AbsListView { // fill a rect where the dividers would be for non-selectable items // If the list is opaque and the background is also opaque, we don't // need to draw anything since the background will do it for us - final boolean fillForMissingDividers = drawDividers && isOpaque() && !super.isOpaque(); + final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { mDividerPaint = new Paint(); @@ -2978,7 +2978,7 @@ public class ListView extends AbsListView { final int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY; if (!mStackFromBottom) { - int bottom = 0; + int bottom; final int scrollY = mScrollY; for (int i = 0; i < count; i++) { @@ -2987,18 +2987,16 @@ public class ListView extends AbsListView { View child = getChildAt(i); bottom = child.getBottom(); // Don't draw dividers next to items that are not enabled - if (drawDividers) { - if ((areAllItemsSelectable || - (adapter.isEnabled(first + i) && (i == count - 1 || - adapter.isEnabled(first + i + 1))))) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - drawDivider(canvas, bounds, i); - } else if (fillForMissingDividers) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - canvas.drawRect(bounds, paint); - } + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } else if (fillForMissingDividers) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + canvas.drawRect(bounds, paint); } } } @@ -3014,7 +3012,7 @@ public class ListView extends AbsListView { View child = getChildAt(i); top = child.getTop(); // Don't draw dividers next to items that are not enabled - if (drawDividers && top > listTop) { + if (top > listTop) { if ((areAllItemsSelectable || (adapter.isEnabled(first + i) && (i == count - 1 || adapter.isEnabled(first + i + 1))))) { @@ -3034,7 +3032,7 @@ public class ListView extends AbsListView { } } - if (count > 0 && scrollY > 0 && drawDividers) { + if (count > 0 && scrollY > 0) { bounds.top = listBottom; bounds.bottom = listBottom + dividerHeight; drawDivider(canvas, bounds, -1); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 0378328..d404ce7 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -16,27 +16,28 @@ package android.widget; -import com.android.internal.R; +import java.lang.ref.WeakReference; import android.content.Context; import android.content.res.TypedArray; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowManager; -import android.view.Gravity; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnScrollChangedListener; -import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.IBinder; import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.View.OnTouchListener; +import android.view.ViewTreeObserver.OnScrollChangedListener; -import java.lang.ref.WeakReference; +import com.android.internal.R; /** * <p>A popup window that can be used to display an arbitrary view. The popup @@ -157,12 +158,21 @@ public class PopupWindow { * <p>The popup does provide a background.</p> */ public PopupWindow(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } + + /** + * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p> + * + * <p>The popup does not provide a background.</p> + */ + public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0); + attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); @@ -1315,6 +1325,7 @@ public class PopupWindow { } private class PopupViewContainer extends FrameLayout { + private static final String TAG = "PopupWindow.PopupViewContainer"; public PopupViewContainer(Context context) { super(context); diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 8e9eb05..71f0c2f 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -715,8 +715,8 @@ public class ProgressBar extends View { mAnimation.setDuration(mDuration); mAnimation.setInterpolator(mInterpolator); mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME); - postInvalidate(); } + postInvalidate(); } /** @@ -729,6 +729,7 @@ public class ProgressBar extends View { ((Animatable) mIndeterminateDrawable).stop(); mShouldStartAnimationDrawable = false; } + postInvalidate(); } /** diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 07c3e4b..50fbb6b 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -48,6 +48,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { private QueryHandler mQueryHandler; private Drawable mBadgeBackground; private Drawable mNoBadgeBackground; + private Drawable mDefaultAvatar; protected String[] mExcludeMimes = null; @@ -117,6 +118,16 @@ public class QuickContactBadge extends ImageView implements OnClickListener { public void setMode(int size) { mMode = size; } + + /** + * Resets the contact photo to the default state. + */ + public void setImageToDefault() { + if (mDefaultAvatar == null) { + mDefaultAvatar = getResources().getDrawable(R.drawable.ic_contact_picture); + } + setImageDrawable(mDefaultAvatar); + } /** * Assign the contact uri that this QuickContactBadge should be associated diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7a70c80..fc02acf 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -60,12 +60,12 @@ public class RemoteViews implements Parcelable, Filter { * The package name of the package containing the layout * resource. (Added to the parcel) */ - private String mPackage; + private final String mPackage; /** * The resource ID of the layout file. (Added to the parcel) */ - private int mLayoutId; + private final int mLayoutId; /** * An array of actions to perform on the view tree once it has been @@ -569,6 +569,7 @@ public class RemoteViews implements Parcelable, Filter { } } + @Override public RemoteViews clone() { final RemoteViews that = new RemoteViews(mPackage, mLayoutId); if (mActions != null) { diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 7d3459e..d1c2270 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -62,7 +62,8 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { private int mStringConversionColumn = -1; private CursorToStringConverter mCursorToStringConverter; private ViewBinder mViewBinder; - private String[] mOriginalFrom; + + String[] mOriginalFrom; /** * Constructor. diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 2f6dd1e..60e8568 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -24,6 +24,7 @@ import android.content.DialogInterface.OnClickListener; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,10 +38,21 @@ import android.view.ViewGroup; */ @Widget public class Spinner extends AbsSpinner implements OnClickListener { + private static final String TAG = "Spinner"; + + /** + * Use a dialog window for selecting spinner options. + */ + public static final int MODE_DIALOG = 0; + + /** + * Use a dropdown anchored to the Spinner for selecting spinner options. + */ + public static final int MODE_DROPDOWN = 1; + + private SpinnerPopup mPopup; + private DropDownAdapter mTempAdapter; - private CharSequence mPrompt; - private AlertDialog mPopup; - public Spinner(Context context) { this(context, null); } @@ -55,9 +67,54 @@ public class Spinner extends AbsSpinner implements OnClickListener { TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Spinner, defStyle, 0); - mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt); + final int mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, + MODE_DIALOG); + + switch (mode) { + case MODE_DIALOG: { + mPopup = new DialogPopup(); + break; + } + + case MODE_DROPDOWN: { + final int hintResource = a.getResourceId( + com.android.internal.R.styleable.Spinner_popupPromptView, 0); + + DropdownPopup popup = new DropdownPopup(context, attrs, defStyle, hintResource); + + popup.setBackgroundDrawable(a.getDrawable( + com.android.internal.R.styleable.Spinner_popupBackground)); + popup.setVerticalOffset(a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0)); + popup.setHorizontalOffset(a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0)); + + mPopup = popup; + break; + } + } + + mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt)); a.recycle(); + + // Base constructor can call setAdapter before we initialize mPopup. + // Finish setting things up if this happened. + if (mTempAdapter != null) { + mPopup.setAdapter(mTempAdapter); + mTempAdapter = null; + } + } + + @Override + public void setAdapter(SpinnerAdapter adapter) { + super.setAdapter(adapter); + + if (mPopup != null) { + mPopup.setAdapter(new DropDownAdapter(adapter)); + } else { + mTempAdapter = new DropDownAdapter(adapter); + } } @Override @@ -194,8 +251,6 @@ public class Spinner extends AbsSpinner implements OnClickListener { return child; } - - /** * Helper for makeAndAddView to set the position of a view * and fill out its layout paramters. @@ -246,15 +301,10 @@ public class Spinner extends AbsSpinner implements OnClickListener { if (!handled) { handled = true; - Context context = getContext(); - - final DropDownAdapter adapter = new DropDownAdapter(getAdapter()); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - if (mPrompt != null) { - builder.setTitle(mPrompt); + if (!mPopup.isShowing()) { + mPopup.show(); } - mPopup = builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show(); } return handled; @@ -271,7 +321,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param prompt the prompt to set */ public void setPrompt(CharSequence prompt) { - mPrompt = prompt; + mPopup.setPromptText(prompt); } /** @@ -279,14 +329,14 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param promptId the resource ID of the prompt to display when the dialog is shown */ public void setPromptId(int promptId) { - mPrompt = getContext().getText(promptId); + setPrompt(getContext().getText(promptId)); } /** * @return The prompt to display when the dialog is shown */ public CharSequence getPrompt() { - return mPrompt; + return mPopup.getHintText(); } /** @@ -384,4 +434,123 @@ public class Spinner extends AbsSpinner implements OnClickListener { return getCount() == 0; } } + + /** + * Implements some sort of popup selection interface for selecting a spinner option. + * Allows for different spinner modes. + */ + private interface SpinnerPopup { + public void setAdapter(ListAdapter adapter); + + /** + * Show the popup + */ + public void show(); + + /** + * Dismiss the popup + */ + public void dismiss(); + + /** + * @return true if the popup is showing, false otherwise. + */ + public boolean isShowing(); + + /** + * Set hint text to be displayed to the user. This should provide + * a description of the choice being made. + * @param hintText Hint text to set. + */ + public void setPromptText(CharSequence hintText); + public CharSequence getHintText(); + } + + private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener { + private AlertDialog mPopup; + private ListAdapter mListAdapter; + private CharSequence mPrompt; + + public void dismiss() { + mPopup.dismiss(); + mPopup = null; + } + + public boolean isShowing() { + return mPopup != null ? mPopup.isShowing() : false; + } + + public void setAdapter(ListAdapter adapter) { + mListAdapter = adapter; + } + + public void setPromptText(CharSequence hintText) { + mPrompt = hintText; + } + + public CharSequence getHintText() { + return mPrompt; + } + + public void show() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + if (mPrompt != null) { + builder.setTitle(mPrompt); + } + mPopup = builder.setSingleChoiceItems(mListAdapter, + getSelectedItemPosition(), this).show(); + } + + public void onClick(DialogInterface dialog, int which) { + setSelection(which); + dismiss(); + } + } + + private class DropdownPopup extends ListPopupWindow implements SpinnerPopup { + private CharSequence mHintText; + private TextView mHintView; + private int mHintResource; + + public DropdownPopup(Context context, AttributeSet attrs, + int defStyleRes, int hintResource) { + super(context, attrs, 0, defStyleRes); + + mHintResource = hintResource; + + setAnchorView(Spinner.this); + setModal(true); + setPromptPosition(POSITION_PROMPT_BELOW); + setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + Spinner.this.setSelection(position); + dismiss(); + } + }); + } + + public CharSequence getHintText() { + return mHintText; + } + + public void setPromptText(CharSequence hintText) { + mHintText = hintText; + if (mHintView != null) { + mHintView.setText(hintText); + } + } + + public void show() { + if (mHintView == null) { + final TextView textView = (TextView) LayoutInflater.from(getContext()).inflate( + mHintResource, null).findViewById(com.android.internal.R.id.text1); + textView.setText(mHintText); + setPromptView(textView); + mHintView = textView; + } + super.show(); + getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); + setSelection(Spinner.this.getSelectedItemPosition()); + } + } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index e6ed70a..0ce8164 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -61,6 +61,7 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.method.ArrowKeyMovementMethod; import android.text.method.DateKeyListener; import android.text.method.DateTimeKeyListener; import android.text.method.DialerKeyListener; @@ -89,10 +90,11 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewDebug; +import android.view.ViewGroup.LayoutParams; import android.view.ViewRoot; import android.view.ViewTreeObserver; -import android.view.ViewGroup.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; @@ -185,7 +187,7 @@ import java.util.ArrayList; */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { - static final String TAG = "TextView"; + static final String LOG_TAG = "TextView"; static final boolean DEBUG_EXTRACT = false; private static int PRIORITY = 100; @@ -321,6 +323,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, attrs, com.android.internal.R.attr.textViewStyle); } + @SuppressWarnings("deprecation") public TextView(Context context, AttributeSet attrs, int defStyle) { @@ -695,9 +698,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener try { setInputExtras(a.getResourceId(attr, 0)); } catch (XmlPullParserException e) { - Log.w("TextView", "Failure reading input extras", e); + Log.w(LOG_TAG, "Failure reading input extras", e); } catch (IOException e) { - Log.w("TextView", "Failure reading input extras", e); + Log.w(LOG_TAG, "Failure reading input extras", e); } break; } @@ -714,7 +717,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (inputMethod != null) { - Class c; + Class<?> c; try { c = Class.forName(inputMethod.toString()); @@ -923,6 +926,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setFocusable(focusable); setClickable(clickable); setLongClickable(longClickable); + + prepareCursorController(); } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -1128,6 +1133,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mText); fixFocusableAndClickableSettings(); + prepareCursorController(); } private void fixFocusableAndClickableSettings() { @@ -2335,6 +2341,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return str + "}"; } + @SuppressWarnings("hiding") public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { @@ -2369,8 +2376,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int end = 0; if (mText != null) { - start = Selection.getSelectionStart(mText); - end = Selection.getSelectionEnd(mText); + start = getSelectionStart(); + end = getSelectionEnd(); if (start >= 0 || end >= 0) { // Or save state if there is a selection save = true; @@ -2442,7 +2449,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener restored = "(restored) "; } - Log.e("TextView", "Saved cursor position " + ss.selStart + + Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd + " out of range for " + restored + "text " + mText); } else { @@ -2694,6 +2701,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (needEditableForNotification) { sendAfterTextChanged((Editable) text); } + + // Depends on canSelectText, which depends on text + prepareCursorController(); } /** @@ -2756,6 +2766,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mChars[off + mStart]; } + @Override public String toString() { return new String(mChars, mStart, mLength); } @@ -2781,6 +2792,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener c.drawText(mChars, start + mStart, end - start, x, y, p); } + public void drawTextRun(Canvas c, int start, int end, + int contextStart, int contextEnd, float x, float y, int flags, Paint p) { + int count = end - start; + int contextCount = contextEnd - contextStart; + c.drawTextRun(mChars, start + mStart, count, contextStart + mStart, + contextCount, x, y, flags, p); + } + public float measureText(int start, int end, Paint p) { return p.measureText(mChars, start + mStart, end - start); } @@ -2788,6 +2807,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public int getTextWidths(int start, int end, float[] widths, Paint p) { return p.getTextWidths(mChars, start + mStart, end - start, widths); } + + public float getTextRunAdvances(int start, int end, int contextStart, + int contextEnd, int flags, float[] advances, int advancesIndex, + Paint p) { + int count = end - start; + int contextCount = contextEnd - contextStart; + return p.getTextRunAdvances(mChars, start + mStart, count, + contextStart + mStart, contextCount, flags, advances, + advancesIndex); + } + + public int getTextRunCursor(int contextStart, int contextEnd, int flags, + int offset, int cursorOpt, Paint p) { + int contextCount = contextEnd - contextStart; + return p.getTextRunCursor(mChars, contextStart + mStart, + contextCount, flags, offset + mStart, cursorOpt); + } } /** @@ -2981,7 +3017,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { input = TextKeyListener.getInstance(); } - mInputType = type; + setRawInputType(type); if (direct) mInput = input; else { setKeyListenerOnly(input); @@ -3198,7 +3234,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @param create If true, the extras will be created if they don't already * exist. Otherwise, null will be returned if none have been created. - * @see #setInputExtras(int)View + * @see #setInputExtras(int) * @see EditorInfo#extras * @attr ref android.R.styleable#TextView_editorExtras */ @@ -3312,7 +3348,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static class ErrorPopup extends PopupWindow { private boolean mAbove = false; - private TextView mView; + private final TextView mView; ErrorPopup(TextView v, int width, int height) { super(v, width, height); @@ -3585,7 +3621,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void invalidateCursor() { - int where = Selection.getSelectionEnd(mText); + int where = getSelectionEnd(); invalidateCursor(where, where, where); } @@ -3661,7 +3697,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean changed = false; if (mMovement != null) { - int curs = Selection.getSelectionEnd(mText); + /* This code also provides auto-scrolling when a cursor is moved using a + * CursorController (insertion point or selection limits). + * For selection, ensure start or end is visible depending on controller's state. + */ + int curs = getSelectionEnd(); + if (mSelectionModifierCursorController != null) { + SelectionModifierCursorController selectionController = + (SelectionModifierCursorController) mSelectionModifierCursorController; + if (selectionController.isSelectionStartDragged()) { + curs = getSelectionStart(); + } + } /* * TODO: This should really only keep the end in view if @@ -3954,8 +4001,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // XXX This is not strictly true -- a program could set the // selection manually if it really wanted to. if (mMovement != null && (isFocused() || isPressed())) { - selStart = Selection.getSelectionStart(mText); - selEnd = Selection.getSelectionEnd(mText); + selStart = getSelectionStart(); + selEnd = getSelectionEnd(); if (mCursorVisible && selStart >= 0 && isEnabled()) { if (mHighlightPath == null) @@ -4061,6 +4108,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ canvas.restore(); + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.draw(canvas); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.draw(canvas); + } } @Override @@ -4475,8 +4529,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener outAttrs.hintText = mHint; if (mText instanceof Editable) { InputConnection ic = new EditableInputConnection(this); - outAttrs.initialSelStart = Selection.getSelectionStart(mText); - outAttrs.initialSelEnd = Selection.getSelectionEnd(mText); + outAttrs.initialSelStart = getSelectionStart(); + outAttrs.initialSelEnd = getSelectionEnd(); outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); return ic; } @@ -4561,8 +4615,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener outText.flags |= ExtractedText.FLAG_SINGLE_LINE; } outText.startOffset = 0; - outText.selectionStart = Selection.getSelectionStart(content); - outText.selectionEnd = Selection.getSelectionEnd(content); + outText.selectionStart = getSelectionStart(); + outText.selectionEnd = getSelectionEnd(); return true; } return false; @@ -4579,7 +4633,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (req != null) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { - if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start=" + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" + ims.mChangedStart + " end=" + ims.mChangedEnd + " delta=" + ims.mChangedDelta); if (ims.mChangedStart < 0 && !contentChanged) { @@ -4587,7 +4641,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, ims.mChangedDelta, ims.mTmpExtracted)) { - if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start=" + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" + ims.mTmpExtracted.partialStartOffset + " end=" + ims.mTmpExtracted.partialEndOffset + ": " + ims.mTmpExtracted.text); @@ -4741,7 +4795,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener void updateAfterEdit() { invalidate(); - int curs = Selection.getSelectionStart(mText); + int curs = getSelectionStart(); if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { @@ -4756,7 +4810,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener makeBlink(); } } - + checkForResize(); } @@ -4847,6 +4901,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case Gravity.RIGHT: + // Note, Layout resolves ALIGN_OPPOSITE to left or + // right based on the paragraph direction. alignment = Layout.Alignment.ALIGN_OPPOSITE; break; @@ -4883,7 +4939,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener w, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad); } - // Log.e("aaa", "Boring: " + mTransformed); mSavedLayout = (BoringLayout) mLayout; } else if (shouldEllipsize && boring.width <= w) { @@ -5478,7 +5533,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // FIXME: Is it okay to truncate this, or should we round? final int x = (int)mLayout.getPrimaryHorizontal(offset); final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line+1); + final int bottom = mLayout.getLineTop(line + 1); int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); @@ -5615,8 +5670,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // viewport coordinates, but requestRectangleOnScreen() // is in terms of content coordinates. - Rect r = new Rect(); - getInterestingRect(r, x, top, bottom, line); + Rect r = new Rect(x, top, x + 1, bottom); + getInterestingRect(r, line); r.offset(mScrollX, mScrollY); if (requestRectangleOnScreen(r)) { @@ -5632,13 +5687,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * to the user. This will not move the cursor if it represents more than * one character (a selection range). This will only work if the * TextView contains spannable text; otherwise it will do nothing. + * + * @return True if the cursor was actually moved, false otherwise. */ public boolean moveCursorToVisibleOffset() { if (!(mText instanceof Spannable)) { return false; } - int start = Selection.getSelectionStart(mText); - int end = Selection.getSelectionEnd(mText); + int start = getSelectionStart(); + int end = getSelectionEnd(); if (start != end) { return false; } @@ -5648,7 +5705,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int line = mLayout.getLineForOffset(start); final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line+1); + final int bottom = mLayout.getLineTop(line + 1); final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); int vslack = (bottom - top) / 2; if (vslack > vspace / 4) @@ -5668,11 +5725,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int leftChar = mLayout.getOffsetForHorizontal(line, hs); final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs); + // line might contain bidirectional text + final int lowChar = leftChar < rightChar ? leftChar : rightChar; + final int highChar = leftChar > rightChar ? leftChar : rightChar; + int newStart = start; - if (newStart < leftChar) { - newStart = leftChar; - } else if (newStart > rightChar) { - newStart = rightChar; + if (newStart < lowChar) { + newStart = lowChar; + } else if (newStart > highChar) { + newStart = highChar; } if (newStart != start) { @@ -5694,22 +5755,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void getInterestingRect(Rect r, int h, int top, int bottom, - int line) { + private void getInterestingRect(Rect r, int line) { + convertFromViewportToContentCoordinates(r); + + // Rectangle can can be expanded on first and last line to take + // padding into account. + // TODO Take left/right padding into account too? + if (line == 0) r.top -= getExtendedPaddingTop(); + if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom(); + } + + private void convertFromViewportToContentCoordinates(Rect r) { int paddingTop = getExtendedPaddingTop(); if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { paddingTop += getVerticalOffset(false); } - top += paddingTop; - bottom += paddingTop; - h += getCompoundPaddingLeft(); + r.top += paddingTop; + r.bottom += paddingTop; - if (line == 0) - top -= getExtendedPaddingTop(); - if (line == mLayout.getLineCount() - 1) - bottom += getExtendedPaddingBottom(); + int paddingLeft = getCompoundPaddingLeft(); + r.left += paddingLeft; + r.right += paddingLeft; - r.set(h, top, h+1, bottom); r.offset(-mScrollX, -mScrollY); } @@ -5877,6 +5944,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (mBlink != null) { mBlink.removeCallbacks(mBlink); } + prepareCursorController(); } private boolean canMarquee() { @@ -5935,7 +6003,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private final WeakReference<TextView> mView; private byte mStatus = MARQUEE_STOPPED; - private float mScrollUnit; + private final float mScrollUnit; private float mMaxScroll; float mMaxFadeScroll; private float mGhostStart; @@ -5947,7 +6015,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Marquee(TextView v) { final float density = v.getContext().getResources().getDisplayMetrics().density; - mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / (float) MARQUEE_RESOLUTION; + mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION; mView = new WeakReference<TextView>(v); } @@ -6291,7 +6359,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } else { - if (DEBUG_EXTRACT) Log.v(TAG, "Span change outside of batch: " + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: " + oldStart + "-" + oldEnd + "," + newStart + "-" + newEnd + what); ims.mContentChanged = true; @@ -6307,7 +6375,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void beforeTextChanged(CharSequence buffer, int start, int before, int after) { - if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); if (AccessibilityManager.getInstance(mContext).isEnabled() @@ -6320,7 +6388,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onTextChanged(CharSequence buffer, int start, int before, int after) { - if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); TextView.this.handleTextChanged(buffer, start, before, after); @@ -6330,10 +6398,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); mBeforeText = null; } + + // TODO. The cursor controller should hide as soon as text is typed. + // But this method is also used for cosmetic changes (underline current word when + // spell corrections are displayed. There is currently no way to make the difference + // between these cosmetic changes and actual text modifications. } public void afterTextChanged(Editable buffer) { - if (DEBUG_EXTRACT) Log.v(TAG, "afterTextChanged: " + buffer); + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer); TextView.this.sendAfterTextChanged(buffer); if (MetaKeyKeyListener.getMetaState(buffer, @@ -6344,19 +6417,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e + " st=" + st + " en=" + en + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, st, e, en); } public void onSpanAdded(Spannable buf, Object what, int s, int e) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, -1, s, -1, e); } public void onSpanRemoved(Spannable buf, Object what, int s, int e) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, -1, e, -1); } @@ -6466,6 +6539,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Don't leave us in the middle of a batch edit. onEndBatchEdit(); + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.hide(); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.hide(); + } } startStopMarquee(focused); @@ -6532,24 +6612,39 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } class CommitSelectionReceiver extends ResultReceiver { - int mNewStart; - int mNewEnd; - - CommitSelectionReceiver() { + private final int mPrevStart, mPrevEnd; + private final int mNewStart, mNewEnd; + + public CommitSelectionReceiver(int mPrevStart, int mPrevEnd, int mNewStart, int mNewEnd) { super(getHandler()); + this.mPrevStart = mPrevStart; + this.mPrevEnd = mPrevEnd; + this.mNewStart = mNewStart; + this.mNewEnd = mNewEnd; } - + + @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode != InputMethodManager.RESULT_SHOWN) { - final int len = mText.length(); - if (mNewStart > len) { - mNewStart = len; - } - if (mNewEnd > len) { - mNewEnd = len; - } - Selection.setSelection((Spannable)mText, mNewStart, mNewEnd); + int start = mNewStart; + int end = mNewEnd; + + // Move the cursor to the new position, unless this tap was actually + // use to show the IMM. Leave cursor unchanged in that case. + if (resultCode == InputMethodManager.RESULT_SHOWN) { + start = mPrevStart; + end = mPrevEnd; + } else if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.show(); + } + + final int len = mText.length(); + if (start > len) { + start = len; } + if (end > len) { + end = len; + } + Selection.setSelection((Spannable)mText, start, end); } } @@ -6562,7 +6657,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTouchFocusSelected = false; mScrolled = false; } - + final boolean superResult = super.onTouchEvent(event); /* @@ -6575,44 +6670,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } - if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) { + if ((mMovement != null || onCheckIsTextEditor()) && + mText instanceof Spannable && mLayout != null) { boolean handled = false; - int oldSelStart = Selection.getSelectionStart(mText); - int oldSelEnd = Selection.getSelectionEnd(mText); - + int oldSelStart = getSelectionStart(); + int oldSelEnd = getSelectionEnd(); + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.onTouchEvent(event); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.onTouchEvent(event); + } + if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } - if (mText instanceof Editable && onCheckIsTextEditor()) { + if (isTextEditable()) { if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - - // This is going to be gross... if tapping on the text view - // causes the IME to be displayed, we don't want the selection - // to change. But the selection has already changed, and - // we won't know right away whether the IME is getting - // displayed, so... - - int newSelStart = Selection.getSelectionStart(mText); - int newSelEnd = Selection.getSelectionEnd(mText); + + final int newSelStart = getSelectionStart(); + final int newSelEnd = getSelectionEnd(); + CommitSelectionReceiver csr = null; if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) { - csr = new CommitSelectionReceiver(); - csr.mNewStart = newSelStart; - csr.mNewEnd = newSelEnd; - } - - if (imm.showSoftInput(this, 0, csr) && csr != null) { - // The IME might get shown -- revert to the old - // selection, and change to the new when we finally - // find out of it is okay. - Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd); - handled = true; + csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd, + newSelStart, newSelEnd); } + + handled |= imm.showSoftInput(this, 0, csr) && (csr != null); } } @@ -6624,6 +6715,47 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } + private void prepareCursorController() { + boolean atLeastOneController = false; + + // TODO Add an extra android:cursorController flag to disable the controller? + if (mCursorVisible) { + atLeastOneController = true; + if (mInsertionPointCursorController == null) { + mInsertionPointCursorController = new InsertionPointCursorController(); + } + } else { + mInsertionPointCursorController = null; + } + + if (canSelectText()) { + atLeastOneController = true; + if (mSelectionModifierCursorController == null) { + mSelectionModifierCursorController = new SelectionModifierCursorController(); + } + } else { + mSelectionModifierCursorController = null; + } + + if (atLeastOneController) { + if (sCursorControllerTempRect == null) { + sCursorControllerTempRect = new Rect(); + } + Resources res = mContext.getResources(); + mCursorControllerVerticalOffset = res.getDimensionPixelOffset( + com.android.internal.R.dimen.cursor_controller_vertical_offset); + } else { + sCursorControllerTempRect = null; + } + } + + /** + * @return True iff this TextView contains a text that can be edited. + */ + private boolean isTextEditable() { + return mText instanceof Editable && onCheckIsTextEditor(); + } + /** * Returns true, only while processing a touch gesture, if the initial * touch down event caused focus to move to the text view and as a result @@ -6657,7 +6789,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private static class Blink extends Handler implements Runnable { - private WeakReference<TextView> mView; + private final WeakReference<TextView> mView; private boolean mCancelled; public Blink(TextView v) { @@ -6674,8 +6806,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener TextView tv = mView.get(); if (tv != null && tv.isFocused()) { - int st = Selection.getSelectionStart(tv.mText); - int en = Selection.getSelectionEnd(tv.mText); + int st = tv.getSelectionStart(); + int en = tv.getSelectionEnd(); if (st == en && st >= 0 && en >= 0) { if (tv.mLayout != null) { @@ -6752,8 +6884,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected int computeHorizontalScrollRange() { - if (mLayout != null) - return mLayout.getWidth(); + if (mLayout != null) { + return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ? + (int) mLayout.getLineWidth(0) : mLayout.getWidth(); + } return super.computeHorizontalScrollRange(); } @@ -6865,6 +6999,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private boolean canSelectText() { + // prepareCursorController() relies on this method. + // If you change this condition, make sure prepareCursorController is called anywhere + // the value of this condition might be changed. if (mText instanceof Spannable && mText.length() != 0 && mMovement != null && mMovement.canSelectArbitrarily()) { return true; @@ -6913,10 +7050,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Returns a word to add to the dictionary from the context menu, - * or null if there is no cursor or no word at the cursor. + * Returns the offsets delimiting the 'word' located at position offset. + * + * @param offset An offset in the text. + * @return The offsets for the start and end of the word located at <code>offset</code>. + * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits. + * Returns a negative value if no valid word was found. */ - private String getWordForDictionary() { + private long getWordLimitsAt(int offset) { /* * Quick return if the input type is one where adding words * to the dictionary doesn't make any sense. @@ -6925,7 +7066,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (klass == InputType.TYPE_CLASS_NUMBER || klass == InputType.TYPE_CLASS_PHONE || klass == InputType.TYPE_CLASS_DATETIME) { - return null; + return -1; } int variation = mInputType & InputType.TYPE_MASK_VARIATION; @@ -6934,13 +7075,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return null; + return -1; } - int end = getSelectionEnd(); + int end = offset; if (end < 0) { - return null; + return -1; } int start = end; @@ -6974,6 +7115,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + if (start == end) { + return -1; + } + + if (end - start > 48) { + return -1; + } + boolean hasLetter = false; for (int i = start; i < end; i++) { if (Character.isLetter(mTransformed.charAt(i))) { @@ -6981,19 +7130,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; } } + if (!hasLetter) { - return null; + return -1; } - if (start == end) { - return null; - } + // Two ints packed in a long + return (((long) start) << 32) | end; + } - if (end - start > 48) { + /** + * Returns a word to add to the dictionary from the context menu, + * or null if there is no cursor or no word at the cursor. + */ + private String getWordForDictionary() { + long wordLimits = getWordLimitsAt(getSelectionEnd()); + if (wordLimits < 0) { return null; + } else { + int start = (int) (wordLimits >>> 32); + int end = (int) (wordLimits & 0x00000000FFFFFFFFL); + return TextUtils.substring(mTransformed, start, end); } - - return TextUtils.substring(mTransformed, start, end); } @Override @@ -7291,7 +7449,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } + @Override public boolean performLongClick() { + // TODO This behavior should be moved to View + // TODO handle legacy code that added items to context menu + if (canSelectText()) { + if (startSelectionMode()) { + mEatTouchRelease = true; + return true; + } + } + if (super.performLongClick()) { mEatTouchRelease = true; return true; @@ -7300,6 +7468,493 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } + private boolean startSelectionMode() { + if (mSelectionModifierCursorController != null) { + int offset = ((SelectionModifierCursorController) mSelectionModifierCursorController). + getTouchOffset(); + + int selectionStart, selectionEnd; + + if (hasSelection()) { + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + if (selectionStart > selectionEnd) { + int tmp = selectionStart; + selectionStart = selectionEnd; + selectionEnd = tmp; + } + if ((offset >= selectionStart) && (offset <= selectionEnd)) { + // Long press in the current selection. + // Should initiate a drag. Return false, to rely on context menu for now. + return false; + } + } + + long wordLimits = getWordLimitsAt(offset); + if (wordLimits >= 0) { + selectionStart = (int) (wordLimits >>> 32); + selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL); + } else { + selectionStart = Math.max(offset - 5, 0); + selectionEnd = Math.min(offset + 5, mText.length()); + } + + Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + + // Has to be done AFTER selection has been changed to correctly position controllers. + mSelectionModifierCursorController.show(); + + return true; + } + + return false; + } + + /** + * Get the offset character closest to the specified absolute position. + * + * @param x The horizontal absolute position of a point on screen + * @param y The vertical absolute position of a point on screen + * @return the character offset for the character whose position is closest to the specified + * position. + * + * @hide + */ + public int getOffset(int x, int y) { + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + + // Clamp the position to inside of the view. + if (x < 0) { + x = 0; + } else if (x >= (getWidth() - getTotalPaddingRight())) { + x = getWidth()-getTotalPaddingRight() - 1; + } + if (y < 0) { + y = 0; + } else if (y >= (getHeight() - getTotalPaddingBottom())) { + y = getHeight()-getTotalPaddingBottom() - 1; + } + + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + final int line = layout.getLineForVertical(y); + final int offset = layout.getOffsetForHorizontal(line, x); + return offset; + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * + * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events + * and send them to this object instead of the cursor. + */ + public interface CursorController { + /* Cursor fade-out animation duration, in milliseconds. */ + static final int FADE_OUT_DURATION = 400; + + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + public void show(); + + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * Update the controller's position. + */ + public void updatePosition(int offset); + + /** + * The controller and the cursor's positions can be link by a fixed offset, + * computed when the controller is touched, and then maintained as it moves + * @return Horizontal offset between the controller and the cursor. + */ + public float getOffsetX(); + + /** + * @return Vertical offset between the controller and the cursor. + */ + public float getOffsetY(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * @param event The touch event + */ + public void onTouchEvent(MotionEvent event); + + /** + * Draws a visual representation of the controller on the canvas. + * + * Called at the end of {@link #draw(Canvas)}, in the content coordinates system. + * @param canvas The Canvas used by this TextView. + */ + public void draw(Canvas canvas); + } + + class InsertionPointCursorController implements CursorController { + private static final int DELAY_BEFORE_FADE_OUT = 2100; + + // Whether or not the cursor control is currently visible + private boolean mIsVisible = false; + // Starting time of the fade timer + private long mFadeOutTimerStart; + // The cursor controller image + private final Drawable mDrawable; + // Used to detect a tap (vs drag) on the controller + private long mOnDownTimerStart; + // Offset between finger hot point on cursor controller and actual cursor + private float mOffsetX, mOffsetY; + + InsertionPointCursorController() { + Resources res = mContext.getResources(); + mDrawable = res.getDrawable(com.android.internal.R.drawable.cursor_controller); + } + + public void show() { + updateDrawablePosition(); + // Has to be done after updatePosition, so that previous position invalidate + // in only done if necessary. + mIsVisible = true; + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.hide(); + } + } + + public void hide() { + if (mIsVisible) { + long time = System.currentTimeMillis(); + // Start fading out, only if not already in progress + if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) { + mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT; + postInvalidate(mDrawable); + } + } + } + + public void draw(Canvas canvas) { + if (mIsVisible) { + int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); + if (time <= DELAY_BEFORE_FADE_OUT) { + postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time, mDrawable); + } else { + time -= DELAY_BEFORE_FADE_OUT; + if (time <= FADE_OUT_DURATION) { + final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION; + mDrawable.setAlpha(alpha); + postInvalidateDelayed(30, mDrawable); + } else { + mDrawable.setAlpha(0); + mIsVisible = false; + } + } + mDrawable.draw(canvas); + } + } + + public void updatePosition(int offset) { + Selection.setSelection((Spannable) mText, offset); + updateDrawablePosition(); + } + + private void updateDrawablePosition() { + if (mIsVisible) { + // Clear previous cursor controller before bounds are updated + postInvalidate(mDrawable); + } + + final int offset = getSelectionStart(); + + if (offset < 0) { + // Should never happen, safety check. + Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); + mIsVisible = false; + return; + } + + positionDrawableUnderCursor(offset, mDrawable); + + mFadeOutTimerStart = System.currentTimeMillis(); + mDrawable.setAlpha(255); + } + + public void onTouchEvent(MotionEvent event) { + if (isFocused() && isTextEditable() && mIsVisible) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN : { + final float x = event.getX(); + final float y = event.getY(); + + if (fingerIsOnDrawable(x, y, mDrawable)) { + show(); + + if (mMovement instanceof ArrowKeyMovementMethod) { + ((ArrowKeyMovementMethod)mMovement).setCursorController(this); + } + + if (mParent != null) { + // Prevent possible scrollView parent from scrolling, so that + // we can use auto-scrolling. + mParent.requestDisallowInterceptTouchEvent(true); + + final Rect bounds = mDrawable.getBounds(); + mOffsetX = (bounds.left + bounds.right) / 2.0f - x; + mOffsetY = bounds.top - mCursorControllerVerticalOffset - y; + + mOnDownTimerStart = event.getEventTime(); + } + } + break; + } + + case MotionEvent.ACTION_UP : { + int time = (int) (event.getEventTime() - mOnDownTimerStart); + + if (time <= ViewConfiguration.getTapTimeout()) { + // A tap on the controller is not grabbed, move the cursor instead + int offset = getOffset((int) event.getX(), (int) event.getY()); + Selection.setSelection((Spannable) mText, offset); + + // Modified by cancelLongPress and prevents the cursor from changing + mScrolled = false; + } + break; + } + } + } + } + + public float getOffsetX() { + return mOffsetX; + } + + public float getOffsetY() { + return mOffsetY; + } + } + + class SelectionModifierCursorController implements CursorController { + // Whether or not the selection controls are currently visible + private boolean mIsVisible = false; + // Whether that start or the end of selection controller is dragged + private boolean mStartIsDragged = false; + // Starting time of the fade timer + private long mFadeOutTimerStart; + // The cursor controller images + private final Drawable mStartDrawable, mEndDrawable; + // Offset between finger hot point on active cursor controller and actual cursor + private float mOffsetX, mOffsetY; + // The offset of that last touch down event. Remembered to start selection there. + private int mTouchOffset; + + SelectionModifierCursorController() { + Resources res = mContext.getResources(); + mStartDrawable = res.getDrawable(com.android.internal.R.drawable.selection_start_handle); + mEndDrawable = res.getDrawable(com.android.internal.R.drawable.selection_end_handle); + } + + public void show() { + updateDrawablesPositions(); + // Has to be done after updatePosition, so that previous position invalidate + // in only done if necessary. + mIsVisible = true; + mFadeOutTimerStart = -1; + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.hide(); + } + } + + public void hide() { + if (mIsVisible && (mFadeOutTimerStart < 0)) { + mFadeOutTimerStart = System.currentTimeMillis(); + postInvalidate(mStartDrawable); + postInvalidate(mEndDrawable); + } + } + + public void draw(Canvas canvas) { + if (mIsVisible) { + if (mFadeOutTimerStart >= 0) { + int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); + if (time <= FADE_OUT_DURATION) { + final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION; + mStartDrawable.setAlpha(alpha); + mEndDrawable.setAlpha(alpha); + postInvalidateDelayed(30, mStartDrawable); + postInvalidateDelayed(30, mEndDrawable); + } else { + mStartDrawable.setAlpha(0); + mEndDrawable.setAlpha(0); + mIsVisible = false; + } + } + mStartDrawable.draw(canvas); + mEndDrawable.draw(canvas); + } + } + + public void updatePosition(int offset) { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + + // Handle the case where start and end are swapped, making sure start <= end + if (mStartIsDragged) { + if (offset <= selectionEnd) { + selectionStart = offset; + } else { + selectionStart = selectionEnd; + selectionEnd = offset; + mStartIsDragged = false; + } + } else { + if (offset >= selectionStart) { + selectionEnd = offset; + } else { + selectionEnd = selectionStart; + selectionStart = offset; + mStartIsDragged = true; + } + } + + Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + updateDrawablesPositions(); + } + + private void updateDrawablesPositions() { + if (mIsVisible) { + // Clear previous cursor controller before bounds are updated + postInvalidate(mStartDrawable); + postInvalidate(mEndDrawable); + } + + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + if ((selectionStart < 0) || (selectionEnd < 0)) { + // Should never happen, safety check. + Log.w(LOG_TAG, "Update selection controller position called with no cursor"); + mIsVisible = false; + return; + } + + positionDrawableUnderCursor(selectionStart, mStartDrawable); + positionDrawableUnderCursor(selectionEnd, mEndDrawable); + + mStartDrawable.setAlpha(255); + mEndDrawable.setAlpha(255); + } + + public void onTouchEvent(MotionEvent event) { + if (isFocused() && isTextEditable() && + (event.getActionMasked() == MotionEvent.ACTION_DOWN)) { + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + // Remember finger down position, to be able to start selection on that point + mTouchOffset = getOffset(x, y); + + if (mIsVisible) { + if (mMovement instanceof ArrowKeyMovementMethod) { + boolean isOnStart = fingerIsOnDrawable(x, y, mStartDrawable); + boolean isOnEnd = fingerIsOnDrawable(x, y, mEndDrawable); + if (isOnStart || isOnEnd) { + if (mParent != null) { + // Prevent possible scrollView parent from scrolling, so that + // we can use auto-scrolling. + mParent.requestDisallowInterceptTouchEvent(true); + } + + // Start handle will be dragged in case BOTH controller are under finger + mStartIsDragged = isOnStart; + final Rect bounds = + (mStartIsDragged ? mStartDrawable : mEndDrawable).getBounds(); + mOffsetX = (bounds.left + bounds.right) / 2.0f - x; + mOffsetY = bounds.top - mCursorControllerVerticalOffset - y; + + ((ArrowKeyMovementMethod)mMovement).setCursorController(this); + } + } + } + } + } + + public int getTouchOffset() { + return mTouchOffset; + } + + public float getOffsetX() { + return mOffsetX; + } + + public float getOffsetY() { + return mOffsetY; + } + + /** + * @return true iff this controller is currently used to move the selection start. + */ + public boolean isSelectionStartDragged() { + return mIsVisible && mStartIsDragged; + } + } + + // Helper methods used by CursorController implementations + + private void positionDrawableUnderCursor(final int offset, Drawable drawable) { + final int drawableWidth = drawable.getIntrinsicWidth(); + final int drawableHeight = drawable.getIntrinsicHeight(); + final int line = mLayout.getLineForOffset(offset); + + final Rect bounds = sCursorControllerTempRect; + bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5 - drawableWidth / 2.0); + bounds.top = mLayout.getLineTop(line + 1); + + // Move cursor controller a little bit up when editing the last line of text + // (or a single line) so that it is visible and easier to grab. + if (line == mLayout.getLineCount() - 1) { + bounds.top -= Math.max(0, drawableHeight / 2 - getExtendedPaddingBottom()); + } + + bounds.right = bounds.left + drawableWidth; + bounds.bottom = bounds.top + drawableHeight; + + convertFromViewportToContentCoordinates(bounds); + drawable.setBounds(bounds); + postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + private boolean fingerIsOnDrawable(float x, float y, Drawable drawable) { + // Simulate a 'fat finger' to ease grabbing of the controller. + // Expands according to controller image size instead of using density. + // Assumes controller imager has a sensible size, proportionnal to density. + final int drawableWidth = drawable.getIntrinsicWidth(); + final int drawableHeight = drawable.getIntrinsicHeight(); + final Rect fingerRect = sCursorControllerTempRect; + fingerRect.set((int) (x - drawableWidth / 2.0), + (int) (y - drawableHeight), + (int) (x + drawableWidth / 2.0), + (int) y); + return Rect.intersects(drawable.getBounds(), fingerRect); + } + + private void postInvalidate(Drawable drawable) { + final Rect bounds = drawable.getBounds(); + postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + private void postInvalidateDelayed(long delay, Drawable drawable) { + final Rect bounds = drawable.getBounds(); + postInvalidateDelayed(delay, bounds.left, bounds.top, bounds.right, bounds.bottom); + } + @ViewDebug.ExportedProperty private CharSequence mText; private CharSequence mTransformed; @@ -7318,16 +7973,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private ArrayList<TextWatcher> mListeners = null; // display attributes - private TextPaint mTextPaint; + private final TextPaint mTextPaint; private boolean mUserSetTextScaleX; - private Paint mHighlightPaint; - private int mHighlightColor = 0xFFBBDDFF; + private final Paint mHighlightPaint; + private int mHighlightColor = 0xD077A14B; private Layout mLayout; private long mShowCursor; private Blink mBlink; private boolean mCursorVisible = true; + // Cursor Controllers. Null when disabled. + private CursorController mInsertionPointCursorController; + private CursorController mSelectionModifierCursorController; + // Stored once and for all. + private int mCursorControllerVerticalOffset; + // Created once and shared by different CursorController helper methods. + private static Rect sCursorControllerTempRect; + private boolean mSelectAllOnFocus = false; private int mGravity = Gravity.TOP | Gravity.LEFT; diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java index 907cfb3..7b66893 100644 --- a/core/java/android/widget/ViewAnimator.java +++ b/core/java/android/widget/ViewAnimator.java @@ -31,11 +31,13 @@ import android.view.animation.AnimationUtils; * * @attr ref android.R.styleable#ViewAnimator_inAnimation * @attr ref android.R.styleable#ViewAnimator_outAnimation + * @attr ref android.R.styleable#ViewAnimator_animateFirstView */ public class ViewAnimator extends FrameLayout { int mWhichChild = 0; boolean mFirstTime = true; + boolean mAnimateFirstTime = true; Animation mInAnimation; @@ -59,6 +61,10 @@ public class ViewAnimator extends FrameLayout { if (resource > 0) { setOutAnimation(context, resource); } + + boolean flag = a.getBoolean(com.android.internal.R.styleable.ViewAnimator_animateFirstView, true); + setAnimateFirstView(flag); + a.recycle(); initViewAnimator(context, attrs); @@ -84,10 +90,10 @@ public class ViewAnimator extends FrameLayout { setMeasureAllChildren(measureAllChildren); a.recycle(); } - + /** * Sets which child view will be displayed. - * + * * @param whichChild the index of the child view to display */ public void setDisplayedChild(int whichChild) { @@ -105,14 +111,14 @@ public class ViewAnimator extends FrameLayout { requestFocus(FOCUS_FORWARD); } } - + /** * Returns the index of the currently displayed child view. */ public int getDisplayedChild() { return mWhichChild; } - + /** * Manually shows the next child. */ @@ -128,25 +134,27 @@ public class ViewAnimator extends FrameLayout { } /** - * Shows only the specified child. The other displays Views exit the screen - * with the {@link #getOutAnimation() out animation} and the specified child - * enters the screen with the {@link #getInAnimation() in animation}. + * Shows only the specified child. The other displays Views exit the screen, + * optionally with the with the {@link #getOutAnimation() out animation} and + * the specified child enters the screen, optionally with the + * {@link #getInAnimation() in animation}. * * @param childIndex The index of the child to be shown. + * @param animate Whether or not to use the in and out animations, defaults + * to true. */ - void showOnly(int childIndex) { + void showOnly(int childIndex, boolean animate) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); - final boolean checkForFirst = (!mFirstTime || mAnimateFirstTime); if (i == childIndex) { - if (checkForFirst && mInAnimation != null) { + if (animate && mInAnimation != null) { child.startAnimation(mInAnimation); } child.setVisibility(View.VISIBLE); mFirstTime = false; } else { - if (checkForFirst && mOutAnimation != null && child.getVisibility() == View.VISIBLE) { + if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) { child.startAnimation(mOutAnimation); } else if (child.getAnimation() == mInAnimation) child.clearAnimation(); @@ -154,6 +162,17 @@ public class ViewAnimator extends FrameLayout { } } } + /** + * Shows only the specified child. The other displays Views exit the screen + * with the {@link #getOutAnimation() out animation} and the specified child + * enters the screen with the {@link #getInAnimation() in animation}. + * + * @param childIndex The index of the child to be shown. + */ + void showOnly(int childIndex) { + final boolean animate = (!mFirstTime || mAnimateFirstTime); + showOnly(childIndex, animate); + } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index 8034961..c6f6e81 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -75,7 +75,7 @@ public class ViewFlipper extends ViewAnimator { updateRunning(); } else if (Intent.ACTION_USER_PRESENT.equals(action)) { mUserPresent = true; - updateRunning(); + updateRunning(false); } } }; @@ -109,7 +109,7 @@ public class ViewFlipper extends ViewAnimator { protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); mVisible = visibility == VISIBLE; - updateRunning(); + updateRunning(false); } /** @@ -144,10 +144,22 @@ public class ViewFlipper extends ViewAnimator { * on {@link #mRunning} and {@link #mVisible} state. */ private void updateRunning() { + updateRunning(true); + } + + /** + * Internal method to start or stop dispatching flip {@link Message} based + * on {@link #mRunning} and {@link #mVisible} state. + * + * @param flipNow Determines whether or not to execute the animation now, in + * addition to queuing future flips. If omitted, defaults to + * true. + */ + private void updateRunning(boolean flipNow) { boolean running = mVisible && mStarted && mUserPresent; if (running != mRunning) { if (running) { - showOnly(mWhichChild); + showOnly(mWhichChild, flipNow); Message msg = mHandler.obtainMessage(FLIP_MSG); mHandler.sendMessageDelayed(msg, mFlipInterval); } else { diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 3df419a..450c966 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -66,8 +66,9 @@ import android.view.WindowManager.LayoutParams; * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. * <p> * If you are using this with a custom View, please call - * {@link #setVisible(boolean) setVisible(false)} from the - * {@link View#onDetachedFromWindow}. + * {@link #setVisible(boolean) setVisible(false)} from + * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged} + * when <code>visibility != View.VISIBLE</code>. * */ public class ZoomButtonsController implements View.OnTouchListener { diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java new file mode 100644 index 0000000..f37021b --- /dev/null +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -0,0 +1,469 @@ +/* + * 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.app; + +import com.android.internal.view.menu.ActionMenu; +import com.android.internal.view.menu.ActionMenuItem; +import com.android.internal.widget.ActionBarContextView; +import com.android.internal.widget.ActionBarView; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.SpinnerAdapter; +import android.widget.ViewAnimator; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * ActionBarImpl is the ActionBar implementation used + * by devices of all screen sizes. If it detects a compatible decor, + * it will split contextual modes across both the ActionBarView at + * the top of the screen and a horizontal LinearLayout at the bottom + * which is normally hidden. + */ +public class ActionBarImpl extends ActionBar { + private static final int NORMAL_VIEW = 0; + private static final int CONTEXT_VIEW = 1; + + private static final int TAB_SWITCH_SHOW_HIDE = 0; + private static final int TAB_SWITCH_ADD_REMOVE = 1; + + private Activity mActivity; + + private ViewAnimator mAnimatorView; + private ActionBarView mActionView; + private ActionBarContextView mUpperContextView; + private LinearLayout mLowerContextView; + + private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>(); + + private int mTabContainerViewId = android.R.id.content; + private TabImpl mSelectedTab; + private int mTabSwitchMode = TAB_SWITCH_ADD_REMOVE; + + private ContextMode mContextMode; + + private static final int CONTEXT_DISPLAY_NORMAL = 0; + private static final int CONTEXT_DISPLAY_SPLIT = 1; + + private int mContextDisplayMode; + + private boolean mClosingContext; + + final Handler mHandler = new Handler(); + final Runnable mCloseContext = new Runnable() { + public void run() { + mUpperContextView.closeMode(); + if (mLowerContextView != null) { + mLowerContextView.removeAllViews(); + } + mClosingContext = false; + } + }; + + public ActionBarImpl(Activity activity) { + final View decor = activity.getWindow().getDecorView(); + mActivity = activity; + mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); + mUpperContextView = (ActionBarContextView) decor.findViewById( + com.android.internal.R.id.action_context_bar); + mLowerContextView = (LinearLayout) decor.findViewById( + com.android.internal.R.id.lower_action_context_bar); + mAnimatorView = (ViewAnimator) decor.findViewById( + com.android.internal.R.id.action_bar_animator); + + if (mActionView == null || mUpperContextView == null || mAnimatorView == null) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with a compatible window decor layout"); + } + + mContextDisplayMode = mLowerContextView == null ? + CONTEXT_DISPLAY_NORMAL : CONTEXT_DISPLAY_SPLIT; + } + + public void setCustomNavigationMode(View view) { + cleanupTabs(); + mActionView.setCustomNavigationView(view); + mActionView.setCallback(null); + } + + public void setDropdownNavigationMode(SpinnerAdapter adapter, NavigationCallback callback) { + cleanupTabs(); + mActionView.setCallback(callback); + mActionView.setNavigationMode(NAVIGATION_MODE_DROPDOWN_LIST); + mActionView.setDropdownAdapter(adapter); + } + + public void setStandardNavigationMode() { + cleanupTabs(); + mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD); + mActionView.setCallback(null); + } + + public void setStandardNavigationMode(CharSequence title) { + cleanupTabs(); + setStandardNavigationMode(title, null); + } + + public void setStandardNavigationMode(CharSequence title, CharSequence subtitle) { + cleanupTabs(); + mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD); + mActionView.setTitle(title); + mActionView.setSubtitle(subtitle); + mActionView.setCallback(null); + } + + private void cleanupTabs() { + if (mSelectedTab != null) { + selectTab(null); + } + if (!mTabs.isEmpty()) { + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + final FragmentTransaction trans = mActivity.openFragmentTransaction(); + final int tabCount = mTabs.size(); + for (int i = 0; i < tabCount; i++) { + trans.remove(mTabs.get(i).getFragment()); + } + trans.commit(); + } + mTabs.clear(); + } + } + + public void setTitle(CharSequence title) { + mActionView.setTitle(title); + } + + public void setSubtitle(CharSequence subtitle) { + mActionView.setSubtitle(subtitle); + } + + public void setDisplayOptions(int options) { + mActionView.setDisplayOptions(options); + } + + public void setDisplayOptions(int options, int mask) { + final int current = mActionView.getDisplayOptions(); + mActionView.setDisplayOptions((options & mask) | (current & ~mask)); + } + + public void setBackgroundDrawable(Drawable d) { + mActionView.setBackgroundDrawable(d); + } + + public View getCustomNavigationView() { + return mActionView.getCustomNavigationView(); + } + + public CharSequence getTitle() { + return mActionView.getTitle(); + } + + public CharSequence getSubtitle() { + return mActionView.getSubtitle(); + } + + public int getNavigationMode() { + return mActionView.getNavigationMode(); + } + + public int getDisplayOptions() { + return mActionView.getDisplayOptions(); + } + + @Override + public void startContextMode(ContextModeCallback callback) { + if (mContextMode != null) { + mContextMode.finish(); + } + + // Don't wait for the close context mode animation to finish. + if (mClosingContext) { + mAnimatorView.clearAnimation(); + mHandler.removeCallbacks(mCloseContext); + mCloseContext.run(); + } + + mContextMode = new ContextMode(callback); + if (callback.onCreateContextMode(mContextMode, mContextMode.getMenu())) { + mContextMode.invalidate(); + mUpperContextView.initForMode(mContextMode); + mAnimatorView.setDisplayedChild(CONTEXT_VIEW); + if (mLowerContextView != null) { + // TODO animate this + mLowerContextView.setVisibility(View.VISIBLE); + } + } + } + + @Override + public void finishContextMode() { + if (mContextMode != null) { + mContextMode.finish(); + } + } + + private void configureTab(Tab tab, int position) { + final TabImpl tabi = (TabImpl) tab; + final boolean isFirstTab = mTabs.isEmpty(); + final FragmentTransaction trans = mActivity.openFragmentTransaction(); + final Fragment frag = tabi.getFragment(); + + tabi.setPosition(position); + mTabs.add(position, tabi); + + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + if (!frag.isAdded()) { + trans.add(mTabContainerViewId, frag); + } + } + + if (isFirstTab) { + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + trans.show(frag); + } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) { + trans.add(mTabContainerViewId, frag); + } + mSelectedTab = tabi; + } else { + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + trans.hide(frag); + } + } + trans.commit(); + } + + @Override + public void addTab(Tab tab) { + mActionView.addTab(tab); + configureTab(tab, mTabs.size()); + } + + @Override + public void insertTab(Tab tab, int position) { + mActionView.insertTab(tab, position); + configureTab(tab, position); + } + + @Override + public Tab newTab() { + return new TabImpl(); + } + + @Override + public void removeTab(Tab tab) { + removeTabAt(tab.getPosition()); + } + + @Override + public void removeTabAt(int position) { + mActionView.removeTabAt(position); + mTabs.remove(position); + + final int newTabCount = mTabs.size(); + for (int i = position; i < newTabCount; i++) { + mTabs.get(i).setPosition(i); + } + + selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); + } + + @Override + public void setTabNavigationMode() { + mActionView.setNavigationMode(NAVIGATION_MODE_TABS); + } + + @Override + public void setTabNavigationMode(int containerViewId) { + mTabContainerViewId = containerViewId; + setTabNavigationMode(); + } + + @Override + public void selectTab(Tab tab) { + if (mSelectedTab == tab) { + return; + } + + mActionView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION); + final FragmentTransaction trans = mActivity.openFragmentTransaction(); + if (mSelectedTab != null) { + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + trans.hide(mSelectedTab.getFragment()); + } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) { + trans.remove(mSelectedTab.getFragment()); + } + } + if (tab != null) { + if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) { + trans.show(tab.getFragment()); + } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) { + trans.add(mTabContainerViewId, tab.getFragment()); + } + } + mSelectedTab = (TabImpl) tab; + trans.commit(); + } + + @Override + public void selectTabAt(int position) { + selectTab(mTabs.get(position)); + } + + /** + * @hide + */ + public class ContextMode extends ActionBar.ContextMode { + private ContextModeCallback mCallback; + private ActionMenu mMenu; + private WeakReference<View> mCustomView; + + public ContextMode(ContextModeCallback callback) { + mCallback = callback; + mMenu = new ActionMenu(mActionView.getContext()); + } + + @Override + public Menu getMenu() { + return mMenu; + } + + @Override + public void finish() { + mCallback.onDestroyContextMode(this); + mAnimatorView.setDisplayedChild(NORMAL_VIEW); + + // Clear out the context mode views after the animation finishes + mClosingContext = true; + mHandler.postDelayed(mCloseContext, mAnimatorView.getOutAnimation().getDuration()); + + if (mLowerContextView != null && mLowerContextView.getVisibility() != View.GONE) { + // TODO Animate this + mLowerContextView.setVisibility(View.GONE); + } + mContextMode = null; + } + + @Override + public void invalidate() { + if (mCallback.onPrepareContextMode(this, mMenu)) { + // Refresh content in both context views + } + } + + @Override + public void setCustomView(View view) { + mUpperContextView.setCustomView(view); + mCustomView = new WeakReference<View>(view); + } + + @Override + public void setSubtitle(CharSequence subtitle) { + mUpperContextView.setSubtitle(subtitle); + } + + @Override + public void setTitle(CharSequence title) { + mUpperContextView.setTitle(title); + } + + @Override + public CharSequence getTitle() { + return mUpperContextView.getTitle(); + } + + @Override + public CharSequence getSubtitle() { + return mUpperContextView.getSubtitle(); + } + + @Override + public View getCustomView() { + return mCustomView != null ? mCustomView.get() : null; + } + + public void dispatchOnContextItemClicked(MenuItem item) { + ActionMenuItem actionItem = (ActionMenuItem) item; + if (!actionItem.invoke()) { + mCallback.onContextItemClicked(this, item); + } + } + } + + /** + * @hide + */ + public class TabImpl extends ActionBar.Tab { + private Fragment mFragment; + private Drawable mIcon; + private CharSequence mText; + private int mPosition; + + @Override + public Fragment getFragment() { + return mFragment; + } + + @Override + public Drawable getIcon() { + return mIcon; + } + + @Override + public int getPosition() { + return mPosition; + } + + public void setPosition(int position) { + mPosition = position; + } + + @Override + public CharSequence getText() { + return mText; + } + + @Override + public void setFragment(Fragment fragment) { + mFragment = fragment; + } + + @Override + public void setIcon(Drawable icon) { + mIcon = icon; + } + + @Override + public void setText(CharSequence text) { + mText = text; + } + + @Override + public void select() { + selectTab(this); + } + } +} diff --git a/core/java/com/android/internal/database/SortCursor.java b/core/java/com/android/internal/database/SortCursor.java index 99410bc..0025512 100644 --- a/core/java/com/android/internal/database/SortCursor.java +++ b/core/java/com/android/internal/database/SortCursor.java @@ -182,24 +182,6 @@ public class SortCursor extends AbstractCursor } @Override - public boolean deleteRow() - { - return mCursor.deleteRow(); - } - - @Override - public boolean commitUpdates() { - int length = mCursors.length; - for (int i = 0 ; i < length ; i++) { - if (mCursors[i] != null) { - mCursors[i].commitUpdates(); - } - } - onChange(true); - return true; - } - - @Override public String getString(int column) { return mCursor.getString(column); @@ -236,6 +218,11 @@ public class SortCursor extends AbstractCursor } @Override + public int getType(int column) { + return mCursor.getType(column); + } + + @Override public boolean isNull(int column) { return mCursor.isNull(column); diff --git a/core/java/com/android/internal/os/SamplingProfilerIntegration.java b/core/java/com/android/internal/os/SamplingProfilerIntegration.java index 5f5c7a4..38362c1 100644 --- a/core/java/com/android/internal/os/SamplingProfilerIntegration.java +++ b/core/java/com/android/internal/os/SamplingProfilerIntegration.java @@ -16,14 +16,15 @@ package com.android.internal.os; +import android.content.pm.PackageInfo; import dalvik.system.SamplingProfiler; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.FileNotFoundException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import android.util.Log; import android.os.*; @@ -35,15 +36,27 @@ public class SamplingProfilerIntegration { private static final String TAG = "SamplingProfilerIntegration"; + public static final String SNAPSHOT_DIR = "/data/snapshots"; + private static final boolean enabled; private static final Executor snapshotWriter; + private static final int samplingProfilerHz; + + /** Whether or not we've created the snapshots dir. */ + private static boolean dirMade = false; + + /** Whether or not a snapshot is being persisted. */ + private static final AtomicBoolean pending = new AtomicBoolean(false); + static { - enabled = "1".equals(SystemProperties.get("persist.sampling_profiler")); - if (enabled) { + samplingProfilerHz = SystemProperties.getInt("persist.sys.profiler_hz", 0); + if (samplingProfilerHz > 0) { snapshotWriter = Executors.newSingleThreadExecutor(); - Log.i(TAG, "Profiler is enabled."); + enabled = true; + Log.i(TAG, "Profiler is enabled. Sampling Profiler Hz: " + samplingProfilerHz); } else { snapshotWriter = null; + enabled = false; Log.i(TAG, "Profiler is disabled."); } } @@ -60,45 +73,45 @@ public class SamplingProfilerIntegration { */ public static void start() { if (!enabled) return; - SamplingProfiler.getInstance().start(10); + SamplingProfiler.getInstance().start(samplingProfilerHz); } - /** Whether or not we've created the snapshots dir. */ - static boolean dirMade = false; - - /** Whether or not a snapshot is being persisted. */ - static volatile boolean pending; - /** - * Writes a snapshot to the SD card if profiling is enabled. + * Writes a snapshot if profiling is enabled. */ - public static void writeSnapshot(final String name) { + public static void writeSnapshot(final String processName, final PackageInfo packageInfo) { if (!enabled) return; /* - * If we're already writing a snapshot, don't bother enqueing another + * If we're already writing a snapshot, don't bother enqueueing another * request right now. This will reduce the number of individual * snapshots and in turn the total amount of memory consumed (one big * snapshot is smaller than N subset snapshots). */ - if (!pending) { - pending = true; + if (pending.compareAndSet(false, true)) { snapshotWriter.execute(new Runnable() { public void run() { - String dir = "/sdcard/snapshots"; if (!dirMade) { - new File(dir).mkdirs(); - if (new File(dir).isDirectory()) { + File dir = new File(SNAPSHOT_DIR); + dir.mkdirs(); + // the directory needs to be writable to anybody + dir.setWritable(true, false); + // the directory needs to be executable to anybody + // don't know why yet, but mode 723 would work, while + // mode 722 throws FileNotFoundExecption at line 151 + dir.setExecutable(true, false); + if (new File(SNAPSHOT_DIR).isDirectory()) { dirMade = true; } else { - Log.w(TAG, "Creation of " + dir + " failed."); + Log.w(TAG, "Creation of " + SNAPSHOT_DIR + " failed."); + pending.set(false); return; } } try { - writeSnapshot(dir, name); + writeSnapshot(SNAPSHOT_DIR, processName, packageInfo); } finally { - pending = false; + pending.set(false); } } }); @@ -110,13 +123,13 @@ public class SamplingProfilerIntegration { */ public static void writeZygoteSnapshot() { if (!enabled) return; - - String dir = "/data/zygote/snapshots"; - new File(dir).mkdirs(); - writeSnapshot(dir, "zygote"); + writeSnapshot("zygote", null); } - private static void writeSnapshot(String dir, String name) { + /** + * pass in PackageInfo to retrieve various values for snapshot header + */ + private static void writeSnapshot(String dir, String processName, PackageInfo packageInfo) { byte[] snapshot = SamplingProfiler.getInstance().snapshot(); if (snapshot == null) { return; @@ -128,39 +141,54 @@ public class SamplingProfilerIntegration { * we capture two snapshots in rapid succession. */ long start = System.currentTimeMillis(); - String path = dir + "/" + name.replace(':', '.') + "-" + - + System.currentTimeMillis() + ".snapshot"; + String name = processName.replaceAll(":", "."); + String path = dir + "/" + name + "-" +System.currentTimeMillis() + ".snapshot"; + FileOutputStream out = null; try { - // Try to open the file a few times. The SD card may not be mounted. - FileOutputStream out; - int count = 0; - while (true) { - try { - out = new FileOutputStream(path); - break; - } catch (FileNotFoundException e) { - if (++count > 3) { - Log.e(TAG, "Could not open " + path + "."); - return; - } - - // Sleep for a bit and then try again. - try { - Thread.sleep(2500); - } catch (InterruptedException e1) { /* ignore */ } + out = new FileOutputStream(path); + generateSnapshotHeader(name, packageInfo, out); + out.write(snapshot); + } catch (IOException e) { + Log.e(TAG, "Error writing snapshot.", e); + } finally { + try { + if(out != null) { + out.close(); } + } catch (IOException ex) { + // let it go. } + } + // set file readable to the world so that SamplingProfilerService + // can put it to dropbox + new File(path).setReadable(true, false); - try { - out.write(snapshot); - } finally { - out.close(); - } - long elapsed = System.currentTimeMillis() - start; - Log.i(TAG, "Wrote snapshot for " + name - + " in " + elapsed + "ms."); - } catch (IOException e) { - Log.e(TAG, "Error writing snapshot.", e); + long elapsed = System.currentTimeMillis() - start; + Log.i(TAG, "Wrote snapshot for " + name + " in " + elapsed + "ms."); + } + + /** + * generate header for snapshots, with the following format (like http header): + * + * Version: <version number of profiler>\n + * Process: <process name>\n + * Package: <package name, if exists>\n + * Package-Version: <version number of the package, if exists>\n + * Build: <fingerprint>\n + * \n + * <the actual snapshot content begins here...> + */ + private static void generateSnapshotHeader(String processName, PackageInfo packageInfo, + FileOutputStream out) throws IOException { + // profiler version + out.write("Version: 1\n".getBytes()); + out.write(("Process: " + processName + "\n").getBytes()); + if(packageInfo != null) { + out.write(("Package: " + packageInfo.packageName + "\n").getBytes()); + out.write(("Package-Version: " + packageInfo.versionCode + "\n").getBytes()); } + out.write(("Build: " + Build.FINGERPRINT + "\n").getBytes()); + // single blank line means the end of snapshot header. + out.write("\n".getBytes()); } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index b677b1e..9fcd3f5 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -66,10 +66,6 @@ public class ZygoteInit { /** when preloading, GC after allocating this many bytes */ private static final int PRELOAD_GC_THRESHOLD = 50000; - /** throw on missing preload, only if this looks like a developer */ - private static final boolean THROW_ON_MISSING_PRELOAD = - "1".equals(SystemProperties.get("persist.service.adb.enable")); - public static final String USAGE_STRING = " <\"true\"|\"false\" for startSystemServer>"; @@ -287,7 +283,6 @@ public class ZygoteInit { int count = 0; String line; - String missingClasses = null; while ((line = br.readLine()) != null) { // Skip comments and blank lines. line = line.trim(); @@ -311,12 +306,7 @@ public class ZygoteInit { } count++; } catch (ClassNotFoundException e) { - Log.e(TAG, "Class not found for preloading: " + line); - if (missingClasses == null) { - missingClasses = line; - } else { - missingClasses += " " + line; - } + Log.w(TAG, "Class not found for preloading: " + line); } catch (Throwable t) { Log.e(TAG, "Error preloading " + line + ".", t); if (t instanceof Error) { @@ -329,13 +319,6 @@ public class ZygoteInit { } } - if (THROW_ON_MISSING_PRELOAD && - missingClasses != null) { - throw new IllegalStateException( - "Missing class(es) for preloading, update preloaded-classes [" - + missingClasses + "]"); - } - Log.i(TAG, "...preloaded " + count + " classes in " + (SystemClock.uptimeMillis()-startTime) + "ms."); } catch (IOException e) { diff --git a/core/java/com/android/internal/util/HierarchicalStateMachine.java b/core/java/com/android/internal/util/HierarchicalStateMachine.java index c599d68..7138b5c 100644 --- a/core/java/com/android/internal/util/HierarchicalStateMachine.java +++ b/core/java/com/android/internal/util/HierarchicalStateMachine.java @@ -1197,6 +1197,35 @@ public class HierarchicalStateMachine { } /** + * Get a message and set Message.target = this, + * what, arg1 and arg2 + * + * @param what is assigned to Message.what + * @param arg1 is assigned to Message.arg1 + * @param arg2 is assigned to Message.arg2 + * @return A Message object from the global pool. + */ + public final Message obtainMessage(int what, int arg1, int arg2) + { + return Message.obtain(mHsmHandler, what, arg1, arg2); + } + + /** + * Get a message and set Message.target = this, + * what, arg1, arg2 and obj + * + * @param what is assigned to Message.what + * @param arg1 is assigned to Message.arg1 + * @param arg2 is assigned to Message.arg2 + * @param obj is assigned to Message.obj + * @return A Message object from the global pool. + */ + public final Message obtainMessage(int what, int arg1, int arg2, Object obj) + { + return Message.obtain(mHsmHandler, what, arg1, arg2, obj); + } + + /** * Enqueue a message to this state machine. */ public final void sendMessage(int what) { diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index 8d8df16..e00a853 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -284,6 +285,26 @@ public class XmlUtils out.endTag(null, "list"); } + + public static final void writeSetXml(Set val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "set"); + if (name != null) { + out.attribute(null, "name", name); + } + + for (Object v : val) { + writeValueXml(v, null, out); + } + + out.endTag(null, "set"); + } /** * Flatten a byte[] into an XmlSerializer. The list can later be read back @@ -426,6 +447,9 @@ public class XmlUtils } else if (v instanceof List) { writeListXml((List)v, name, out); return; + } else if (v instanceof Set) { + writeSetXml((Set)v, name, out); + return; } else if (v instanceof CharSequence) { // XXX This is to allow us to at least write something if // we encounter styled text... but it means we will drop all @@ -476,7 +500,7 @@ public class XmlUtils * * @param in The InputStream from which to read. * - * @return HashMap The resulting list. + * @return ArrayList The resulting list. * * @see #readMapXml * @see #readValueXml @@ -490,6 +514,29 @@ public class XmlUtils parser.setInput(in, null); return (ArrayList)readValueXml(parser, new String[1]); } + + + /** + * Read a HashSet from an InputStream containing XML. The stream can + * previously have been written by writeSetXml(). + * + * @param in The InputStream from which to read. + * + * @return HashSet The resulting set. + * + * @throws XmlPullParserException + * @throws java.io.IOException + * + * @see #readValueXml + * @see #readThisSetXml + * @see #writeSetXml + */ + public static final HashSet readSetXml(InputStream in) + throws XmlPullParserException, java.io.IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + return (HashSet) readValueXml(parser, new String[1]); + } /** * Read a HashMap object from an XmlPullParser. The XML data could @@ -573,6 +620,47 @@ public class XmlUtils throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } + + /** + * Read a HashSet object from an XmlPullParser. The XML data could previously + * have been generated by writeSetXml(). The XmlPullParser must be positioned + * <em>after</em> the tag that begins the set. + * + * @param parser The XmlPullParser from which to read the set data. + * @param endTag Name of the tag that will end the set, usually "set". + * @param name An array of one string, used to return the name attribute + * of the set's tag. + * + * @return HashSet The newly generated set. + * + * @throws XmlPullParserException + * @throws java.io.IOException + * + * @see #readSetXml + */ + public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + HashSet set = new HashSet(); + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + Object val = readThisValueXml(parser, name); + set.add(val); + //System.out.println("Adding to set: " + val); + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return set; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } /** * Read an int[] object from an XmlPullParser. The XML data could @@ -740,6 +828,12 @@ public class XmlUtils name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; + } else if (tagName.equals("set")) { + parser.next(); + res = readThisSetXml(parser, "set", name); + name[0] = valueName; + //System.out.println("Returning value for " + valueName + ": " + res); + return res; } else { throw new XmlPullParserException( "Unknown tag: " + tagName); diff --git a/core/java/com/android/internal/view/menu/ActionMenu.java b/core/java/com/android/internal/view/menu/ActionMenu.java new file mode 100644 index 0000000..3d44ebc --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenu.java @@ -0,0 +1,263 @@ +/* + * 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.menu; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; + +/** + * @hide + */ +public class ActionMenu implements Menu { + private Context mContext; + + private boolean mIsQwerty; + + private ArrayList<ActionMenuItem> mItems; + + public ActionMenu(Context context) { + mContext = context; + mItems = new ArrayList<ActionMenuItem>(); + } + + public Context getContext() { + return mContext; + } + + public MenuItem add(CharSequence title) { + return add(0, 0, 0, title); + } + + public MenuItem add(int titleRes) { + return add(0, 0, 0, titleRes); + } + + public MenuItem add(int groupId, int itemId, int order, int titleRes) { + return add(groupId, itemId, order, mContext.getResources().getString(titleRes)); + } + + public MenuItem add(int groupId, int itemId, int order, CharSequence title) { + ActionMenuItem item = new ActionMenuItem(getContext(), + groupId, itemId, 0, order, title); + mItems.add(order, item); + return item; + } + + public int addIntentOptions(int groupId, int itemId, int order, + ComponentName caller, Intent[] specifics, Intent intent, int flags, + MenuItem[] outSpecificItems) { + PackageManager pm = mContext.getPackageManager(); + final List<ResolveInfo> lri = + pm.queryIntentActivityOptions(caller, specifics, intent, 0); + final int N = lri != null ? lri.size() : 0; + + if ((flags & FLAG_APPEND_TO_GROUP) == 0) { + removeGroup(groupId); + } + + for (int i=0; i<N; i++) { + final ResolveInfo ri = lri.get(i); + Intent rintent = new Intent( + ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); + rintent.setComponent(new ComponentName( + ri.activityInfo.applicationInfo.packageName, + ri.activityInfo.name)); + final MenuItem item = add(groupId, itemId, order, ri.loadLabel(pm)) + .setIcon(ri.loadIcon(pm)) + .setIntent(rintent); + if (outSpecificItems != null && ri.specificIndex >= 0) { + outSpecificItems[ri.specificIndex] = item; + } + } + + return N; + } + + public SubMenu addSubMenu(CharSequence title) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int titleRes) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int groupId, int itemId, int order, + CharSequence title) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) { + // TODO Implement submenus + return null; + } + + public void clear() { + mItems.clear(); + } + + public void close() { + } + + private int findItemIndex(int id) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + for (int i = 0; i < itemCount; i++) { + if (items.get(i).getItemId() == id) { + return i; + } + } + + return -1; + } + + public MenuItem findItem(int id) { + return mItems.get(findItemIndex(id)); + } + + public MenuItem getItem(int index) { + return mItems.get(index); + } + + public boolean hasVisibleItems() { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + if (items.get(i).isVisible()) { + return true; + } + } + + return false; + } + + private ActionMenuItem findItemWithShortcut(int keyCode, KeyEvent event) { + // TODO Make this smarter. + final boolean qwerty = mIsQwerty; + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + final char shortcut = qwerty ? item.getAlphabeticShortcut() : + item.getNumericShortcut(); + if (keyCode == shortcut) { + return item; + } + } + return null; + } + + public boolean isShortcutKey(int keyCode, KeyEvent event) { + return findItemWithShortcut(keyCode, event) != null; + } + + public boolean performIdentifierAction(int id, int flags) { + final int index = findItemIndex(id); + if (index < 0) { + return false; + } + + return mItems.get(index).invoke(); + } + + public boolean performShortcut(int keyCode, KeyEvent event, int flags) { + ActionMenuItem item = findItemWithShortcut(keyCode, event); + if (item == null) { + return false; + } + + return item.invoke(); + } + + public void removeGroup(int groupId) { + final ArrayList<ActionMenuItem> items = mItems; + int itemCount = items.size(); + int i = 0; + while (i < itemCount) { + if (items.get(i).getGroupId() == groupId) { + items.remove(i); + itemCount--; + } else { + i++; + } + } + } + + public void removeItem(int id) { + mItems.remove(findItemIndex(id)); + } + + public void setGroupCheckable(int group, boolean checkable, + boolean exclusive) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setCheckable(checkable); + item.setExclusiveCheckable(exclusive); + } + } + } + + public void setGroupEnabled(int group, boolean enabled) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setEnabled(enabled); + } + } + } + + public void setGroupVisible(int group, boolean visible) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setVisible(visible); + } + } + } + + public void setQwertyMode(boolean isQwerty) { + mIsQwerty = isQwerty; + } + + public int size() { + return mItems.size(); + } +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java new file mode 100644 index 0000000..035875a --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java @@ -0,0 +1,225 @@ +/* + * 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.menu; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.ContextMenu.ContextMenuInfo; + +/** + * @hide + */ +public class ActionMenuItem implements MenuItem { + private final int mId; + private final int mGroup; + private final int mCategoryOrder; + private final int mOrdering; + + private CharSequence mTitle; + private CharSequence mTitleCondensed; + private Intent mIntent; + private char mShortcutNumericChar; + private char mShortcutAlphabeticChar; + + private Drawable mIconDrawable; + private int mIconResId = NO_ICON; + + private Context mContext; + + private MenuItem.OnMenuItemClickListener mClickListener; + + private static final int NO_ICON = 0; + + private int mFlags = ENABLED; + private static final int CHECKABLE = 0x00000001; + private static final int CHECKED = 0x00000002; + private static final int EXCLUSIVE = 0x00000004; + private static final int HIDDEN = 0x00000008; + private static final int ENABLED = 0x00000010; + + public ActionMenuItem(Context context, int group, int id, int categoryOrder, int ordering, + CharSequence title) { + mContext = context; + mId = id; + mGroup = group; + mCategoryOrder = categoryOrder; + mOrdering = ordering; + mTitle = title; + } + + public char getAlphabeticShortcut() { + return mShortcutAlphabeticChar; + } + + public int getGroupId() { + return mGroup; + } + + public Drawable getIcon() { + return mIconDrawable; + } + + public Intent getIntent() { + return mIntent; + } + + public int getItemId() { + return mId; + } + + public ContextMenuInfo getMenuInfo() { + return null; + } + + public char getNumericShortcut() { + return mShortcutNumericChar; + } + + public int getOrder() { + return mOrdering; + } + + public SubMenu getSubMenu() { + return null; + } + + public CharSequence getTitle() { + return mTitle; + } + + public CharSequence getTitleCondensed() { + return mTitleCondensed; + } + + public boolean hasSubMenu() { + return false; + } + + public boolean isCheckable() { + return (mFlags & CHECKABLE) != 0; + } + + public boolean isChecked() { + return (mFlags & CHECKED) != 0; + } + + public boolean isEnabled() { + return (mFlags & ENABLED) != 0; + } + + public boolean isVisible() { + return (mFlags & HIDDEN) == 0; + } + + public MenuItem setAlphabeticShortcut(char alphaChar) { + mShortcutAlphabeticChar = alphaChar; + return this; + } + + public MenuItem setCheckable(boolean checkable) { + mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); + return this; + } + + public ActionMenuItem setExclusiveCheckable(boolean exclusive) { + mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); + return this; + } + + public MenuItem setChecked(boolean checked) { + mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); + return this; + } + + public MenuItem setEnabled(boolean enabled) { + mFlags = (mFlags & ~ENABLED) | (enabled ? ENABLED : 0); + return this; + } + + public MenuItem setIcon(Drawable icon) { + mIconDrawable = icon; + mIconResId = NO_ICON; + return this; + } + + public MenuItem setIcon(int iconRes) { + mIconResId = iconRes; + mIconDrawable = mContext.getResources().getDrawable(iconRes); + return this; + } + + public MenuItem setIntent(Intent intent) { + mIntent = intent; + return this; + } + + public MenuItem setNumericShortcut(char numericChar) { + mShortcutNumericChar = numericChar; + return this; + } + + public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) { + mClickListener = menuItemClickListener; + return this; + } + + public MenuItem setShortcut(char numericChar, char alphaChar) { + mShortcutNumericChar = numericChar; + mShortcutAlphabeticChar = alphaChar; + return this; + } + + public MenuItem setTitle(CharSequence title) { + mTitle = title; + return this; + } + + public MenuItem setTitle(int title) { + mTitle = mContext.getResources().getString(title); + return this; + } + + public MenuItem setTitleCondensed(CharSequence title) { + mTitleCondensed = title; + return this; + } + + public MenuItem setVisible(boolean visible) { + mFlags = (mFlags & HIDDEN) | (visible ? 0 : HIDDEN); + return this; + } + + public boolean invoke() { + if (mClickListener != null && mClickListener.onMenuItemClick(this)) { + return true; + } + + if (mIntent != null) { + mContext.startActivity(mIntent); + return true; + } + + return false; + } + + public void setShowAsAction(int show) { + // Do nothing. ActionMenuItems always show as action buttons. + } +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java new file mode 100644 index 0000000..f0d9f60 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java @@ -0,0 +1,112 @@ +/* + * 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.menu; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.SoundEffectConstants; +import android.view.View; +import android.widget.ImageButton; + +/** + * @hide + */ +public class ActionMenuItemView extends ImageButton implements MenuView.ItemView { + private static final String TAG = "ActionMenuItemView"; + + private MenuItemImpl mItemData; + private CharSequence mTitle; + private MenuBuilder.ItemInvoker mItemInvoker; + + public ActionMenuItemView(Context context) { + this(context, null); + } + + public ActionMenuItemView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.actionButtonStyle); + } + + public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public MenuItemImpl getItemData() { + return mItemData; + } + + public void initialize(MenuItemImpl itemData, int menuType) { + mItemData = itemData; + + setClickable(true); + setFocusable(true); + setTitle(itemData.getTitle()); + setIcon(itemData.getIcon()); + setId(itemData.getItemId()); + + setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE); + setEnabled(itemData.isEnabled()); + } + + @Override + public boolean performClick() { + // Let the view's listener have top priority + if (super.performClick()) { + return true; + } + + if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) { + playSoundEffect(SoundEffectConstants.CLICK); + return true; + } else { + return false; + } + } + + public void setItemInvoker(MenuBuilder.ItemInvoker invoker) { + mItemInvoker = invoker; + } + + public boolean prefersCondensedTitle() { + return false; + } + + public void setCheckable(boolean checkable) { + // TODO Support checkable action items + } + + public void setChecked(boolean checked) { + // TODO Support checkable action items + } + + public void setIcon(Drawable icon) { + setImageDrawable(icon); + } + + public void setShortcut(boolean showShortcut, char shortcutKey) { + // Action buttons don't show text for shortcut keys. + } + + public void setTitle(CharSequence title) { + mTitle = title; + } + + public boolean showsIcon() { + return true; + } + +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuView.java b/core/java/com/android/internal/view/menu/ActionMenuView.java new file mode 100644 index 0000000..c3fe5dc --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuView.java @@ -0,0 +1,120 @@ +/* + * 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.menu; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import java.util.ArrayList; + +/** + * @hide + */ +public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { + private static final String TAG = "ActionMenuView"; + + private MenuBuilder mMenu; + + private int mItemPadding; + private int mItemMargin; + private int mMaxItems; + + public ActionMenuView(Context context) { + this(context, null); + } + + public ActionMenuView(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Theme); + mItemPadding = a.getDimensionPixelOffset( + com.android.internal.R.styleable.Theme_actionButtonPadding, 0); + mItemMargin = mItemPadding / 2; + a.recycle(); + + final Resources res = getResources(); + final int size = res.getDimensionPixelSize(com.android.internal.R.dimen.action_icon_size); + final int spaceAvailable = res.getDisplayMetrics().widthPixels / 2; + final int itemSpace = size + mItemPadding; + + mMaxItems = spaceAvailable / (itemSpace > 0 ? itemSpace : 1); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + if (p instanceof LayoutParams) { + LayoutParams lp = (LayoutParams) p; + return lp.leftMargin == mItemMargin && lp.rightMargin == mItemMargin && + lp.width == LayoutParams.WRAP_CONTENT && lp.height == LayoutParams.WRAP_CONTENT; + } + return false; + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + params.leftMargin = mItemMargin; + params.rightMargin = mItemMargin; + return params; + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + public int getItemMargin() { + return mItemMargin; + } + + public boolean invokeItem(MenuItemImpl item) { + return mMenu.performItemAction(item, 0); + } + + public int getWindowAnimations() { + return 0; + } + + public void initialize(MenuBuilder menu, int menuType) { + menu.setMaxActionItems(mMaxItems); + mMenu = menu; + updateChildren(true); + } + + public void updateChildren(boolean cleared) { + removeAllViews(); + + final ArrayList<MenuItemImpl> itemsToShow = mMenu.getActionItems(); + final int itemCount = itemsToShow.size(); + + for (int i = 0; i < itemCount; i++) { + final MenuItemImpl itemData = itemsToShow.get(i); + addItemView((ActionMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ACTION_BUTTON, + this)); + } + } + + private void addItemView(ActionMenuItemView view) { + view.setItemInvoker(this); + addView(view); + } +} diff --git a/core/java/com/android/internal/view/menu/IconMenuView.java b/core/java/com/android/internal/view/menu/IconMenuView.java index beb57ba..bbf7c68 100644 --- a/core/java/com/android/internal/view/menu/IconMenuView.java +++ b/core/java/com/android/internal/view/menu/IconMenuView.java @@ -337,7 +337,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi // This method does a clear refresh of children removeAllViews(); - final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems(); + final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems(); final int numItems = itemsToShow.size(); final int numItemsThatCanFit = mMaxItems; // Minimum of the num that can fit and the num that we have diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 228d5d0..94a9f65 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -27,16 +27,17 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; +import android.util.Log; import android.util.SparseArray; import android.view.ContextThemeWrapper; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; -import android.view.LayoutInflater; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AdapterView; import android.widget.BaseAdapter; @@ -54,7 +55,7 @@ public class MenuBuilder implements Menu { private static final String LOGTAG = "MenuBuilder"; /** The number of different menu types */ - public static final int NUM_TYPES = 3; + public static final int NUM_TYPES = 5; /** The menu type that represents the icon menu view */ public static final int TYPE_ICON = 0; /** The menu type that represents the expanded menu view */ @@ -65,14 +66,24 @@ public class MenuBuilder implements Menu { * have an ItemView. */ public static final int TYPE_DIALOG = 2; + /** + * The menu type that represents a button in the application's action bar. + */ + public static final int TYPE_ACTION_BUTTON = 3; + /** + * The menu type that represents a menu popup. + */ + public static final int TYPE_POPUP = 4; private static final String VIEWS_TAG = "android:views"; - + // Order must be the same order as the TYPE_* static final int THEME_RES_FOR_TYPE[] = new int[] { com.android.internal.R.style.Theme_IconMenu, com.android.internal.R.style.Theme_ExpandedMenu, 0, + 0, + 0, }; // Order must be the same order as the TYPE_* @@ -80,6 +91,8 @@ public class MenuBuilder implements Menu { com.android.internal.R.layout.icon_menu_layout, com.android.internal.R.layout.expanded_menu_layout, 0, + com.android.internal.R.layout.action_menu_layout, + 0, }; // Order must be the same order as the TYPE_* @@ -87,6 +100,8 @@ public class MenuBuilder implements Menu { com.android.internal.R.layout.icon_menu_item_layout, com.android.internal.R.layout.list_menu_item_layout, com.android.internal.R.layout.list_menu_item_layout, + com.android.internal.R.layout.action_menu_item_layout, + com.android.internal.R.layout.list_menu_item_layout, }; private static final int[] sCategoryToOrder = new int[] { @@ -130,6 +145,24 @@ public class MenuBuilder implements Menu { * fetched from {@link #getVisibleItems()} */ private boolean mIsVisibleItemsStale; + + /** + * Contains only the items that should appear in the Action Bar, if present. + */ + private ArrayList<MenuItemImpl> mActionItems; + /** + * Contains items that should NOT appear in the Action Bar, if present. + */ + private ArrayList<MenuItemImpl> mNonActionItems; + /** + * The number of visible action buttons permitted in this menu + */ + private int mMaxActionItems; + /** + * Whether or not the items (or any one item's action state) has changed since it was + * last fetched. + */ + private boolean mIsActionItemsStale; /** * Current use case is Context Menus: As Views populate the context menu, each one has @@ -281,6 +314,10 @@ public class MenuBuilder implements Menu { mVisibleItems = new ArrayList<MenuItemImpl>(); mIsVisibleItemsStale = true; + mActionItems = new ArrayList<MenuItemImpl>(); + mNonActionItems = new ArrayList<MenuItemImpl>(); + mIsActionItemsStale = true; + mShortcutsVisible = (mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS); } @@ -900,6 +937,7 @@ public class MenuBuilder implements Menu { private void onItemsChanged(boolean cleared) { if (!mPreventDispatchingItemsChanged) { if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true; + if (mIsActionItemsStale == false) mIsActionItemsStale = true; MenuType[] menuTypes = mMenuTypes; for (int i = 0; i < NUM_TYPES; i++) { @@ -920,6 +958,15 @@ public class MenuBuilder implements Menu { onItemsChanged(false); } + /** + * Called by {@link MenuItemImpl} when its action request status is changed. + * @param item The item that has gone through a change in action request status. + */ + void onItemActionRequestChanged(MenuItemImpl item) { + // Notify of items being changed + onItemsChanged(false); + } + ArrayList<MenuItemImpl> getVisibleItems() { if (!mIsVisibleItemsStale) return mVisibleItems; @@ -934,9 +981,64 @@ public class MenuBuilder implements Menu { } mIsVisibleItemsStale = false; + mIsActionItemsStale = true; return mVisibleItems; } + + private void flagActionItems() { + if (!mIsActionItemsStale) { + return; + } + + final ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); + final int itemsSize = visibleItems.size(); + int maxActions = mMaxActionItems; + + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.requiresActionButton()) { + maxActions--; + } + } + + // Flag as many more requested items as will fit. + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.requestsActionButton()) { + item.setIsActionButton(maxActions > 0); + maxActions--; + } + } + + mActionItems.clear(); + mNonActionItems.clear(); + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.isActionButton()) { + mActionItems.add(item); + } else { + mNonActionItems.add(item); + } + } + + mIsActionItemsStale = false; + } + + ArrayList<MenuItemImpl> getActionItems() { + flagActionItems(); + return mActionItems; + } + + ArrayList<MenuItemImpl> getNonActionItems() { + flagActionItems(); + return mNonActionItems; + } + + void setMaxActionItems(int maxActionItems) { + mMaxActionItems = maxActionItems; + mIsActionItemsStale = true; + } public void clearHeader() { mHeaderIcon = null; @@ -1155,7 +1257,19 @@ public class MenuBuilder implements Menu { } public View getView(int position, View convertView, ViewGroup parent) { - return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent); + if (convertView != null) { + MenuView.ItemView itemView = (MenuView.ItemView) convertView; + itemView.getItemData().setItemView(mMenuType, null); + + MenuItemImpl item = (MenuItemImpl) getItem(position); + itemView.initialize(item, mMenuType); + item.setItemView(mMenuType, itemView); + return convertView; + } else { + MenuItemImpl item = (MenuItemImpl) getItem(position); + item.setItemView(mMenuType, null); + return item.getItemView(mMenuType, parent); + } } } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 9b58205..fecbd77 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -74,6 +74,9 @@ public final class MenuItemImpl implements MenuItem { private static final int EXCLUSIVE = 0x00000004; private static final int HIDDEN = 0x00000008; private static final int ENABLED = 0x00000010; + private static final int IS_ACTION = 0x00000020; + + private int mShowAsAction = SHOW_AS_ACTION_NEVER; /** Used for the icon resource ID if this item does not have an icon */ static final int NO_ICON = 0; @@ -580,6 +583,10 @@ public final class MenuItemImpl implements MenuItem { return (View) mItemViews[menuType].get(); } + void setItemView(int menuType, ItemView view) { + mItemViews[menuType] = new WeakReference<ItemView>(view); + } + /** * Create and initializes a menu item view that implements {@link MenuView.ItemView}. * @param menuType The type of menu to get a View for (must be one of @@ -628,6 +635,34 @@ public final class MenuItemImpl implements MenuItem { * @return Whether the given menu type should show icons for menu items. */ public boolean shouldShowIcon(int menuType) { - return menuType == MenuBuilder.TYPE_ICON || mMenu.getOptionalIconsVisible(); + return menuType == MenuBuilder.TYPE_ICON || + menuType == MenuBuilder.TYPE_ACTION_BUTTON || + menuType == MenuBuilder.TYPE_POPUP || + mMenu.getOptionalIconsVisible(); + } + + public boolean isActionButton() { + return (mFlags & IS_ACTION) == IS_ACTION || requiresActionButton(); + } + + public boolean requestsActionButton() { + return mShowAsAction == SHOW_AS_ACTION_IF_ROOM; + } + + public boolean requiresActionButton() { + return mShowAsAction == SHOW_AS_ACTION_ALWAYS; + } + + public void setIsActionButton(boolean isActionButton) { + if (isActionButton) { + mFlags |= IS_ACTION; + } else { + mFlags &= ~IS_ACTION; + } + } + + public void setShowAsAction(int actionEnum) { + mShowAsAction = actionEnum; + mMenu.onItemActionRequestChanged(this); } } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java new file mode 100644 index 0000000..751ecda --- /dev/null +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -0,0 +1,91 @@ +/* + * 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.menu; + +import com.android.internal.view.menu.MenuBuilder.MenuAdapter; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.View.MeasureSpec; +import android.widget.AdapterView; +import android.widget.ListPopupWindow; + +/** + * @hide + */ +public class MenuPopupHelper implements AdapterView.OnItemClickListener { + private static final String TAG = "MenuPopupHelper"; + + private Context mContext; + private ListPopupWindow mPopup; + private SubMenuBuilder mSubMenu; + private int mPopupMaxWidth; + + public MenuPopupHelper(Context context, SubMenuBuilder subMenu) { + mContext = context; + mSubMenu = subMenu; + + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mPopupMaxWidth = metrics.widthPixels / 2; + } + + public void show() { + // TODO Use a style from the theme here + mPopup = new ListPopupWindow(mContext, null, 0, + com.android.internal.R.style.Widget_Spinner); + mPopup.setOnItemClickListener(this); + + final MenuAdapter adapter = mSubMenu.getMenuAdapter(MenuBuilder.TYPE_POPUP); + mPopup.setAdapter(adapter); + mPopup.setModal(true); + + final MenuItemImpl itemImpl = (MenuItemImpl) mSubMenu.getItem(); + final View anchorView = itemImpl.getItemView(MenuBuilder.TYPE_ACTION_BUTTON, null); + mPopup.setAnchorView(anchorView); + + mPopup.setContentWidth(Math.min(measureContentWidth(adapter), mPopupMaxWidth)); + mPopup.show(); + } + + public void dismiss() { + mPopup.dismiss(); + mPopup = null; + } + + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + mSubMenu.performItemAction(mSubMenu.getItem(position), 0); + mPopup.dismiss(); + } + + private int measureContentWidth(MenuAdapter adapter) { + // Menus don't tend to be long, so this is more sane than it looks. + int width = 0; + View itemView = null; + final int widthMeasureSpec = + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int heightMeasureSpec = + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + itemView = adapter.getView(i, itemView, null); + itemView.measure(widthMeasureSpec, heightMeasureSpec); + width = Math.max(width, itemView.getMeasuredWidth()); + } + return width; + } +} diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java new file mode 100644 index 0000000..b57b7a8 --- /dev/null +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -0,0 +1,309 @@ +/* + * 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.widget; + +import com.android.internal.R; +import com.android.internal.app.ActionBarImpl; + +import android.app.ActionBar; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.MeasureSpec; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @hide + */ +public class ActionBarContextView extends ViewGroup { + // TODO: This must be defined in the default theme + private static final int CONTENT_HEIGHT_DIP = 50; + + private int mItemPadding; + private int mItemMargin; + private int mContentHeight; + + private CharSequence mTitle; + private CharSequence mSubtitle; + + private ImageButton mCloseButton; + private View mCustomView; + private LinearLayout mTitleLayout; + private TextView mTitleView; + private TextView mSubtitleView; + private Drawable mCloseDrawable; + + public ActionBarContextView(Context context) { + this(context, null, 0); + } + + public ActionBarContextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActionBarContextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Theme); + mItemPadding = a.getDimensionPixelOffset( + com.android.internal.R.styleable.Theme_actionButtonPadding, 0); + setBackgroundDrawable(a.getDrawable( + com.android.internal.R.styleable.Theme_actionBarContextBackground)); + mCloseDrawable = a.getDrawable( + com.android.internal.R.styleable.Theme_actionBarCloseContextDrawable); + mItemMargin = mItemPadding / 2; + + mContentHeight = CONTENT_HEIGHT_DIP; + a.recycle(); + } + + public void setCustomView(View view) { + if (mCustomView != null) { + removeView(mCustomView); + } + mCustomView = view; + if (mTitleLayout != null) { + removeView(mTitleLayout); + mTitleLayout = null; + } + if (view != null) { + addView(view); + } + requestLayout(); + } + + public void setTitle(CharSequence title) { + mTitle = title; + initTitle(); + } + + public void setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + initTitle(); + } + + public CharSequence getTitle() { + return mTitle; + } + + public CharSequence getSubtitle() { + return mSubtitle; + } + + private void initTitle() { + if (mTitleLayout == null) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item, null); + mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title); + mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle); + if (mTitle != null) { + mTitleView.setText(mTitle); + } + if (mSubtitle != null) { + mSubtitleView.setText(mSubtitle); + } + addView(mTitleLayout); + } else { + mTitleView.setText(mTitle); + mSubtitleView.setText(mSubtitle); + if (mTitleLayout.getParent() == null) { + addView(mTitleLayout); + } + } + } + + public void initForMode(final ActionBar.ContextMode mode) { + final ActionBarImpl.ContextMode implMode = (ActionBarImpl.ContextMode) mode; + + if (mCloseButton == null) { + mCloseButton = new ImageButton(getContext()); + mCloseButton.setImageDrawable(mCloseDrawable); + mCloseButton.setBackgroundDrawable(null); + mCloseButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mode.finish(); + } + }); + } + addView(mCloseButton); + + final Context context = getContext(); + final Menu menu = mode.getMenu(); + final int itemCount = menu.size(); + for (int i = 0; i < itemCount; i++) { + final MenuItem item = menu.getItem(i); + final ImageButton button = new ImageButton(context, null, + com.android.internal.R.attr.actionButtonStyle); + button.setClickable(true); + button.setFocusable(true); + button.setImageDrawable(item.getIcon()); + button.setId(item.getItemId()); + button.setVisibility(item.isVisible() ? VISIBLE : GONE); + button.setEnabled(item.isEnabled()); + + button.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + implMode.dispatchOnContextItemClicked(item); + } + }); + + addView(button); + } + requestLayout(); + } + + public void closeMode() { + removeAllViews(); + mCustomView = null; + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + // Used by custom views if they don't supply layout params. Everything else + // added to an ActionBarContextView should have them already. + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_width=\"match_parent\" (or fill_parent)"); + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode != MeasureSpec.AT_MOST) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_height=\"wrap_content\""); + } + + final int contentWidth = MeasureSpec.getSize(widthMeasureSpec); + final int itemMargin = mItemPadding; + + int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); + final int height = mContentHeight - getPaddingTop() - getPaddingBottom(); + final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + + if (mCloseButton != null) { + availableWidth = measureChildView(mCloseButton, availableWidth, + childSpecHeight, itemMargin); + } + + if (mTitleLayout != null && mCustomView == null) { + availableWidth = measureChildView(mTitleLayout, availableWidth, + childSpecHeight, itemMargin); + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child == mCloseButton || child == mTitleLayout || child == mCustomView) { + continue; + } + + availableWidth = measureChildView(child, availableWidth, childSpecHeight, itemMargin); + } + + if (mCustomView != null) { + LayoutParams lp = mCustomView.getLayoutParams(); + final int customWidthMode = lp.width != LayoutParams.WRAP_CONTENT ? + MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + final int customWidth = lp.width >= 0 ? + Math.min(lp.width, availableWidth) : availableWidth; + final int customHeightMode = lp.height != LayoutParams.WRAP_CONTENT ? + MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + final int customHeight = lp.height >= 0 ? + Math.min(lp.height, height) : height; + mCustomView.measure(MeasureSpec.makeMeasureSpec(customWidth, customWidthMode), + MeasureSpec.makeMeasureSpec(customHeight, customHeightMode)); + } + + setMeasuredDimension(contentWidth, mContentHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int x = getPaddingLeft(); + final int y = getPaddingTop(); + final int contentHeight = b - t - getPaddingTop() - getPaddingBottom(); + final int itemMargin = mItemPadding; + + if (mCloseButton != null && mCloseButton.getVisibility() != GONE) { + x += positionChild(mCloseButton, x, y, contentHeight); + } + + if (mTitleLayout != null && mCustomView == null) { + x += positionChild(mTitleLayout, x, y, contentHeight) + itemMargin; + } + + if (mCustomView != null) { + x += positionChild(mCustomView, x, y, contentHeight) + itemMargin; + } + + x = r - l - getPaddingRight(); + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child == mCloseButton || child == mTitleLayout || child == mCustomView) { + continue; + } + + x -= positionChildInverse(child, x, y, contentHeight) + itemMargin; + } + } + + private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { + child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + childSpecHeight); + + availableWidth -= child.getMeasuredWidth(); + availableWidth -= spacing; + + return availableWidth; + } + + private int positionChild(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x, childTop, x + childWidth, childTop + childHeight); + + return childWidth; + } + + private int positionChildInverse(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x - childWidth, childTop, x, childTop + childHeight); + + return childWidth; + } +} diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java new file mode 100644 index 0000000..fbff8ae --- /dev/null +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -0,0 +1,658 @@ +/* + * 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.widget; + +import com.android.internal.R; +import com.android.internal.view.menu.ActionMenuItem; +import com.android.internal.view.menu.ActionMenuView; +import com.android.internal.view.menu.MenuBuilder; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.ActionBar.NavigationCallback; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +/** + * @hide + */ +public class ActionBarView extends ViewGroup { + private static final String TAG = "ActionBarView"; + + // TODO: This must be defined in the default theme + private static final int CONTENT_HEIGHT_DIP = 50; + private static final int CONTENT_PADDING_DIP = 3; + private static final int CONTENT_SPACING_DIP = 6; + private static final int CONTENT_ACTION_SPACING_DIP = 12; + + /** + * Display options applied by default + */ + public static final int DISPLAY_DEFAULT = 0; + + /** + * Display options that require re-layout as opposed to a simple invalidate + */ + private static final int DISPLAY_RELAYOUT_MASK = + ActionBar.DISPLAY_HIDE_HOME | + ActionBar.DISPLAY_USE_LOGO; + + private final int mContentHeight; + + private int mNavigationMode; + private int mDisplayOptions; + private int mSpacing; + private int mActionSpacing; + private CharSequence mTitle; + private CharSequence mSubtitle; + private Drawable mIcon; + private Drawable mLogo; + + private ImageView mIconView; + private ImageView mLogoView; + private LinearLayout mTitleLayout; + private TextView mTitleView; + private TextView mSubtitleView; + private Spinner mSpinner; + private LinearLayout mTabLayout; + private View mCustomNavView; + + private boolean mShowMenu; + private boolean mUserTitle; + + private MenuBuilder mOptionsMenu; + private ActionMenuView mMenuView; + + private ActionMenuItem mLogoNavItem; + + private NavigationCallback mCallback; + + private final AdapterView.OnItemSelectedListener mNavItemSelectedListener = + new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mCallback != null) { + mCallback.onNavigationItemSelected(position, id); + } + } + public void onNothingSelected(AdapterView parent) { + // Do nothing + } + }; + + private OnClickListener mHomeClickListener = null; + + public ActionBarView(Context context, AttributeSet attrs) { + super(context, attrs); + + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mContentHeight = (int) (CONTENT_HEIGHT_DIP * metrics.density + 0.5f); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar); + + final int colorFilter = a.getColor(R.styleable.ActionBar_colorFilter, 0); + + if (colorFilter != 0) { + final Drawable d = getBackground(); + d.setDither(true); + d.setColorFilter(new PorterDuffColorFilter(colorFilter, PorterDuff.Mode.OVERLAY)); + } + + ApplicationInfo info = context.getApplicationInfo(); + PackageManager pm = context.getPackageManager(); + mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode, + ActionBar.NAVIGATION_MODE_STANDARD); + mTitle = a.getText(R.styleable.ActionBar_title); + mSubtitle = a.getText(R.styleable.ActionBar_subtitle); + mDisplayOptions = a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT); + + mLogo = a.getDrawable(R.styleable.ActionBar_logo); + if (mLogo == null) { + mLogo = info.loadLogo(pm); + } + mIcon = a.getDrawable(R.styleable.ActionBar_icon); + if (mIcon == null) { + mIcon = info.loadIcon(pm); + } + + Drawable background = a.getDrawable(R.styleable.ActionBar_background); + if (background != null) { + setBackgroundDrawable(background); + } + + final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0); + if (customNavId != 0) { + LayoutInflater inflater = LayoutInflater.from(context); + mCustomNavView = (View) inflater.inflate(customNavId, null); + mNavigationMode = ActionBar.NAVIGATION_MODE_CUSTOM; + } + + a.recycle(); + + // TODO: Set this in the theme + int padding = (int) (CONTENT_PADDING_DIP * metrics.density + 0.5f); + setPadding(padding, padding, padding, padding); + + mSpacing = (int) (CONTENT_SPACING_DIP * metrics.density + 0.5f); + mActionSpacing = (int) (CONTENT_ACTION_SPACING_DIP * metrics.density + 0.5f); + + if (mLogo != null || mIcon != null || mTitle != null) { + mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle); + mHomeClickListener = new OnClickListener() { + public void onClick(View v) { + Context context = getContext(); + if (context instanceof Activity) { + Activity activity = (Activity) context; + activity.onOptionsItemSelected(mLogoNavItem); + } + } + }; + } + } + + public void setCallback(NavigationCallback callback) { + mCallback = callback; + } + + public void setMenu(Menu menu) { + MenuBuilder builder = (MenuBuilder) menu; + mOptionsMenu = builder; + if (mMenuView != null) { + removeView(mMenuView); + } + final ActionMenuView menuView = (ActionMenuView) builder.getMenuView( + MenuBuilder.TYPE_ACTION_BUTTON, null); + mActionSpacing = menuView.getItemMargin(); + final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT); + menuView.setLayoutParams(layoutParams); + addView(menuView); + mMenuView = menuView; + } + + public void setCustomNavigationView(View view) { + mCustomNavView = view; + if (view != null) { + setNavigationMode(ActionBar.NAVIGATION_MODE_CUSTOM); + } + } + + public CharSequence getTitle() { + return mTitle; + } + + /** + * Set the action bar title. This will always replace or override window titles. + * @param title Title to set + * + * @see #setWindowTitle(CharSequence) + */ + public void setTitle(CharSequence title) { + mUserTitle = true; + setTitleImpl(title); + } + + /** + * Set the window title. A window title will always be replaced or overridden by a user title. + * @param title Title to set + * + * @see #setTitle(CharSequence) + */ + public void setWindowTitle(CharSequence title) { + if (!mUserTitle) { + setTitleImpl(title); + } + } + + private void setTitleImpl(CharSequence title) { + mTitle = title; + if (mTitleView != null) { + mTitleView.setText(title); + } + if (mLogoNavItem != null) { + mLogoNavItem.setTitle(title); + } + } + + public CharSequence getSubtitle() { + return mSubtitle; + } + + public void setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + if (mSubtitleView != null) { + mSubtitleView.setText(subtitle); + } + } + + public void setDisplayOptions(int options) { + final int flagsChanged = options ^ mDisplayOptions; + mDisplayOptions = options; + if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) { + final int vis = (options & ActionBar.DISPLAY_HIDE_HOME) != 0 ? GONE : VISIBLE; + if (mLogoView != null) { + mLogoView.setVisibility(vis); + } + if (mIconView != null) { + mIconView.setVisibility(vis); + } + + requestLayout(); + } else { + invalidate(); + } + } + + public void setNavigationMode(int mode) { + final int oldMode = mNavigationMode; + if (mode != oldMode) { + switch (oldMode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + if (mTitleLayout != null) { + removeView(mTitleLayout); + mTitleLayout = null; + mTitleView = null; + mSubtitleView = null; + } + break; + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + if (mSpinner != null) { + removeView(mSpinner); + mSpinner = null; + } + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mCustomNavView != null) { + removeView(mCustomNavView); + mCustomNavView = null; + } + break; + case ActionBar.NAVIGATION_MODE_TABS: + if (mTabLayout != null) { + removeView(mTabLayout); + mTabLayout = null; + } + } + + switch (mode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + initTitle(); + break; + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + mSpinner = new Spinner(mContext, null, + com.android.internal.R.attr.dropDownSpinnerStyle); + mSpinner.setOnItemSelectedListener(mNavItemSelectedListener); + addView(mSpinner); + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + addView(mCustomNavView); + break; + case ActionBar.NAVIGATION_MODE_TABS: + mTabLayout = new LinearLayout(getContext()); + addView(mTabLayout); + break; + } + mNavigationMode = mode; + requestLayout(); + } + } + + public void setDropdownAdapter(SpinnerAdapter adapter) { + mSpinner.setAdapter(adapter); + } + + public View getCustomNavigationView() { + return mCustomNavView; + } + + public int getNavigationMode() { + return mNavigationMode; + } + + public int getDisplayOptions() { + return mDisplayOptions; + } + + private TabView createTabView(ActionBar.Tab tab) { + final TabView tabView = new TabView(getContext(), tab); + tabView.setFocusable(true); + tabView.setOnClickListener(new TabClickListener()); + return tabView; + } + + public void addTab(ActionBar.Tab tab) { + final boolean isFirst = mTabLayout.getChildCount() == 0; + final TabView tabView = createTabView(tab); + mTabLayout.addView(tabView); + if (isFirst) { + tabView.setSelected(true); + } + } + + public void insertTab(ActionBar.Tab tab, int position) { + final boolean isFirst = mTabLayout.getChildCount() == 0; + final TabView tabView = createTabView(tab); + mTabLayout.addView(tabView, position); + if (isFirst) { + tabView.setSelected(true); + } + } + + public void removeTabAt(int position) { + mTabLayout.removeViewAt(position); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + // Used by custom nav views if they don't supply layout params. Everything else + // added to an ActionBarView should have them already. + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + if ((mDisplayOptions & ActionBar.DISPLAY_HIDE_HOME) == 0) { + if (mLogo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) { + mLogoView = new ImageView(getContext()); + mLogoView.setAdjustViewBounds(true); + mLogoView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + mLogoView.setImageDrawable(mLogo); + mLogoView.setClickable(true); + mLogoView.setFocusable(true); + mLogoView.setOnClickListener(mHomeClickListener); + addView(mLogoView); + } else if (mIcon != null) { + mIconView = new ImageView(getContext()); + mIconView.setAdjustViewBounds(true); + mIconView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + mIconView.setImageDrawable(mIcon); + mIconView.setClickable(true); + mIconView.setFocusable(true); + mIconView.setOnClickListener(mHomeClickListener); + addView(mIconView); + } + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + if (mLogoView == null) { + initTitle(); + } + break; + + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + throw new UnsupportedOperationException( + "Inflating dropdown list navigation isn't supported yet!"); + + case ActionBar.NAVIGATION_MODE_TABS: + throw new UnsupportedOperationException( + "Inflating tab navigation isn't supported yet!"); + + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mCustomNavView != null) { + addView(mCustomNavView); + } + break; + } + } + + private void initTitle() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item, null); + mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title); + mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle); + if (mTitle != null) { + mTitleView.setText(mTitle); + } + if (mSubtitle != null) { + mSubtitleView.setText(mSubtitle); + mSubtitleView.setVisibility(VISIBLE); + } + addView(mTitleLayout); + } + + public void setTabSelected(int position) { + final int tabCount = mTabLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + final View child = mTabLayout.getChildAt(i); + child.setSelected(i == position); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_width=\"match_parent\" (or fill_parent)"); + } + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode != MeasureSpec.AT_MOST) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_height=\"wrap_content\""); + } + + int contentWidth = MeasureSpec.getSize(widthMeasureSpec); + + int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); + final int height = mContentHeight - getPaddingTop() - getPaddingBottom(); + final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + + if (mLogoView != null && mLogoView.getVisibility() != GONE) { + availableWidth = measureChildView(mLogoView, availableWidth, childSpecHeight, mSpacing); + } + if (mIconView != null && mIconView.getVisibility() != GONE) { + availableWidth = measureChildView(mIconView, availableWidth, childSpecHeight, mSpacing); + } + + if (mMenuView != null) { + availableWidth = measureChildView(mMenuView, availableWidth, + childSpecHeight, 0); + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + if (mTitleLayout != null) { + measureChildView(mTitleLayout, availableWidth, childSpecHeight, mSpacing); + } + break; + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + if (mSpinner != null) { + mSpinner.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mCustomNavView != null) { + LayoutParams lp = mCustomNavView.getLayoutParams(); + final int customNavWidthMode = lp.width != LayoutParams.WRAP_CONTENT ? + MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + final int customNavWidth = lp.width >= 0 ? + Math.min(lp.width, availableWidth) : availableWidth; + final int customNavHeightMode = lp.height != LayoutParams.WRAP_CONTENT ? + MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + final int customNavHeight = lp.height >= 0 ? + Math.min(lp.height, height) : height; + mCustomNavView.measure( + MeasureSpec.makeMeasureSpec(customNavWidth, customNavWidthMode), + MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode)); + } + break; + case ActionBar.NAVIGATION_MODE_TABS: + if (mTabLayout != null) { + mTabLayout.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + break; + } + + setMeasuredDimension(contentWidth, mContentHeight); + } + + private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { + child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + childSpecHeight); + + availableWidth -= child.getMeasuredWidth(); + availableWidth -= spacing; + + return availableWidth; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int x = getPaddingLeft(); + final int y = getPaddingTop(); + final int contentHeight = b - t - getPaddingTop() - getPaddingBottom(); + + if (mLogoView != null && mLogoView.getVisibility() != GONE) { + x += positionChild(mLogoView, x, y, contentHeight) + mSpacing; + } + if (mIconView != null && mIconView.getVisibility() != GONE) { + x += positionChild(mIconView, x, y, contentHeight) + mSpacing; + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + if (mTitleLayout != null) { + x += positionChild(mTitleLayout, x, y, contentHeight) + mSpacing; + } + break; + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + if (mSpinner != null) { + x += positionChild(mSpinner, x, y, contentHeight) + mSpacing; + } + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mCustomNavView != null) { + x += positionChild(mCustomNavView, x, y, contentHeight) + mSpacing; + } + break; + case ActionBar.NAVIGATION_MODE_TABS: + if (mTabLayout != null) { + x += positionChild(mTabLayout, x, y, contentHeight) + mSpacing; + } + } + + x = r - l - getPaddingRight(); + + if (mMenuView != null) { + x -= positionChildInverse(mMenuView, x + mActionSpacing, y, contentHeight) + - mActionSpacing; + } + } + + private int positionChild(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x, childTop, x + childWidth, childTop + childHeight); + + return childWidth; + } + + private int positionChildInverse(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x - childWidth, childTop, x, childTop + childHeight); + + return childWidth; + } + + private static class TabView extends LinearLayout { + private ActionBar.Tab mTab; + + public TabView(Context context, ActionBar.Tab tab) { + super(context); + mTab = tab; + + // TODO Style tabs based on the theme + + final Drawable icon = tab.getIcon(); + final CharSequence text = tab.getText(); + + if (icon != null) { + ImageView iconView = new ImageView(context); + iconView.setImageDrawable(icon); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + iconView.setLayoutParams(lp); + addView(iconView); + } + + if (text != null) { + TextView textView = new TextView(context); + textView.setText(text); + textView.setSingleLine(); + textView.setEllipsize(TruncateAt.END); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + textView.setLayoutParams(lp); + addView(textView); + } + + setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, 1)); + } + + public ActionBar.Tab getTab() { + return mTab; + } + } + + private class TabClickListener implements OnClickListener { + public void onClick(View view) { + TabView tabView = (TabView) view; + tabView.getTab().select(); + final int tabCount = mTabLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + final View child = mTabLayout.getChildAt(i); + child.setSelected(child == view); + } + } + } +} diff --git a/core/java/com/android/internal/widget/ContactHeaderWidget.java b/core/java/com/android/internal/widget/ContactHeaderWidget.java deleted file mode 100644 index f421466..0000000 --- a/core/java/com/android/internal/widget/ContactHeaderWidget.java +++ /dev/null @@ -1,661 +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 com.android.internal.widget; - -import com.android.internal.R; - -import android.Manifest; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.SystemClock; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.RawContacts; -import android.provider.ContactsContract.StatusUpdates; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; -import android.widget.QuickContactBadge; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -/** - * Header used across system for displaying a title bar with contact info. You - * can bind specific values on the header, or use helper methods like - * {@link #bindFromContactId(long)} to populate asynchronously. - * <p> - * The parent must request the {@link Manifest.permission#READ_CONTACTS} - * permission to access contact data. - */ -public class ContactHeaderWidget extends FrameLayout implements View.OnClickListener { - - private static final String TAG = "ContactHeaderWidget"; - - private TextView mDisplayNameView; - private View mAggregateBadge; - private TextView mPhoneticNameView; - private CheckBox mStarredView; - private QuickContactBadge mPhotoView; - private ImageView mPresenceView; - private TextView mStatusView; - private TextView mStatusAttributionView; - private int mNoPhotoResource; - private QueryHandler mQueryHandler; - - protected Uri mContactUri; - - protected String[] mExcludeMimes = null; - - protected ContentResolver mContentResolver; - - /** - * Interface for callbacks invoked when the user interacts with a header. - */ - public interface ContactHeaderListener { - public void onPhotoClick(View view); - public void onDisplayNameClick(View view); - } - - private ContactHeaderListener mListener; - - - private interface ContactQuery { - //Projection used for the summary info in the header. - String[] COLUMNS = new String[] { - Contacts._ID, - Contacts.LOOKUP_KEY, - Contacts.PHOTO_ID, - Contacts.DISPLAY_NAME, - Contacts.PHONETIC_NAME, - Contacts.STARRED, - Contacts.CONTACT_PRESENCE, - Contacts.CONTACT_STATUS, - Contacts.CONTACT_STATUS_TIMESTAMP, - Contacts.CONTACT_STATUS_RES_PACKAGE, - Contacts.CONTACT_STATUS_LABEL, - }; - int _ID = 0; - int LOOKUP_KEY = 1; - int PHOTO_ID = 2; - int DISPLAY_NAME = 3; - int PHONETIC_NAME = 4; - //TODO: We need to figure out how we're going to get the phonetic name. - //static final int HEADER_PHONETIC_NAME_COLUMN_INDEX - int STARRED = 5; - int CONTACT_PRESENCE_STATUS = 6; - int CONTACT_STATUS = 7; - int CONTACT_STATUS_TIMESTAMP = 8; - int CONTACT_STATUS_RES_PACKAGE = 9; - int CONTACT_STATUS_LABEL = 10; - } - - private interface PhotoQuery { - String[] COLUMNS = new String[] { - Photo.PHOTO - }; - - int PHOTO = 0; - } - - //Projection used for looking up contact id from phone number - protected static final String[] PHONE_LOOKUP_PROJECTION = new String[] { - PhoneLookup._ID, - PhoneLookup.LOOKUP_KEY, - }; - protected static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0; - protected static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1; - - //Projection used for looking up contact id from email address - protected static final String[] EMAIL_LOOKUP_PROJECTION = new String[] { - RawContacts.CONTACT_ID, - Contacts.LOOKUP_KEY, - }; - protected static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0; - protected static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1; - - protected static final String[] CONTACT_LOOKUP_PROJECTION = new String[] { - Contacts._ID, - }; - protected static final int CONTACT_LOOKUP_ID_COLUMN_INDEX = 0; - - private static final int TOKEN_CONTACT_INFO = 0; - private static final int TOKEN_PHONE_LOOKUP = 1; - private static final int TOKEN_EMAIL_LOOKUP = 2; - private static final int TOKEN_PHOTO_QUERY = 3; - - public ContactHeaderWidget(Context context) { - this(context, null); - } - - public ContactHeaderWidget(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ContactHeaderWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mContentResolver = mContext.getContentResolver(); - - LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.contact_header, this); - - mDisplayNameView = (TextView) findViewById(R.id.name); - mAggregateBadge = findViewById(R.id.aggregate_badge); - - mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name); - - mStarredView = (CheckBox)findViewById(R.id.star); - mStarredView.setOnClickListener(this); - - mPhotoView = (QuickContactBadge) findViewById(R.id.photo); - - mPresenceView = (ImageView) findViewById(R.id.presence); - - mStatusView = (TextView)findViewById(R.id.status); - mStatusAttributionView = (TextView)findViewById(R.id.status_date); - - // Set the photo with a random "no contact" image - long now = SystemClock.elapsedRealtime(); - int num = (int) now & 0xf; - if (num < 9) { - // Leaning in from right, common - mNoPhotoResource = R.drawable.ic_contact_picture; - } else if (num < 14) { - // Leaning in from left uncommon - mNoPhotoResource = R.drawable.ic_contact_picture_2; - } else { - // Coming in from the top, rare - mNoPhotoResource = R.drawable.ic_contact_picture_3; - } - - resetAsyncQueryHandler(); - } - - public void enableClickListeners() { - mDisplayNameView.setOnClickListener(this); - mPhotoView.setOnClickListener(this); - } - - /** - * Set the given {@link ContactHeaderListener} to handle header events. - */ - public void setContactHeaderListener(ContactHeaderListener listener) { - mListener = listener; - } - - private void performPhotoClick() { - if (mListener != null) { - mListener.onPhotoClick(mPhotoView); - } - } - - private void performDisplayNameClick() { - if (mListener != null) { - mListener.onDisplayNameClick(mDisplayNameView); - } - } - - private class QueryHandler extends AsyncQueryHandler { - - public QueryHandler(ContentResolver cr) { - super(cr); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - try{ - if (this != mQueryHandler) { - Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!"); - return; - } - - switch (token) { - case TOKEN_PHOTO_QUERY: { - //Set the photo - Bitmap photoBitmap = null; - if (cursor != null && cursor.moveToFirst() - && !cursor.isNull(PhotoQuery.PHOTO)) { - byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO); - photoBitmap = BitmapFactory.decodeByteArray(photoData, 0, - photoData.length, null); - } - - if (photoBitmap == null) { - photoBitmap = loadPlaceholderPhoto(null); - } - mPhotoView.setImageBitmap(photoBitmap); - if (cookie != null && cookie instanceof Uri) { - mPhotoView.assignContactUri((Uri) cookie); - } - invalidate(); - break; - } - case TOKEN_CONTACT_INFO: { - if (cursor != null && cursor.moveToFirst()) { - bindContactInfo(cursor); - Uri lookupUri = Contacts.getLookupUri(cursor.getLong(ContactQuery._ID), - cursor.getString(ContactQuery.LOOKUP_KEY)); - - final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); - - if (photoId == 0) { - mPhotoView.setImageBitmap(loadPlaceholderPhoto(null)); - if (cookie != null && cookie instanceof Uri) { - mPhotoView.assignContactUri((Uri) cookie); - } - invalidate(); - } else { - startPhotoQuery(photoId, lookupUri, - false /* don't reset query handler */); - } - } else { - // shouldn't really happen - setDisplayName(null, null); - setSocialSnippet(null); - setPhoto(loadPlaceholderPhoto(null)); - } - break; - } - case TOKEN_PHONE_LOOKUP: { - if (cursor != null && cursor.moveToFirst()) { - long contactId = cursor.getLong(PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX); - String lookupKey = cursor.getString( - PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX); - bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey), - false /* don't reset query handler */); - } else { - String phoneNumber = (String) cookie; - setDisplayName(phoneNumber, null); - setSocialSnippet(null); - setPhoto(loadPlaceholderPhoto(null)); - mPhotoView.assignContactFromPhone(phoneNumber, true); - } - break; - } - case TOKEN_EMAIL_LOOKUP: { - if (cursor != null && cursor.moveToFirst()) { - long contactId = cursor.getLong(EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX); - String lookupKey = cursor.getString( - EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX); - bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey), - false /* don't reset query handler */); - } else { - String emailAddress = (String) cookie; - setDisplayName(emailAddress, null); - setSocialSnippet(null); - setPhoto(loadPlaceholderPhoto(null)); - mPhotoView.assignContactFromEmail(emailAddress, true); - } - break; - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - } - - /** - * Turn on/off showing of the aggregate badge element. - */ - public void showAggregateBadge(boolean showBagde) { - mAggregateBadge.setVisibility(showBagde ? View.VISIBLE : View.GONE); - } - - /** - * Turn on/off showing of the star element. - */ - public void showStar(boolean showStar) { - mStarredView.setVisibility(showStar ? View.VISIBLE : View.GONE); - } - - /** - * Manually set the starred state of this header widget. This doesn't change - * the underlying {@link Contacts} value, only the UI state. - */ - public void setStared(boolean starred) { - mStarredView.setChecked(starred); - } - - /** - * Manually set the presence. - */ - public void setPresence(int presence) { - mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence)); - } - - /** - * Manually set the contact uri - */ - public void setContactUri(Uri uri) { - setContactUri(uri, true); - } - - /** - * Manually set the contact uri - */ - public void setContactUri(Uri uri, boolean sendToFastrack) { - mContactUri = uri; - if (sendToFastrack) { - mPhotoView.assignContactUri(uri); - } - } - - /** - * Manually set the photo to display in the header. This doesn't change the - * underlying {@link Contacts}, only the UI state. - */ - public void setPhoto(Bitmap bitmap) { - mPhotoView.setImageBitmap(bitmap); - } - - /** - * Manually set the display name and phonetic name to show in the header. - * This doesn't change the underlying {@link Contacts}, only the UI state. - */ - public void setDisplayName(CharSequence displayName, CharSequence phoneticName) { - mDisplayNameView.setText(displayName); - if (!TextUtils.isEmpty(phoneticName)) { - mPhoneticNameView.setText(phoneticName); - mPhoneticNameView.setVisibility(View.VISIBLE); - } else { - mPhoneticNameView.setVisibility(View.GONE); - } - } - - /** - * Manually set the social snippet text to display in the header. - */ - public void setSocialSnippet(CharSequence snippet) { - if (snippet == null) { - mStatusView.setVisibility(View.GONE); - mStatusAttributionView.setVisibility(View.GONE); - } else { - mStatusView.setText(snippet); - mStatusView.setVisibility(View.VISIBLE); - } - } - - /** - * 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} - * profile icon. - */ - public void setExcludeMimes(String[] excludeMimes) { - mExcludeMimes = excludeMimes; - mPhotoView.setExcludeMimes(excludeMimes); - } - - /** - * Convenience method for binding all available data from an existing - * contact. - * - * @param contactLookupUri a {Contacts.CONTENT_LOOKUP_URI} style URI. - */ - public void bindFromContactLookupUri(Uri contactLookupUri) { - bindFromContactUriInternal(contactLookupUri, true /* reset query handler */); - } - - /** - * Convenience method for binding all available data from an existing - * contact. - * - * @param contactUri a {Contacts.CONTENT_URI} style URI. - * @param resetQueryHandler whether to use a new AsyncQueryHandler or not. - */ - private void bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler) { - mContactUri = contactUri; - startContactQuery(contactUri, resetQueryHandler); - } - - /** - * Convenience method for binding all available data from an existing - * contact. - * - * @param emailAddress The email address used to do a reverse lookup in - * the contacts database. If more than one contact contains this email - * address, one of them will be chosen to bind to. - */ - public void bindFromEmail(String emailAddress) { - resetAsyncQueryHandler(); - - mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, emailAddress, - Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)), - EMAIL_LOOKUP_PROJECTION, null, null, null); - } - - /** - * Convenience method for binding all available data from an existing - * contact. - * - * @param number The phone number used to do a reverse lookup in - * the contacts database. If more than one contact contains this phone - * number, one of them will be chosen to bind to. - */ - public void bindFromPhoneNumber(String number) { - resetAsyncQueryHandler(); - - mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, number, - Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), - PHONE_LOOKUP_PROJECTION, null, null, null); - } - - /** - * startContactQuery - * - * internal method to query contact by Uri. - * - * @param contactUri the contact uri - * @param resetQueryHandler whether to use a new AsyncQueryHandler or not - */ - private void startContactQuery(Uri contactUri, boolean resetQueryHandler) { - if (resetQueryHandler) { - resetAsyncQueryHandler(); - } - - mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS, - null, null, null); - } - - /** - * startPhotoQuery - * - * internal method to query contact photo by photo id and uri. - * - * @param photoId the photo id. - * @param lookupKey the lookup uri. - * @param resetQueryHandler whether to use a new AsyncQueryHandler or not. - */ - protected void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) { - if (resetQueryHandler) { - resetAsyncQueryHandler(); - } - - mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey, - ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS, - null, null, null); - } - - /** - * Method to force this widget to forget everything it knows about the contact. - * We need to stop any existing async queries for phone, email, contact, and photos. - */ - public void wipeClean() { - resetAsyncQueryHandler(); - - setDisplayName(null, null); - setPhoto(loadPlaceholderPhoto(null)); - setSocialSnippet(null); - setPresence(0); - mContactUri = null; - mExcludeMimes = null; - } - - - private void resetAsyncQueryHandler() { - // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really - // need the old async queries to be cancelled, let's do it the hard way. - mQueryHandler = new QueryHandler(mContentResolver); - } - - /** - * Bind the contact details provided by the given {@link Cursor}. - */ - protected void bindContactInfo(Cursor c) { - final String displayName = c.getString(ContactQuery.DISPLAY_NAME); - final String phoneticName = c.getString(ContactQuery.PHONETIC_NAME); - this.setDisplayName(displayName, phoneticName); - - final boolean starred = c.getInt(ContactQuery.STARRED) != 0; - mStarredView.setChecked(starred); - - //Set the presence status - if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) { - int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS); - mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence)); - mPresenceView.setVisibility(View.VISIBLE); - } else { - mPresenceView.setVisibility(View.GONE); - } - - //Set the status update - String status = c.getString(ContactQuery.CONTACT_STATUS); - if (!TextUtils.isEmpty(status)) { - mStatusView.setText(status); - mStatusView.setVisibility(View.VISIBLE); - - CharSequence timestamp = null; - - if (!c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)) { - long date = c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP); - - // Set the date/time field by mixing relative and absolute - // times. - int flags = DateUtils.FORMAT_ABBREV_RELATIVE; - - timestamp = DateUtils.getRelativeTimeSpanString(date, - System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags); - } - - String label = null; - - if (!c.isNull(ContactQuery.CONTACT_STATUS_LABEL)) { - String resPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE); - int labelResource = c.getInt(ContactQuery.CONTACT_STATUS_LABEL); - Resources resources; - if (TextUtils.isEmpty(resPackage)) { - resources = getResources(); - } else { - PackageManager pm = getContext().getPackageManager(); - try { - resources = pm.getResourcesForApplication(resPackage); - } catch (NameNotFoundException e) { - Log.w(TAG, "Contact status update resource package not found: " - + resPackage); - resources = null; - } - } - - if (resources != null) { - try { - label = resources.getString(labelResource); - } catch (NotFoundException e) { - Log.w(TAG, "Contact status update resource not found: " + resPackage + "@" - + labelResource); - } - } - } - - CharSequence attribution; - if (timestamp != null && label != null) { - attribution = getContext().getString( - R.string.contact_status_update_attribution_with_date, - timestamp, label); - } else if (timestamp == null && label != null) { - attribution = getContext().getString( - R.string.contact_status_update_attribution, - label); - } else if (timestamp != null) { - attribution = timestamp; - } else { - attribution = null; - } - if (attribution != null) { - mStatusAttributionView.setText(attribution); - mStatusAttributionView.setVisibility(View.VISIBLE); - } else { - mStatusAttributionView.setVisibility(View.GONE); - } - } else { - mStatusView.setVisibility(View.GONE); - mStatusAttributionView.setVisibility(View.GONE); - } - } - - public void onClick(View view) { - switch (view.getId()) { - case R.id.star: { - // Toggle "starred" state - // Make sure there is a contact - if (mContactUri != null) { - final ContentValues values = new ContentValues(1); - values.put(Contacts.STARRED, mStarredView.isChecked()); - mContentResolver.update(mContactUri, values, null, null); - } - break; - } - case R.id.photo: { - performPhotoClick(); - break; - } - case R.id.name: { - performDisplayNameClick(); - break; - } - } - } - - private Bitmap loadPlaceholderPhoto(BitmapFactory.Options options) { - if (mNoPhotoResource == 0) { - return null; - } - return BitmapFactory.decodeResource(mContext.getResources(), - mNoPhotoResource, options); - } -} diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java index fa47ff6..23e2277 100644 --- a/core/java/com/android/internal/widget/DigitalClock.java +++ b/core/java/com/android/internal/widget/DigitalClock.java @@ -30,7 +30,7 @@ import android.provider.Settings; import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.View; -import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import java.text.DateFormatSymbols; @@ -39,7 +39,7 @@ import java.util.Calendar; /** * Displays the time */ -public class DigitalClock extends LinearLayout { +public class DigitalClock extends RelativeLayout { private final static String M12 = "h:mm"; private final static String M24 = "kk:mm"; diff --git a/core/java/com/android/internal/widget/EditStyledText.java b/core/java/com/android/internal/widget/EditStyledText.java deleted file mode 100644 index 82197c0..0000000 --- a/core/java/com/android/internal/widget/EditStyledText.java +++ /dev/null @@ -1,1663 +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 com.android.internal.widget; - -import java.io.InputStream; -import java.util.ArrayList; - -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RectShape; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.Html; -import android.text.Layout; -import android.text.Spannable; -import android.text.Spanned; -import android.text.method.ArrowKeyMovementMethod; -import android.text.style.AbsoluteSizeSpan; -import android.text.style.AlignmentSpan; -import android.text.style.CharacterStyle; -import android.text.style.ForegroundColorSpan; -import android.text.style.ImageSpan; -import android.text.style.ParagraphStyle; -import android.text.style.QuoteSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; - -/** - * EditStyledText extends EditText for managing the flow and status to edit - * the styled text. This manages the states and flows of editing, supports - * inserting image, import/export HTML. - */ -public class EditStyledText extends EditText { - - private static final String LOG_TAG = "EditStyledText"; - private static final boolean DBG = false; - - /** - * The modes of editing actions. - */ - /** The mode that no editing action is done. */ - public static final int MODE_NOTHING = 0; - /** The mode of copy. */ - public static final int MODE_COPY = 1; - /** The mode of paste. */ - public static final int MODE_PASTE = 2; - /** The mode of changing size. */ - public static final int MODE_SIZE = 3; - /** The mode of changing color. */ - public static final int MODE_COLOR = 4; - /** The mode of selection. */ - public static final int MODE_SELECT = 5; - /** The mode of changing alignment. */ - public static final int MODE_ALIGN = 6; - /** The mode of changing cut. */ - public static final int MODE_CUT = 7; - - /** - * The state of selection. - */ - /** The state that selection isn't started. */ - public static final int STATE_SELECT_OFF = 0; - /** The state that selection is started. */ - public static final int STATE_SELECT_ON = 1; - /** The state that selection is done, but not fixed. */ - public static final int STATE_SELECTED = 2; - /** The state that selection is done and not fixed. */ - public static final int STATE_SELECT_FIX = 3; - - /** - * The help message strings. - */ - public static final int HINT_MSG_NULL = 0; - public static final int HINT_MSG_COPY_BUF_BLANK = 1; - public static final int HINT_MSG_SELECT_START = 2; - public static final int HINT_MSG_SELECT_END = 3; - public static final int HINT_MSG_PUSH_COMPETE = 4; - - - /** - * The help message strings. - */ - public static final int DEFAULT_BACKGROUND_COLOR = 0x00FFFFFF; - - /** - * EditStyledTextInterface provides functions for notifying messages to - * calling class. - */ - public interface EditStyledTextNotifier { - public void notifyHintMsg(int msgId); - public void notifyStateChanged(int mode, int state); - } - - private EditStyledTextNotifier mESTInterface; - - /** - * EditStyledTextEditorManager manages the flow and status of each - * function for editing styled text. - */ - private EditorManager mManager; - private StyledTextConverter mConverter; - private StyledTextDialog mDialog; - private Drawable mDefaultBackground; - private int mBackgroundColor; - - /** - * EditStyledText extends EditText for managing flow of each editing - * action. - */ - public EditStyledText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - public EditStyledText(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public EditStyledText(Context context) { - super(context); - init(); - } - - /** - * Set Notifier. - */ - public void setNotifier(EditStyledTextNotifier estInterface) { - mESTInterface = estInterface; - } - - /** - * Set Builder for AlertDialog. - * - * @param builder - * Builder for opening Alert Dialog. - */ - public void setBuilder(Builder builder) { - mDialog.setBuilder(builder); - } - - /** - * Set Parameters for ColorAlertDialog. - * - * @param colortitle - * Title for Alert Dialog. - * @param colornames - * List of name of selecting color. - * @param colorints - * List of int of color. - */ - public void setColorAlertParams(CharSequence colortitle, - CharSequence[] colornames, CharSequence[] colorints) { - mDialog.setColorAlertParams(colortitle, colornames, colorints); - } - - /** - * Set Parameters for SizeAlertDialog. - * - * @param sizetitle - * Title for Alert Dialog. - * @param sizenames - * List of name of selecting size. - * @param sizedisplayints - * List of int of size displayed in TextView. - * @param sizesendints - * List of int of size exported to HTML. - */ - public void setSizeAlertParams(CharSequence sizetitle, - CharSequence[] sizenames, CharSequence[] sizedisplayints, - CharSequence[] sizesendints) { - mDialog.setSizeAlertParams(sizetitle, sizenames, sizedisplayints, - sizesendints); - } - - public void setAlignAlertParams(CharSequence aligntitle, - CharSequence[] alignnames) { - mDialog.setAlignAlertParams(aligntitle, alignnames); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mManager.isSoftKeyBlocked() && - event.getAction() == MotionEvent.ACTION_UP) { - cancelLongPress(); - } - final boolean superResult = super.onTouchEvent(event); - if (event.getAction() == MotionEvent.ACTION_UP) { - if (DBG) { - Log.d(LOG_TAG, "--- onTouchEvent"); - } - mManager.onCursorMoved(); - } - return superResult; - } - - /** - * Start editing. This function have to be called before other editing - * actions. - */ - public void onStartEdit() { - mManager.onStartEdit(); - } - - /** - * End editing. - */ - public void onEndEdit() { - mManager.onEndEdit(); - } - - /** - * Start "Copy" action. - */ - public void onStartCopy() { - mManager.onStartCopy(); - } - - /** - * Start "Cut" action. - */ - public void onStartCut() { - mManager.onStartCut(); - } - - /** - * Start "Paste" action. - */ - public void onStartPaste() { - mManager.onStartPaste(); - } - - /** - * Start changing "Size" action. - */ - public void onStartSize() { - mManager.onStartSize(); - } - - /** - * Start changing "Color" action. - */ - public void onStartColor() { - mManager.onStartColor(); - } - - /** - * Start changing "BackgroundColor" action. - */ - public void onStartBackgroundColor() { - mManager.onStartBackgroundColor(); - } - - /** - * Start changing "Alignment" action. - */ - public void onStartAlign() { - mManager.onStartAlign(); - } - - /** - * Start "Select" action. - */ - public void onStartSelect() { - mManager.onStartSelect(); - } - - /** - * Start "SelectAll" action. - */ - public void onStartSelectAll() { - mManager.onStartSelectAll(); - } - - /** - * Fix Selected Item. - */ - public void onFixSelectedItem() { - mManager.onFixSelectedItem(); - } - - /** - * InsertImage to TextView by using URI - * - * @param uri - * URI of the iamge inserted to TextView. - */ - public void onInsertImage(Uri uri) { - mManager.onInsertImage(uri); - } - - /** - * InsertImage to TextView by using resource ID - * - * @param resId - * Resource ID of the iamge inserted to TextView. - */ - public void onInsertImage(int resId) { - mManager.onInsertImage(resId); - } - - public void onInsertHorizontalLine() { - mManager.onInsertHorizontalLine(); - } - - public void onClearStyles() { - mManager.onClearStyles(); - } - /** - * Set Size of the Item. - * - * @param size - * The size of the Item. - */ - public void setItemSize(int size) { - mManager.setItemSize(size); - } - - /** - * Set Color of the Item. - * - * @param color - * The color of the Item. - */ - public void setItemColor(int color) { - mManager.setItemColor(color); - } - - /** - * Set Alignment of the Item. - * - * @param color - * The color of the Item. - */ - public void setAlignment(Layout.Alignment align) { - mManager.setAlignment(align); - } - - /** - * Set Background color of View. - * - * @param color - * The background color of view. - */ - @Override - public void setBackgroundColor(int color) { - super.setBackgroundColor(color); - mBackgroundColor = color; - } - - /** - * Set html to EditStyledText. - * - * @param html - * The html to be set. - */ - public void setHtml(String html) { - mConverter.SetHtml(html); - } - /** - * Check whether editing is started or not. - * - * @return Whether editing is started or not. - */ - public boolean isEditting() { - return mManager.isEditting(); - } - - /** - * Check whether styled text or not. - * - * @return Whether styled text or not. - */ - public boolean isStyledText() { - return mManager.isStyledText(); - } - /** - * Check whether SoftKey is Blocked or not. - * - * @return whether SoftKey is Blocked or not. - */ - public boolean isSoftKeyBlocked() { - return mManager.isSoftKeyBlocked(); - } - - /** - * Get the mode of the action. - * - * @return The mode of the action. - */ - public int getEditMode() { - return mManager.getEditMode(); - } - - /** - * Get the state of the selection. - * - * @return The state of the selection. - */ - public int getSelectState() { - return mManager.getSelectState(); - } - - @Override - public Bundle getInputExtras(boolean create) { - if (DBG) { - Log.d(LOG_TAG, "---getInputExtras"); - } - Bundle bundle = super.getInputExtras(create); - if (bundle != null) { - bundle = new Bundle(); - } - bundle.putBoolean("allowEmoji", true); - return bundle; - } - - /** - * Get the state of the selection. - * - * @return The state of the selection. - */ - public String getHtml() { - return mConverter.getHtml(); - } - - /** - * Get the state of the selection. - * - * @param uris - * The array of used uris. - * @return The state of the selection. - */ - public String getHtml(ArrayList<Uri> uris) { - mConverter.getUriArray(uris, getText()); - return mConverter.getHtml(); - } - - /** - * Get Background color of View. - * - * @return The background color of View. - */ - public int getBackgroundColor() { - return mBackgroundColor; - } - - /** - * Get Foreground color of View. - * - * @return The background color of View. - */ - public int getForeGroundColor(int pos) { - if (DBG) { - Log.d(LOG_TAG, "---getForeGroundColor: " + pos); - } - if (pos < 0 || pos > getText().length()) { - Log.e(LOG_TAG, "---getForeGroundColor: Illigal position."); - return DEFAULT_BACKGROUND_COLOR; - } else { - ForegroundColorSpan[] spans = - getText().getSpans(pos, pos, ForegroundColorSpan.class); - if (spans.length > 0) { - return spans[0].getForegroundColor(); - } else { - return DEFAULT_BACKGROUND_COLOR; - } - } - } - - /** - * Initialize members. - */ - private void init() { - if (DBG) { - Log.d(LOG_TAG, "--- init"); - } - requestFocus(); - mDefaultBackground = getBackground(); - mBackgroundColor = DEFAULT_BACKGROUND_COLOR; - mManager = new EditorManager(this); - mConverter = new StyledTextConverter(this); - mDialog = new StyledTextDialog(this); - setMovementMethod(new StyledTextArrowKeyMethod(mManager)); - mManager.blockSoftKey(); - mManager.unblockSoftKey(); - } - - /** - * Show Foreground Color Selecting Dialog. - */ - private void onShowForegroundColorAlert() { - mDialog.onShowForegroundColorAlertDialog(); - } - - /** - * Show Background Color Selecting Dialog. - */ - private void onShowBackgroundColorAlert() { - mDialog.onShowBackgroundColorAlertDialog(); - } - - /** - * Show Size Selecting Dialog. - */ - private void onShowSizeAlert() { - mDialog.onShowSizeAlertDialog(); - } - - /** - * Show Alignment Selecting Dialog. - */ - private void onShowAlignAlert() { - mDialog.onShowAlignAlertDialog(); - } - - /** - * Notify hint messages what action is expected to calling class. - * - * @param msgId - * Id of the hint message. - */ - private void setHintMessage(int msgId) { - if (mESTInterface != null) { - mESTInterface.notifyHintMsg(msgId); - } - } - - /** - * Notify the event that the mode and state are changed. - * - * @param mode - * Mode of the editing action. - * @param state - * Mode of the selection state. - */ - private void notifyStateChanged(int mode, int state) { - if (mESTInterface != null) { - mESTInterface.notifyStateChanged(mode, state); - } - } - - /** - * EditorManager manages the flow and status of editing actions. - */ - private class EditorManager { - private boolean mEditFlag = false; - private boolean mSoftKeyBlockFlag = false; - private int mMode = 0; - private int mState = 0; - private int mCurStart = 0; - private int mCurEnd = 0; - private EditStyledText mEST; - - EditorManager(EditStyledText est) { - mEST = est; - } - - public void onStartEdit() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartEdit"); - } - Log.d(LOG_TAG, "--- onstartedit:"); - handleResetEdit(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onEndEdit() { - if (DBG) { - Log.d(LOG_TAG, "--- onEndEdit"); - } - handleCancel(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartCopy() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartCopy"); - } - handleCopy(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartCut() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartCut"); - } - handleCut(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartPaste() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartPaste"); - } - handlePaste(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartSize() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartSize"); - } - handleSize(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartAlign() { - if (DBG) { - Log.d(LOG_TAG, "--- onStartAlignRight"); - } - handleAlign(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartColor() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickColor"); - } - handleColor(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartBackgroundColor() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickColor"); - } - mEST.onShowBackgroundColorAlert(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onStartSelect() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickSelect"); - } - mMode = MODE_SELECT; - if (mState == STATE_SELECT_OFF) { - handleSelect(); - } else { - unsetSelect(); - handleSelect(); - } - mEST.notifyStateChanged(mMode, mState); - } - - public void onCursorMoved() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickView"); - } - if (mState == STATE_SELECT_ON || mState == STATE_SELECTED) { - handleSelect(); - mEST.notifyStateChanged(mMode, mState); - } - } - - public void onStartSelectAll() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickSelectAll"); - } - handleSelectAll(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onFixSelectedItem() { - if (DBG) { - Log.d(LOG_TAG, "--- onClickComplete"); - } - handleComplete(); - mEST.notifyStateChanged(mMode, mState); - } - - public void onInsertImage(Uri uri) { - if (DBG) { - Log.d(LOG_TAG, "--- onInsertImage by URI: " + uri.getPath() - + "," + uri.toString()); - } - insertImageSpan(new ImageSpan(mEST.getContext(), uri)); - mEST.notifyStateChanged(mMode, mState); - } - - public void onInsertImage(int resID) { - if (DBG) { - Log.d(LOG_TAG, "--- onInsertImage by resID"); - } - insertImageSpan(new ImageSpan(mEST.getContext(), resID)); - mEST.notifyStateChanged(mMode, mState); - } - - public void onInsertHorizontalLine() { - if (DBG) { - Log.d(LOG_TAG, "--- onInsertHorizontalLine:"); - } - insertImageSpan(new HorizontalLineSpan(0xFF000000, mEST)); - mEST.notifyStateChanged(mMode, mState); - } - - public void onClearStyles() { - if (DBG) { - Log.d(LOG_TAG, "--- onClearStyles"); - } - Editable txt = mEST.getText(); - int len = txt.length(); - Object[] styles = txt.getSpans(0, len, Object.class); - for (Object style : styles) { - if (style instanceof ParagraphStyle || - style instanceof QuoteSpan || - style instanceof CharacterStyle) { - if (style instanceof ImageSpan) { - int start = txt.getSpanStart(style); - int end = txt.getSpanEnd(style); - txt.replace(start, end, ""); - } - txt.removeSpan(style); - } - } - mEST.setBackgroundDrawable(mEST.mDefaultBackground); - mEST.mBackgroundColor = DEFAULT_BACKGROUND_COLOR; - } - - public void setItemSize(int size) { - if (DBG) { - Log.d(LOG_TAG, "--- onClickSizeItem"); - } - if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) { - changeSizeSelectedText(size); - handleResetEdit(); - } - } - - public void setItemColor(int color) { - if (DBG) { - Log.d(LOG_TAG, "--- onClickColorItem"); - } - if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) { - changeColorSelectedText(color); - handleResetEdit(); - } - } - - public void setAlignment(Layout.Alignment align) { - if (DBG) { - Log.d(LOG_TAG, "--- onClickColorItem"); - } - if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) { - changeAlign(align); - handleResetEdit(); - } - } - - public boolean isEditting() { - return mEditFlag; - } - - /* If the style of the span is added, add check case for that style */ - public boolean isStyledText() { - Editable txt = mEST.getText(); - int len = txt.length(); - if (txt.getSpans(0, len -1, ParagraphStyle.class).length > 0 || - txt.getSpans(0, len -1, QuoteSpan.class).length > 0 || - txt.getSpans(0, len -1, CharacterStyle.class).length > 0 || - mEST.mBackgroundColor != DEFAULT_BACKGROUND_COLOR) { - return true; - } - return false; - } - - public boolean isSoftKeyBlocked() { - return mSoftKeyBlockFlag; - } - - public int getEditMode() { - return mMode; - } - - public int getSelectState() { - return mState; - } - - public int getSelectionStart() { - return mCurStart; - } - - public int getSelectionEnd() { - return mCurEnd; - } - - private void doNextHandle() { - if (DBG) { - Log.d(LOG_TAG, "--- doNextHandle: " + mMode + "," + mState); - } - switch (mMode) { - case MODE_COPY: - handleCopy(); - break; - case MODE_CUT: - handleCut(); - break; - case MODE_PASTE: - handlePaste(); - break; - case MODE_SIZE: - handleSize(); - break; - case MODE_COLOR: - handleColor(); - break; - case MODE_ALIGN: - handleAlign(); - break; - default: - break; - } - } - - private void handleCancel() { - if (DBG) { - Log.d(LOG_TAG, "--- handleCancel"); - } - mMode = MODE_NOTHING; - mState = STATE_SELECT_OFF; - mEditFlag = false; - Log.d(LOG_TAG, "--- handleCancel:" + mEST.getInputType()); - unblockSoftKey(); - unsetSelect(); - } - - private void handleComplete() { - if (DBG) { - Log.d(LOG_TAG, "--- handleComplete"); - } - if (!mEditFlag) { - return; - } - if (mState == STATE_SELECTED) { - mState = STATE_SELECT_FIX; - } - doNextHandle(); - } - - private void handleTextViewFunc(int mode, int id) { - if (DBG) { - Log.d(LOG_TAG, "--- handleTextView: " + mMode + "," + mState + - "," + id); - } - if (!mEditFlag) { - return; - } - if (mMode == MODE_NOTHING || mMode == MODE_SELECT) { - mMode = mode; - if (mState == STATE_SELECTED) { - mState = STATE_SELECT_FIX; - handleTextViewFunc(mode, id); - } else { - handleSelect(); - } - } else if (mMode != mode) { - handleCancel(); - mMode = mode; - handleTextViewFunc(mode, id); - } else if (mState == STATE_SELECT_FIX) { - mEST.onTextContextMenuItem(id); - handleResetEdit(); - } - } - - private void handleCopy() { - if (DBG) { - Log.d(LOG_TAG, "--- handleCopy: " + mMode + "," + mState); - } - handleTextViewFunc(MODE_COPY, android.R.id.copy); - } - - private void handleCut() { - if (DBG) { - Log.d(LOG_TAG, "--- handleCopy: " + mMode + "," + mState); - } - handleTextViewFunc(MODE_CUT, android.R.id.cut); - } - - private void handlePaste() { - if (DBG) { - Log.d(LOG_TAG, "--- handlePaste"); - } - if (!mEditFlag) { - return; - } - mEST.onTextContextMenuItem(android.R.id.paste); - } - - private void handleSetSpan(int mode) { - if (DBG) { - Log.d(LOG_TAG, "--- handleSetSpan:" + mEditFlag + "," - + mState + ',' + mMode); - } - if (!mEditFlag) { - Log.e(LOG_TAG, "--- handleSetSpan: Editing is not started."); - return; - } - if (mMode == MODE_NOTHING || mMode == MODE_SELECT) { - mMode = mode; - if (mState == STATE_SELECTED) { - mState = STATE_SELECT_FIX; - handleSetSpan(mode); - } else { - handleSelect(); - } - } else if (mMode != mode) { - handleCancel(); - mMode = mode; - handleSetSpan(mode); - } else { - if (mState == STATE_SELECT_FIX) { - mEST.setHintMessage(HINT_MSG_NULL); - switch (mode) { - case MODE_COLOR: - mEST.onShowForegroundColorAlert(); - break; - case MODE_SIZE: - mEST.onShowSizeAlert(); - break; - case MODE_ALIGN: - mEST.onShowAlignAlert(); - break; - default: - Log.e(LOG_TAG, "--- handleSetSpan: invalid mode."); - break; - } - } else { - Log.d(LOG_TAG, "--- handleSetSpan: do nothing."); - } - } - } - - private void handleSize() { - handleSetSpan(MODE_SIZE); - } - - private void handleColor() { - handleSetSpan(MODE_COLOR); - } - - private void handleAlign() { - handleSetSpan(MODE_ALIGN); - } - - private void handleSelect() { - if (DBG) { - Log.d(LOG_TAG, "--- handleSelect:" + mEditFlag + "," + mState); - } - if (!mEditFlag) { - return; - } - if (mState == STATE_SELECT_OFF) { - if (isTextSelected()) { - Log.e(LOG_TAG, "Selection is off, but selected"); - } - setSelectStartPos(); - blockSoftKey(); - mEST.setHintMessage(HINT_MSG_SELECT_END); - } else if (mState == STATE_SELECT_ON) { - if (isTextSelected()) { - Log.e(LOG_TAG, "Selection now start, but selected"); - } - setSelectedEndPos(); - mEST.setHintMessage(HINT_MSG_PUSH_COMPETE); - doNextHandle(); - } else if (mState == STATE_SELECTED) { - if (!isTextSelected()) { - Log.e(LOG_TAG, "Selection is done, but not selected"); - } - setSelectedEndPos(); - doNextHandle(); - } - } - - private void handleSelectAll() { - if (DBG) { - Log.d(LOG_TAG, "--- handleSelectAll"); - } - if (!mEditFlag) { - return; - } - mEST.selectAll(); - mState = STATE_SELECTED; - } - - private void handleResetEdit() { - if (DBG) { - Log.d(LOG_TAG, "Reset Editor"); - } - blockSoftKey(); - handleCancel(); - mEditFlag = true; - mEST.setHintMessage(HINT_MSG_SELECT_START); - } - - private void setSelection() { - if (DBG) { - Log.d(LOG_TAG, "--- onSelect:" + mCurStart + "," + mCurEnd); - } - if (mCurStart >= 0 && mCurStart <= mEST.getText().length() - && mCurEnd >= 0 && mCurEnd <= mEST.getText().length()) { - if (mCurStart < mCurEnd) { - mEST.setSelection(mCurStart, mCurEnd); - } else { - mEST.setSelection(mCurEnd, mCurStart); - } - mState = STATE_SELECTED; - } else { - Log.e(LOG_TAG, - "Select is on, but cursor positions are illigal.:" - + mEST.getText().length() + "," + mCurStart - + "," + mCurEnd); - } - } - - private void unsetSelect() { - if (DBG) { - Log.d(LOG_TAG, "--- offSelect"); - } - int currpos = mEST.getSelectionStart(); - mEST.setSelection(currpos, currpos); - mState = STATE_SELECT_OFF; - } - - private void setSelectStartPos() { - if (DBG) { - Log.d(LOG_TAG, "--- setSelectStartPos"); - } - mCurStart = mEST.getSelectionStart(); - mState = STATE_SELECT_ON; - } - - private void setSelectedEndPos() { - if (DBG) { - Log.d(LOG_TAG, "--- setSelectEndPos:"); - } - if (mEST.getSelectionStart() == mCurStart) { - setSelectedEndPos(mEST.getSelectionEnd()); - } else { - setSelectedEndPos(mEST.getSelectionStart()); - } - } - - public void setSelectedEndPos(int pos) { - if (DBG) { - Log.d(LOG_TAG, "--- setSelectedEndPos:"); - } - mCurEnd = pos; - setSelection(); - } - - private boolean isTextSelected() { - if (DBG) { - Log.d(LOG_TAG, "--- isTextSelected:" + mCurStart + "," - + mCurEnd); - } - return (mCurStart != mCurEnd) - && (mState == STATE_SELECTED || - mState == STATE_SELECT_FIX); - } - - private void setStyledTextSpan(Object span, int start, int end) { - if (DBG) { - Log.d(LOG_TAG, "--- setStyledTextSpan:" + mMode + "," - + start + "," + end); - } - if (start < end) { - mEST.getText().setSpan(span, start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - mEST.getText().setSpan(span, end, start, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - private void changeSizeSelectedText(int size) { - if (DBG) { - Log.d(LOG_TAG, "--- changeSize:" + size); - } - setStyledTextSpan(new AbsoluteSizeSpan(size), - mCurStart, mCurEnd); - } - - private void changeColorSelectedText(int color) { - if (DBG) { - Log.d(LOG_TAG, "--- changeColor:" + color); - } - setStyledTextSpan(new ForegroundColorSpan(color), - mCurStart, mCurEnd); - } - - private void changeAlign(Layout.Alignment align) { - if (DBG) { - Log.d(LOG_TAG, "--- changeAlign:" + align); - } - setStyledTextSpan(new AlignmentSpan.Standard(align), - findLineStart(mEST.getText(), mCurStart), - findLineEnd(mEST.getText(), mCurEnd)); - } - - private int findLineStart(Editable text, int current) { - if (DBG) { - Log.d(LOG_TAG, "--- findLineStart: curr:" + current + - ", length:" + text.length()); - } - int pos = current; - for (; pos > 0; pos--) { - if (text.charAt(pos - 1) == '\n') { - break; - } - } - return pos; - } - - private void insertImageSpan(ImageSpan span) { - if (DBG) { - Log.d(LOG_TAG, "--- insertImageSpan"); - } - if (span != null) { - Log.d(LOG_TAG, "--- insertimagespan:" + span.getDrawable().getIntrinsicHeight() + "," + span.getDrawable().getIntrinsicWidth()); - Log.d(LOG_TAG, "--- insertimagespan:" + span.getDrawable().getClass()); - int curpos = mEST.getSelectionStart(); - mEST.getText().insert(curpos, "\uFFFC"); - mEST.getText().setSpan(span, curpos, curpos + 1, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - mEST.notifyStateChanged(mMode, mState); - } else { - Log.e(LOG_TAG, "--- insertImageSpan: null span was inserted"); - } - } - - private int findLineEnd(Editable text, int current) { - if (DBG) { - Log.d(LOG_TAG, "--- findLineEnd: curr:" + current + - ", length:" + text.length()); - } - int pos = current; - for (; pos < text.length(); pos++) { - if (pos > 0 && text.charAt(pos - 1) == '\n') { - break; - } - } - return pos; - } - - private void blockSoftKey() { - if (DBG) { - Log.d(LOG_TAG, "--- blockSoftKey:"); - } - InputMethodManager imm = (InputMethodManager) mEST.getContext(). - getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mEST.getWindowToken(), 0); - mEST.setOnClickListener( - new OnClickListener() { - public void onClick(View v) { - Log.d(LOG_TAG, "--- ontrackballclick:"); - onFixSelectedItem(); - } - }); - mSoftKeyBlockFlag = true; - } - - private void unblockSoftKey() { - if (DBG) { - Log.d(LOG_TAG, "--- unblockSoftKey:"); - } - mEST.setOnClickListener(null); - mSoftKeyBlockFlag = false; - } - } - - private class StyledTextConverter { - private EditStyledText mEST; - - public StyledTextConverter(EditStyledText est) { - mEST = est; - } - - public String getHtml() { - String htmlBody = Html.toHtml(mEST.getText()); - if (DBG) { - Log.d(LOG_TAG, "--- getConvertedBody:" + htmlBody); - } - return htmlBody; - } - - public void getUriArray(ArrayList<Uri> uris, Editable text) { - uris.clear(); - if (DBG) { - Log.d(LOG_TAG, "--- getUriArray:"); - } - int len = text.length(); - int next; - for (int i = 0; i < text.length(); i = next) { - next = text.nextSpanTransition(i, len, ImageSpan.class); - ImageSpan[] images = text.getSpans(i, next, ImageSpan.class); - for (int j = 0; j < images.length; j++) { - if (DBG) { - Log.d(LOG_TAG, "--- getUriArray: foundArray" + - ((ImageSpan) images[j]).getSource()); - } - uris.add(Uri.parse( - ((ImageSpan) images[j]).getSource())); - } - } - } - - public void SetHtml (String html) { - final Spanned spanned = Html.fromHtml(html, new Html.ImageGetter() { - public Drawable getDrawable(String src) { - Log.d(LOG_TAG, "--- sethtml: src="+src); - if (src.startsWith("content://")) { - Uri uri = Uri.parse(src); - try { - InputStream is = mEST.getContext().getContentResolver().openInputStream(uri); - Bitmap bitmap = BitmapFactory.decodeStream(is); - Drawable drawable = new BitmapDrawable( - getContext().getResources(), bitmap); - drawable.setBounds(0, 0, - drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight()); - is.close(); - return drawable; - } catch (Exception e) { - Log.e(LOG_TAG, "--- set html: Failed to loaded content " + uri, e); - return null; - } - } - Log.d(LOG_TAG, " unknown src="+src); - return null; - } - }, null); - mEST.setText(spanned); - } - } - - private class StyledTextDialog { - Builder mBuilder; - CharSequence mColorTitle; - CharSequence mSizeTitle; - CharSequence mAlignTitle; - CharSequence[] mColorNames; - CharSequence[] mColorInts; - CharSequence[] mSizeNames; - CharSequence[] mSizeDisplayInts; - CharSequence[] mSizeSendInts; - CharSequence[] mAlignNames; - EditStyledText mEST; - - public StyledTextDialog(EditStyledText est) { - mEST = est; - } - - public void setBuilder(Builder builder) { - mBuilder = builder; - } - - public void setColorAlertParams(CharSequence colortitle, - CharSequence[] colornames, CharSequence[] colorints) { - mColorTitle = colortitle; - mColorNames = colornames; - mColorInts = colorints; - } - - public void setSizeAlertParams(CharSequence sizetitle, - CharSequence[] sizenames, CharSequence[] sizedisplayints, - CharSequence[] sizesendints) { - mSizeTitle = sizetitle; - mSizeNames = sizenames; - mSizeDisplayInts = sizedisplayints; - mSizeSendInts = sizesendints; - } - - public void setAlignAlertParams(CharSequence aligntitle, - CharSequence[] alignnames) { - mAlignTitle = aligntitle; - mAlignNames = alignnames; - } - - private boolean checkColorAlertParams() { - if (DBG) { - Log.d(LOG_TAG, "--- checkParams"); - } - if (mBuilder == null) { - Log.e(LOG_TAG, "--- builder is null."); - return false; - } else if (mColorTitle == null || mColorNames == null - || mColorInts == null) { - Log.e(LOG_TAG, "--- color alert params are null."); - return false; - } else if (mColorNames.length != mColorInts.length) { - Log.e(LOG_TAG, "--- the length of color alert params are " - + "different."); - return false; - } - return true; - } - - private boolean checkSizeAlertParams() { - if (DBG) { - Log.d(LOG_TAG, "--- checkParams"); - } - if (mBuilder == null) { - Log.e(LOG_TAG, "--- builder is null."); - return false; - } else if (mSizeTitle == null || mSizeNames == null - || mSizeDisplayInts == null || mSizeSendInts == null) { - Log.e(LOG_TAG, "--- size alert params are null."); - return false; - } else if (mSizeNames.length != mSizeDisplayInts.length - && mSizeSendInts.length != mSizeDisplayInts.length) { - Log.e(LOG_TAG, "--- the length of size alert params are " - + "different."); - return false; - } - return true; - } - - private boolean checkAlignAlertParams() { - if (DBG) { - Log.d(LOG_TAG, "--- checkAlignAlertParams"); - } - if (mBuilder == null) { - Log.e(LOG_TAG, "--- builder is null."); - return false; - } else if (mAlignTitle == null) { - Log.e(LOG_TAG, "--- align alert params are null."); - return false; - } - return true; - } - - private void onShowForegroundColorAlertDialog() { - if (DBG) { - Log.d(LOG_TAG, "--- onShowForegroundColorAlertDialog"); - } - if (!checkColorAlertParams()) { - return; - } - mBuilder.setTitle(mColorTitle); - mBuilder.setIcon(0); - mBuilder. - setItems(mColorNames, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - Log.d("EETVM", "mBuilder.onclick:" + which); - int color = Integer.parseInt( - (String) mColorInts[which], 16) - 0x01000000; - mEST.setItemColor(color); - } - }); - mBuilder.show(); - } - - private void onShowBackgroundColorAlertDialog() { - if (DBG) { - Log.d(LOG_TAG, "--- onShowBackgroundColorAlertDialog"); - } - if (!checkColorAlertParams()) { - return; - } - mBuilder.setTitle(mColorTitle); - mBuilder.setIcon(0); - mBuilder. - setItems(mColorNames, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - Log.d("EETVM", "mBuilder.onclick:" + which); - int color = Integer.parseInt( - (String) mColorInts[which], 16) - 0x01000000; - mEST.setBackgroundColor(color); - } - }); - mBuilder.show(); - } - - private void onShowSizeAlertDialog() { - if (DBG) { - Log.d(LOG_TAG, "--- onShowSizeAlertDialog"); - } - if (!checkSizeAlertParams()) { - return; - } - mBuilder.setTitle(mSizeTitle); - mBuilder.setIcon(0); - mBuilder. - setItems(mSizeNames, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - Log.d(LOG_TAG, "mBuilder.onclick:" + which); - int size = Integer - .parseInt((String) mSizeDisplayInts[which]); - mEST.setItemSize(size); - } - }); - mBuilder.show(); - } - - private void onShowAlignAlertDialog() { - if (DBG) { - Log.d(LOG_TAG, "--- onShowAlignAlertDialog"); - } - if (!checkAlignAlertParams()) { - return; - } - mBuilder.setTitle(mAlignTitle); - mBuilder.setIcon(0); - mBuilder. - setItems(mAlignNames, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - Log.d(LOG_TAG, "mBuilder.onclick:" + which); - Layout.Alignment align = Layout.Alignment.ALIGN_NORMAL; - switch (which) { - case 0: - align = Layout.Alignment.ALIGN_NORMAL; - break; - case 1: - align = Layout.Alignment.ALIGN_CENTER; - break; - case 2: - align = Layout.Alignment.ALIGN_OPPOSITE; - break; - default: - break; - } - mEST.setAlignment(align); - } - }); - mBuilder.show(); - } - } - - private class StyledTextArrowKeyMethod extends ArrowKeyMovementMethod { - EditorManager mManager; - StyledTextArrowKeyMethod(EditorManager manager) { - super(); - mManager = manager; - } - - @Override - public boolean onKeyDown(TextView widget, Spannable buffer, - int keyCode, KeyEvent event) { - if (!mManager.isSoftKeyBlocked()) { - return super.onKeyDown(widget, buffer, keyCode, event); - } - if (executeDown(widget, buffer, keyCode)) { - return true; - } - return false; - } - - private int getEndPos(TextView widget) { - int end; - if (widget.getSelectionStart() == mManager.getSelectionStart()) { - end = widget.getSelectionEnd(); - } else { - end = widget.getSelectionStart(); - } - return end; - } - - private boolean up(TextView widget, Spannable buffer) { - if (DBG) { - Log.d(LOG_TAG, "--- up:"); - } - Layout layout = widget.getLayout(); - int end = getEndPos(widget); - int line = layout.getLineForOffset(end); - if (line > 0) { - int to; - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line - 1)) { - float h = layout.getPrimaryHorizontal(end); - to = layout.getOffsetForHorizontal(line - 1, h); - } else { - to = layout.getLineStart(line - 1); - } - mManager.setSelectedEndPos(to); - mManager.onCursorMoved(); - return true; - } - return false; - } - - private boolean down(TextView widget, Spannable buffer) { - if (DBG) { - Log.d(LOG_TAG, "--- down:"); - } - Layout layout = widget.getLayout(); - int end = getEndPos(widget); - int line = layout.getLineForOffset(end); - if (line < layout.getLineCount() - 1) { - int to; - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line + 1)) { - float h = layout.getPrimaryHorizontal(end); - to = layout.getOffsetForHorizontal(line + 1, h); - } else { - to = layout.getLineStart(line + 1); - } - mManager.setSelectedEndPos(to); - mManager.onCursorMoved(); - return true; - } - return false; - } - - private boolean left(TextView widget, Spannable buffer) { - if (DBG) { - Log.d(LOG_TAG, "--- left:"); - } - Layout layout = widget.getLayout(); - int to = layout.getOffsetToLeftOf(getEndPos(widget)); - mManager.setSelectedEndPos(to); - mManager.onCursorMoved(); - return true; - } - - private boolean right(TextView widget, Spannable buffer) { - if (DBG) { - Log.d(LOG_TAG, "--- right:"); - } - Layout layout = widget.getLayout(); - int to = layout.getOffsetToRightOf(getEndPos(widget)); - mManager.setSelectedEndPos(to); - mManager.onCursorMoved(); - return true; - } - - private boolean executeDown(TextView widget, Spannable buffer, - int keyCode) { - if (DBG) { - Log.d(LOG_TAG, "--- executeDown: " + keyCode); - } - boolean handled = false; - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - handled |= up(widget, buffer); - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - handled |= down(widget, buffer); - break; - case KeyEvent.KEYCODE_DPAD_LEFT: - handled |= left(widget, buffer); - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - handled |= right(widget, buffer); - break; - case KeyEvent.KEYCODE_DPAD_CENTER: - mManager.onFixSelectedItem(); - handled = true; - break; - } - return handled; - } - } - - public class HorizontalLineSpan extends ImageSpan { - public HorizontalLineSpan(int color, View view) { - super(new HorizontalLineDrawable(color, view)); - } - } - public class HorizontalLineDrawable extends ShapeDrawable { - private View mView; - public HorizontalLineDrawable(int color, View view) { - super(new RectShape()); - mView = view; - renewColor(color); - renewBounds(view); - } - @Override - public void draw(Canvas canvas) { - if (DBG) { - Log.d(LOG_TAG, "--- draw:"); - } - renewColor(); - renewBounds(mView); - super.draw(canvas); - } - - private void renewBounds(View view) { - if (DBG) { - int width = mView.getBackground().getBounds().width(); - int height = mView.getBackground().getBounds().height(); - Log.d(LOG_TAG, "--- renewBounds:" + width + "," + height); - Log.d(LOG_TAG, "--- renewBounds:" + mView.getClass()); - } - int width = mView.getWidth(); - if (width > 20) { - width -= 20; - } - setBounds(0, 0, width, 2); - } - private void renewColor(int color) { - if (DBG) { - Log.d(LOG_TAG, "--- renewColor:" + color); - } - getPaint().setColor(color); - } - private void renewColor() { - if (DBG) { - Log.d(LOG_TAG, "--- renewColor:"); - } - if (mView instanceof View) { - ImageSpan parent = getParentSpan(); - Editable text = ((EditStyledText)mView).getText(); - int start = text.getSpanStart(parent); - ForegroundColorSpan[] spans = text.getSpans(start, start, ForegroundColorSpan.class); - if (spans.length > 0) { - renewColor(spans[spans.length - 1].getForegroundColor()); - } - } - } - private ImageSpan getParentSpan() { - if (DBG) { - Log.d(LOG_TAG, "--- getParentSpan:"); - } - if (mView instanceof EditStyledText) { - Editable text = ((EditStyledText)mView).getText(); - ImageSpan[] images = text.getSpans(0, text.length(), ImageSpan.class); - if (images.length > 0) { - for (ImageSpan image: images) { - if (image.getDrawable() == this) { - return image; - } - } - } - } - Log.e(LOG_TAG, "---renewBounds: Couldn't find"); - return null; - } - } -} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index dbbd286..0b62a67 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -92,6 +92,8 @@ public class LockPatternUtils { public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type"; private final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt"; + private final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory"; + private final Context mContext; private final ContentResolver mContentResolver; private DevicePolicyManager mDevicePolicyManager; @@ -138,6 +140,33 @@ public class LockPatternUtils { return getDevicePolicyManager().getPasswordQuality(null); } + public int getRequestedPasswordHistoryLength() { + return getDevicePolicyManager().getPasswordHistoryLength(null); + } + + public int getRequestedPasswordMinimumLetters() { + return getDevicePolicyManager().getPasswordMinimumLetters(null); + } + + public int getRequestedPasswordMinimumUpperCase() { + return getDevicePolicyManager().getPasswordMinimumUpperCase(null); + } + + public int getRequestedPasswordMinimumLowerCase() { + return getDevicePolicyManager().getPasswordMinimumLowerCase(null); + } + + public int getRequestedPasswordMinimumNumeric() { + return getDevicePolicyManager().getPasswordMinimumNumeric(null); + } + + public int getRequestedPasswordMinimumSymbols() { + return getDevicePolicyManager().getPasswordMinimumSymbols(null); + } + + public int getRequestedPasswordMinimumNonLetter() { + return getDevicePolicyManager().getPasswordMinimumNonLetter(null); + } /** * Returns the actual password mode, as set by keyguard after updating the password. * @@ -202,8 +231,36 @@ public class LockPatternUtils { } /** - * Checks to see if the given file exists and contains any data. Returns true if it does, - * false otherwise. + * Check to see if a password matches any of the passwords stored in the + * password history. + * + * @param password The password to check. + * @return Whether the password matches any in the history. + */ + public boolean checkPasswordHistory(String password) { + String passwordHashString = new String(passwordToHash(password)); + String passwordHistory = getString(PASSWORD_HISTORY_KEY); + if (passwordHistory == null) { + return false; + } + // Password History may be too long... + int passwordHashLength = passwordHashString.length(); + int passwordHistoryLength = getRequestedPasswordHistoryLength(); + if(passwordHistoryLength == 0) { + return false; + } + int neededPasswordHistoryLength = passwordHashLength * passwordHistoryLength + + passwordHistoryLength - 1; + if (passwordHistory.length() > neededPasswordHistoryLength) { + passwordHistory = passwordHistory.substring(0, neededPasswordHistoryLength); + } + return passwordHistory.contains(passwordHashString); + } + + /** + * Checks to see if the given file exists and contains any data. Returns + * true if it does, false otherwise. + * * @param filename * @return true if file exists and is non-empty. */ @@ -274,6 +331,11 @@ public class LockPatternUtils { activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; } break; + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + if (isLockPasswordEnabled()) { + activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; + } + break; } return activePasswordQuality; } @@ -282,8 +344,6 @@ public class LockPatternUtils { * Clear any lock pattern or password. */ public void clearLock() { - getDevicePolicyManager().setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); saveLockPassword(null, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); setLockPatternEnabled(false); saveLockPattern(null); @@ -296,7 +356,7 @@ public class LockPatternUtils { */ public void saveLockPattern(List<LockPatternView.Cell> pattern) { // Compute the hash - final byte[] hash = LockPatternUtils.patternToHash(pattern); + final byte[] hash = LockPatternUtils.patternToHash(pattern); try { // Write the hash to file RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "rw"); @@ -311,14 +371,15 @@ public class LockPatternUtils { if (pattern != null) { setBoolean(PATTERN_EVER_CHOSEN_KEY, true); setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); - dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern.size()); + dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern + .size(), 0, 0, 0, 0, 0, 0); } else { - dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); + dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, + 0, 0, 0, 0, 0); } } catch (FileNotFoundException fnfe) { - // Cant do much, unless we want to fail over to using the settings provider + // Cant do much, unless we want to fail over to using the settings + // provider Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename); } catch (IOException ioe) { // Cant do much @@ -376,17 +437,59 @@ public class LockPatternUtils { DevicePolicyManager dpm = getDevicePolicyManager(); if (password != null) { int computedQuality = computePasswordQuality(password); - setLong(PASSWORD_TYPE_KEY, computedQuality); + setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality)); if (computedQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { - dpm.setActivePasswordState(computedQuality, password.length()); + int letters = 0; + int uppercase = 0; + int lowercase = 0; + int numbers = 0; + int symbols = 0; + int nonletter = 0; + for (int i = 0; i < password.length(); i++) { + char c = password.charAt(i); + if (c >= 'A' && c <= 'Z') { + letters++; + uppercase++; + } else if (c >= 'a' && c <= 'z') { + letters++; + lowercase++; + } else if (c >= '0' && c <= '9') { + numbers++; + nonletter++; + } else { + symbols++; + nonletter++; + } + } + dpm.setActivePasswordState(Math.max(quality, computedQuality), password + .length(), letters, uppercase, lowercase, numbers, symbols, nonletter); } else { // The password is not anything. dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0); + } + // Add the password to the password history. We assume all + // password + // hashes have the same length for simplicity of implementation. + String passwordHistory = getString(PASSWORD_HISTORY_KEY); + if (passwordHistory == null) { + passwordHistory = new String(); + } + int passwordHistoryLength = getRequestedPasswordHistoryLength(); + if (passwordHistoryLength == 0) { + passwordHistory = ""; + } else { + passwordHistory = new String(hash) + "," + passwordHistory; + // Cut it to contain passwordHistoryLength hashes + // and passwordHistoryLength -1 commas. + passwordHistory = passwordHistory.substring(0, Math.min(hash.length + * passwordHistoryLength + passwordHistoryLength - 1, passwordHistory + .length())); } + setString(PASSWORD_HISTORY_KEY, passwordHistory); } else { dpm.setActivePasswordState( - DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0); } } catch (FileNotFoundException fnfe) { // Cant do much, unless we want to fail over to using the settings provider @@ -526,7 +629,8 @@ public class LockPatternUtils { return savedPasswordExists() && (mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC || mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC); + || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC + || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX); } /** @@ -650,12 +754,21 @@ public class LockPatternUtils { android.provider.Settings.Secure.putLong(mContentResolver, secureSettingKey, value); } + private String getString(String secureSettingKey) { + return android.provider.Settings.Secure.getString(mContentResolver, secureSettingKey); + } + + private void setString(String secureSettingKey, String value) { + android.provider.Settings.Secure.putString(mContentResolver, secureSettingKey, value); + } + public boolean isSecure() { long mode = getKeyguardStoredPasswordQuality(); final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; final boolean isPassword = mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC - || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; + || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC + || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; final boolean secure = isPattern && isLockPatternEnabled() && savedPatternExists() || isPassword && savedPasswordExists(); return secure; diff --git a/core/java/com/android/internal/widget/SlidingTab.java b/core/java/com/android/internal/widget/SlidingTab.java index 9152729..3218ba8 100644 --- a/core/java/com/android/internal/widget/SlidingTab.java +++ b/core/java/com/android/internal/widget/SlidingTab.java @@ -17,10 +17,8 @@ package com.android.internal.widget; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Vibrator; @@ -38,6 +36,7 @@ import android.view.animation.Animation.AnimationListener; import android.widget.ImageView; import android.widget.TextView; import android.widget.ImageView.ScaleType; + import com.android.internal.R; /** @@ -69,21 +68,21 @@ public class SlidingTab extends ViewGroup { private int mGrabbedState = OnTriggerListener.NO_HANDLE; private boolean mTriggered = false; private Vibrator mVibrator; - private float mDensity; // used to scale dimensions for bitmaps. + private final float mDensity; // used to scale dimensions for bitmaps. /** * Either {@link #HORIZONTAL} or {@link #VERTICAL}. */ - private int mOrientation; + private final int mOrientation; - private Slider mLeftSlider; - private Slider mRightSlider; + private final Slider mLeftSlider; + private final Slider mRightSlider; private Slider mCurrentSlider; private boolean mTracking; private float mThreshold; private Slider mOtherSlider; private boolean mAnimating; - private Rect mTmpRect; + private final Rect mTmpRect; /** * Listener used to reset the view when the current animation completes. @@ -608,14 +607,7 @@ public class SlidingTab extends ViewGroup { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - mTracking = false; - mTriggered = false; - mOtherSlider.show(true); - mCurrentSlider.reset(false); - mCurrentSlider.hideTarget(); - mCurrentSlider = null; - mOtherSlider = null; - setGrabbedState(OnTriggerListener.NO_HANDLE); + cancelGrab(); break; } } @@ -623,6 +615,17 @@ public class SlidingTab extends ViewGroup { return mTracking || super.onTouchEvent(event); } + private void cancelGrab() { + mTracking = false; + mTriggered = false; + mOtherSlider.show(true); + mCurrentSlider.reset(false); + mCurrentSlider.hideTarget(); + mCurrentSlider = null; + mOtherSlider = null; + setGrabbedState(OnTriggerListener.NO_HANDLE); + } + void startAnimating(final boolean holdAfter) { mAnimating = true; final Animation trans1; @@ -832,6 +835,17 @@ public class SlidingTab extends ViewGroup { } } + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // When visibility changes and the user has a tab selected, unselect it and + // make sure their callback gets called. + if (changedView == this && visibility != VISIBLE + && mGrabbedState != OnTriggerListener.NO_HANDLE) { + cancelGrab(); + } + } + /** * Sets the current grabbed state, and dispatches a grabbed state change * event to our listener. |