diff options
Diffstat (limited to 'core')
362 files changed, 40198 insertions, 26030 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. diff --git a/core/jni/Android.mk b/core/jni/Android.mk index a008e96..d19cae4 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -24,7 +24,6 @@ LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES -DEGL_EGLEXT_PROTOTYPES LOCAL_SRC_FILES:= \ ActivityManager.cpp \ AndroidRuntime.cpp \ - CursorWindow.cpp \ Time.cpp \ com_google_android_gles_jni_EGLImpl.cpp \ com_google_android_gles_jni_GLImpl.cpp.arm \ @@ -48,6 +47,8 @@ LOCAL_SRC_FILES:= \ android_view_InputChannel.cpp \ android_view_InputQueue.cpp \ android_view_KeyEvent.cpp \ + android_view_HardwareRenderer.cpp \ + android_view_GLES20Canvas.cpp \ android_view_MotionEvent.cpp \ android_text_AndroidCharacter.cpp \ android_text_AndroidBidi.cpp \ @@ -104,6 +105,7 @@ LOCAL_SRC_FILES:= \ android/graphics/Rasterizer.cpp \ android/graphics/Region.cpp \ android/graphics/Shader.cpp \ + android/graphics/TextLayout.cpp \ android/graphics/Typeface.cpp \ android/graphics/Xfermode.cpp \ android/graphics/YuvToJpegEncoder.cpp \ @@ -139,6 +141,7 @@ LOCAL_SRC_FILES:= \ LOCAL_C_INCLUDES += \ $(JNI_H_INCLUDE) \ $(LOCAL_PATH)/android/graphics \ + $(LOCAL_PATH)/../../libs/hwui \ $(call include-path-for, bluedroid) \ $(call include-path-for, libhardware)/hardware \ $(call include-path-for, libhardware_legacy)/hardware_legacy \ @@ -167,6 +170,7 @@ LOCAL_SHARED_LIBRARIES := \ libbinder \ libnetutils \ libui \ + libhwui \ libsurfaceflinger_client \ libcamera_client \ libskiagl \ diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 6fb1369..19bb36f 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -114,6 +114,8 @@ extern int register_android_graphics_Xfermode(JNIEnv* env); extern int register_android_graphics_PixelFormat(JNIEnv* env); extern int register_com_android_internal_graphics_NativeUtils(JNIEnv *env); extern int register_android_view_Display(JNIEnv* env); +extern int register_android_view_GLES20Canvas(JNIEnv* env); +extern int register_android_view_HardwareRenderer(JNIEnv* env); extern int register_android_view_Surface(JNIEnv* env); extern int register_android_view_ViewRoot(JNIEnv* env); extern int register_android_database_CursorWindow(JNIEnv* env); @@ -1211,6 +1213,8 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_nio_utils), REG_JNI(register_android_graphics_PixelFormat), REG_JNI(register_android_graphics_Graphics), + REG_JNI(register_android_view_GLES20Canvas), + REG_JNI(register_android_view_HardwareRenderer), REG_JNI(register_android_view_Surface), REG_JNI(register_android_view_ViewRoot), REG_JNI(register_com_google_android_gles_jni_EGLImpl), diff --git a/core/jni/CursorWindow.cpp b/core/jni/CursorWindow.cpp deleted file mode 100644 index 7877921..0000000 --- a/core/jni/CursorWindow.cpp +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (C) 2006-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. - */ - -#undef LOG_TAG -#define LOG_TAG "CursorWindow" - -#include <utils/Log.h> -#include <binder/MemoryHeapBase.h> -#include <binder/MemoryBase.h> - -#include <assert.h> -#include <string.h> -#include <stdlib.h> - -#include <jni.h> -#include <JNIHelp.h> - -#include "CursorWindow.h" - - -namespace android { - -CursorWindow::CursorWindow(size_t maxSize) : - mMaxSize(maxSize) -{ -} - -bool CursorWindow::setMemory(const sp<IMemory>& memory) -{ - mMemory = memory; - mData = (uint8_t *) memory->pointer(); - if (mData == NULL) { - return false; - } - mHeader = (window_header_t *) mData; - - // Make the window read-only - ssize_t size = memory->size(); - mSize = size; - mMaxSize = size; - mFreeOffset = size; -LOG_WINDOW("Created CursorWindow from existing IMemory: mFreeOffset = %d, numRows = %d, numColumns = %d, mSize = %d, mMaxSize = %d, mData = %p", mFreeOffset, mHeader->numRows, mHeader->numColumns, mSize, mMaxSize, mData); - return true; -} - -bool CursorWindow::initBuffer(bool localOnly) -{ - //TODO Use a non-memory dealer mmap region for localOnly - - sp<MemoryHeapBase> heap; - heap = new MemoryHeapBase(mMaxSize, 0, "CursorWindow"); - if (heap != NULL) { - mMemory = new MemoryBase(heap, 0, mMaxSize); - if (mMemory != NULL) { - mData = (uint8_t *) mMemory->pointer(); - if (mData) { - mHeader = (window_header_t *) mData; - mSize = mMaxSize; - - // Put the window into a clean state - clear(); - LOG_WINDOW("Created CursorWindow with new MemoryDealer: mFreeOffset = %d, mSize = %d, mMaxSize = %d, mData = %p", mFreeOffset, mSize, mMaxSize, mData); - return true; - } - } - LOGE("CursorWindow heap allocation failed"); - return false; - } else { - LOGE("failed to create the CursorWindow heap"); - return false; - } -} - -CursorWindow::~CursorWindow() -{ - // Everything that matters is a smart pointer -} - -void CursorWindow::clear() -{ - mHeader->numRows = 0; - mHeader->numColumns = 0; - mFreeOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE; - // Mark the first chunk's next 'pointer' as null - *((uint32_t *)(mData + mFreeOffset - sizeof(uint32_t))) = 0; -} - -int32_t CursorWindow::freeSpace() -{ - int32_t freeSpace = mSize - mFreeOffset; - if (freeSpace < 0) { - freeSpace = 0; - } - return freeSpace; -} - -field_slot_t * CursorWindow::allocRow() -{ - // Fill in the row slot - row_slot_t * rowSlot = allocRowSlot(); - if (rowSlot == NULL) { - return NULL; - } - - // Allocate the slots for the field directory - size_t fieldDirSize = mHeader->numColumns * sizeof(field_slot_t); - uint32_t fieldDirOffset = alloc(fieldDirSize); - if (!fieldDirOffset) { - mHeader->numRows--; - LOGE("The row failed, so back out the new row accounting from allocRowSlot %d", mHeader->numRows); - return NULL; - } - field_slot_t * fieldDir = (field_slot_t *)offsetToPtr(fieldDirOffset); - memset(fieldDir, 0x0, fieldDirSize); - -LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %d bytes at offset %u\n", (mHeader->numRows - 1), ((uint8_t *)rowSlot) - mData, fieldDirSize, fieldDirOffset); - rowSlot->offset = fieldDirOffset; - - return fieldDir; -} - -uint32_t CursorWindow::alloc(size_t requestedSize, bool aligned) -{ - int32_t size; - uint32_t padding; - if (aligned) { - // 4 byte alignment - padding = 4 - (mFreeOffset & 0x3); - } else { - padding = 0; - } - - size = requestedSize + padding; - - if (size > freeSpace()) { - LOGE("need to grow: mSize = %d, size = %d, freeSpace() = %d, numRows = %d", mSize, size, freeSpace(), mHeader->numRows); - // Only grow the window if the first row doesn't fit - if (mHeader->numRows > 1) { -LOGE("not growing since there are already %d row(s), max size %d", mHeader->numRows, mMaxSize); - return 0; - } - - // Find a new size that will fit the allocation - int allocated = mSize - freeSpace(); - int newSize = mSize + WINDOW_ALLOCATION_SIZE; - while (size > (newSize - allocated)) { - newSize += WINDOW_ALLOCATION_SIZE; - if (newSize > mMaxSize) { - LOGE("Attempting to grow window beyond max size (%d)", mMaxSize); - return 0; - } - } -LOG_WINDOW("found size %d", newSize); - mSize = newSize; - } - - uint32_t offset = mFreeOffset + padding; - mFreeOffset += size; - return offset; -} - -row_slot_t * CursorWindow::getRowSlot(int row) -{ - LOG_WINDOW("enter getRowSlot current row num %d, this row %d", mHeader->numRows, row); - int chunkNum = row / ROW_SLOT_CHUNK_NUM_ROWS; - int chunkPos = row % ROW_SLOT_CHUNK_NUM_ROWS; - int chunkPtrOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t); - uint8_t * rowChunk = mData + sizeof(window_header_t); - for (int i = 0; i < chunkNum; i++) { - rowChunk = offsetToPtr(*((uint32_t *)(mData + chunkPtrOffset))); - chunkPtrOffset = rowChunk - mData + (ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)); - } - return (row_slot_t *)(rowChunk + (chunkPos * sizeof(row_slot_t))); - LOG_WINDOW("exit getRowSlot current row num %d, this row %d", mHeader->numRows, row); -} - -row_slot_t * CursorWindow::allocRowSlot() -{ - int chunkNum = mHeader->numRows / ROW_SLOT_CHUNK_NUM_ROWS; - int chunkPos = mHeader->numRows % ROW_SLOT_CHUNK_NUM_ROWS; - int chunkPtrOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t); - uint8_t * rowChunk = mData + sizeof(window_header_t); -LOG_WINDOW("Allocating row slot, mHeader->numRows is %d, chunkNum is %d, chunkPos is %d", mHeader->numRows, chunkNum, chunkPos); - for (int i = 0; i < chunkNum; i++) { - uint32_t nextChunkOffset = *((uint32_t *)(mData + chunkPtrOffset)); -LOG_WINDOW("nextChunkOffset is %d", nextChunkOffset); - if (nextChunkOffset == 0) { - // Allocate a new row chunk - nextChunkOffset = alloc(ROW_SLOT_CHUNK_SIZE, true); - if (nextChunkOffset == 0) { - return NULL; - } - rowChunk = offsetToPtr(nextChunkOffset); -LOG_WINDOW("allocated new chunk at %d, rowChunk = %p", nextChunkOffset, rowChunk); - *((uint32_t *)(mData + chunkPtrOffset)) = rowChunk - mData; - // Mark the new chunk's next 'pointer' as null - *((uint32_t *)(rowChunk + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t))) = 0; - } else { -LOG_WINDOW("follwing 'pointer' to next chunk, offset of next pointer is %d", chunkPtrOffset); - rowChunk = offsetToPtr(nextChunkOffset); - chunkPtrOffset = rowChunk - mData + (ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)); - } - } - mHeader->numRows++; - - return (row_slot_t *)(rowChunk + (chunkPos * sizeof(row_slot_t))); -} - -field_slot_t * CursorWindow::getFieldSlotWithCheck(int row, int column) -{ - if (row < 0 || row >= mHeader->numRows || column < 0 || column >= mHeader->numColumns) { - LOGE("Bad request for field slot %d,%d. numRows = %d, numColumns = %d", row, column, mHeader->numRows, mHeader->numColumns); - return NULL; - } - row_slot_t * rowSlot = getRowSlot(row); - if (!rowSlot) { - LOGE("Failed to find rowSlot for row %d", row); - return NULL; - } - if (rowSlot->offset == 0 || rowSlot->offset >= mSize) { - LOGE("Invalid rowSlot, offset = %d", rowSlot->offset); - return NULL; - } - int fieldDirOffset = rowSlot->offset; - return ((field_slot_t *)offsetToPtr(fieldDirOffset)) + column; -} - -uint32_t CursorWindow::read_field_slot(int row, int column, field_slot_t * slotOut) -{ - if (row < 0 || row >= mHeader->numRows || column < 0 || column >= mHeader->numColumns) { - LOGE("Bad request for field slot %d,%d. numRows = %d, numColumns = %d", row, column, mHeader->numRows, mHeader->numColumns); - return -1; - } - row_slot_t * rowSlot = getRowSlot(row); - if (!rowSlot) { - LOGE("Failed to find rowSlot for row %d", row); - return -1; - } - if (rowSlot->offset == 0 || rowSlot->offset >= mSize) { - LOGE("Invalid rowSlot, offset = %d", rowSlot->offset); - return -1; - } -LOG_WINDOW("Found field directory for %d,%d at rowSlot %d, offset %d", row, column, (uint8_t *)rowSlot - mData, rowSlot->offset); - field_slot_t * fieldDir = (field_slot_t *)offsetToPtr(rowSlot->offset); -LOG_WINDOW("Read field_slot_t %d,%d: offset = %d, size = %d, type = %d", row, column, fieldDir[column].data.buffer.offset, fieldDir[column].data.buffer.size, fieldDir[column].type); - - // Copy the data to the out param - slotOut->data.buffer.offset = fieldDir[column].data.buffer.offset; - slotOut->data.buffer.size = fieldDir[column].data.buffer.size; - slotOut->type = fieldDir[column].type; - return 0; -} - -void CursorWindow::copyIn(uint32_t offset, uint8_t const * data, size_t size) -{ - assert(offset + size <= mSize); - memcpy(mData + offset, data, size); -} - -void CursorWindow::copyIn(uint32_t offset, int64_t data) -{ - assert(offset + sizeof(int64_t) <= mSize); - memcpy(mData + offset, (uint8_t *)&data, sizeof(int64_t)); -} - -void CursorWindow::copyIn(uint32_t offset, double data) -{ - assert(offset + sizeof(double) <= mSize); - memcpy(mData + offset, (uint8_t *)&data, sizeof(double)); -} - -void CursorWindow::copyOut(uint32_t offset, uint8_t * data, size_t size) -{ - assert(offset + size <= mSize); - memcpy(data, mData + offset, size); -} - -int64_t CursorWindow::copyOutLong(uint32_t offset) -{ - int64_t value; - assert(offset + sizeof(int64_t) <= mSize); - memcpy(&value, mData + offset, sizeof(int64_t)); - return value; -} - -double CursorWindow::copyOutDouble(uint32_t offset) -{ - double value; - assert(offset + sizeof(double) <= mSize); - memcpy(&value, mData + offset, sizeof(double)); - return value; -} - -bool CursorWindow::putLong(unsigned int row, unsigned int col, int64_t value) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot) { - return false; - } - -#if WINDOW_STORAGE_INLINE_NUMERICS - fieldSlot->data.l = value; -#else - int offset = alloc(sizeof(int64_t)); - if (!offset) { - return false; - } - - copyIn(offset, value); - - fieldSlot->data.buffer.offset = offset; - fieldSlot->data.buffer.size = sizeof(int64_t); -#endif - fieldSlot->type = FIELD_TYPE_INTEGER; - return true; -} - -bool CursorWindow::putDouble(unsigned int row, unsigned int col, double value) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot) { - return false; - } - -#if WINDOW_STORAGE_INLINE_NUMERICS - fieldSlot->data.d = value; -#else - int offset = alloc(sizeof(int64_t)); - if (!offset) { - return false; - } - - copyIn(offset, value); - - fieldSlot->data.buffer.offset = offset; - fieldSlot->data.buffer.size = sizeof(double); -#endif - fieldSlot->type = FIELD_TYPE_FLOAT; - return true; -} - -bool CursorWindow::putNull(unsigned int row, unsigned int col) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot) { - return false; - } - - fieldSlot->type = FIELD_TYPE_NULL; - fieldSlot->data.buffer.offset = 0; - fieldSlot->data.buffer.size = 0; - return true; -} - -bool CursorWindow::getLong(unsigned int row, unsigned int col, int64_t * valueOut) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot || fieldSlot->type != FIELD_TYPE_INTEGER) { - return false; - } - -#if WINDOW_STORAGE_INLINE_NUMERICS - *valueOut = fieldSlot->data.l; -#else - *valueOut = copyOutLong(fieldSlot->data.buffer.offset); -#endif - return true; -} - -bool CursorWindow::getDouble(unsigned int row, unsigned int col, double * valueOut) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot || fieldSlot->type != FIELD_TYPE_FLOAT) { - return false; - } - -#if WINDOW_STORAGE_INLINE_NUMERICS - *valueOut = fieldSlot->data.d; -#else - *valueOut = copyOutDouble(fieldSlot->data.buffer.offset); -#endif - return true; -} - -bool CursorWindow::getNull(unsigned int row, unsigned int col, bool * valueOut) -{ - field_slot_t * fieldSlot = getFieldSlotWithCheck(row, col); - if (!fieldSlot) { - return false; - } - - if (fieldSlot->type != FIELD_TYPE_NULL) { - *valueOut = false; - } else { - *valueOut = true; - } - return true; -} - -}; // namespace android diff --git a/core/jni/CursorWindow.h b/core/jni/CursorWindow.h deleted file mode 100644 index 3fcb560..0000000 --- a/core/jni/CursorWindow.h +++ /dev/null @@ -1,202 +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. - */ - -#ifndef _ANDROID__DATABASE_WINDOW_H -#define _ANDROID__DATABASE_WINDOW_H - -#include <cutils/log.h> -#include <stddef.h> -#include <stdint.h> - -#include <binder/IMemory.h> -#include <utils/RefBase.h> - -#include <jni.h> - -#define DEFAULT_WINDOW_SIZE 4096 -#define MAX_WINDOW_SIZE (1024 * 1024) -#define WINDOW_ALLOCATION_SIZE 4096 - -#define ROW_SLOT_CHUNK_NUM_ROWS 16 - -// Row slots are allocated in chunks of ROW_SLOT_CHUNK_NUM_ROWS, -// with an offset after the rows that points to the next chunk -#define ROW_SLOT_CHUNK_SIZE ((ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)) + sizeof(uint32_t)) - - -#if LOG_NDEBUG - -#define IF_LOG_WINDOW() if (false) -#define LOG_WINDOW(...) - -#else - -#define IF_LOG_WINDOW() IF_LOG(LOG_DEBUG, "CursorWindow") -#define LOG_WINDOW(...) LOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__) - -#endif - - -// When defined to true strings are stored as UTF8, otherwise they're UTF16 -#define WINDOW_STORAGE_UTF8 1 - -// When defined to true numberic values are stored inline in the field_slot_t, otherwise they're allocated in the window -#define WINDOW_STORAGE_INLINE_NUMERICS 1 - -namespace android { - -typedef struct -{ - uint32_t numRows; - uint32_t numColumns; -} window_header_t; - -typedef struct -{ - uint32_t offset; -} row_slot_t; - -typedef struct -{ - uint8_t type; - union { - double d; - int64_t l; - struct { - uint32_t offset; - uint32_t size; - } buffer; - } data; -} __attribute__((packed)) field_slot_t; - -#define FIELD_TYPE_INTEGER 1 -#define FIELD_TYPE_FLOAT 2 -#define FIELD_TYPE_STRING 3 -#define FIELD_TYPE_BLOB 4 -#define FIELD_TYPE_NULL 5 - -/** - * This class stores a set of rows from a database in a buffer. The begining of the - * window has first chunk of row_slot_ts, which are offsets to the row directory, followed by - * an offset to the next chunk in a linked-list of additional chunk of row_slot_ts in case - * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a - * field_slot_t per column, which has the size, offset, and type of the data for that field. - * Note that the data types come from sqlite3.h. - */ -class CursorWindow -{ -public: - CursorWindow(size_t maxSize); - CursorWindow(){} - bool setMemory(const sp<IMemory>&); - ~CursorWindow(); - - bool initBuffer(bool localOnly); - sp<IMemory> getMemory() {return mMemory;} - - size_t size() {return mSize;} - uint8_t * data() {return mData;} - uint32_t getNumRows() {return mHeader->numRows;} - uint32_t getNumColumns() {return mHeader->numColumns;} - void freeLastRow() { - if (mHeader->numRows > 0) { - mHeader->numRows--; - } - } - bool setNumColumns(uint32_t numColumns) - { - uint32_t cur = mHeader->numColumns; - if (cur > 0 && cur != numColumns) { - LOGE("Trying to go from %d columns to %d", cur, numColumns); - return false; - } - mHeader->numColumns = numColumns; - return true; - } - - int32_t freeSpace(); - - void clear(); - - /** - * Allocate a row slot and its directory. The returned - * pointer points to the begining of the row's directory - * or NULL if there wasn't room. The directory is - * initialied with NULL entries for each field. - */ - field_slot_t * allocRow(); - - /** - * Allocate a portion of the window. Returns the offset - * of the allocation, or 0 if there isn't enough space. - * If aligned is true, the allocation gets 4 byte alignment. - */ - uint32_t alloc(size_t size, bool aligned = false); - - uint32_t read_field_slot(int row, int column, field_slot_t * slot); - - /** - * Copy data into the window at the given offset. - */ - void copyIn(uint32_t offset, uint8_t const * data, size_t size); - void copyIn(uint32_t offset, int64_t data); - void copyIn(uint32_t offset, double data); - - void copyOut(uint32_t offset, uint8_t * data, size_t size); - int64_t copyOutLong(uint32_t offset); - double copyOutDouble(uint32_t offset); - - bool putLong(unsigned int row, unsigned int col, int64_t value); - bool putDouble(unsigned int row, unsigned int col, double value); - bool putNull(unsigned int row, unsigned int col); - - bool getLong(unsigned int row, unsigned int col, int64_t * valueOut); - bool getDouble(unsigned int row, unsigned int col, double * valueOut); - bool getNull(unsigned int row, unsigned int col, bool * valueOut); - - uint8_t * offsetToPtr(uint32_t offset) {return mData + offset;} - - row_slot_t * allocRowSlot(); - - row_slot_t * getRowSlot(int row); - - /** - * return NULL if Failed to find rowSlot or - * Invalid rowSlot - */ - field_slot_t * getFieldSlotWithCheck(int row, int column); - field_slot_t * getFieldSlot(int row, int column) - { - int fieldDirOffset = getRowSlot(row)->offset; - return ((field_slot_t *)offsetToPtr(fieldDirOffset)) + column; - } - -private: - uint8_t * mData; - size_t mSize; - size_t mMaxSize; - window_header_t * mHeader; - sp<IMemory> mMemory; - - /** - * Offset of the lowest unused data byte in the array. - */ - uint32_t mFreeOffset; -}; - -}; // namespace android - -#endif diff --git a/core/jni/android/graphics/Bitmap.cpp b/core/jni/android/graphics/Bitmap.cpp index 88bbafd..b062264 100644 --- a/core/jni/android/graphics/Bitmap.cpp +++ b/core/jni/android/graphics/Bitmap.cpp @@ -313,6 +313,10 @@ static int Bitmap_config(JNIEnv* env, jobject, SkBitmap* bitmap) { return bitmap->config();
}
+static int Bitmap_getGenerationId(JNIEnv* env, jobject, SkBitmap* bitmap) {
+ return bitmap->getGenerationID();
+}
+
static jboolean Bitmap_hasAlpha(JNIEnv* env, jobject, SkBitmap* bitmap) {
return !bitmap->isOpaque();
}
@@ -606,6 +610,7 @@ static JNINativeMethod gBitmapMethods[] = { (void*)Bitmap_writeToParcel },
{ "nativeExtractAlpha", "(II[I)Landroid/graphics/Bitmap;",
(void*)Bitmap_extractAlpha },
+ { "nativeGenerationId", "(I)I", (void*)Bitmap_getGenerationId },
{ "nativeGetPixel", "(III)I", (void*)Bitmap_getPixel },
{ "nativeGetPixels", "(I[IIIIIII)V", (void*)Bitmap_getPixels },
{ "nativeSetPixel", "(IIII)V", (void*)Bitmap_setPixel },
diff --git a/core/jni/android/graphics/Canvas.cpp b/core/jni/android/graphics/Canvas.cpp index e1e9536..558f5ff 100644 --- a/core/jni/android/graphics/Canvas.cpp +++ b/core/jni/android/graphics/Canvas.cpp @@ -30,6 +30,13 @@ #include "SkBoundaryPatch.h" #include "SkMeshUtils.h" +#include "TextLayout.h" + +#include "unicode/ubidi.h" +#include "unicode/ushape.h" + +#include <utils/Log.h> + #define TIME_DRAWx static uint32_t get_thread_msec() { @@ -743,45 +750,49 @@ public: canvas->drawVertices(mode, ptCount, verts, texs, colors, NULL, indices, indexCount, *paint); } - - static void drawText___CIIFFPaint(JNIEnv* env, jobject, SkCanvas* canvas, + + + static void drawText___CIIFFIPaint(JNIEnv* env, jobject, SkCanvas* canvas, jcharArray text, int index, int count, - jfloat x, jfloat y, SkPaint* paint) { + jfloat x, jfloat y, int flags, SkPaint* paint) { jchar* textArray = env->GetCharArrayElements(text, NULL); - jsize textCount = env->GetArrayLength(text); - SkScalar x_ = SkFloatToScalar(x); - SkScalar y_ = SkFloatToScalar(y); - canvas->drawText(textArray + index, count << 1, x_, y_, *paint); - env->ReleaseCharArrayElements(text, textArray, 0); + TextLayout::drawText(paint, textArray + index, count, flags, x, y, canvas); + env->ReleaseCharArrayElements(text, textArray, JNI_ABORT); } - - static void drawText__StringIIFFPaint(JNIEnv* env, jobject, - SkCanvas* canvas, jstring text, int start, int end, - jfloat x, jfloat y, SkPaint* paint) { - const void* text_ = env->GetStringChars(text, NULL); - SkScalar x_ = SkFloatToScalar(x); - SkScalar y_ = SkFloatToScalar(y); - canvas->drawText((const uint16_t*)text_ + start, (end - start) << 1, - x_, y_, *paint); - env->ReleaseStringChars(text, (const jchar*) text_); + + static void drawText__StringIIFFIPaint(JNIEnv* env, jobject, + SkCanvas* canvas, jstring text, + int start, int end, + jfloat x, jfloat y, int flags, SkPaint* paint) { + const jchar* textArray = env->GetStringChars(text, NULL); + TextLayout::drawText(paint, textArray + start, end - start, flags, x, y, canvas); + env->ReleaseStringChars(text, textArray); } - - static void drawString(JNIEnv* env, jobject canvas, jstring text, - jfloat x, jfloat y, jobject paint) { - NPE_CHECK_RETURN_VOID(env, canvas); - NPE_CHECK_RETURN_VOID(env, paint); - NPE_CHECK_RETURN_VOID(env, text); - size_t count = env->GetStringLength(text); - if (0 == count) { - return; - } - const jchar* text_ = env->GetStringChars(text, NULL); - SkCanvas* c = GraphicsJNI::getNativeCanvas(env, canvas); - c->drawText(text_, count << 1, SkFloatToScalar(x), SkFloatToScalar(y), - *GraphicsJNI::getNativePaint(env, paint)); - env->ReleaseStringChars(text, text_); + + static void drawTextRun___CIIIIFFIPaint( + JNIEnv* env, jobject, SkCanvas* canvas, jcharArray text, int index, + int count, int contextIndex, int contextCount, + jfloat x, jfloat y, int dirFlags, SkPaint* paint) { + + jchar* chars = env->GetCharArrayElements(text, NULL); + TextLayout::drawTextRun(paint, chars + contextIndex, index - contextIndex, + count, contextCount, dirFlags, x, y, canvas); + env->ReleaseCharArrayElements(text, chars, JNI_ABORT); } - + + static void drawTextRun__StringIIIIFFIPaint( + JNIEnv* env, jobject obj, SkCanvas* canvas, jstring text, jint start, + jint end, jint contextStart, jint contextEnd, + jfloat x, jfloat y, jint dirFlags, SkPaint* paint) { + + jint count = end - start; + jint contextCount = contextEnd - contextStart; + const jchar* chars = env->GetStringChars(text, NULL); + TextLayout::drawTextRun(paint, chars + contextStart, start - contextStart, + count, contextCount, dirFlags, x, y, canvas); + env->ReleaseStringChars(text, chars); + } + static void drawPosText___CII_FPaint(JNIEnv* env, jobject, SkCanvas* canvas, jcharArray text, int index, int count, jfloatArray pos, SkPaint* paint) { @@ -804,10 +815,11 @@ public: } delete[] posPtr; } - + static void drawPosText__String_FPaint(JNIEnv* env, jobject, SkCanvas* canvas, jstring text, - jfloatArray pos, SkPaint* paint) { + jfloatArray pos, + SkPaint* paint) { const void* text_ = text ? env->GetStringChars(text, NULL) : NULL; int byteLength = text ? env->GetStringLength(text) : 0; float* posArray = pos ? env->GetFloatArrayElements(pos, NULL) : NULL; @@ -827,27 +839,27 @@ public: } delete[] posPtr; } - + static void drawTextOnPath___CIIPathFFPaint(JNIEnv* env, jobject, - SkCanvas* canvas, jcharArray text, int index, int count, - SkPath* path, jfloat hOffset, jfloat vOffset, SkPaint* paint) { + SkCanvas* canvas, jcharArray text, int index, int count, + SkPath* path, jfloat hOffset, jfloat vOffset, jint bidiFlags, SkPaint* paint) { jchar* textArray = env->GetCharArrayElements(text, NULL); - canvas->drawTextOnPathHV(textArray + index, count << 1, *path, - SkFloatToScalar(hOffset), SkFloatToScalar(vOffset), *paint); + TextLayout::drawTextOnPath(paint, textArray, count, bidiFlags, hOffset, vOffset, + path, canvas); env->ReleaseCharArrayElements(text, textArray, 0); } - + static void drawTextOnPath__StringPathFFPaint(JNIEnv* env, jobject, - SkCanvas* canvas, jstring text, SkPath* path, - jfloat hOffset, jfloat vOffset, SkPaint* paint) { + SkCanvas* canvas, jstring text, SkPath* path, + jfloat hOffset, jfloat vOffset, jint bidiFlags, SkPaint* paint) { const jchar* text_ = env->GetStringChars(text, NULL); - int byteLength = env->GetStringLength(text) << 1; - canvas->drawTextOnPathHV(text_, byteLength, *path, - SkFloatToScalar(hOffset), SkFloatToScalar(vOffset), *paint); + int count = env->GetStringLength(text); + TextLayout::drawTextOnPath(paint, text_, count, bidiFlags, hOffset, vOffset, + path, canvas); env->ReleaseStringChars(text, text_); } - + static bool getClipBounds(JNIEnv* env, jobject, SkCanvas* canvas, jobject bounds) { SkRect r; @@ -940,26 +952,27 @@ static JNINativeMethod gCanvasMethods[] = { (void*) SkCanvasGlue::drawBitmapRR}, {"native_drawBitmap", "(I[IIIFFIIZI)V", (void*)SkCanvasGlue::drawBitmapArray}, - {"nativeDrawBitmapMatrix", "(IIII)V", (void*)SkCanvasGlue::drawBitmapMatrix}, {"nativeDrawBitmapMesh", "(IIII[FI[III)V", (void*)SkCanvasGlue::drawBitmapMesh}, {"nativeDrawVertices", "(III[FI[FI[II[SIII)V", (void*)SkCanvasGlue::drawVertices}, - {"native_drawText","(I[CIIFFI)V", - (void*) SkCanvasGlue::drawText___CIIFFPaint}, - {"native_drawText","(ILjava/lang/String;IIFFI)V", - (void*) SkCanvasGlue::drawText__StringIIFFPaint}, - {"drawText","(Ljava/lang/String;FFLandroid/graphics/Paint;)V", - (void*) SkCanvasGlue::drawString}, + {"native_drawText","(I[CIIFFII)V", + (void*) SkCanvasGlue::drawText___CIIFFIPaint}, + {"native_drawText","(ILjava/lang/String;IIFFII)V", + (void*) SkCanvasGlue::drawText__StringIIFFIPaint}, + {"native_drawTextRun","(I[CIIIIFFII)V", + (void*) SkCanvasGlue::drawTextRun___CIIIIFFIPaint}, + {"native_drawTextRun","(ILjava/lang/String;IIIIFFII)V", + (void*) SkCanvasGlue::drawTextRun__StringIIIIFFIPaint}, {"native_drawPosText","(I[CII[FI)V", (void*) SkCanvasGlue::drawPosText___CII_FPaint}, {"native_drawPosText","(ILjava/lang/String;[FI)V", (void*) SkCanvasGlue::drawPosText__String_FPaint}, - {"native_drawTextOnPath","(I[CIIIFFI)V", + {"native_drawTextOnPath","(I[CIIIFFII)V", (void*) SkCanvasGlue::drawTextOnPath___CIIPathFFPaint}, - {"native_drawTextOnPath","(ILjava/lang/String;IFFI)V", + {"native_drawTextOnPath","(ILjava/lang/String;IFFII)V", (void*) SkCanvasGlue::drawTextOnPath__StringPathFFPaint}, {"native_drawPicture", "(II)V", (void*) SkCanvasGlue::drawPicture}, diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp index 780badc..e4d4850 100644 --- a/core/jni/android/graphics/Paint.cpp +++ b/core/jni/android/graphics/Paint.cpp @@ -31,6 +31,11 @@ #include "SkShader.h" #include "SkTypeface.h" #include "SkXfermode.h" +#include "unicode/ushape.h" +#include "TextLayout.h" + +// temporary for debugging +#include <utils/Log.h> namespace android { @@ -57,6 +62,9 @@ static void defaultSettingsForAndroid(SkPaint* paint) { class SkPaintGlue { public: + enum MoveOpt { + AFTER, AT_OR_AFTER, BEFORE, AT_OR_BEFORE, AT + }; static void finalizer(JNIEnv* env, jobject clazz, SkPaint* obj) { delete obj; @@ -395,20 +403,161 @@ public: env->ReleaseStringChars(text, textArray); return count; } - - static void getTextPath___CIIFFPath(JNIEnv* env, jobject clazz, SkPaint* paint, jcharArray text, int index, int count, jfloat x, jfloat y, SkPath* path) { + + static jfloat doTextRunAdvances(JNIEnv *env, SkPaint *paint, const jchar *text, + jint start, jint count, jint contextCount, jint flags, + jfloatArray advances, jint advancesIndex) { + jfloat advancesArray[count]; + jfloat totalAdvance; + + TextLayout::getTextRunAdvances(paint, text, start, count, contextCount, flags, + advancesArray, totalAdvance); + + if (advances != NULL) { + env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray); + } + return totalAdvance; + } + + static float getTextRunAdvances___CIIIII_FI(JNIEnv* env, jobject clazz, SkPaint* paint, + jcharArray text, jint index, jint count, jint contextIndex, jint contextCount, + jint flags, jfloatArray advances, jint advancesIndex) { + jchar* textArray = env->GetCharArrayElements(text, NULL); + jfloat result = doTextRunAdvances(env, paint, textArray + contextIndex, + index - contextIndex, count, contextCount, flags, advances, advancesIndex); + env->ReleaseCharArrayElements(text, textArray, JNI_ABORT); + return result; + } + + static float getTextRunAdvances__StringIIIII_FI(JNIEnv* env, jobject clazz, SkPaint* paint, + jstring text, jint start, jint end, jint contextStart, jint contextEnd, jint flags, + jfloatArray advances, jint advancesIndex) { + const jchar* textArray = env->GetStringChars(text, NULL); + jfloat result = doTextRunAdvances(env, paint, textArray + contextStart, + start - contextStart, end - start, contextEnd - contextStart, flags, advances, + advancesIndex); + env->ReleaseStringChars(text, textArray); + return result; + } + + static jint doTextRunCursor(JNIEnv *env, SkPaint* paint, const jchar *text, jint start, + jint count, jint flags, jint offset, jint opt) { + SkScalar scalarArray[count]; + jchar buffer[count]; + + // this is where we'd call harfbuzz + // for now we just use ushape.c and widths returned from skia + + int widths; + if (flags & 0x1) { // rtl, call arabic shaping in case + UErrorCode status = U_ZERO_ERROR; + // Use fixed length since we need to keep start and count valid + u_shapeArabic(text + start, count, buffer, count, + U_SHAPE_LENGTH_FIXED_SPACES_NEAR | U_SHAPE_TEXT_DIRECTION_LOGICAL | + U_SHAPE_LETTERS_SHAPE | U_SHAPE_X_LAMALEF_SUB_ALTERNATE, &status); + // we shouldn't fail unless there's an out of memory condition, + // in which case we're hosed anyway + for (int i = 0; i < count; ++i) { + if (buffer[i] == 0xffff) { + buffer[i] = 0x200b; // zero-width-space for skia + } + } + widths = paint->getTextWidths(buffer, count << 1, scalarArray); + } else { + widths = paint->getTextWidths(text + start, count << 1, scalarArray); + } + + if (widths < count) { + // Skia operates on code points, not code units, so surrogate pairs return only one + // value. Expand the result so we have one value per UTF-16 code unit. + + // Note, skia's getTextWidth gets confused if it encounters a surrogate pair, + // leaving the remaining widths zero. Not nice. + const jchar *chars = text + start; + for (int i = count, p = widths - 1; --i > p;) { + if (chars[i] >= 0xdc00 && chars[i] < 0xe000 && + chars[i-1] >= 0xd800 && chars[i-1] < 0xdc00) { + scalarArray[i] = 0; + } else { + scalarArray[i] = scalarArray[--p]; + } + } + } + + jint pos = offset - start; + switch (opt) { + case AFTER: + if (pos < count) { + pos += 1; + } + // fall through + case AT_OR_AFTER: + while (pos < count && scalarArray[pos] == 0) { + ++pos; + } + break; + case BEFORE: + if (pos > 0) { + --pos; + } + // fall through + case AT_OR_BEFORE: + while (pos > 0 && scalarArray[pos] == 0) { + --pos; + } + break; + case AT: + default: + if (scalarArray[pos] == 0) { + pos = -1; + } + break; + } + + if (pos != -1) { + pos += start; + } + + return pos; + } + + static jint getTextRunCursor___C(JNIEnv* env, jobject clazz, SkPaint* paint, jcharArray text, + jint contextStart, jint contextCount, jint flags, jint offset, jint cursorOpt) { + jchar* textArray = env->GetCharArrayElements(text, NULL); + jint result = doTextRunCursor(env, paint, textArray, contextStart, contextCount, flags, + offset, cursorOpt); + env->ReleaseCharArrayElements(text, textArray, JNI_ABORT); + return result; + } + + static jint getTextRunCursor__String(JNIEnv* env, jobject clazz, SkPaint* paint, jstring text, + jint contextStart, jint contextEnd, jint flags, jint offset, jint cursorOpt) { + const jchar* textArray = env->GetStringChars(text, NULL); + jint result = doTextRunCursor(env, paint, textArray, contextStart, + contextEnd - contextStart, flags, offset, cursorOpt); + env->ReleaseStringChars(text, textArray); + return result; + } + + static void getTextPath(JNIEnv* env, SkPaint* paint, const jchar* text, jint count, + jint bidiFlags, jfloat x, jfloat y, SkPath *path) { + TextLayout::getTextPath(paint, text, count, bidiFlags, x, y, path); + } + + static void getTextPath___C(JNIEnv* env, jobject clazz, SkPaint* paint, jint bidiFlags, + jcharArray text, int index, int count, jfloat x, jfloat y, SkPath* path) { const jchar* textArray = env->GetCharArrayElements(text, NULL); - paint->getTextPath(textArray + index, count << 1, SkFloatToScalar(x), SkFloatToScalar(y), path); - env->ReleaseCharArrayElements(text, const_cast<jchar*>(textArray), - JNI_ABORT); + getTextPath(env, paint, textArray + index, count, bidiFlags, x, y, path); + env->ReleaseCharArrayElements(text, const_cast<jchar*>(textArray), JNI_ABORT); } - - static void getTextPath__StringIIFFPath(JNIEnv* env, jobject clazz, SkPaint* paint, jstring text, int start, int end, jfloat x, jfloat y, SkPath* path) { + + static void getTextPath__String(JNIEnv* env, jobject clazz, SkPaint* paint, jint bidiFlags, + jstring text, int start, int end, jfloat x, jfloat y, SkPath* path) { const jchar* textArray = env->GetStringChars(text, NULL); - paint->getTextPath(textArray + start, (end - start) << 1, SkFloatToScalar(x), SkFloatToScalar(y), path); + getTextPath(env, paint, textArray + start, end - start, bidiFlags, x, y, path); env->ReleaseStringChars(text, textArray); } - + static void setShadowLayer(JNIEnv* env, jobject jpaint, jfloat radius, jfloat dx, jfloat dy, int color) { NPE_CHECK_RETURN_VOID(env, jpaint); @@ -576,8 +725,15 @@ static JNINativeMethod methods[] = { {"native_breakText","(Ljava/lang/String;ZF[F)I", (void*) SkPaintGlue::breakTextS}, {"native_getTextWidths","(I[CII[F)I", (void*) SkPaintGlue::getTextWidths___CII_F}, {"native_getTextWidths","(ILjava/lang/String;II[F)I", (void*) SkPaintGlue::getTextWidths__StringII_F}, - {"native_getTextPath","(I[CIIFFI)V", (void*) SkPaintGlue::getTextPath___CIIFFPath}, - {"native_getTextPath","(ILjava/lang/String;IIFFI)V", (void*) SkPaintGlue::getTextPath__StringIIFFPath}, + {"native_getTextRunAdvances","(I[CIIIII[FI)F", (void*) + SkPaintGlue::getTextRunAdvances___CIIIII_FI}, + {"native_getTextRunAdvances","(ILjava/lang/String;IIIII[FI)F", + (void*) SkPaintGlue::getTextRunAdvances__StringIIIII_FI}, + {"native_getTextRunCursor", "(I[CIIIII)I", (void*) SkPaintGlue::getTextRunCursor___C}, + {"native_getTextRunCursor", "(ILjava/lang/String;IIIII)I", + (void*) SkPaintGlue::getTextRunCursor__String}, + {"native_getTextPath","(II[CIIFFI)V", (void*) SkPaintGlue::getTextPath___C}, + {"native_getTextPath","(IILjava/lang/String;IIFFI)V", (void*) SkPaintGlue::getTextPath__String}, {"nativeGetStringBounds", "(ILjava/lang/String;IILandroid/graphics/Rect;)V", (void*) SkPaintGlue::getStringBounds }, {"nativeGetCharArrayBounds", "(I[CIILandroid/graphics/Rect;)V", diff --git a/core/jni/android/graphics/TextLayout.cpp b/core/jni/android/graphics/TextLayout.cpp new file mode 100644 index 0000000..e2536ee --- /dev/null +++ b/core/jni/android/graphics/TextLayout.cpp @@ -0,0 +1,324 @@ +/* + * 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. + */ + +#include "TextLayout.h" + +#include <android_runtime/AndroidRuntime.h> + +#include "SkTemplates.h" +#include "unicode/ubidi.h" +#include "unicode/ushape.h" +#include <utils/Log.h> + + +namespace android { +// Returns true if we might need layout. If bidiFlags force LTR, assume no layout, if +// bidiFlags indicate there probably is RTL, assume we do, otherwise scan the text +// looking for a character >= the first RTL character in unicode and assume we do if +// we find one. +bool TextLayout::needsLayout(const jchar* text, jint len, jint bidiFlags) { + if (bidiFlags == kBidi_Force_LTR) { + return false; + } + if ((bidiFlags == kBidi_RTL) || (bidiFlags == kBidi_Default_RTL) || + bidiFlags == kBidi_Force_RTL) { + return true; + } + for (int i = 0; i < len; ++i) { + if (text[i] >= 0x0590) { + return true; + } + } + return false; +} + +/** + * Character-based Arabic shaping. + * + * We'll use harfbuzz and glyph-based shaping instead once we're set up for it. + * + * @context the text context + * @start the start of the text to render + * @count the length of the text to render, start + count must be <= contextCount + * @contextCount the length of the context + * @shaped where to put the shaped text, must have capacity for count uchars + * @return the length of the shaped text, or -1 if error + */ +int TextLayout::shapeRtlText(const jchar* context, jsize start, jsize count, jsize contextCount, + jchar* shaped, UErrorCode &status) { + jchar buffer[contextCount]; + + // Use fixed length since we need to keep start and count valid + u_shapeArabic(context, contextCount, buffer, contextCount, + U_SHAPE_LENGTH_FIXED_SPACES_NEAR | + U_SHAPE_TEXT_DIRECTION_LOGICAL | U_SHAPE_LETTERS_SHAPE | + U_SHAPE_X_LAMALEF_SUB_ALTERNATE, &status); + + if (U_SUCCESS(status)) { + // trim out 0xffff following ligatures, if any + int end = 0; + for (int i = start, e = start + count; i < e; ++i) { + if (buffer[i] != 0xffff) { + buffer[end++] = buffer[i]; + } + } + count = end; + // LOG(LOG_INFO, "CSRTL", "start %d count %d ccount %d\n", start, count, contextCount); + ubidi_writeReverse(buffer, count, shaped, count, UBIDI_DO_MIRRORING | UBIDI_OUTPUT_REVERSE + | UBIDI_KEEP_BASE_COMBINING, &status); + if (U_SUCCESS(status)) { + return count; + } + } + + return -1; +} + +/** + * Basic character-based layout supporting rtl and arabic shaping. + * Runs bidi on the text and generates a reordered, shaped line in buffer, returning + * the length. + * @text the text + * @len the length of the text in uchars + * @dir receives the resolved paragraph direction + * @buffer the buffer to receive the reordered, shaped line. Must have capacity of + * at least len jchars. + * @flags line bidi flags + * @return the length of the reordered, shaped line, or -1 if error + */ +jint TextLayout::layoutLine(const jchar* text, jint len, jint flags, int &dir, jchar* buffer, + UErrorCode &status) { + static const int RTL_OPTS = UBIDI_DO_MIRRORING | UBIDI_KEEP_BASE_COMBINING | + UBIDI_REMOVE_BIDI_CONTROLS | UBIDI_OUTPUT_REVERSE; + + UBiDiLevel bidiReq = 0; + switch (flags) { + case kBidi_LTR: bidiReq = 0; break; // no ICU constant, canonical LTR level + case kBidi_RTL: bidiReq = 1; break; // no ICU constant, canonical RTL level + case kBidi_Default_LTR: bidiReq = UBIDI_DEFAULT_LTR; break; + case kBidi_Default_RTL: bidiReq = UBIDI_DEFAULT_RTL; break; + case kBidi_Force_LTR: memcpy(buffer, text, len * sizeof(jchar)); return len; + case kBidi_Force_RTL: return shapeRtlText(text, 0, len, len, buffer, status); + } + + int32_t result = -1; + + UBiDi* bidi = ubidi_open(); + if (bidi) { + ubidi_setPara(bidi, text, len, bidiReq, NULL, &status); + if (U_SUCCESS(status)) { + dir = ubidi_getParaLevel(bidi) & 0x1; // 0 if ltr, 1 if rtl + + int rc = ubidi_countRuns(bidi, &status); + if (U_SUCCESS(status)) { + // LOG(LOG_INFO, "LAYOUT", "para bidiReq=%d dir=%d rc=%d\n", bidiReq, dir, rc); + + int32_t slen = 0; + for (int i = 0; i < rc; ++i) { + int32_t start; + int32_t length; + UBiDiDirection runDir = ubidi_getVisualRun(bidi, i, &start, &length); + + if (runDir == UBIDI_RTL) { + slen += shapeRtlText(text + start, 0, length, length, buffer + slen, status); + } else { + memcpy(buffer + slen, text + start, length * sizeof(jchar)); + slen += length; + } + } + if (U_SUCCESS(status)) { + result = slen; + } + } + } + ubidi_close(bidi); + } + + return result; +} + +// Draws or gets the path of a paragraph of text on a single line, running bidi and shaping. +// This will draw if canvas is not null, otherwise path must be non-null and it will create +// a path representing the text that would have been drawn. +void TextLayout::handleText(SkPaint *paint, const jchar* text, jsize len, + jint bidiFlags, jfloat x, jfloat y,SkCanvas *canvas, SkPath *path) { + + const jchar *workText = text; + jchar *buffer = NULL; + int dir = kDirection_LTR; + if (needsLayout(text, len, bidiFlags)) { + buffer =(jchar *) malloc(len * sizeof(jchar)); + if (!buffer) { + return; + } + UErrorCode status = U_ZERO_ERROR; + len = layoutLine(text, len, bidiFlags, dir, buffer, status); // might change len, dir + if (!U_SUCCESS(status)) { + LOG(LOG_WARN, "LAYOUT", "drawText error %d\n", status); + free(buffer); + return; // can't render + } + + workText = buffer; // use the shaped text + } + + bool trimLeft = false; + bool trimRight = false; + + SkPaint::Align horiz = paint->getTextAlign(); + switch (horiz) { + case SkPaint::kLeft_Align: trimLeft = dir & kDirection_Mask; break; + case SkPaint::kCenter_Align: trimLeft = trimRight = true; break; + case SkPaint::kRight_Align: trimRight = !(dir & kDirection_Mask); + default: break; + } + const jchar* workLimit = workText + len; + + if (trimLeft) { + while (workText < workLimit && *workText == ' ') { + ++workText; + } + } + if (trimRight) { + while (workLimit > workText && *(workLimit - 1) == ' ') { + --workLimit; + } + } + + int32_t workBytes = (workLimit - workText) << 1; + SkScalar x_ = SkFloatToScalar(x); + SkScalar y_ = SkFloatToScalar(y); + if (canvas) { + canvas->drawText(workText, workBytes, x_, y_, *paint); + } else { + paint->getTextPath(workText, workBytes, x_, y_, path); + } + + free(buffer); +} + +void TextLayout::drawTextRun(SkPaint* paint, const jchar* chars, + jint start, jint count, jint contextCount, + int dirFlags, jfloat x, jfloat y, SkCanvas* canvas) { + + SkScalar x_ = SkFloatToScalar(x); + SkScalar y_ = SkFloatToScalar(y); + + uint8_t rtl = dirFlags & 0x1; + if (rtl) { + SkAutoSTMalloc<80, jchar> buffer(contextCount); + UErrorCode status = U_ZERO_ERROR; + count = shapeRtlText(chars, start, count, contextCount, buffer.get(), status); + if (U_SUCCESS(status)) { + canvas->drawText(buffer.get(), count << 1, x_, y_, *paint); + } else { + LOG(LOG_WARN, "LAYOUT", "drawTextRun error %d\n", status); + } + } else { + canvas->drawText(chars + start, count << 1, x_, y_, *paint); + } + } + +void TextLayout::getTextRunAdvances(SkPaint *paint, const jchar *chars, jint start, + jint count, jint contextCount, jint dirFlags, + jfloat *resultAdvances, jfloat &resultTotalAdvance) { + jchar buffer[contextCount]; + + SkScalar* scalarArray = (SkScalar *)resultAdvances; + resultTotalAdvance = 0; + + // this is where we'd call harfbuzz + // for now we just use ushape.c + + int widths; + const jchar* text; + if (dirFlags & 0x1) { // rtl, call arabic shaping in case + UErrorCode status = U_ZERO_ERROR; + // Use fixed length since we need to keep start and count valid + u_shapeArabic(chars, contextCount, buffer, contextCount, + U_SHAPE_LENGTH_FIXED_SPACES_NEAR | + U_SHAPE_TEXT_DIRECTION_LOGICAL | U_SHAPE_LETTERS_SHAPE | + U_SHAPE_X_LAMALEF_SUB_ALTERNATE, &status); + // we shouldn't fail unless there's an out of memory condition, + // in which case we're hosed anyway + for (int i = start, e = i + count; i < e; ++i) { + if (buffer[i] == 0xffff) { + buffer[i] = 0x200b; // zero-width-space for skia + } + } + text = buffer + start; + widths = paint->getTextWidths(text, count << 1, scalarArray); + } else { + text = chars + start; + widths = paint->getTextWidths(text, count << 1, scalarArray); + } + + if (widths < count) { + // Skia operates on code points, not code units, so surrogate pairs return only + // one value. Expand the result so we have one value per UTF-16 code unit. + + // Note, skia's getTextWidth gets confused if it encounters a surrogate pair, + // leaving the remaining widths zero. Not nice. + for (int i = 0, p = 0; i < widths; ++i) { + resultTotalAdvance += resultAdvances[p++] = SkScalarToFloat(scalarArray[i]); + if (p < count && text[p] >= 0xdc00 && text[p] < 0xe000 && + text[p-1] >= 0xd800 && text[p-1] < 0xdc00) { + resultAdvances[p++] = 0; + } + } + } else { + for (int i = 0; i < count; i++) { + resultTotalAdvance += resultAdvances[i] = SkScalarToFloat(scalarArray[i]); + } + } +} + + +// Draws a paragraph of text on a single line, running bidi and shaping +void TextLayout::drawText(SkPaint* paint, const jchar* text, jsize len, + int bidiFlags, jfloat x, jfloat y, SkCanvas* canvas) { + + handleText(paint, text, len, bidiFlags, x, y, canvas, NULL); +} + +void TextLayout::getTextPath(SkPaint *paint, const jchar *text, jsize len, + jint bidiFlags, jfloat x, jfloat y, SkPath *path) { + handleText(paint, text, len, bidiFlags, x, y, NULL, path); +} + + +void TextLayout::drawTextOnPath(SkPaint* paint, const jchar* text, int count, + int bidiFlags, jfloat hOffset, jfloat vOffset, + SkPath* path, SkCanvas* canvas) { + + SkScalar h_ = SkFloatToScalar(hOffset); + SkScalar v_ = SkFloatToScalar(vOffset); + + if (!needsLayout(text, count, bidiFlags)) { + canvas->drawTextOnPathHV(text, count << 1, *path, h_, v_, *paint); + return; + } + + SkAutoSTMalloc<80, jchar> buffer(count); + int dir = kDirection_LTR; + UErrorCode status = U_ZERO_ERROR; + count = layoutLine(text, count, bidiFlags, dir, buffer.get(), status); + if (U_SUCCESS(status)) { + canvas->drawTextOnPathHV(buffer.get(), count << 1, *path, h_, v_, *paint); + } +} + +} diff --git a/core/jni/android/graphics/TextLayout.h b/core/jni/android/graphics/TextLayout.h new file mode 100644 index 0000000..c0d9f75 --- /dev/null +++ b/core/jni/android/graphics/TextLayout.h @@ -0,0 +1,77 @@ +/* + * 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. + */ + +#include "jni.h" + +#include "SkCanvas.h" +#include "SkPaint.h" +#include "unicode/utypes.h" + +namespace android { + +class TextLayout { +public: + + enum { + kDirection_LTR = 0, + kDirection_RTL = 1, + + kDirection_Mask = 0x1 + }; + + enum { + kBidi_LTR = 0, + kBidi_RTL = 1, + kBidi_Default_LTR = 2, + kBidi_Default_RTL = 3, + kBidi_Force_LTR = 4, + kBidi_Force_RTL = 5, + + kBidi_Mask = 0x7 + }; + + /* + * Draws a unidirectional run of text. + */ + static void drawTextRun(SkPaint* paint, const jchar* chars, + jint start, jint count, jint contextCount, + int dirFlags, jfloat x, jfloat y, SkCanvas* canvas); + + static void getTextRunAdvances(SkPaint *paint, const jchar *chars, jint start, + jint count, jint contextCount, jint dirFlags, + jfloat *resultAdvances, jfloat &resultTotalAdvance); + + static void drawText(SkPaint* paint, const jchar* text, jsize len, + jint bidiFlags, jfloat x, jfloat y, SkCanvas* canvas); + + static void getTextPath(SkPaint *paint, const jchar *text, jsize len, + jint bidiFlags, jfloat x, jfloat y, SkPath *path); + + static void drawTextOnPath(SkPaint* paint, const jchar* text, jsize len, + int bidiFlags, jfloat hOffset, jfloat vOffset, + SkPath* path, SkCanvas* canvas); + +private: + static bool needsLayout(const jchar* text, jint len, jint bidiFlags); + static int shapeRtlText(const jchar* context, jsize start, jsize count, jsize contextCount, + jchar* shaped, UErrorCode &status); + static jint layoutLine(const jchar* text, jint len, jint flags, int &dir, jchar* buffer, + UErrorCode &status); + static void handleText(SkPaint *paint, const jchar* text, jsize len, + int bidiFlags, jfloat x, jfloat y,SkCanvas *canvas, SkPath *path); +}; + +} diff --git a/core/jni/android_bluetooth_common.cpp b/core/jni/android_bluetooth_common.cpp index 9a8f1b8..53ac625 100644 --- a/core/jni/android_bluetooth_common.cpp +++ b/core/jni/android_bluetooth_common.cpp @@ -68,6 +68,10 @@ static Properties adapter_properties[] = { {"UUIDs", DBUS_TYPE_ARRAY}, }; +static Properties input_properties[] = { + {"Connected", DBUS_TYPE_BOOLEAN}, +}; + typedef union { char *str_val; int int_val; @@ -698,6 +702,11 @@ jobjectArray parse_remote_device_property_change(JNIEnv *env, DBusMessage *msg) sizeof(remote_device_properties) / sizeof(Properties)); } +jobjectArray parse_input_property_change(JNIEnv *env, DBusMessage *msg) { + return parse_property_change(env, msg, (Properties *) &input_properties, + sizeof(input_properties) / sizeof(Properties)); +} + jobjectArray parse_adapter_properties(JNIEnv *env, DBusMessageIter *iter) { return parse_properties(env, iter, (Properties *) &adapter_properties, sizeof(adapter_properties) / sizeof(Properties)); @@ -708,6 +717,11 @@ jobjectArray parse_remote_device_properties(JNIEnv *env, DBusMessageIter *iter) sizeof(remote_device_properties) / sizeof(Properties)); } +jobjectArray parse_input_properties(JNIEnv *env, DBusMessageIter *iter) { + return parse_properties(env, iter, (Properties *) &input_properties, + sizeof(input_properties) / sizeof(Properties)); +} + int get_bdaddr(const char *str, bdaddr_t *ba) { char *d = ((char *)ba) + 5, *endp; int i; diff --git a/core/jni/android_bluetooth_common.h b/core/jni/android_bluetooth_common.h index 378bb6f..27a00ae 100644 --- a/core/jni/android_bluetooth_common.h +++ b/core/jni/android_bluetooth_common.h @@ -162,6 +162,8 @@ jobjectArray parse_adapter_properties(JNIEnv *env, DBusMessageIter *iter); jobjectArray parse_remote_device_properties(JNIEnv *env, DBusMessageIter *iter); jobjectArray parse_remote_device_property_change(JNIEnv *env, DBusMessage *msg); jobjectArray parse_adapter_property_change(JNIEnv *env, DBusMessage *msg); +jobjectArray parse_input_properties(JNIEnv *env, DBusMessageIter *iter); +jobjectArray parse_input_property_change(JNIEnv *env, DBusMessage *msg); void append_variant(DBusMessageIter *iter, int type, void *val); int get_bdaddr(const char *str, bdaddr_t *ba); void get_bdaddr_as_string(const bdaddr_t *ba, char *str); diff --git a/core/jni/android_database_CursorWindow.cpp b/core/jni/android_database_CursorWindow.cpp index 91449bc..040dac3 100644 --- a/core/jni/android_database_CursorWindow.cpp +++ b/core/jni/android_database_CursorWindow.cpp @@ -29,7 +29,7 @@ #include <string.h> #include <unistd.h> -#include "CursorWindow.h" +#include "binder/CursorWindow.h" #include "sqlite3_exception.h" #include "android_util_Binder.h" @@ -225,70 +225,6 @@ LOG_WINDOW("Getting blob for %d,%d from %p", row, column, window); return NULL; } -static jboolean isBlob_native(JNIEnv* env, jobject object, jint row, jint column) -{ - int32_t err; - CursorWindow * window = GET_WINDOW(env, object); -LOG_WINDOW("Checking if column is a blob or null for %d,%d from %p", row, column, window); - - field_slot_t field; - err = window->read_field_slot(row, column, &field); - if (err != 0) { - throwExceptionWithRowCol(env, row, column); - return NULL; - } - - return field.type == FIELD_TYPE_BLOB || field.type == FIELD_TYPE_NULL; -} - -static jboolean isString_native(JNIEnv* env, jobject object, jint row, jint column) -{ - int32_t err; - CursorWindow * window = GET_WINDOW(env, object); -LOG_WINDOW("Checking if column is a string or null for %d,%d from %p", row, column, window); - - field_slot_t field; - err = window->read_field_slot(row, column, &field); - if (err != 0) { - throwExceptionWithRowCol(env, row, column); - return NULL; - } - - return field.type == FIELD_TYPE_STRING || field.type == FIELD_TYPE_NULL; -} - -static jboolean isInteger_native(JNIEnv* env, jobject object, jint row, jint column) -{ - int32_t err; - CursorWindow * window = GET_WINDOW(env, object); -LOG_WINDOW("Checking if column is an integer for %d,%d from %p", row, column, window); - - field_slot_t field; - err = window->read_field_slot(row, column, &field); - if (err != 0) { - throwExceptionWithRowCol(env, row, column); - return NULL; - } - - return field.type == FIELD_TYPE_INTEGER; -} - -static jboolean isFloat_native(JNIEnv* env, jobject object, jint row, jint column) -{ - int32_t err; - CursorWindow * window = GET_WINDOW(env, object); -LOG_WINDOW("Checking if column is a float for %d,%d from %p", row, column, window); - - field_slot_t field; - err = window->read_field_slot(row, column, &field); - if (err != 0) { - throwExceptionWithRowCol(env, row, column); - return NULL; - } - - return field.type == FIELD_TYPE_FLOAT; -} - static jstring getString_native(JNIEnv* env, jobject object, jint row, jint column) { int32_t err; @@ -487,10 +423,9 @@ LOG_WINDOW("Getting double for %d,%d from %p", row, column, window); } } -static jboolean isNull_native(JNIEnv* env, jobject object, jint row, jint column) +bool isNull_native(CursorWindow *window, jint row, jint column) { - CursorWindow * window = GET_WINDOW(env, object); -LOG_WINDOW("Checking for NULL at %d,%d from %p", row, column, window); + LOG_WINDOW("Checking for NULL at %d,%d from %p", row, column, window); bool isNull; if (window->getNull(row, column, &isNull)) { @@ -652,6 +587,26 @@ static void freeLastRow(JNIEnv * env, jobject object) { window->freeLastRow(); } +static jint getType_native(JNIEnv* env, jobject object, jint row, jint column) +{ + int32_t err; + CursorWindow * window = GET_WINDOW(env, object); + LOG_WINDOW("returning column type affinity for %d,%d from %p", row, column, window); + + if (isNull_native(window, row, column)) { + return FIELD_TYPE_NULL; + } + + field_slot_t field; + err = window->read_field_slot(row, column, &field); + if (err != 0) { + throwExceptionWithRowCol(env, row, column); + return NULL; + } + + return field.type; +} + static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ @@ -662,11 +617,9 @@ static JNINativeMethod sMethods[] = {"close_native", "()V", (void *)native_close}, {"getLong_native", "(II)J", (void *)getLong_native}, {"getBlob_native", "(II)[B", (void *)getBlob_native}, - {"isBlob_native", "(II)Z", (void *)isBlob_native}, {"getString_native", "(II)Ljava/lang/String;", (void *)getString_native}, {"copyStringToBuffer_native", "(IIILandroid/database/CharArrayBuffer;)[C", (void *)copyStringToBuffer_native}, {"getDouble_native", "(II)D", (void *)getDouble_native}, - {"isNull_native", "(II)Z", (void *)isNull_native}, {"getNumRows_native", "()I", (void *)getNumRows}, {"setNumColumns_native", "(I)Z", (void *)setNumColumns}, {"allocRow_native", "()Z", (void *)allocRow}, @@ -676,9 +629,7 @@ static JNINativeMethod sMethods[] = {"putDouble_native", "(DII)Z", (void *)putDouble_native}, {"freeLastRow_native", "()V", (void *)freeLastRow}, {"putNull_native", "(II)Z", (void *)putNull_native}, - {"isString_native", "(II)Z", (void *)isString_native}, - {"isFloat_native", "(II)Z", (void *)isFloat_native}, - {"isInteger_native", "(II)Z", (void *)isInteger_native}, + {"getType_native", "(II)I", (void *)getType_native}, }; int register_android_database_CursorWindow(JNIEnv * env) diff --git a/core/jni/android_database_SQLiteCompiledSql.cpp b/core/jni/android_database_SQLiteCompiledSql.cpp index 8d1c39e..de4c5c8 100644 --- a/core/jni/android_database_SQLiteCompiledSql.cpp +++ b/core/jni/android_database_SQLiteCompiledSql.cpp @@ -91,22 +91,11 @@ static void native_compile(JNIEnv* env, jobject object, jstring sqlString) compile(env, object, GET_HANDLE(env, object), sqlString); } -static void native_finalize(JNIEnv* env, jobject object) -{ - int err; - sqlite3_stmt * statement = GET_STATEMENT(env, object); - - if (statement != NULL) { - sqlite3_finalize(statement); - env->SetIntField(object, gStatementField, 0); - } -} static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ {"native_compile", "(Ljava/lang/String;)V", (void *)native_compile}, - {"native_finalize", "()V", (void *)native_finalize}, }; int register_android_database_SQLiteCompiledSql(JNIEnv * env) diff --git a/core/jni/android_database_SQLiteDatabase.cpp b/core/jni/android_database_SQLiteDatabase.cpp index 36234a9..5a92193 100644 --- a/core/jni/android_database_SQLiteDatabase.cpp +++ b/core/jni/android_database_SQLiteDatabase.cpp @@ -63,8 +63,8 @@ enum { static jfieldID offset_db_handle; -static char *createStr(const char *path) { - int len = strlen(path); +static char *createStr(const char *path, short extra) { + int len = strlen(path) + extra; char *str = (char *)malloc(len + 1); strncpy(str, path, len); str[len] = NULL; @@ -85,7 +85,7 @@ static void registerLoggingFunc(const char *path) { } LOGV("Registering sqlite logging func \n"); - int err = sqlite3_config(SQLITE_CONFIG_LOG, &sqlLogger, (void *)createStr(path)); + int err = sqlite3_config(SQLITE_CONFIG_LOG, &sqlLogger, (void *)createStr(path, 0)); if (err != SQLITE_OK) { LOGE("sqlite_config failed error_code = %d. THIS SHOULD NEVER occur.\n", err); return; @@ -176,13 +176,17 @@ done: if (handle != NULL) sqlite3_close(handle); } -static char *getDatabaseName(JNIEnv* env, sqlite3 * handle, jstring databaseName) { +static char *getDatabaseName(JNIEnv* env, sqlite3 * handle, jstring databaseName, short connNum) { char const *path = env->GetStringUTFChars(databaseName, NULL); if (path == NULL) { LOGE("Failure in getDatabaseName(). VM ran out of memory?\n"); return NULL; // VM would have thrown OutOfMemoryError } - char *dbNameStr = createStr(path); + char *dbNameStr = createStr(path, 4); + if (connNum > 999) { // TODO: if number of pooled connections > 999, fix this line. + connNum = -1; + } + sprintf(dbNameStr + strlen(path), "|%03d", connNum); env->ReleaseStringUTFChars(databaseName, path); return dbNameStr; } @@ -192,10 +196,10 @@ static void sqlTrace(void *databaseName, const char *sql) { } /* public native void enableSqlTracing(); */ -static void enableSqlTracing(JNIEnv* env, jobject object, jstring databaseName) +static void enableSqlTracing(JNIEnv* env, jobject object, jstring databaseName, jshort connType) { sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle); - sqlite3_trace(handle, &sqlTrace, (void *)getDatabaseName(env, handle, databaseName)); + sqlite3_trace(handle, &sqlTrace, (void *)getDatabaseName(env, handle, databaseName, connType)); } static void sqlProfile(void *databaseName, const char *sql, sqlite3_uint64 tm) { @@ -204,13 +208,13 @@ static void sqlProfile(void *databaseName, const char *sql, sqlite3_uint64 tm) { } /* public native void enableSqlProfiling(); */ -static void enableSqlProfiling(JNIEnv* env, jobject object, jstring databaseName) +static void enableSqlProfiling(JNIEnv* env, jobject object, jstring databaseName, jshort connType) { sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle); - sqlite3_profile(handle, &sqlProfile, (void *)getDatabaseName(env, handle, databaseName)); + sqlite3_profile(handle, &sqlProfile, (void *)getDatabaseName(env, handle, databaseName, + connType)); } - /* public native void close(); */ static void dbclose(JNIEnv* env, jobject object) { @@ -251,7 +255,8 @@ static void native_execSQL(JNIEnv* env, jobject object, jstring sqlString) jsize sqlLen = env->GetStringLength(sqlString); if (sql == NULL || sqlLen == 0) { - jniThrowException(env, "java/lang/IllegalArgumentException", "You must supply an SQL string"); + jniThrowException(env, "java/lang/IllegalArgumentException", + "You must supply an SQL string"); return; } @@ -261,7 +266,8 @@ static void native_execSQL(JNIEnv* env, jobject object, jstring sqlString) if (err != SQLITE_OK) { char const * sql8 = env->GetStringUTFChars(sqlString, NULL); - LOGE("Failure %d (%s) on %p when preparing '%s'.\n", err, sqlite3_errmsg(handle), handle, sql8); + LOGE("Failure %d (%s) on %p when preparing '%s'.\n", err, sqlite3_errmsg(handle), + handle, sql8); throw_sqlite3_exception(env, handle, sql8); env->ReleaseStringUTFChars(sqlString, sql8); return; @@ -272,10 +278,12 @@ static void native_execSQL(JNIEnv* env, jobject object, jstring sqlString) if (stepErr != SQLITE_DONE) { if (stepErr == SQLITE_ROW) { - throw_sqlite3_exception(env, "Queries cannot be performed using execSQL(), use query() instead."); + throw_sqlite3_exception(env, + "Queries cannot be performed using execSQL(), use query() instead."); } else { char const * sql8 = env->GetStringUTFChars(sqlString, NULL); - LOGE("Failure %d (%s) on %p when executing '%s'\n", err, sqlite3_errmsg(handle), handle, sql8); + LOGE("Failure %d (%s) on %p when executing '%s'\n", err, sqlite3_errmsg(handle), + handle, sql8); throw_sqlite3_exception(env, handle, sql8); env->ReleaseStringUTFChars(sqlString, sql8); @@ -443,19 +451,27 @@ static jint native_releaseMemory(JNIEnv *env, jobject clazz) return sqlite3_release_memory(SQLITE_SOFT_HEAP_LIMIT); } +static void native_finalize(JNIEnv* env, jobject object, jint statementId) +{ + if (statementId > 0) { + sqlite3_finalize((sqlite3_stmt *)statementId); + } +} + static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ {"dbopen", "(Ljava/lang/String;I)V", (void *)dbopen}, {"dbclose", "()V", (void *)dbclose}, - {"enableSqlTracing", "(Ljava/lang/String;)V", (void *)enableSqlTracing}, - {"enableSqlProfiling", "(Ljava/lang/String;)V", (void *)enableSqlProfiling}, + {"enableSqlTracing", "(Ljava/lang/String;S)V", (void *)enableSqlTracing}, + {"enableSqlProfiling", "(Ljava/lang/String;S)V", (void *)enableSqlProfiling}, {"native_execSQL", "(Ljava/lang/String;)V", (void *)native_execSQL}, {"lastInsertRow", "()J", (void *)lastInsertRow}, {"lastChangeCount", "()I", (void *)lastChangeCount}, {"native_setLocale", "(Ljava/lang/String;I)V", (void *)native_setLocale}, {"native_getDbLookaside", "()I", (void *)native_getDbLookaside}, {"releaseMemory", "()I", (void *)native_releaseMemory}, + {"native_finalize", "(I)V", (void *)native_finalize}, }; int register_android_database_SQLiteDatabase(JNIEnv *env) @@ -474,7 +490,8 @@ int register_android_database_SQLiteDatabase(JNIEnv *env) return -1; } - return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteDatabase", sMethods, NELEM(sMethods)); + return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteDatabase", + sMethods, NELEM(sMethods)); } /* throw a SQLiteException with a message appropriate for the error in handle */ @@ -523,6 +540,7 @@ void throw_sqlite3_exception(JNIEnv* env, int errcode, exceptionClass = "android/database/sqlite/SQLiteDiskIOException"; break; case SQLITE_CORRUPT: + case SQLITE_NOTADB: // treat "unsupported file format" error as corruption also exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException"; break; case SQLITE_CONSTRAINT: diff --git a/core/jni/android_database_SQLiteQuery.cpp b/core/jni/android_database_SQLiteQuery.cpp index 4427168..747ee50 100644 --- a/core/jni/android_database_SQLiteQuery.cpp +++ b/core/jni/android_database_SQLiteQuery.cpp @@ -29,7 +29,7 @@ #include <string.h> #include <unistd.h> -#include "CursorWindow.h" +#include "binder/CursorWindow.h" #include "sqlite3_exception.h" diff --git a/core/jni/android_database_SQLiteStatement.cpp b/core/jni/android_database_SQLiteStatement.cpp index ff2ed5d..0341e0b 100644 --- a/core/jni/android_database_SQLiteStatement.cpp +++ b/core/jni/android_database_SQLiteStatement.cpp @@ -58,7 +58,9 @@ static void native_execute(JNIEnv* env, jobject object) err = sqlite3_step(statement); // Throw an exception if an error occured - if (err != SQLITE_DONE) { + if (err == SQLITE_ROW) { + LOGV("Queries cannot be performed using execute(). use SQLiteDatabase.query() instead."); + } else if (err != SQLITE_DONE) { throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle)); } diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index 50df9d3..3cde9d6 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -22,8 +22,29 @@ #include <utils/Log.h> #include <arpa/inet.h> -#include <netutils/ifc.h> -#include <netutils/dhcp.h> +extern "C" { +int ifc_enable(const char *ifname); +int ifc_disable(const char *ifname); +int ifc_add_host_route(const char *ifname, uint32_t addr); +int ifc_remove_host_routes(const char *ifname); +int ifc_set_default_route(const char *ifname, uint32_t gateway); +int ifc_get_default_route(const char *ifname); +int ifc_remove_default_route(const char *ifname); +int ifc_reset_connections(const char *ifname); +int ifc_configure(const char *ifname, in_addr_t ipaddr, in_addr_t netmask, in_addr_t gateway, in_addr_t dns1, in_addr_t dns2); + +int dhcp_do_request(const char *ifname, + in_addr_t *ipaddr, + in_addr_t *gateway, + in_addr_t *mask, + in_addr_t *dns1, + in_addr_t *dns2, + in_addr_t *server, + uint32_t *lease); +int dhcp_stop(const char *ifname); +int dhcp_release_lease(const char *ifname); +char *dhcp_get_errmsg(); +} #define NETUTILS_PKG_NAME "android/net/NetworkUtils" @@ -201,10 +222,10 @@ static JNINativeMethod gNetworkUtilMethods[] = { { "enableInterface", "(Ljava/lang/String;)I", (void *)android_net_utils_enableInterface }, { "disableInterface", "(Ljava/lang/String;)I", (void *)android_net_utils_disableInterface }, - { "addHostRoute", "(Ljava/lang/String;I)I", (void *)android_net_utils_addHostRoute }, + { "addHostRouteNative", "(Ljava/lang/String;I)I", (void *)android_net_utils_addHostRoute }, { "removeHostRoutes", "(Ljava/lang/String;)I", (void *)android_net_utils_removeHostRoutes }, - { "setDefaultRoute", "(Ljava/lang/String;I)I", (void *)android_net_utils_setDefaultRoute }, - { "getDefaultRoute", "(Ljava/lang/String;)I", (void *)android_net_utils_getDefaultRoute }, + { "setDefaultRouteNative", "(Ljava/lang/String;I)I", (void *)android_net_utils_setDefaultRoute }, + { "getDefaultRouteNative", "(Ljava/lang/String;)I", (void *)android_net_utils_getDefaultRoute }, { "removeDefaultRoute", "(Ljava/lang/String;)I", (void *)android_net_utils_removeDefaultRoute }, { "resetConnections", "(Ljava/lang/String;)I", (void *)android_net_utils_resetConnections }, { "runDhcp", "(Ljava/lang/String;Landroid/net/DhcpInfo;)Z", (void *)android_net_utils_runDhcp }, diff --git a/core/jni/android_net_wifi_Wifi.cpp b/core/jni/android_net_wifi_Wifi.cpp index 3fc0d58..7392442 100644 --- a/core/jni/android_net_wifi_Wifi.cpp +++ b/core/jni/android_net_wifi_Wifi.cpp @@ -30,6 +30,23 @@ namespace android { static jboolean sScanModeActive = false; +/* + * The following remembers the jfieldID's of the fields + * of the DhcpInfo Java object, so that we don't have + * to look them up every time. + */ +static struct fieldIds { + jclass dhcpInfoClass; + jmethodID constructorId; + jfieldID ipaddress; + jfieldID gateway; + jfieldID netmask; + jfieldID dns1; + jfieldID dns2; + jfieldID serverAddress; + jfieldID leaseDuration; +} dhcpInfoFieldIds; + static int doCommand(const char *cmd, char *replybuf, int replybuflen) { size_t reply_len = replybuflen - 1; @@ -476,6 +493,28 @@ static jboolean android_net_wifi_clearBlacklistCommand(JNIEnv* env, jobject claz return doBooleanCommand("BLACKLIST clear", "OK"); } +static jboolean android_net_wifi_doDhcpRequest(JNIEnv* env, jobject clazz, jobject info) +{ + jint ipaddr, gateway, mask, dns1, dns2, server, lease; + jboolean succeeded = ((jboolean)::do_dhcp_request(&ipaddr, &gateway, &mask, + &dns1, &dns2, &server, &lease) == 0); + if (succeeded && dhcpInfoFieldIds.dhcpInfoClass != NULL) { + env->SetIntField(info, dhcpInfoFieldIds.ipaddress, ipaddr); + env->SetIntField(info, dhcpInfoFieldIds.gateway, gateway); + env->SetIntField(info, dhcpInfoFieldIds.netmask, mask); + env->SetIntField(info, dhcpInfoFieldIds.dns1, dns1); + env->SetIntField(info, dhcpInfoFieldIds.dns2, dns2); + env->SetIntField(info, dhcpInfoFieldIds.serverAddress, server); + env->SetIntField(info, dhcpInfoFieldIds.leaseDuration, lease); + } + return succeeded; +} + +static jstring android_net_wifi_getDhcpError(JNIEnv* env, jobject clazz) +{ + return env->NewStringUTF(::get_dhcp_error_string()); +} + // ---------------------------------------------------------------------------- /* @@ -532,6 +571,9 @@ static JNINativeMethod gWifiMethods[] = { { "setScanResultHandlingCommand", "(I)Z", (void*) android_net_wifi_setScanResultHandlingCommand }, { "addToBlacklistCommand", "(Ljava/lang/String;)Z", (void*) android_net_wifi_addToBlacklistCommand }, { "clearBlacklistCommand", "()Z", (void*) android_net_wifi_clearBlacklistCommand }, + + { "doDhcpRequest", "(Landroid/net/DhcpInfo;)Z", (void*) android_net_wifi_doDhcpRequest }, + { "getDhcpError", "()Ljava/lang/String;", (void*) android_net_wifi_getDhcpError }, }; int register_android_net_wifi_WifiManager(JNIEnv* env) @@ -539,6 +581,18 @@ int register_android_net_wifi_WifiManager(JNIEnv* env) jclass wifi = env->FindClass(WIFI_PKG_NAME); LOG_FATAL_IF(wifi == NULL, "Unable to find class " WIFI_PKG_NAME); + dhcpInfoFieldIds.dhcpInfoClass = env->FindClass("android/net/DhcpInfo"); + if (dhcpInfoFieldIds.dhcpInfoClass != NULL) { + dhcpInfoFieldIds.constructorId = env->GetMethodID(dhcpInfoFieldIds.dhcpInfoClass, "<init>", "()V"); + dhcpInfoFieldIds.ipaddress = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "ipAddress", "I"); + dhcpInfoFieldIds.gateway = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "gateway", "I"); + dhcpInfoFieldIds.netmask = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "netmask", "I"); + dhcpInfoFieldIds.dns1 = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "dns1", "I"); + dhcpInfoFieldIds.dns2 = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "dns2", "I"); + dhcpInfoFieldIds.serverAddress = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "serverAddress", "I"); + dhcpInfoFieldIds.leaseDuration = env->GetFieldID(dhcpInfoFieldIds.dhcpInfoClass, "leaseDuration", "I"); + } + return AndroidRuntime::registerNativeMethods(env, WIFI_PKG_NAME, gWifiMethods, NELEM(gWifiMethods)); } diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 3ee404a..4a877d2 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#define LOG_TAG "android.os.Debug" #include "JNIHelp.h" #include "jni.h" #include "utils/misc.h" @@ -24,6 +25,8 @@ #include <unistd.h> #include <time.h> #include <sys/time.h> +#include <errno.h> +#include <assert.h> #ifdef HAVE_MALLOC_H #include <malloc.h> @@ -274,6 +277,176 @@ jint android_os_Debug_getLocalObjectCount(JNIEnv* env, jobject clazz); jint android_os_Debug_getProxyObjectCount(JNIEnv* env, jobject clazz); jint android_os_Debug_getDeathObjectCount(JNIEnv* env, jobject clazz); + +#ifdef HAVE_ANDROID_OS +/* pulled out of bionic */ +extern "C" void get_malloc_leak_info(uint8_t** info, size_t* overallSize, + size_t* infoSize, size_t* totalMemory, size_t* backtraceSize); +extern "C" void free_malloc_leak_info(uint8_t* info); +#define SIZE_FLAG_ZYGOTE_CHILD (1<<31) +#define BACKTRACE_SIZE 32 + +/* + * This is a qsort() callback. + * + * See dumpNativeHeap() for comments about the data format and sort order. + */ +static int compareHeapRecords(const void* vrec1, const void* vrec2) +{ + const size_t* rec1 = (const size_t*) vrec1; + const size_t* rec2 = (const size_t*) vrec2; + size_t size1 = *rec1; + size_t size2 = *rec2; + + if (size1 < size2) { + return 1; + } else if (size1 > size2) { + return -1; + } + + intptr_t* bt1 = (intptr_t*)(rec1 + 2); + intptr_t* bt2 = (intptr_t*)(rec2 + 2); + for (size_t idx = 0; idx < BACKTRACE_SIZE; idx++) { + intptr_t addr1 = bt1[idx]; + intptr_t addr2 = bt2[idx]; + if (addr1 == addr2) { + if (addr1 == 0) + break; + continue; + } + if (addr1 < addr2) { + return -1; + } else if (addr1 > addr2) { + return 1; + } + } + + return 0; +} + +/* + * The get_malloc_leak_info() call returns an array of structs that + * look like this: + * + * size_t size + * size_t allocations + * intptr_t backtrace[32] + * + * "size" is the size of the allocation, "backtrace" is a fixed-size + * array of function pointers, and "allocations" is the number of + * allocations with the exact same size and backtrace. + * + * The entries are sorted by descending total size (i.e. size*allocations) + * then allocation count. For best results with "diff" we'd like to sort + * primarily by individual size then stack trace. Since the entries are + * fixed-size, and we're allowed (by the current implementation) to mangle + * them, we can do this in place. + */ +static void dumpNativeHeap(FILE* fp) +{ + uint8_t* info = NULL; + size_t overallSize, infoSize, totalMemory, backtraceSize; + + get_malloc_leak_info(&info, &overallSize, &infoSize, &totalMemory, + &backtraceSize); + if (info == NULL) { + fprintf(fp, "Native heap dump not available. To enable, run these" + " commands (requires root):\n"); + fprintf(fp, "$ adb shell setprop libc.debug.malloc 1\n"); + fprintf(fp, "$ adb shell stop\n"); + fprintf(fp, "$ adb shell start\n"); + return; + } + assert(infoSize != 0); + assert(overallSize % infoSize == 0); + + fprintf(fp, "Android Native Heap Dump v1.0\n\n"); + + size_t recordCount = overallSize / infoSize; + fprintf(fp, "Total memory: %zu\n", totalMemory); + fprintf(fp, "Allocation records: %zd\n", recordCount); + if (backtraceSize != BACKTRACE_SIZE) { + fprintf(fp, "WARNING: mismatched backtrace sizes (%d vs. %d)\n", + backtraceSize, BACKTRACE_SIZE); + } + fprintf(fp, "\n"); + + /* re-sort the entries */ + qsort(info, recordCount, infoSize, compareHeapRecords); + + /* dump the entries to the file */ + const uint8_t* ptr = info; + for (size_t idx = 0; idx < recordCount; idx++) { + size_t size = *(size_t*) ptr; + size_t allocations = *(size_t*) (ptr + sizeof(size_t)); + intptr_t* backtrace = (intptr_t*) (ptr + sizeof(size_t) * 2); + + fprintf(fp, "z %d sz %8zu num %4zu bt", + (size & SIZE_FLAG_ZYGOTE_CHILD) != 0, + size & ~SIZE_FLAG_ZYGOTE_CHILD, + allocations); + for (size_t bt = 0; bt < backtraceSize; bt++) { + if (backtrace[bt] == 0) { + break; + } else { + fprintf(fp, " %08x", backtrace[bt]); + } + } + fprintf(fp, "\n"); + + ptr += infoSize; + } + + fprintf(fp, "END\n"); + free_malloc_leak_info(info); +} +#endif /*HAVE_ANDROID_OS*/ + +/* + * Dump the native heap, writing human-readable output to the specified + * file descriptor. + */ +static void android_os_Debug_dumpNativeHeap(JNIEnv* env, jobject clazz, + jobject fileDescriptor) +{ + if (fileDescriptor == NULL) { + jniThrowNullPointerException(env, NULL); + return; + } + int origFd = jniGetFDFromFileDescriptor(env, fileDescriptor); + if (origFd < 0) { + jniThrowRuntimeException(env, "Invalid file descriptor"); + return; + } + + /* dup() the descriptor so we don't close the original with fclose() */ + int fd = dup(origFd); + if (fd < 0) { + LOGW("dup(%d) failed: %s\n", origFd, strerror(errno)); + jniThrowRuntimeException(env, "dup() failed"); + return; + } + + FILE* fp = fdopen(fd, "w"); + if (fp == NULL) { + LOGW("fdopen(%d) failed: %s\n", fd, strerror(errno)); + close(fd); + jniThrowRuntimeException(env, "fdopen() failed"); + return; + } + +#ifdef HAVE_ANDROID_OS + LOGD("Native heap dump starting...\n"); + dumpNativeHeap(fp); + LOGD("Native heap dump complete.\n"); +#else + fprintf(fp, "Native heap dump not available on this platform\n"); +#endif + + fclose(fp); +} + + /* * JNI registration. */ @@ -289,6 +462,8 @@ static JNINativeMethod gMethods[] = { (void*) android_os_Debug_getDirtyPages }, { "getMemoryInfo", "(ILandroid/os/Debug$MemoryInfo;)V", (void*) android_os_Debug_getDirtyPagesPid }, + { "dumpNativeHeap", "(Ljava/io/FileDescriptor;)V", + (void*) android_os_Debug_dumpNativeHeap }, { "getBinderSentTransactions", "()I", (void*) android_os_Debug_getBinderSentTransactions }, { "getBinderReceivedTransactions", "()I", @@ -320,4 +495,4 @@ int register_android_os_Debug(JNIEnv *env) return jniRegisterNativeMethods(env, "android/os/Debug", gMethods, NELEM(gMethods)); } -}; +}; // namespace android diff --git a/core/jni/android_server_BluetoothEventLoop.cpp b/core/jni/android_server_BluetoothEventLoop.cpp index 01b6711..3c88158 100644 --- a/core/jni/android_server_BluetoothEventLoop.cpp +++ b/core/jni/android_server_BluetoothEventLoop.cpp @@ -64,6 +64,8 @@ static jmethodID method_onDisplayPasskey; static jmethodID method_onAgentAuthorize; static jmethodID method_onAgentCancel; +static jmethodID method_onInputDevicePropertyChanged; + typedef event_loop_native_data_t native_data_t; #define EVENT_LOOP_REFS 10 @@ -116,6 +118,9 @@ static void classInitNative(JNIEnv* env, jclass clazz) { "(Ljava/lang/String;I)V"); method_onDisplayPasskey = env->GetMethodID(clazz, "onDisplayPasskey", "(Ljava/lang/String;II)V"); + method_onInputDevicePropertyChanged = env->GetMethodID(clazz, "onInputDevicePropertyChanged", + "(Ljava/lang/String;[Ljava/lang/String;)V"); + field_mNativeData = env->GetFieldID(clazz, "mNativeData", "I"); #endif @@ -853,6 +858,22 @@ static DBusHandlerResult event_filter(DBusConnection *conn, DBusMessage *msg, method_onDeviceDisconnectRequested, env->NewStringUTF(remote_device_path)); goto success; + } else if (dbus_message_is_signal(msg, + "org.bluez.Input", + "PropertyChanged")) { + + jobjectArray str_array = + parse_input_property_change(env, msg); + if (str_array != NULL) { + const char *c_path = dbus_message_get_path(msg); + env->CallVoidMethod(nat->me, + method_onInputDevicePropertyChanged, + env->NewStringUTF(c_path), + str_array); + } else { + LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); + } + goto success; } ret = a2dp_event_filter(msg, env); diff --git a/core/jni/android_server_BluetoothService.cpp b/core/jni/android_server_BluetoothService.cpp index 4420aca..a52a74c 100644 --- a/core/jni/android_server_BluetoothService.cpp +++ b/core/jni/android_server_BluetoothService.cpp @@ -16,6 +16,8 @@ #define DBUS_ADAPTER_IFACE BLUEZ_DBUS_BASE_IFC ".Adapter" #define DBUS_DEVICE_IFACE BLUEZ_DBUS_BASE_IFC ".Device" +#define DBUS_INPUT_IFACE BLUEZ_DBUS_BASE_IFC ".Input" + #define LOG_TAG "BluetoothService.cpp" #include "android_bluetooth_common.h" @@ -881,6 +883,43 @@ static jboolean setLinkTimeoutNative(JNIEnv *env, jobject object, jstring object return JNI_FALSE; } +static jboolean connectInputDeviceNative(JNIEnv *env, jobject object, jstring path) { + LOGV(__FUNCTION__); +#ifdef HAVE_BLUETOOTH + native_data_t *nat = get_native_data(env, object); + if (nat) { + const char *c_path = env->GetStringUTFChars(path, NULL); + + bool ret = dbus_func_args_async(env, nat->conn, -1, NULL, NULL, nat, + c_path, DBUS_INPUT_IFACE, "Connect", + DBUS_TYPE_INVALID); + + env->ReleaseStringUTFChars(path, c_path); + return ret ? JNI_TRUE : JNI_FALSE; + } +#endif + return JNI_FALSE; +} + +static jboolean disconnectInputDeviceNative(JNIEnv *env, jobject object, + jstring path) { + LOGV(__FUNCTION__); +#ifdef HAVE_BLUETOOTH + native_data_t *nat = get_native_data(env, object); + if (nat) { + const char *c_path = env->GetStringUTFChars(path, NULL); + + bool ret = dbus_func_args_async(env, nat->conn, -1, NULL, NULL, nat, + c_path, DBUS_INPUT_IFACE, "Disconnect", + DBUS_TYPE_INVALID); + + env->ReleaseStringUTFChars(path, c_path); + return ret ? JNI_TRUE : JNI_FALSE; + } +#endif + return JNI_FALSE; +} + static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ {"classInitNative", "()V", (void*)classInitNative}, @@ -926,6 +965,9 @@ static JNINativeMethod sMethods[] = { {"addRfcommServiceRecordNative", "(Ljava/lang/String;JJS)I", (void *)addRfcommServiceRecordNative}, {"removeServiceRecordNative", "(I)Z", (void *)removeServiceRecordNative}, {"setLinkTimeoutNative", "(Ljava/lang/String;I)Z", (void *)setLinkTimeoutNative}, + // HID functions + {"connectInputDeviceNative", "(Ljava/lang/String;)Z", (void *)connectInputDeviceNative}, + {"disconnectInputDeviceNative", "(Ljava/lang/String;)Z", (void *)disconnectInputDeviceNative}, }; int register_android_server_BluetoothService(JNIEnv *env) { diff --git a/core/jni/android_view_GLES20Canvas.cpp b/core/jni/android_view_GLES20Canvas.cpp new file mode 100644 index 0000000..1bbac71 --- /dev/null +++ b/core/jni/android_view_GLES20Canvas.cpp @@ -0,0 +1,282 @@ +/* + * 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. + */ + +#include "jni.h" +#include <nativehelper/JNIHelp.h> +#include <android_runtime/AndroidRuntime.h> +#include <utils/ResourceTypes.h> + +#include <SkBitmap.h> +#include <SkCanvas.h> +#include <SkMatrix.h> +#include <SkPaint.h> +#include <SkXfermode.h> + +#include <OpenGLRenderer.h> +#include <Rect.h> +#include <ui/Rect.h> + +namespace android { + +using namespace uirenderer; + +// ---------------------------------------------------------------------------- +// Java APIs +// ---------------------------------------------------------------------------- + +static struct { + jclass clazz; + jmethodID set; +} gRectClassInfo; + +// ---------------------------------------------------------------------------- +// Constructors +// ---------------------------------------------------------------------------- + +static OpenGLRenderer* android_view_GLES20Canvas_createRenderer(JNIEnv* env, jobject canvas) { + return new OpenGLRenderer; +} + +static void android_view_GLES20Canvas_destroyRenderer(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer) { + delete renderer; +} + +// ---------------------------------------------------------------------------- +// Setup +// ---------------------------------------------------------------------------- + +static void android_view_GLES20Canvas_setViewport(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jint width, jint height) { + renderer->setViewport(width, height); +} + +static void android_view_GLES20Canvas_prepare(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer) { + renderer->prepare(); +} + +// ---------------------------------------------------------------------------- +// State +// ---------------------------------------------------------------------------- + +static jint android_view_GLES20Canvas_save(JNIEnv* env, jobject canvas, OpenGLRenderer* renderer, + jint flags) { + return renderer->save(flags); +} + +static jint android_view_GLES20Canvas_getSaveCount(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer) { + return renderer->getSaveCount(); +} + +static void android_view_GLES20Canvas_restore(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer) { + renderer->restore(); +} + +static void android_view_GLES20Canvas_restoreToCount(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jint saveCount) { + renderer->restoreToCount(saveCount); +} + +// ---------------------------------------------------------------------------- +// Layers +// ---------------------------------------------------------------------------- + +static jint android_view_GLES20Canvas_saveLayer(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat left, jfloat top, jfloat right, jfloat bottom, + SkPaint* paint, jint saveFlags) { + return renderer->saveLayer(left, top, right, bottom, paint, saveFlags); +} + +static jint android_view_GLES20Canvas_saveLayerAlpha(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat left, jfloat top, jfloat right, jfloat bottom, + jint alpha, jint saveFlags) { + return renderer->saveLayerAlpha(left, top, right, bottom, alpha, saveFlags); +} + +// ---------------------------------------------------------------------------- +// Clipping +// ---------------------------------------------------------------------------- + +static bool android_view_GLES20Canvas_quickReject(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat left, jfloat top, jfloat right, jfloat bottom, + SkCanvas::EdgeType edge) { + return renderer->quickReject(left, top, right, bottom); +} + +static bool android_view_GLES20Canvas_clipRectF(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat left, jfloat top, jfloat right, jfloat bottom) { + return renderer->clipRect(left, top, right, bottom); +} + +static bool android_view_GLES20Canvas_clipRect(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jint left, jint top, jint right, jint bottom) { + return renderer->clipRect(float(left), float(top), float(right), float(bottom)); +} + +static bool android_view_GLES20Canvas_getClipBounds(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jobject rect) { + const android::uirenderer::Rect& bounds(renderer->getClipBounds()); + + env->CallVoidMethod(rect, gRectClassInfo.set, + int(bounds.left), int(bounds.top), int(bounds.right), int(bounds.bottom)); + + return !bounds.isEmpty(); +} + +// ---------------------------------------------------------------------------- +// Transforms +// ---------------------------------------------------------------------------- + +static void android_view_GLES20Canvas_translate(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat dx, jfloat dy) { + renderer->translate(dx, dy); +} + +static void android_view_GLES20Canvas_rotate(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat degrees) { + renderer->rotate(degrees); +} + +static void android_view_GLES20Canvas_scale(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat sx, jfloat sy) { + renderer->scale(sx, sy); +} + +static void android_view_GLES20Canvas_setMatrix(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkMatrix* matrix) { + renderer->setMatrix(matrix); +} + +static void android_view_GLES20Canvas_getMatrix(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkMatrix* matrix) { + renderer->getMatrix(matrix); +} + +static void android_view_GLES20Canvas_concatMatrix(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkMatrix* matrix) { + renderer->concatMatrix(matrix); +} + +// ---------------------------------------------------------------------------- +// Drawing +// ---------------------------------------------------------------------------- + +static void android_view_GLES20Canvas_drawBitmap(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkBitmap* bitmap, float left, float top, SkPaint* paint) { + renderer->drawBitmap(bitmap, left, top, paint); +} + +static void android_view_GLES20Canvas_drawBitmapRect(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkBitmap* bitmap, + float srcLeft, float srcTop, float srcRight, float srcBottom, + float dstLeft, float dstTop, float dstRight, float dstBottom, SkPaint* paint) { + renderer->drawBitmap(bitmap, srcLeft, srcTop, srcRight, srcBottom, + dstLeft, dstTop, dstRight, dstBottom, paint); +} + +static void android_view_GLES20Canvas_drawBitmapMatrix(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkBitmap* bitmap, SkMatrix* matrix, SkPaint* paint) { + renderer->drawBitmap(bitmap, matrix, paint); +} + +static void android_view_GLES20Canvas_drawPatch(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, SkBitmap* bitmap, jbyteArray chunks, + float left, float top, float right, float bottom, SkPaint* paint) { + + jbyte* storage = env->GetByteArrayElements(chunks, NULL); + Res_png_9patch* patch = reinterpret_cast<Res_png_9patch*>(storage); + Res_png_9patch::deserialize(patch); + + renderer->drawPatch(bitmap, patch, left, top, right, bottom, paint); + + // TODO: make sure that 0 is correct for the flags + env->ReleaseByteArrayElements(chunks, storage, 0); +} + +static void android_view_GLES20Canvas_drawColor(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jint color, SkXfermode::Mode mode) { + renderer->drawColor(color, mode); +} + +static void android_view_GLES20Canvas_drawRect(JNIEnv* env, jobject canvas, + OpenGLRenderer* renderer, jfloat left, jfloat top, jfloat right, jfloat bottom, + SkPaint* paint) { + renderer->drawRect(left, top, right, bottom, paint); +} + +// ---------------------------------------------------------------------------- +// JNI Glue +// ---------------------------------------------------------------------------- + +const char* const kClassPathName = "android/view/GLES20Canvas"; + +static JNINativeMethod gMethods[] = { + { "nCreateRenderer", "()I", (void*) android_view_GLES20Canvas_createRenderer }, + { "nDestroyRenderer", "(I)V", (void*) android_view_GLES20Canvas_destroyRenderer }, + { "nSetViewport", "(III)V", (void*) android_view_GLES20Canvas_setViewport }, + { "nPrepare", "(I)V", (void*) android_view_GLES20Canvas_prepare }, + + { "nSave", "(II)I", (void*) android_view_GLES20Canvas_save }, + { "nRestore", "(I)V", (void*) android_view_GLES20Canvas_restore }, + { "nRestoreToCount", "(II)V", (void*) android_view_GLES20Canvas_restoreToCount }, + { "nGetSaveCount", "(I)I", (void*) android_view_GLES20Canvas_getSaveCount }, + + { "nSaveLayer", "(IFFFFII)I", (void*) android_view_GLES20Canvas_saveLayer }, + { "nSaveLayerAlpha", "(IFFFFII)I", (void*) android_view_GLES20Canvas_saveLayerAlpha }, + + { "nQuickReject", "(IFFFFI)Z", (void*) android_view_GLES20Canvas_quickReject }, + { "nClipRect", "(IFFFF)Z", (void*) android_view_GLES20Canvas_clipRectF }, + { "nClipRect", "(IIIII)Z", (void*) android_view_GLES20Canvas_clipRect }, + + { "nTranslate", "(IFF)V", (void*) android_view_GLES20Canvas_translate }, + { "nRotate", "(IF)V", (void*) android_view_GLES20Canvas_rotate }, + { "nScale", "(IFF)V", (void*) android_view_GLES20Canvas_scale }, + + { "nSetMatrix", "(II)V", (void*) android_view_GLES20Canvas_setMatrix }, + { "nGetMatrix", "(II)V", (void*) android_view_GLES20Canvas_getMatrix }, + { "nConcatMatrix", "(II)V", (void*) android_view_GLES20Canvas_concatMatrix }, + + { "nDrawBitmap", "(IIFFI)V", (void*) android_view_GLES20Canvas_drawBitmap }, + { "nDrawBitmap", "(IIFFFFFFFFI)V", (void*) android_view_GLES20Canvas_drawBitmapRect }, + { "nDrawBitmap", "(IIII)V", (void*) android_view_GLES20Canvas_drawBitmapMatrix }, + { "nDrawPatch", "(II[BFFFFI)V", (void*) android_view_GLES20Canvas_drawPatch }, + { "nDrawColor", "(III)V", (void*) android_view_GLES20Canvas_drawColor }, + { "nDrawRect", "(IFFFFI)V", (void*) android_view_GLES20Canvas_drawRect }, + + { "nGetClipBounds", "(ILandroid/graphics/Rect;)Z", + (void*) android_view_GLES20Canvas_getClipBounds }, +}; + +#define FIND_CLASS(var, className) \ + var = env->FindClass(className); \ + LOG_FATAL_IF(! var, "Unable to find class " className); \ + var = jclass(env->NewGlobalRef(var)); + +#define GET_METHOD_ID(var, clazz, methodName, methodDescriptor) \ + var = env->GetMethodID(clazz, methodName, methodDescriptor); \ + LOG_FATAL_IF(! var, "Unable to find method " methodName); + +int register_android_view_GLES20Canvas(JNIEnv* env) { + FIND_CLASS(gRectClassInfo.clazz, "android/graphics/Rect"); + GET_METHOD_ID(gRectClassInfo.set, gRectClassInfo.clazz, "set", "(IIII)V"); + + return AndroidRuntime::registerNativeMethods(env, kClassPathName, gMethods, NELEM(gMethods)); +} + +}; diff --git a/core/jni/android_view_HardwareRenderer.cpp b/core/jni/android_view_HardwareRenderer.cpp new file mode 100644 index 0000000..abd788b --- /dev/null +++ b/core/jni/android_view_HardwareRenderer.cpp @@ -0,0 +1,46 @@ +/* + * 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. + */ + +#include <utils/SkGLCanvas.h> + +#include "jni.h" +#include <nativehelper/JNIHelp.h> +#include <android_runtime/AndroidRuntime.h> +#include <utils/misc.h> + +// ---------------------------------------------------------------------------- + +namespace android { + +static void android_view_HardwareRenderer_abandonGlCaches(JNIEnv* env, jobject) { + SkGLCanvas::AbandonAllTextures(); +} + +// ---------------------------------------------------------------------------- + +const char* const kClassPathName = "android/view/HardwareRenderer"; + +static JNINativeMethod gMethods[] = { + { "nativeAbandonGlCaches", "()V", + (void*)android_view_HardwareRenderer_abandonGlCaches }, +}; + +int register_android_view_HardwareRenderer(JNIEnv* env) { + return AndroidRuntime::registerNativeMethods(env, + kClassPathName, gMethods, NELEM(gMethods)); +} + +}; diff --git a/core/jni/android_view_ViewRoot.cpp b/core/jni/android_view_ViewRoot.cpp index 5173bb8..2988ae8 100644 --- a/core/jni/android_view_ViewRoot.cpp +++ b/core/jni/android_view_ViewRoot.cpp @@ -76,10 +76,6 @@ static void android_view_ViewRoot_showFPS(JNIEnv* env, jobject, jobject jcanvas, canvas->restore(); } -static void android_view_ViewRoot_abandonGlCaches(JNIEnv* env, jobject) { - SkGLCanvas::AbandonAllTextures(); -} - // ---------------------------------------------------------------------------- @@ -87,9 +83,7 @@ const char* const kClassPathName = "android/view/ViewRoot"; static JNINativeMethod gMethods[] = { { "nativeShowFPS", "(Landroid/graphics/Canvas;I)V", - (void*)android_view_ViewRoot_showFPS }, - { "nativeAbandonGlCaches", "()V", - (void*)android_view_ViewRoot_abandonGlCaches } + (void*)android_view_ViewRoot_showFPS } }; int register_android_view_ViewRoot(JNIEnv* env) { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 82f822f..a66fe86 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -348,6 +348,13 @@ android:description="@string/permdesc_accountManagerService" android:label="@string/permlab_accountManagerService" /> + <!-- Allows an internal user to use privaledged ConnectivityManager + APIs. + @hide --> + <permission android:name="android.permission.CONNECTIVITY_INTERNAL" + android:permissionGroup="android.permission-group.NETWORK" + android:protectionLevel="signatureOrSystem" /> + <!-- ================================== --> <!-- Permissions for accessing accounts --> <!-- ================================== --> diff --git a/core/res/assets/images/combobox-disabled.png b/core/res/assets/images/combobox-disabled.png Binary files differdeleted file mode 100644 index fe220e4..0000000 --- a/core/res/assets/images/combobox-disabled.png +++ /dev/null diff --git a/core/res/assets/images/combobox-noHighlight.png b/core/res/assets/images/combobox-noHighlight.png Binary files differdeleted file mode 100644 index abcdf72..0000000 --- a/core/res/assets/images/combobox-noHighlight.png +++ /dev/null diff --git a/core/res/assets/webkit/hyph_en_US.dic b/core/res/assets/webkit/hyph_en_US.dic new file mode 100644 index 0000000..d91204b --- /dev/null +++ b/core/res/assets/webkit/hyph_en_US.dic @@ -0,0 +1,9784 @@ +ISO8859-1 +LEFTHYPHENMIN 2 +RIGHTHYPHENMIN 3 +.a2ch4 +.ad4der +.a2d +.ad1d4 +.a2f1t +.a2f +.a4l3t +.am5at +.4a1ma +.an5c +.a2n +.2ang4 +.an1i5m +.an1t4 +.an3te +.anti5s +.ant2i +.a4r5s2 +.2a2r +.ar4t2ie4 +.ar1ti +.ar4ty +.as3c +.as1p +.a2s1s +.aster5 +.a2tom5 +.a1to +.au1d +.av4i +.awn4 +.ba4g +.ba5na +.ba2n +.bas4e +.ber4 +.be5r1a +.be3s1m +.4bes4 +.b4e5s2to +.bri2 +.but4ti +.bu4t3t2 +.cam4pe +.1ca +.ca4m1p +.can5c +.ca2n +.capa5b +.ca1pa +.car5ol +.c2a2r +.ca4t +.ce4la +.2ch4 +.chill5i +.ch4il2 +.chil1l +.1ci2 +.cit5r +.2c1it +.co3e2 +.1co +.co4r +.cor5n1er +.corn2e +.de4moi2 +.d4em +.de1mo +.de3o +.de3r1a +.de3r1i +.de1s4c +.des2 +.dic1t2io5 +.3di2c1t +.do4t +.1do +.du4c +.1du +.du4m1b5 +.earth5 +.ear2t +.e2a2r +.eas3i +.2e1b4 +.eer4 +.eg2 +.e2l5d +.el3em +.enam3 +.e1na +.en3g +.e2n3s2 +.eq5ui5t +.e1q +.equ2 +.eq2ui2 +.er4ri +.er1r4 +.es3 +.4eu3 +.eye5 +.fes3 +.for5mer +.1fo +.fo2r +.for1m +.for2me +.1ga2 +.ge2 +.gen3t4 +.1gen +.ge5o2g +.1geo +.1g2i5a +.gi4b +.go4r +.1go +.hand5i +.ha2n +.h4and +.ha4n5k2 +.he2 +.hero5i2 +.h2ero +.h1es3 +.he4t3 +.hi3b +.hi3er +.h2ie4 +.hon5ey +.ho2n +.hon3o +.hov5 +.id4l +.2id +.idol3 +.i1do +.im3m +.im5p1i2n +.i4m1p +.im2pi +.in1 +.in3ci +.2ine2 +.4i4n2k2 +.2i2n3s2 +.ir5r4 +.4ir +.is4i +.ju3r +.la4cy +.la4m +.lat5er +.l4ath5 +.le2 +.leg5e +.len4 +.lep5 +.lev1 +.l2i4g +.li1g5a +.li2n +.l2i3o +.l1i4t +.ma1g5a5 +.1ma +.mal5o +.ma1n5a +.ma2n +.mar5ti +.m2a2r +.me2 +.mer3c +.me5ter +.me1te +.m2is1 +.mis4t5i +.mon3e +.1mo +.mo2n +.mo3ro +.mo2r +.mu5ta +.1mu +.mu2ta5b +.ni4c +.od2 +.od1d5 +.of5te +.o2ft +.or5a1to +.o1ra +.or3c +.or1d +.or3t +.os3 +.os4tl +.4oth3 +.out3 +.ou2 +.ped5al +.2p2ed +.p2e2d2a +.pe5te +.pe2t +.pe5tit +.p2i4e4 +.pio5n4 +.3p2i1o +.pi2t +.pre3m +.pr2 +.ra4c +.ran4t +.ra2n +.ratio5n1a +.ratio2n4 +.ra1t2io +.ree2 +.re5mit +.res2 +.re5stat +.res2t +.res1ta +.r2i4g +.ri2t5u +.ro4q +.ros5t +.row5d +.ru4d +.3s4c2i3e4 +.s1ci +.5se2l2f5 +.sel1l5 +.se2n +.se5r2ie4 +.ser1i +.s2h2 +.si2 +.s3ing4 +.2s1in +.st4 +.sta5b2l2 +.s1ta +.s2tab +.s4y2 +.1ta4 +.te4 +.3ten5a2n +.te1na +.th2 +.ti2 +.til4 +.ti1m5o5 +.1tim +.ting4 +.2t1in +.t4i4n5k2 +.to1n4a +.1to +.to2n +.to4p +.top5i +.to2u5s +.tou2 +.trib5ut +.tr4ib +.u1n1a +.un3ce +.under5 +.un1de +.u2n1e +.u4n5k2 +.un5o +.un3u4 +.up3 +.ure3 +.us5a2 +.2us +.ven4de +.ve5r1a +.wil5i +.wi2 +.wil2 +.ye4 +4ab. +a5bal +a5ba2n +abe2 +ab5erd +ab2i5a +ab5i2t5ab +abi2t +abi1ta +ab5lat +ab2l2 +ab5o5l1iz +abol2i +4abr +ab5rog +ab3ul +a4c2a2r +a1ca +ac5ard +ac5aro +a5ceou2 +ac1er +a5che4t +a2ch +ache2 +4a2ci +a3c2ie4 +a2c1in +a3c2io +ac5rob +act5if2 +a2c1t +ac3ul +ac4um +a2d +ad4d1in +ad1d4 +ad5er. +2adi +a3d4i3a +ad3i1ca +adi4er +ad2ie4 +a3d2io +a3dit +a5di1u +ad4le +ad3ow +a1do +ad5ra2n +a1dr +ad4su +a2d1s2 +4a1du +a3du2c +ad5um +ae4r +aer2i4e4 +aer1i +a2f +a4f1f4 +a4gab +a1ga +aga4n +ag5el1l +a1ge4o +4ag4eu +ag1i +4ag4l2 +ag1n +a2go +3a3g4o4g +ag3o3ni +ago2n2 +a5guer +a2gue +ag5ul +a4gy +a3ha +a3he +a4h4l4 +a3ho +ai2 +a5i1a +a3ic. +ai5ly +a4i4n +ain5in +a2ini +a2i1n5o +ait5en +a2ite +a1j +ak1en +al5ab +al3a2d +a4l2a2r +4aldi4 +a2ld +2ale +al3end +a4lent2i +a1len1t +a5le5o +al1i +al4ia. +al2i1a +al2i4e4 +al5lev +al1l +al2le +4allic +all2i +4a2lm +a5log. +a4ly. +a1ly +4a2lys4 +5a5lys1t +5alyt +3alyz +4a1ma +a2m5ab +am3ag +ama5ra +am2a2r +am5asc +a4ma3tis +a4m5a1to +am5er1a +am3ic +am5if +am5i1ly +am1in +am2i4no +a2mo +a5mo2n +amor5i +amo2r +amp5en +a4m1p +a2n +an3age +a1na +3ana1ly +a3n2a2r +an3ar3c +anar4i +a3nati +an2at +4and +ande4s2 +an1de +an3dis1 +an1dl +an4dow +an1do +a5nee +a3nen +an5e2st. +a1nes +a2nest +a3n4eu +2ang +ang5ie4 +an1gl2 +a4n1ic +a3nies +an2ie4 +an3i3f +an4ime +an1im +a5nim1i +a5n2ine +an1in +an3i4o +a3n2ip +an3is2h +an3it +a3ni1u +an4kli +a4nk2 +an1k1l +5anniz +a4n1n2 +ano4 +an5ot +an4oth5 +an2sa2 +a2n1s2 +an4s1co +ans4c +an4s1n4 +an2sp +ans3po +an4st +an4su2r +an1su +anta2l4 +an1t +an1ta +an4t2ie4 +ant2i +4an1to +an2tr +an4tw4 +an3u1a +an3ul +a5nur +4ao +ap2a2r4 +a1pa +ap5at +ap5er3o +a3ph4er +4aphi +a4pilla +apil1l +ap5ill2a2r +ap3i2n +ap3i1ta +a3pi2tu +a2p2l2 +apo4c5 +ap5o1la +apor5i +a1p4or +apos3t +a1pos +aps5e4s +a2p1s2 +ap2se +a3pu +aque5 +aqu2 +2a2r +ar3a2c1t +a5rade +ara2d +ar5adis1 +ar2adi +ar3al +a5rame1te +aram3et +ar2an4g +ara2n +ara3p +ar4at +a5ra1t2io +ar5a1t2iv +a5rau +ar5av4 +araw4 +arbal4 +ar1b +ar4cha2n +ar1c +ar3cha +ar2ch +ar5d2ine +ard2i +ard1in4 +ar4dr +ar5eas +a3ree +ar3en1t +a5r2e2ss +ar4fi +ar1f +ar4f4l2 +ar1i +ar5i2al +ar2i3a +ar3i2a2n +a3ri5et +ar2ie4 +ar4im +ar5in2at +ar2i1na +ar3i1o +ar2iz +ar2mi +ar1m +ar5o5d +a5roni +aro2n +a3roo2 +ar2p +ar3q +arre4 +ar1r4 +ar4sa2 +a4rs2 +ar2s2h +4as. +a2s4ab +asa2 +as3an1t +asa2n +ashi4 +as2h +a5sia. +as2i1a +a3si1b +a3sic +5a5si4t +ask3i +ask2 +as4l2 +a4soc +a1so +as5ph +as4s2h +a2ss +as3ten +as1t4r +asu1r5a +a1su +asu2r +a2ta +at3ab2l2 +a2tab +at5ac +at3alo +ata2l +at5ap +ate5c +at5e2ch +at3e1go +ateg4 +at3en. +at3er1a +ater5n +a5ter1na +at3est +at5ev +4ath +ath5em +ath2e +a5the2n +at4ho +ath5om +4ati. +a5t2i1a +a2t5i5b +at1ic +at3if2 +ation5a2r +a1t2io +atio2n +atio1n1a +at3i1tu +a4tog +a1to +a2tom +at5om2iz +a4top +a4tos2 +a1tr +at5rop +at4sk2 +a4t1s2 +at4tag +a4t3t2 +at1ta +at5te +at4th +a2tu +at5u1a +a4t5ue +at3ul +at3u1ra +a2ty +au4b +augh3 +au3gu +au4l2 +aun5d +au3r +au5si1b +a2us +a4ut5en +au1th +a2va +av3ag4 +a5va2n +av4e4no +av3er1a +av5ern +av5ery +av1i +avi4er +av2ie4 +av3ig +av5oc +a1vor +3away +aw3i2 +aw4ly +aws4 +ax4i5c +ax3i +ax4id +ay5al +aye4 +ays4 +azi4er +a2z1i +az2ie4 +az2z5i +a4z1z2 +5ba. +bad5ger +ba2d +ba4ge +bal1a +ban5dag +ba2n +b4and +ban1d2a +ban4e +ban3i +barbi5 +b2a2r +bar1b +bar2i4a +bar1i +bas4si +ba2ss +1bat +ba4z +2b1b +b2be +b3ber +bbi4na +4b1d +4be. +beak4 +bea2t3 +4be2d +b2e3d2a +be3de +b4e3di +be3gi +be5gu +1bel +be1l2i +be3lo +4be5m +be5n2ig +be5nu +4bes4 +be3sp +b2e5st4r +3bet +be1t5iz +be5tr +be3tw4 +be3w +be5y1o4 +2bf +4b3h +bi2b +b2i4d +3b2ie4 +bi5en +bi4er +2b3if +1bil +bi3l2iz +bil1i +bin2a5r4 +bi1na +b4in4d +bi5net +b2ine +bi3o2gr +b2io +bi5ou2 +bi2t +3b2i3t2io +bi1ti +bi3tr +3bit5u1a +bi1tu +b5i4tz +b1j +bk4 +b2l2 +bl4ath5 +b4le. +blen4 +5ble1sp +bles2 +b3lis +b4lo +blun4t +4b1m +4b3n +bne5g +3bod +bod3i +bo4e +bol3ic +bol2i +bom4bi +bo4m1b +bo1n4a +bo2n +bon5at +3boo2 +5bor. +4b1o1ra +bor5d +5bore +5bori +5bos4 +b5o1ta +b4oth5 +bo4to +boun2d3 +bou2 +4bp +4brit +br4oth3 +2b5s2 +bsor4 +b1so +2bt +b2t4l +b4to +b3tr +buf4fer1 +bu4f1f +bu4ga +bu3l2i +bu1mi4 +bu4n +bunt4i +bun1t +bu3re +bus5ie4 +b2us +buss4e +bu2ss +5bust +4bu1ta +3bu1t2io +b4u1t2i +b5u1to +b1v +4b5w +5by. +bys4 +1ca +cab3in +ca1b2l2 +ca2ch4 +ca5den +ca2d +4cag4 +2c5ah +ca3lat +cal4la +cal1l +cal2l5in4 +call2i +4calo +c4an5d +ca2n +can4e +ca4n4ic +can5is +can3iz +can4ty +can1t +cany4 +ca5per +car5om +c2a2r +cast5er +cas5t2ig +cast2i +4cas4y +c4a4th +4ca1t2iv +cav5al +ca2va +c3c +ccha5 +c2ch +c3c2i4a +c1ci +ccom1pa5 +c1co +cco4m1p +cco2n4 +ccou3t +ccou2 +2ce. +4ced. +4ce1den +3cei2 +5cel. +3cel1l +1cen +3cenc +2cen4e +4ceni +3cen1t +3cep +ce5ram +cer1a +4ce1s4a2 +3ces1si +c2e2ss +ces5si5b +ces5t +cet4 +c5e4ta +cew4 +2ch +4ch. +4ch3ab +5cha4n1ic +cha2n +ch5a5nis +che2 +cheap3 +4ch4ed +ch5e5lo +3chemi +ch5ene +che2n +ch3er. +ch3e4r1s2 +4ch1in +5chi2ne. +ch2ine +ch5i5n2e2ss +chi1nes +5ch2ini +5ch2io +3chit +chi2z +3cho2 +ch4ti +1ci +3c2i1a +ci2a5b +ci2a5r +ci5c +4cier +c2ie4 +5c4i2f3ic. +ci1fi +4c4i5i4 +ci4la +3cil1i +2cim +2cin +c4i1na +3cin2at +cin3em +c2ine +c1ing +c5ing. +5c2i1no +cio2n4 +c2io +4cipe4 +c2ip +ci3ph +4cip4ic +cip3i +4cis1ta +4cis1t2i +2c1it +ci1t3iz +ci1ti +5ciz +ck1 +ck3i +1c4l4 +4cl2a2r +c5la5ra1t2io +clar4at +5clare +cle4m +4clic +clim4 +c1ly4 +c5n +1co +co5ag +c4oa +coe2 +2cog +co4gr +coi4 +co3inc +col5i +5colo +col3o4r +com5er +co2me +co1n4a +co2n +c4one +con3g +con5t +co3pa +cop3ic +co4p2l2 +4cor1b +coro3n +cos4e +cov1 +cove4 +cow5a +co2z5e +co5z1i +c1q +cras5t +cr2as +5crat. +5crat1ic +cre3a2t +5c2r2ed +4c3re1ta +cre4v2 +cri2 +cri5f +c4rin +cr2is4 +5cri1ti +cro4p2l2 +crop5o +cros4e +cru4d +4c3s2 +2c1t +c2ta4b +c1ta +ct5ang +cta2n +c5tan1t +c2te +c3ter +c4t4ic1u +ctim3i +c1tim +ctu4r +c1tu +c4tw4 +cud5 +c4uf +c4ui2 +cu5i1ty +5cul2i +cul4tis4 +cul1ti +cu4lt +3c4ul1tu2 +cu2ma +c3ume +cu4mi +3cun +cu3pi +cu5py +cu2r5a4b +cu1ra +cu5r2i3a +1c2us +cus1s4i +cu2ss +3c4ut +cu4t2ie4 +c4u1t2i +4c5u1t2iv +4cutr +1cy +c2ze4 +1d2a +5da. +2d3a4b +da2ch4 +4da2f +2dag +da2m2 +d2an3g +da2n +dard5 +d2a2r +dark5 +4dary +3dat +4da1t2iv +4da1to +5dav4 +dav5e +5day +d1b +d5c +d1d4 +2de. +dea2f5 +de4b5i2t +d2e1b +de4bo2n +deca2n4 +de1ca +de4cil +de1c2i +de5com +de1co +2d1ed +4dee. +de5if +dei2 +del2i4e4 +del2i +de4l5i5q +de5lo +d4em +5dem. +3demic +dem5ic. +de5mil +de4mo2n3s2 +de1mo +demo2n +demo2r5 +1den +de4n2a2r +de1na +d4e3no +denti5f2 +den1t +dent2i +de3nu +de1p +de3pa +depi4 +de2pu +d3e1q +d4er1h4 +5der3m4 +d5ern5iz +de4r5s2 +des2 +d2es. +de1s2c +de2s5o +des3t2i +d2e3st4r +de4su +de1t +de2to +de1v +de2v3i4l +de1vi +4dey +4d1f +d4ga +d3ge4t +dg1i +d2gy +d1h2 +5di. +1d4i3a +dia5b +d4i4cam +di1ca +d4ice +3di2c1t +3d2id +5di3en +d2ie4 +d1if +di3ge +d2ig +di4la1to +di1la +d1in +1di1na +3di2ne. +d2ine +5d2ini +di5niz +1d2io +dio5g +di4p2l2 +d2ip +d4ir2 +di1re +dir1t5i +dis1 +5disi +d4is3t +d2i1ti +1d2i1v +d1j +d5k2 +4d5la +3dle. +3dled +3dles. +dles2 +4d3l2e2ss +2d3lo +4d5lu +2d1ly +d1m +4d1n4 +1do +3do. +do5de +5doe +2d5of +d4og +do4la +dol2i4 +do5lo4r +dom5iz +do3n2at +do2n +do1n1a +doni4 +doo3d +doo2 +do4p4p +d4or +3dos +4d5out +dou2 +do4v +3dox +d1p +1dr +drag5o2n2 +dra2go +4dr2ai2 +dre4 +dre2a5r +5dren +dr4i4b +dril4 +dro4p +4drow +5drupli +dru3p2l2 +4dry +2d1s2 +ds4p +d4sw2 +d4s4y +d2th +1du +d1u1a +du2c +d1u3ca +duc5er +4duct. +du2c1t +4duc4t1s2 +du5el +du4g +d3ul4e +dum4be +du4m1b +du4n +4dup +du4pe +d1v +d1w +d2y +5dyn +dy4s2e +dys5p +e1a4b +e3a2c1t +ea2d1 +ead5ie4 +e2adi +ea4ge +ea5ger +ea4l +eal5er +e2ale +eal3ou2 +eam3er +e5and +ea2n +ear3a +e2a2r +ear4c +ear5es +ear4ic +ear1i +ear4il +ear5k +ear2t +eart3e +ea5sp +e3a2ss +east3 +ea2t +eat5en +eath3i +e4ath +e5at3if2 +e4a3tu +ea2v +eav3en +eav5i +eav5o +2e1b +e4bel. +e1bel +e4be2l1s2 +e4ben +e4bi2t +e3br +e4ca2d +e1ca +ecan5c +eca2n +ec1ca5 +ec3c +e1ce +ec5es1sa2 +ec2e2ss +e1c2i +e4cib +ec5ificat +eci1fi +ecifi1ca +ec5i3f2ie4 +ec5i1fy +e2c3im +e2c1i4t +e5c2ite +e4clam +e1c4l4 +e4cl2us +e2col +e1co +e4com1m +e4compe +eco4m1p +e4con1c +eco2n +e2cor +ec3o1ra +eco5ro +e1cr +e4crem +ec4ta2n +e2c1t +ec1ta +ec4te +e1cu +e4cul +ec3u1la +2e2d2a +4ed3d4 +e4d1er +ede4s2 +4edi +e3d4i3a +ed3ib +ed3i1ca +ed3im +ed1it +edi5z +4e1do +e4dol +edo2n2 +e4dri +e1dr +e4dul +e1du +ed5u1l4o +ee2c +e4ed3i +ee2f +eel3i +ee4ly +ee2m +ee4na +ee4p1 +ee2s4 +eest4 +ee4ty +e5ex +e1f +e4f3ere +efer1 +1e4f1f +e4fic +e1fi +5ef2i1c4i +efil4 +e3f2i2ne +e2fin +ef5i5n2ite +ef2ini +efin2it +3efit +efor5es +e1fo +efo2r +e4fu4se. +e3fu +ef2us +4egal +e1ga +eger4 +eg5ib +eg4ic +eg5ing +e5git5 +eg5n +e4go. +e1go +e4gos +eg1ul +e5gur +5e1gy +e1h4 +eher4 +ei2 +e5ic +e2i5d +e2ig2 +ei5g4l2 +e3i4m1b +e3in3f +e1ing +e5inst +e2i2n1s2 +eir4d +e4ir +e2it3e +e2i3th +e5i1ty +e1j +e4jud +ej5udi +eki4n +ek1i +ek4la +ek1l +e1la +e4la. +e4lac +e3l4an4d +ela2n +e4l5a1t2iv +e4law +elax1a4 +e3le2a +el5ebra +el2e1b +ele3br +5elec +e4led +el3e1ga +e5len +e4l1er +e1les2 +e2l2f +el2i +e3libe4 +e4l5ic. +el3i1ca +e3lier +el2ie4 +el5i3gib +el2ig +el4igi +e5lim +e4l3ing +e3l2io +e2lis +el5is2h +e3l2iv3 +4ella +el1l +el4lab +ell4o4 +e5loc +el5og +el3op. +el2s2h +e2l1s2 +el4ta +e4lt +e5lud +el5ug +e4mac +e1ma +e4mag +e5ma2n +em5a1na +e4m5b +e1me +e2mel +e4met +em3i1ca +em2i4e4 +em5igra +em2ig4 +emi1gr +em1in2 +em5ine +em3i3ni +e4m2is +em5is2h +e5m4i2s1s +em3iz +5emniz +e4m1n +emo4g +e1mo +emo3n2i5o +emo2n +em3pi +e4m1p +e4mul +e1mu +em5u1la +emu3n2 +e3my +en5a2mo +e1na +e4nan1t +en2a2n +ench4er +en2ch +enche2 +en3dic +e5nea +e5nee +en3em +en5ero +en1er +en5e1si +e1nes +e2n5est +en3etr +e3ne4w +en5i4c3s2 +e5n2ie4 +e5nil +e3n2i4o +en3is2h +en3it +e5ni1u +5eniz +4e4n1n2 +4eno +e4no4g +e4nos +en3ov +en4sw2 +e2n1s2 +ent5age +en1t +en1ta +4enth1es +enth2e +en3u1a +en5uf +e3ny. +4e4n3z +e5of +eo2g +e4oi4 +e3ol +eop3a2r +eo2pa +e1or +eo3re +eo5rol +eos4 +e4ot +eo4to +e5out +eou2 +e5ow +e2pa +e3p4ai2 +ep5anc +epa2n +e5pel +e3pen1t +ep5e5t2i1t2io +epe2t +epeti1ti +ephe4 +e4pli +e1p2l2 +e1po +e4prec +epr2 +ep5re1ca +e4p2r2ed +ep3re1h4 +e3pro +e4prob +ep4s4h +e2p1s2 +ep5ti5b +e2p1t +e4pu2t +ep5u1ta +e1q +equi3l +equ2 +eq2ui2 +e4q3ui3s +er1a +e2ra4b +4er4and +era2n +er3a2r +4er4ati. +2er1b +er4b2l2 +er3ch +er1c +er4che2 +2e2re. +e3re1a4l +ere5co +ere3in +erei2 +er5el. +er3e1mo +er5e1na +er5ence +4erene +er3en1t +ere4q +er5e2ss +er3es2t +eret4 +er1h4 +er1i +e1r2i3a4 +5erick1 +e3rien +er2ie4 +eri4er +er3in4e +e1r2i1o +4erit +er4i1u +er2i4v +e4ri1va +er3m4 +er4nis4 +4er3n2it +5erniz +er3no4 +2ero +er5ob +e5r2oc +ero4r +er1ou2 +e4r1s2 +er3set +er2se +ert3er +4er2tl +er3tw4 +4eru +eru4t +5erwau +er1w +e1s4a2 +e4sa2ge. +e4sages +es2c +e2s1ca +es5ca2n +e3scr +es5cu +e1s2e +e2sec +es5e1cr +e4s5enc +e4sert. +e4ser4t1s2 +e4ser1va +4es2h +e3sha +esh5e2n +e1si +e2sic +e2s2id +es5i1den +e4s5ig1n4a +es2ig +e2s5im +e2s4i4n +esis4te +e1sis +e5si4u +e5skin +esk2 +esk1i +es4mi +e2s1m +e2sol +e1so +es3olu +e2so2n +es5o1n1a4 +e1sp +e2s3per +es5pi1ra +esp4ir +es4pre +espr2 +2e2ss +es4si4b +es1si +esta2n4 +es1ta +es3t2ig +est2i +es5tim +4es2to +e3sto2n +2est4r +e5stro +estruc5 +e2su2r +e1su +es5ur1r4 +es4w2 +e2ta4b +e1ta +e3ten4d +e3teo +ethod3 +et1ic +e5tide +et2id +e2t1in4 +et2i4no +e5t4ir +e5t2i1t2io +eti1ti +et5i1t2iv +4e2t1n2 +et5o1n1a +e1to +eto2n +e3tra +e3tre +et3ric +et5rif +et3rog +et5ros +et3u1a +e1tu +et5ym +e1ty +e4t5z +4eu +e5un +e3up +eu3ro +e2us4 +eute4 +euti5l +e4u1t2i +eu5tr +eva2p5 +e1va +e2vas +ev5ast +e5vea +ev3el1l +eve4l3o +e5veng +even4i +ev1er +e5v2er1b +e1vi +ev3id +e2vi4l +e4v1in +e3v2i4v +e5voc +e5vu +e1wa +e4wag +e5wee +e3wh +ewil5 +ewi2 +ew3in4g +e3wit +1ex3p +5ey1c +5eye. +eys4 +1fa +fa3b2l2 +f4ab3r +fa4ce +4fag +fa4i4n4 +fai2 +fal2l5e +fal1l +4f4a4ma +fam5is +5f2a2r +far5th +fa3ta +fa3th2e +f4ath +4fa1to +fau4lt5 +fau4l2 +4f5b +4fd +4fe. +feas4 +fe4ath3 +fea2t +f2e4b +4fe1ca +5fe2c1t +2fed +fe3l2i +fe4mo +fen2d +fen1d5e +fer1 +5fer1r4 +fev4 +4f1f +f4fes +f4f2ie4 +f1fi +f5f2in. +f2fin +f2f5is +f4f2ly5 +ff4l2 +f2fy +4fh +1fi +f2i3a +2f3ic. +4f3ical +fi1ca +f3ica2n +4ficate +f3i1cen +fi3cer +f2i1c4i +5fi3c2i1a +5fic2ie4 +4fi4c3s2 +fi3cu +fi5del +f2id +fight5 +f2ig +fil5i +fil2l5in4 +fil1l +fill2i +4fi1ly +2fin +5fi1na +f4in2d5 +f2i2ne +f1in3g +f2i4n4n2 +fis4t2i +f4l2 +f5l2e2ss +fles2 +flin4 +flo3re +f2ly5 +4fm +4fn +1fo +5fo2n +fon4de +f2ond +fon4t +fo2r +fo5rat +fo1ra +for5ay +fore5t +for4i +for1t5a +fos5 +4f5p +fra4t +f5rea +fres5c +fri2 +fril4 +frol5 +2f3s +2ft +f4to +f2ty +3fu +fu5el +4fug +fu4min +fu1mi +fu5ne +fu3ri +fusi4 +f2us +fu2s4s +4fu1ta +1fy +1ga +ga2f4 +5gal. +3gal1i +ga3lo +2gam +ga5met +g5a2mo +gan5is +ga2n +ga3niz +gani5za1 +4gano4 +gar5n4 +g2a2r +ga2ss4 +g4ath3 +4ga1t2iv +4gaz +g3b +gd4 +2ge. +2ged +geez4 +gel4in +gel2i +ge5lis +ge5l1iz +4ge1ly +1gen +ge4n2at +ge1na +g5e5niz +4g4eno +4geny +1geo +ge3om +g4ery +5ge1si +geth5 +4ge1to +ge4ty +ge4v +4g1g2 +g2ge +g3ger +gglu5 +ggl2 +g1go4 +gh3in +gh5out +ghou2 +gh4to +5gi. +1g2i4a +gi2a5r +g1ic +5gi3c2i1a +g2i1ci +g4i1co +gien5 +g2ie4 +5gies. +gil4 +g3i1men +3g4in. +g4in5ge +5g4i2n1s2 +5g2io +3g4ir +gir4l +g3is1l2 +gi4u +5g2iv +3giz +gl2 +gla4 +gl2ad5i +gla2d +5glas +1gle +gli4b +g3l2ig +3glo +glo3r +g1m +g4my +g1n4a +g4na. +gne4t4t2 +g1ni +g2n1in +g4n2i4o +g1no +g4no4n +1go +3go. +gob5 +5goe +3g4o4g +go3is +goi2 +go2n2 +4g3o3n1a +gon5do5 +g2ond +go3ni +5goo2 +go5riz +gor5ou2 +5gos. +gov1 +g3p +1gr +4gra1d2a +gra2d +g4r2ai2 +gra2n2 +5gra4ph. +g5ra3ph4er +5graph1ic +gr4aphi +4g3ra1phy +4gray +gre4n +4gress. +gr2e2ss +4grit +g4ro +gruf4 +gs2 +g5ste +gth3 +gu4a +3guar2d +gu2a2r +2gue +5gui5t +g2ui2 +3gun +3g2us +4gu4t +g3w +1gy +2g5y3n +gy5ra +h3ab4l2 +ha2ch4 +hae4m +hae4t +h5agu +ha3la +hala3m +ha4m +han4ci +ha2n +han4cy +5hand. +h4and +h2an4g +hang5er +han1g5o +h5a5niz +ha4n4k2 +han4te +han1t +ha2p3l2 +ha2p5t +ha3ra2n +h2a2r +ha5r2as +har2d +hard3e +har4le4 +har1l +harp5en +har2p +har5ter +ha2s5s +haun4 +5haz +haz3a1 +h1b +1hea2d1 +3he2a2r +he4ca2n +he1ca +h5ecat +h4ed +h4e5do5 +he3l4i +hel4lis +hel1l +hell2i +hel4ly +h5elo +he4m4p +he2n +he1na4 +hen5at +he1o5r +hep5 +h4er1a +hera3p +her4ba +h2er1b +here5a +h3ern +h5er1ou2 +h2ero +h3ery +h1es +he2s5p +he4t +he2t4ed +h4eu4 +h1f +h1h +hi5a2n +h2i1a +hi4co +high5 +h2ig +h4il2 +himer4 +h4i1na +hion4e +h2io +hio2n +h2i4p +hir4l +h4ir +hi3ro +hir4p +hir4r4 +his3el +h4ise +h4i2s4s +hith5er +h2ith +hith2e +h2i2v +4hk +4h1l4 +hla2n4 +h2lo +hlo3ri +4h1m +hmet4 +2h1n +h5odiz +h5o2d1s2 +ho4g +ho1ge4 +hol5a2r +ho1la +3hol4e +ho4ma +ho2me3 +ho1n4a +ho2n +ho5ny +3hood +hoo2 +hoo2n4 +hor5at +ho1ra +ho5r2is +hort3e +ho5ru +hos4e +ho5sen +hos1p +1ho2us +hou2 +house3 +hov5el +4h5p +4hr4 +hree5 +hro5niz +hro2n +hro3po +4h1s2 +h4s2h +h4t2a2r +h1ta +ht1en +ht5es +h4ty +hu4g +hu4min +hu1mi +hun5ke +hu4nk2 +hun4t +hus3t4 +h2us +hu4t +h1w +h4war4t +hw2a2r +hy3pe +hy3ph +hy2s +2i1a +i2al +iam4 +iam5e1te +i2a2n +4ianc +ian3i +4ian4t +ia5pe +ia2ss4 +i4a1t2iv +ia4tric +ia1tr +i4a2tu +ibe4 +ib3er1a +ib5ert +ib5i1a +ib3in +ib5it. +ibi2t +ib5ite +i1b2l2 +ib3li +i5bo +i1br +i2b5ri +i5bu4n +4icam +i1ca +5icap +4ic2a2r +i4car. +i4cara +icas5 +i4cay +iccu4 +ic3c +4iceo +4i2ch +2i1ci +i5c2id +ic5i1na +i2cin +i2c2ip +ic3i1pa +i4c1ly4 +i1c4l4 +i2c5oc +i1co +4i1cr +5icra +i4cry +ic4te +i2c1t +ic1tu2 +ic4t3u1a +ic3u1la +ic4um +ic5uo +i3cur +2id +i4dai2 +i1d2a +id5anc +ida2n +id5d4 +ide3a4l +ide4s2 +i2di +id5i2a2n +i1d4i3a +idi4a2r +i5d2ie4 +i1d3io +idi5ou2 +id1it +id5i1u +i3dle +i4dom +i1do +id3ow +i4dr +i2du +id5uo +2ie4 +ied4e +5ie5ga +ie2ld3 +ie1n5a4 +ien4e +i5e4n1n2 +i3ent2i +ien1t +i1er. +i3es2c +i1est +i3et +4if. +if5ero +ifer1 +iff5en +i4f1f +if4fr +4i2f3ic. +i1fi +i3f2ie4 +i3f4l2 +4i2ft +2ig +iga5b +i1ga +ig3er1a +ight3i +4igi +i3gib +ig3il4 +ig3in +ig3it +i4g4l2 +i2go +ig3or +ig5ot +i5gre +i1gr +ig2u5i2 +ig1ur +i3h +4i5i4 +i3j +4ik +i1la +il3a4b +i4l4ade +ila2d +i2l5am +ila5ra +il2a2r +i3leg +il1er +ilev4 +i2l5f +il1i +il3i1a +il2ib +il3io +il4ist +2il1it +il2iz +ill5ab +il1l +4i2l1n2 +il3o1q +il4ty +i4lt +il5ur +il3v +i4mag +i1ma +im3age +ima5ry +im2a2r +iment2a5r +i1men +i3men1t +imen1ta +4imet +im1i +im5i1d4a +im2id +imi5le +i5m2ini +4imit +im4ni +i4m1n +i3mo2n +i1mo +i2mu +im3u1la +2in. +i4n3au +i1na +4inav +incel4 +in3cer +4ind +in5dling +2ine +i3nee +in4er4a2r +in1er +iner1a +i5n2e2ss +i1nes +4in1ga +4inge +in5gen +4ingi +in5gling +ingl2 +4in1go +4in1gu +2ini +i5ni. +i4n4i1a +in3i4o +in1is +i5ni4te. +in2it +in2ite +5i3n2i1t2io +ini1ti +in3i1ty +4i4nk2 +4i4n1l +2i4n1n2 +2i1no +i4no4c +ino4s +i4not +2i2n1s2 +in3se +insu1r5a +in1su +insu2r +2int. +in1t +2in4th +in1u +i5n2us +4iny +2io +4io. +io1ge4 +io2gr +i1ol +io4m +ion3at +io2n +io1n1a +ion4ery +ion1er +ion3i +i2o5ph +ior3i +i4os +i4o5th +i5oti +io4to +i4our +iou2 +2ip +ipe4 +iphr2as4 +ip4hr4 +ip3i +ip4ic +ip4re4 +ipr2 +ip3ul +i3qua +iqu2 +iq5ue1f +iq3u2id +iq2ui2 +iq3ui3t +4ir +i1ra +i2ra4b +i4rac +ird5e +ire4de +i2r2ed +i4re1f +i4rel4 +i4res +ir5gi +irg2 +ir1i +iri5de +ir2id +ir4is +iri3tu +5i5r2iz +ir4min +ir1m +iro4g +5iron. +iro2n +ir5ul +2is. +is5ag +isa2 +is3a2r +isas5 +2is1c +is3ch2 +4ise +is3er +3i4s3f +is5ha2n +is2h +is3ho2n3 +isho4 +ish5op +is3i1b +is2i4d +i5sis +is5i1t2iv +isi1ti +4is4k2 +isla2n4 +is1l2 +4is4m1s2 +i2s1m +i2so +iso5mer +i3som +iso2me +is1p +is2pi +is4py +4i2s1s +is4sal +is1sa2 +issen4 +is4s1e4s +is4ta. +is1ta +is1te +is1t2i +ist4ly +is2tl +4istral +ist4r +is1tra +i2su +is5us +4i3ta. +i1ta +ita4bi +i2tab +i4tag +4ita5m +i3ta2n +i3tat +2ite +it3er1a +i5ter1i +it4es +2ith +i1ti +4i1t2i1a +4i2tic +it3i1ca +5i5tick1 +i2t3ig +it5il1l +i2tim +2i1t2io +4itis +i4ti2s4m +i2t5o5m +i1to +4ito2n +i4tram +i1tra +it5ry +4i4t3t2 +it3u1at +i1tu +itu1a +i5tud2 +it3ul +4itz. +i4tz +i1u +2iv +iv3el1l +iv3en. +i4v3er. +i4vers. +ive4r1s2 +iv5il. +i2vil +iv5io +iv1it +i5vore +iv3o3ro +i4v3ot +4i5w +ix4o +4iy +4iz2a2r2 +iza1 +i2z1i4 +5izon1t +i1zo +izo2n +5ja +jac4q +ja4p +1je +je4r5s2 +4jes4t2ie4 +jest2i +4jes2ty +jew3 +jo4p +5judg +3ka. +k3ab +k5ag +kais4 +kai2 +kal4 +k1b +k2ed +1kee +ke4g +ke5l2i +k3en4d +k1er +kes4 +k3e2st. +ke4ty +k3f +kh4 +k1i +5ki. +5k2ic +k4il1l +kilo5 +k4im +k4in. +kin4de +k4ind +k5i5n2e2ss +k2ine +ki1nes +kin4g +k2i4p +kis4 +k5is2h +kk4 +k1l +4k3ley +4k1ly +k1m +k5nes +1k2no +ko5r +kos2h4 +k3ou2 +kro5n +4k1s2 +k4sc +ks4l2 +k4s4y +k5t +k1w +lab3ic +l4abo +l4a2ci4 +l4ade +la2d +la3d2y +lag4n +la2m3o +3l4and +la2n +lan4dl +lan5et +lan4te +lan1t +lar4g2 +l2a2r +lar3i +las4e +la5ta2n +la2ta +4latel2i4 +4la1t2iv +4lav +la4v4a +2l1b +lbin4 +4l1c2 +lce4 +l3ci +2ld +l2de +ld4ere +ld4er1i +ldi4 +ld5is1 +l3dr +l4dri +le2a +le4bi +l2e1b +le2ft5 +le1f +5leg. +5le4g1g2 +le4mat +le1ma +lem5at1ic +4len. +3lenc +5le2ne. +1len1t +le3ph +le4pr2 +le2ra5b +ler1a +ler4e +3lerg2 +3l4er1i +l4ero +les2 +le5s1co +les2c +5lesq +3l2e2ss +5less. +l3e1va +lev4er. +lev1er +lev4er1a +lev4e4r1s2 +3ley +4leye +2lf +l5fr +4l1g4 +l5ga +lg2a2r3 +l4ges +l1go3 +2l3h +li4ag +l2i1a +li2am4 +liar5iz +li2a2r +liar1i +li4as +li4a1to +li5bi +5lic2io +l2i1ci +li4cor +li1co +4li4c3s2 +4lict. +li2c1t +l4icu +l3i1cy +l3i1d2a +l2id +lid5er +3li2di +lif3er1 +l4i4f1f +li4f4l2 +5ligate +l2ig +li1ga +3ligh +li4gra +li1gr +3l4ik +4l4i4l +lim4b2l2 +li4m1b +lim3i +li4mo +l4i4m4p +l4i1na +1l4ine +lin3ea +l2in3i +link5er +l4i4nk2 +li5og +l2io +4l4iq +lis4p +l1it +l2it. +5lit3i1ca +li1ti +l4i2tic +l5i5ti4c3s2 +liv3er +l2iv +l1iz +4lj +lka3 +l3kal4 +lka4t +l1l +l4law +l2le +l5le2a +l3lec +l3leg +l3lel +l3le4n +l3le4t +ll2i +l2lin4 +l5l4i1na +ll4o +lloq2ui5 +llo1q +lloqu2 +l2l5out +llou2 +l5low +2lm +l5met +lm3ing +l4mo2d1 +l1mo +lmo2n4 +2l1n2 +3lo. +lob5al +lo4ci +4lof +3log1ic +l5o1go +3logu +lom3er +lo2me +5long +lo2n +lon4i +l3o3niz +lood5 +loo2 +5lo4pe. +lop3i +l3o4p1m +lo1ra4 +lo4ra1to +lo5r2ie4 +lor5ou2 +5los. +los5et +5los5o3phiz +lo2so +los4op +los2oph +5los5o1phy +los4t +lo4ta +loun5d +lou2 +2lout +4lov +2lp +lpa5b +l1pa +l3pha +l5phi +lp5ing +lpi2n +l3pit +l4p2l2 +l5pr2 +4l1r +2l1s2 +l4sc +l2se +l4s2ie4 +4lt +lt5ag +l1ta +ltane5 +lta2n +l1te +lten4 +lter1a4 +lth3i +l5ties. +lt2ie4 +ltis4 +l1tr +l1tu2 +ltu1r3a +lu5a +lu3br +lu2ch4 +lu3ci +lu3en +luf4 +lu5id +l2ui2 +lu4ma +5lu1mi +l5umn. +lu4m1n +5lum3n4i1a +lu3o +luo3r +4lup +lu2ss4 +l2us +lus3te +1lut +l5ven +l5vet4 +2l1w +1ly +4lya +4ly1b +ly5me4 +ly3no +2lys4 +l5y3s2e +1ma +2mab +ma2ca +ma5ch2ine +ma2ch +ma4ch1in +ma4c4l4 +mag5in +mag1i +5mag1n +2mah +ma2id5 +mai2 +4ma2ld +ma3l2ig +mal1i +ma5lin +mal4l2i +mal1l +mal4ty +ma4lt +5ma3n4i1a +ma2n +man5is +man3iz +4map +ma5ri2ne. +m2a2r +mar1i +mar2in4e +ma5r2iz +mar4ly +mar1l +mar3v +ma5sce +mas4e +mas1t +5mate +m4ath3 +ma3tis +4mati3za1 +ma1tiz +4m1b +m1ba4t5 +m5bil +m4b3ing +mb2i4v +4m5c +4me. +2med +4med. +5me3d4i3a +m4edi +me3d2ie4 +m5e5d2y +me2g +mel5o2n +me4l4t +me2m +me1m1o3 +1men +me1n4a +men5ac +men4de +4mene +men4i +me2n1s4 +men1su5 +3men1t +men4te +me5o2n +m5er1sa2 +me4r1s2 +2mes +3mest2i +me4ta +met3a2l +me1te +me5thi +m4etr +5met3ric +me5tr2ie4 +me3try +me4v +4m1f +2mh +5mi. +m2i3a +mi1d4a +m2id +mid4g +m2ig4 +3mil3i1a +mil1i +m5i5l2ie4 +m4il1l +mi1n4a +3m4ind +m5i3nee +m2ine +m4ingl2 +min5gli +m5ing1ly +min4t +m4in1u +miot4 +m2io +m2is +mi4s4er. +m4ise +mis3er +mis5l2 +mis4t2i +m5i4stry +mist4r +4m2ith +m2iz +4mk +4m1l +m1m +mma5ry +m1ma +mm2a2r +4m1n +m1n4a +m4n1in +mn4o +1mo +4mocr +5moc5ra1tiz +mo2d1 +mo4go +mois2 +moi2 +mo4i5se +4m2ok +mo5lest +moles2 +mo3me +mon5et +mo2n +mon5ge +mo3n4i3a +mon4i2s1m +mon1is +mon4ist +mo3niz +monol4 +mo3ny. +mo2r +4mo5ra. +mo1ra +mos2 +mo5sey +mo3sp +m4oth3 +m5ouf +mou2 +3mo2us +mo2v +4m1p +mpara5 +m1pa +mp2a2r +mpa5rab +mp4a4r5i +m3pe2t +mphas4 +m2pi +mp2i4a +mp5ies +mp2ie4 +m4p1i2n +m5p4ir +mp5is +mpo3ri +m1p4or +mpos5ite +m1pos +m4po2us +mpou2 +mpov5 +mp4tr +m2p1t +m2py +4m3r +4m1s2 +m4s2h +m5si +4mt +1mu +mul2a5r4 +mu1la +5mu4lt +mul1ti3 +3mum +mun2 +4mup +mu4u +4mw +1na +2n1a2b +n4abu +4nac. +na4ca +n5a2c1t +nag5er. +nak4 +na4l1i +na5l2i1a +4na4lt +na5mit +n2a2n +nan1ci4 +nan4it +na4nk4 +nar3c +n2a2r +4nare +nar3i +nar4l +n5ar1m +n4as +nas4c +nas5t2i +n2at +na3ta2l +na2ta +nat5o5m2iz +na2tom +na1to +n2au +nau3se +na2us +3naut +nav4e +4n1b4 +nc2a2r5 +n1ca +n4ces. +n3cha +n2ch +n5cheo +nche2 +n5ch4il2 +n3chis +n2c1in +n1ci +n2c4it +ncou1r5a +n1co +ncou2 +n1cr +n1cu +n4dai2 +n1d2a +n5da2n +n1de +nd5e2st. +ndes2 +ndi4b +n5d2if +n1dit +n3diz +n5du2c +n1du +ndu4r +nd2we +nd1w +2ne. +n3e2a2r +n2e2b +neb3u +ne2c +5neck1 +2ned +ne4gat +ne1ga +ne4g5a1t2iv +5nege +ne4la +nel5iz +nel2i +ne5mi +ne4mo +1nen +4nene +3neo +ne4po +ne2q +n1er +ne2ra5b +ner1a +n4er3a2r +n2ere +n4er5i +ner4r4 +1nes +2nes. +4ne1sp +2nest +4nes4w2 +3net1ic +ne4v +n5eve +ne4w +n3f +n4gab +n1ga +n3gel +nge4n4e +n1gen +n5gere +n3ger1i +ng5ha +n3gib +ng1in +n5git +n4gla4 +ngl2 +ngov4 +n1go +ng5s2h +ngs2 +n1gu +n4gum +n2gy +4n1h4 +nha4 +nhab3 +nhe4 +3n4i1a +ni3a2n +ni4ap +ni3ba +ni4b2l2 +n2i4d +ni5di +ni4er +n2ie4 +ni2fi +ni5ficat +nifi1ca +n5i1gr +n2ig +n4ik4 +n1im +ni3m2iz +nim1i +n1in +5ni2ne. +n2ine +nin4g +n2i4o +5n2is. +nis4ta +n2it +n4ith +3n2i1t2io +ni1ti +n3itor +ni1to +ni3tr +n1j +4nk2 +n5k2ero +nk1er +n3ket +nk3in +nk1i +n1k1l +4n1l +n5m +nme4 +nmet4 +4n1n2 +nne4 +nni3al +n3n4i1a +nn2i4v +nob4l2 +no3ble +n5o1c4l4 +4n3o2d +3noe +4nog +no1ge4 +nois5i +noi2 +no5l4i +5nol1o1gis +3nomic +n5o5m2iz +no4mo +no3my +no4n +non4ag +no1n1a +non5i +n5oniz +4nop +5nop5o5l2i +no2r5ab +no1ra +no4rary +nor2a2r +4nos2c +nos4e +nos5t +no5ta +1nou2 +3noun +nov3el3 +nowl3 +n1p4 +npi4 +npre4c +npr2 +n1q +n1r +nru4 +2n1s2 +n2s5ab +nsa2 +nsati4 +ns4c +n2se +n4s3e4s +ns2id1 +ns2ig4 +n2s1l2 +n2s3m +n4soc +n1so +ns4pe +n5spi +nsta5b2l2 +ns1ta +ns2tab +n1t +n2ta4b +n1ta +nte4r3s2 +nt2i +n5ti2b +nti4er +nt2ie4 +nti2f2 +n3t2ine +n2t1in +n4t3ing +nt2i4p +ntrol5l2i +ntrol1l +n4t4s2 +ntu3me +n1tu +n3tum +nu1a +nu4d +nu5en +nuf4fe +nu4f1f +n3ui4n +n2ui2 +3nu3it +n4um +nu1me +n5u1mi +3nu4n +n3uo +nu3tr +n1v2 +n1w4 +nym4 +nyp4 +4nz +n3za1 +4oa +oa2d3 +o5a5les2 +o2ale +oard3 +o2a2r +oas4e +oast5e +oat5i +ob3a3b +o5b2a2r +o1be4l +o1bi +o2bin +ob5ing +o3br +ob3ul +o1ce +o2ch4 +o3che4t +oche2 +ocif3 +o1ci +o4cil +o4clam +o1c4l4 +o4cod +o1co +oc3rac +oc5ra1tiz +ocre3 +5ocrit +ocri2 +octo2r5a +o2c1t +oc1to +oc3u1la +o5cure +od5d1ed +od1d4 +od3ic +o1d2i3o +o2do4 +od4or3 +o4d5uct. +o1du +odu2c +odu2c1t +o4d5uc4t1s2 +o4el +o5eng +o3er +oe4ta +o3ev +o2fi +of5ite +of4i4t4t2 +o2g5a5r +o1ga +o4g5a1t2iv +o4ga1to +o1ge +o5gene +o1gen +o5geo +o4ger +o3g2ie4 +1o1gis +og3it +o4gl2 +o5g2ly +3ogniz +og1ni +o4g4ro +o1gr +og2u5i2 +1o1gy +2o2g5y3n +o1h2 +ohab5 +oi2 +oic3es +oi3der +o2id +oi4f1f4 +o2ig4 +oi5let +o3ing +oint5er +oin1t +o5i2s1m +oi5so2n +oi2so +oist5en +ois1te +oi3ter +o2ite +o5j +2ok +o3ken +ok5ie4 +ok1i +o1la +o4la2n +ola2ss4 +o2l2d +ol2d1e +ol3er +o3les2c +oles2 +o3let +ol4fi +o2lf +ol2i +o3l2i1a +o3lice +ol5id. +ol2id +o3li4f +o5l4i4l +ol3ing +o5l2io +o5l2is. +ol3is2h +o5l2ite +ol1it +o5l2i1t2io +oli1ti +o5l2iv +oll2i4e4 +ol1l +oll2i +ol5o3giz +olo4r +ol5p2l2 +o2lp +o4l2t +ol3ub +ol3ume +ol3un +o5l2us +ol2v +o2ly +o2m5ah +o1ma +oma5l +om5a1tiz +om2be +o4m1b +om4b2l2 +o2me +om3e1n4a +o1men +om5er2se +ome4r1s2 +o4met +om5e3try +om4etr +o3m2i3a +om3ic. +om3i1ca +o5m2id +om1in +o5m2ini +5ommend +om1m +om1men +omo4ge +o1mo +o4mo2n +om3pi +o4m1p +ompro5 +ompr2 +o2n +o1n1a +on4ac +o3n2a2n +on1c +3oncil +on1ci +2ond +on5do +o3nen +o2n5est +o1nes +on4gu +on1ic +o3n2i4o +on1is +o5ni1u +on3key +o4nk2 +on4odi +o4n3o2d +on3o3my +o2n3s2 +on5spi4 +onspi1r5a +onsp4ir +on1su4 +onten4 +on1t +on3t4i +onti2f5 +on5um +on1va5 +on1v2 +oo2 +ood5e +ood5i +o2o4k +oop3i +o3ord +oost5 +o2pa +o2p2e5d +op1er +3oper1a +4op4erag +2oph +o5pha2n +o5ph4er +op3ing +opi2n +o3pit +o5po2n +o4posi +o1pos +o1pr2 +op1u +opy5 +o1q +o1ra +o5ra. +o4r3ag +or5al1iz +oral1i +or5an4ge +ora2n +or2ang +ore5a +o5re1a4l +or3ei2 +or4e5s2h +or5e2st. +ores2t +orew4 +or4gu +org2 +4o5r2i3a +or3i1ca +o5ril +or1in +o1r2i1o +or3i1ty +o3ri1u +or2mi +or1m +orn2e +o5rof +or3oug +orou2 +or5pe +or1p +3orrh4 +or1r4 +or4se +o4rs2 +ors5en +orst4 +or3thi +or3thy +or4ty +o5rum +o1ry +os3al +osa2 +os2c +os4ce +o3scop +os1co +4oscopi +o5scr +os4i4e4 +os5i1t2iv +osi1ti +os3i1to +os3i1ty +o5si4u +os4l2 +o2so +o2s4pa +os4po +os2ta +o5stati +os5til +ost2i +os5tit +o4ta2n +o1ta +otele4g +ot3er. +ot5e4r1s2 +o4tes +4oth +oth5e1si +oth2e +oth1es +oth3i4 +ot3ic. +ot5i1ca +o3tice +o3tif2 +o3tis +oto5s2 +o1to +ou2 +ou3b2l2 +ouch5i +ou2ch +ou5et +ou4l +ounc5er +oun2d +ou5v2 +ov4en +over4ne +ove4r3s2 +ov4ert +o3vis +o4vi1ti4 +o5v4ol +ow3der +ow3el +ow5est3 +ow1i2 +own5i +o4wo2 +oy1a +1pa +pa4ca +pa4ce +pa2c4t +p4a2d +5paga4n +pa1ga +p3agat +p4ai2 +pa4i4n4 +p4al +pa1n4a +pa2n +pan3el +pan4ty +pan1t +pa3ny +pa1p +pa4pu +para5b2l2 +p2a2r +pa2rab +par5age +par5d2i +3pare +par5el +p4a4r1i +par4is +pa2te +pa5ter +5pathic +p4ath +pa5thy +pa4tric +pa1tr +pav4 +3pay +4p1b +pd4 +4pe. +3pe4a +pear4l +pe2a2r +pe2c +2p2ed +3pede +3p4edi +pe3d4i3a4 +ped4ic +p4ee +pee4d +pek4 +pe4la +pel2i4e4 +pel2i +pe4n2a2n +pe1na +p4enc +pen4th +pen1t +pe5o2n +p4era. +per1a +pera5b2l2 +pe2ra4b +p4erag +p4er1i +peri5st +per2is +per4mal +per3m4 +per1ma +per2me5 +p4ern +p2er3o +per3ti +p4e5ru +per1v +pe2t +pe5ten +pe5tiz +4pf +4pg +4ph. +phar5i +ph2a2r +ph4e3no +phe2n +ph4er +ph4es. +ph1es +ph1ic +5ph2ie4 +ph5ing +5phis1t2i +3phiz +p4h2l4 +3phob +3phone +pho2n +5phoni +pho4r +4p4h1s2 +ph3t +5phu +1phy +p2i3a +pi2a2n4 +pi4c2ie4 +p2i1ci +pi4cy +p4id +p5i1d2a +pi3de +5pi2di +3piec +p2ie4 +pi3en +pi4grap +p2ig +pi1gr +pi3lo +pi2n +p4in. +p4ind4 +p4i1no +3p2i1o +pio2n4 +p3ith +pi5tha +pi2tu +2p3k2 +1p2l2 +3pla2n +plas5t +pl2i3a +pli5er +pl2ie4 +4pl2ig +pli4n +ploi4 +plu4m +plu4m4b +4p1m +2p3n +po4c +5pod. +po5em +po3et5 +5po4g +poin2 +poi2 +5poin1t +poly5t +po2ly +po4ni +po2n +po4p +1p4or +po4ry +1pos +po2s1s +p4ot +po4ta +5poun +pou2 +4p1p +ppa5ra +p1pa +pp2a2r +p2pe +p4p2ed +p5pel +p3pen +p3per +p3pe2t +ppo5s2ite +p1pos +pr2 +pray4e4 +5pre1c2i +pre5co +pre3e2m +pre4f5ac +pre1f +pre1fa +pre4la +pr1e3r4 +p3re1s2e +3pr2e2ss +pre5ten +pre3v2 +5pr2i4e4 +prin4t3 +pr2i4s +pri2s3o +p3ro1ca +pr2oc +prof5it +pro2fi +pro3l +pros3e +pro1t +2p1s2 +p2se +ps4h +p4si1b +2p1t +p2t5a4b +p1ta +p2te +p2th +p1ti3m +ptu4r +p1tu +p4tw4 +pub3 +pue4 +puf4 +pu4l3c2 +pu4m +pu2n +pur4r4 +5p2us +pu2t +5pute +put3er +pu3tr +put4t1ed +pu4t3t2 +put4t1in +p3w +qu2 +qua5v4 +2que. +3quer +3quet +2rab +ra3bi +rach4e2 +ra2ch +r5a1c4l4 +raf5fi +ra2f +ra4f1f4 +ra2f4t +r2ai2 +ra4lo +ram3et +r2ami +ra3ne5o +ra2n +ran4ge +r2ang +r4ani +ra5no4 +rap3er +3ra1phy +rar5c +r2a2r +rare4 +rar5e1f +4raril +rar1i +r2as +ratio2n4 +ra1t2io +rau4t +ra5vai2 +ra2va +rav3el +ra5z2ie4 +ra2z1i +r1b +r4bab +r4bag +rbi2 +r2b3i4f +r2bin +r5b2ine +rb5ing. +rb4o +r1c +r2ce +r1cen4 +r3cha +r2ch +rch4er +rche2 +r4ci4b +r1ci +r2c4it +rcum3 +r4dal +r1d2a +rd2i +r1d4i4a +rdi4er +rd2ie4 +rd1in4 +rd3ing +2re. +re1a4l +re3a2n +re5ar1r4 +re2a2r +5rea2v +re4aw +r5ebrat +r2e1b +re3br +rec5ol1l +re2col +re1co +re4c5ompe +reco4m1p +re4cre +re1cr +2r2ed +re1de +re3dis1 +r4edi +red5it +re4fac +re1f +re1fa +re2fe +re5fer. +refer1 +re3fi +re4fy +reg3is +re5it +rei2 +re1l2i +re5lu +r4en4ta +ren1t +ren4te +re1o +re5pi2n +re4posi +re1po +re1pos +re1pu +r1er4 +r4er1i +r2ero4 +r4e5ru +r4es. +re4spi +re1sp +res4s5i4b +r2e2ss +res1si +res2t +re5s2ta2l +res1ta +r2e3st4r +re4ter +re4ti4z +re3tri +r4eu2 +re5u1t2i +rev2 +re4val +re1va +rev3el +r5ev5er. +rev1er +re5ve4r1s2 +re5vert +re5vi4l +re1vi +rev5olu +re4wh +r1f +r3fu4 +r4fy +rg2 +rg3er +r3get +r3g1ic +rgi4n +rg3ing +r5gis +r5git +r1gl2 +rgo4n2 +r1go +r3gu +rh4 +4rh. +4rhal +r2i3a +ria4b +ri4ag +r4ib +rib3a +ric5as5 +ri1ca +r4ice +4r2i1ci +5ri5c2id +ri4c2ie4 +r4i1co +rid5er +r2id +ri3enc +r2ie4 +ri3en1t +ri1er +ri5et +rig5a2n +r2ig +ri1ga +5r4igi +ril3iz +ril1i +5rima2n +ri1ma +rim5i +3ri1mo +rim4pe +ri4m1p +r2i1na +5rina. +r4in4d +r2in4e +rin4g +r2i1o +5riph +r2ip +riph5e +ri2p2l2 +rip5lic +r4iq +r2is +r4is. +r2is4c +r3is2h +ris4p +ri3ta3b +ri1ta +r5ited. +r2ite +ri2t1ed +rit5er. +rit5e4r1s2 +r4i2t3ic +ri1ti +ri2tu +rit5ur +riv5el +r2iv +riv3et +riv3i +r3j +r3ket +rk4le +rk1l +rk4lin +r1l +rle4 +r2led +r4l2ig +r4lis +rl5is2h +r3lo4 +r1m +rma5c +r1ma +r2me +r3men +rm5e4r1s2 +rm3ing +r4ming. +r4m2io +r3mit +r4my +r4n2a2r +r1na +r3nel +r4n1er +r5net +r3ney +r5nic +r1nis4 +r3n2it +r3n2iv +rno4 +r4nou2 +r3nu +rob3l2 +r2oc +ro3cr +ro4e +ro1fe +ro5fil +ro2fi +r2ok2 +ro5k1er +5role. +rom5e1te +ro2me +ro4met +rom4i +ro4m4p +ron4al +ro2n +ro1n1a +ron4e +ro5n4is +ron4ta +ron1t +1room +roo2 +5root +ro3pel +rop3ic +ror3i +ro5ro +ro2s5per +ro2s4s +ro4th2e +r4oth +ro4ty +ro4va +rov5el +rox5 +r1p +r4pe4a +r5pen1t +rp5er. +r3pe2t +rp4h4 +rp3ing +rpi2n +r3po +r1r4 +rre4c +rre4f +r4re1o +rre4s2t +rr2i4o +rr2i4v +rro2n4 +rros4 +rrys4 +4rs2 +r1sa2 +rsa5ti +rs4c +r2se +r3sec +rse4cr +r4s5er. +rs3e4s +r5se5v2 +r1s2h +r5sha +r1si +r4si4b +rso2n3 +r1so +r1sp +r5sw2 +rta2ch4 +r1ta +r4tag +r3t2e1b +r3ten4d +r1te5o +r1ti +r2t5i2b +rt2i4d +r4tier +rt2ie4 +r3t2ig +rtil3i +rtil4l +r4ti1ly +r4tist +r4t2iv +r3tri +rtr2oph4 +rt4s2h4 +r4t1s2 +ru3a +ru3e4l +ru3en +ru4gl2 +ru3i4n +r2ui2 +rum3p2l2 +ru4m2p +ru2n +ru4nk5 +run4ty +run1t +r5usc2 +r2us +ru2t1i5n +r4u1t2i +rv4e +rvel4i +r3ven +rv5er. +r5vest +rv4e2s +r3vey +r3vic +r3v2i4v +r3vo +r1w +ry4c +5rynge +ryn5g +ry3t +sa2 +2s1ab +5sack1 +sac3ri2 +s3a2c1t +5sai2 +sa4l2a2r4 +s4a2l4m +sa5lo +sa4l4t +3sanc +sa2n +san4de +s4and +s1ap +sa5ta +5sa3t2io +sa2t3u +sau4 +sa5vor +5saw +4s5b +scan4t5 +s1ca +sca2n +sca4p +scav5 +s4ced +4s3cei2 +s4ces +s2ch2 +s4cho2 +3s4c2ie4 +s1ci +5sc4in4d +s2cin +scle5 +s1c4l4 +s4cli +scof4 +s1co +4scopy5 +scou1r5a +scou2 +s1cu +4s5d +4se. +se4a +seas4 +sea5w +se2c3o +3se2c1t +4s4ed +se4d4e +s5edl +se2g +se1g3r +5sei2 +se1le +5se2l2f +5selv +4se1me +se4mol +se1mo +sen5at +se1na +4senc +sen4d +s5e2ned +sen5g +s5en1in +4sen4t1d +sen1t +4sen2tl +se2p3a3 +4s1er. +s4er1l +s2er4o +4ser3vo +s1e4s +s4e5s2h +ses5t +5se5um +s4eu +5sev +sev3en +sew4i2 +5sex +4s3f +2s3g +s2h +2sh. +sh1er +5shev +sh1in +sh3io +3sh2i4p +sh2i2v5 +sho4 +sh5o2l2d +sho2n3 +shor4 +short5 +4sh1w +si1b +s5ic3c +3si2de. +s2id +5side4s2 +5si2di +si5diz +4sig1n4a +s2ig +sil4e +4si1ly +2s1in +s2i1na +5si2ne. +s2ine +s3ing +1s2io +5sio2n +sio1n5a +s4i2r +si1r5a +1sis +3s2i1t2io +si1ti +5si1u +1s2iv +5siz +sk2 +4ske +s3ket +sk5ine +sk1i +sk5in4g +s1l2 +s3lat +s2le +sl2ith5 +sl1it +2s1m +s3ma +smal1l3 +sma2n3 +smel4 +s5men +5s4m2ith +smo2l5d4 +s1mo +s1n4 +1so +so4ce +so2ft3 +so4lab +so1la +so2l3d2 +so3lic +sol2i +5sol2v +3som +3s4on. +so2n +so1n1a4 +son4g +s4op +5soph1ic +s2oph +s5o3phiz +s5o1phy +sor5c +sor5d +4sov +so5vi +2s1pa +5sp4ai2 +spa4n +spen4d +2s5peo +2sper +s2phe +3sph4er +spho5 +spil4 +sp5ing +spi2n +4s3p2i1o +s4p1ly +s1p2l2 +s4po2n +s1p4or4 +4sp4ot +squal4l +squ2 +s1r +2ss +s1sa2 +ssas3 +s2s5c +s3sel +s5sen5g +s4ses. +ss1e4s +s5set +s1si +s4s2ie4 +ssi4er +s4s5i1ly +s4s1l2 +ss4li +s4s1n4 +sspen4d4 +ss2t +ssu1r5a +s1su +ssu2r +ss5w2 +2st. +s2tag +s1ta +s2ta2l +stam4i +5st4and +sta2n +s4ta4p +5stat. +s4t1ed +stern5i +s5t2ero +ste2w +ste1w5a +s3th2e +st2i +s4ti. +s5t2i1a +s1tic +5s4tick1 +s4t2ie4 +s3tif2 +st3ing +s2t1in +5st4ir +s1tle +s2tl +5stock1 +s1to +sto2m3a +5stone +sto2n +s4top +3store +st4r +s4tra2d +s1tra +5stra2tu +s4tray +s4tr2id +4stry +4st3w4 +s2ty +1su +su1al +su4b3 +su2g3 +su5is +s2ui2 +suit3 +s4ul +su2m +su1m3i +su2n +su2r +4sv +sw2 +4s1wo2 +s4y +4sy1c +3syl +syn5o +sy5rin +1ta +3ta. +2tab +ta5bles2 +tab2l2 +5tab5o5l1iz +tabol2i +4t4a2ci +ta5do +ta2d +4ta2f4 +tai5lo +tai2 +ta2l +ta5la +tal5en +t2ale +tal3i +4talk +tal4lis +tal1l +tall2i +ta5log +ta5mo +tan4de +ta2n +t4and +1tan1ta3 +tan1t +ta5per +ta5p2l2 +tar4a +t2a2r +4tar1c +4tare +ta3r2iz +tar1i +tas4e +ta5s4y +4tat1ic +ta4tur +ta2tu +taun4 +tav4 +2taw +tax4is +tax3i +2t1b +4tc +t4ch +tch5e4t +tche2 +4t1d +4te. +te2ad4i +tea2d1 +4tea2t +te1ce4 +5te2c1t +2t1ed +t4e5di +1tee +teg4 +te5ger4 +te5gi +3tel. +tel2i4 +5te2l1s2 +te2ma2 +tem3at +3ten2a2n +te1na +3tenc +3tend +4te1nes +1ten1t +ten4tag +ten1ta +1teo +te4p +te5pe +ter3c +5ter3d +1ter1i +ter5ies +ter2ie4 +ter3is +teri5za1 +5t4er3n2it +ter5v +4tes. +4t2e2ss +t3ess. +teth5e +3t4eu +3tex +4tey +2t1f +4t1g +2th. +tha2n4 +th2e +4thea +th3eas +the5a2t +the3is +thei2 +3the4t +th5ic. +th5i1ca +4th4il2 +5th4i4nk2 +4t4h1l4 +th5ode +5thod3ic +4thoo2 +thor5it +tho5riz +2t4h1s2 +1t2i1a +ti4ab +ti4a1to +2ti2b +4tick1 +t4i1co +t4ic1u +5ti2di +t2id +3tien +t2ie4 +tif2 +ti5fy +2t2ig +5tigu +til2l5in4 +til1l +till2i +1tim +4ti4m1p +tim5ul +ti2mu +2t1in +t2i1na +3ti2ne. +t2ine +3t2ini +1t2io +ti5oc +tion5ee +tio2n +5tiq +ti3sa2 +3t4ise +ti2s4m +ti5so +tis4p +5tisti1ca +tis1t2i +tis1tic +ti3tl +ti4u +1t2iv +ti1v4a +1tiz +ti3za1 +ti3ze4n +ti2ze +2tl +t5la +tla2n4 +3tle. +3tled +3tles. +tles2 +t5let. +t5lo +4t1m +tme4 +2t1n2 +1to +to3b +to5crat +4to2do4 +2tof +to2gr +to5ic +toi2 +to2ma +to4m4b +to3my +ton4a4l1i +to2n +to1n1a +to3n2at +4tono +4tony +to2ra +to3r2ie4 +tor5iz +tos2 +5tour +tou2 +4tout +to3w2a2r +4t1p +1tra +t2ra3b +tra5ch +tr4a2ci4 +tra2c4it +trac4te +tra2c1t +tr2as4 +tra5ven +trav5e2s5 +tre5f +tre4m +trem5i +5tr2i3a +tri5ces +tr4ice +5tri3c2i1a +t4r2i1ci +4tri4c3s2 +2trim +tr2i4v +tro5m4i +tron5i +tro2n +4trony +tro5phe +tr2oph +tro3sp +tro3v +tr2u5i2 +tr2us4 +4t1s2 +t4sc +ts2h4 +t4sw2 +4t3t2 +t4tes +t5to +t1tu4 +1tu +tu1a +tu3a2r +tu4b4i +tud2 +4tue +4tuf4 +5t2u3i2 +3tum +tu4nis +tu1ni +2t3up. +3ture +5turi +tur3is +tur5o +tu5ry +3t2us +4tv +tw4 +4t1wa +twis4 +twi2 +4t1wo2 +1ty +4tya +2tyl +type3 +ty5ph +4tz +t2z4e +4uab +uac4 +ua5na +ua2n +uan4i +uar5an1t +u2a2r +uara2n +uar2d +uar3i +uar3t +u1at +uav4 +ub4e +u4bel +u3ber +u4b2ero +u1b4i +u4b5ing +u3b4le. +ub2l2 +u3ca +uci4b +u1ci +u2c4it +ucle3 +u1c4l4 +u3cr +u3cu +u4cy +ud5d4 +ud3er +ud5est +udes2 +ude1v4 +u1dic +ud3ied +ud2ie4 +ud3ies +ud5is1 +u5dit +u4do2n +u1do +ud4si +u2d1s2 +u4du +u4ene +ue2n1s4 +uen4te +uen1t +uer4il +uer1i +3u1fa +u3f4l2 +ugh3e2n +ug5in +2ui2 +uil5iz +uil1i +ui4n +u1ing +uir4m +u4ir +ui1ta4 +u2iv3 +ui4v4er. +u5j +4uk +u1la +ula5b +u5lati +ul2ch4 +u4l1c2 +5ulche2 +ul3der +u2ld +ul2de +ul4e +u1len +ul4gi +u4l1g4 +ul2i +u5l2i1a +ul3ing +ul5is2h +ul4l2a2r +ul1l +ul4li4b +ull2i +ul4lis +4u2l3m +u1l4o +4u2l1s2 +uls5e4s +ul2se +ul1ti +u4lt +ul1tra3 +ul1tr +4ul1tu2 +u3lu +ul5ul +ul5v +u2m5ab +u1ma +um4bi +u4m1b +um4b1ly +umb2l2 +u1mi +u4m3ing +umor5o +u1mo +umo2r +u4m2p +un2at4 +u1na +u2ne +un4er +u1ni +un4im +u2n1in +un5is2h +un2i3v +u2n3s4 +un4sw2 +un2t3a4b +un1t +un1ta +un4ter. +un4tes +unu4 +un5y +u4n5z +u4o4rs2 +u5os +u1ou2 +u1pe +upe4r5s2 +u5p2i3a +up3ing +upi2n +u3p2l2 +u4p3p +upport5 +up1p4or +up2t5i2b +u2p1t +up1tu4 +u1ra +4ura. +u4rag +u4r2as +ur4be +ur1b +ur1c4 +ur1d +ure5a2t +ur4fer1 +ur1f +ur4fr +u3rif +uri4fic +uri1fi +ur1in +u3r2i1o +u1rit +ur3iz +ur2l +url5ing. +ur4no4 +uros4 +ur4pe +ur1p +ur4pi +urs5er +u4rs2 +ur2se +ur5tes +ur3th2e +ur1ti4 +ur4t2ie4 +u3ru +2us +u5sa2d +usa2 +u5sa2n +us4ap +usc2 +us3ci +use5a +u5s2i1a +u3sic +us4lin +us1l2 +us1p +us5s1l2 +u2ss +us5tere +us1t4r +u2su +usu2r4 +u2ta4b +u1ta +u3tat +4u4te. +4utel +4uten +uten4i +4u1t2i +uti5l2iz +util1i +u3t2ine +u2t1in +ut3ing +utio1n5a +u1t2io +utio2n +u4tis +5u5tiz +u4t1l +u2t5of +u1to +uto5g +uto5mat1ic +uto2ma +u5to2n +u4tou2 +u4t1s4 +u3u +uu4m +u1v2 +ux1u3 +u2z4e +1va +5va. +2v1a4b +vac5il +v4a2ci +vac3u +vag4 +va4ge +va5l2i4e4 +val1i +val5o +val1u +va5mo +va5niz +va2n +va5pi +var5ied +v2a2r +var1i +var2ie4 +3vat +4ve. +4ved +veg3 +v3el. +vel3l2i +vel1l +ve4lo +v4e1ly +ven3om +v4eno +v5enue +v4erd +5v2e2re. +v4erel +v3eren +ver5enc +v4eres +ver3ie4 +ver1i +vermi4n +ver3m4 +3ver2se +ve4r1s2 +ver3th +v4e2s +4ves. +ves4te +ve4te +vet3er +ve4ty +vi5al1i +v2i1a +vi2al +5vi2a2n +5vi2de. +v2id +5vi2d1ed +4v3i1den +5vide4s2 +5vi2di +v3if +vi5gn +v2ig +v4ik4 +2vil +5v2il1it +vil1i +v3i3l2iz +v1in +4vi4na +v2inc +v4in5d +4ving +vi1o3l +v2io +v3io4r +vi1ou2 +v2i4p +vi5ro +v4ir +vis3it +vi3so +vi3su +4vi1ti +vit3r +4vi1ty +3v2iv +5vo. +voi4 +3v2ok +vo4la +v5ole +5vo4l2t +3vol2v +vom5i +vo2r5ab +vo1ra +vori4 +vo4ry +vo4ta +4vo1tee +4vv4 +v4y +w5ab2l2 +2wac +wa5ger +wa2g5o +wait5 +wai2 +w5al. +wam4 +war4t +w2a2r +was4t +wa1te +wa5ver +w1b +wea5r2ie4 +we2a2r +wear1i +we4ath3 +wea2t +we4d4n4 +weet3 +wee5v +wel4l +w1er +west3 +w3ev +whi4 +wi2 +wil2 +wil2l5in4 +wil1l +will2i +win4de +w4ind +win4g +w4ir4 +3w4ise +w2ith3 +wiz5 +w4k +wl4es2 +wl3in +w4no +1wo2 +wom1 +wo5v4en +w5p +wra4 +wri4 +wri1ta4 +w3s2h +ws4l2 +ws4pe +w5s4t +4wt +wy4 +x1a +xac5e +x4a2go +xam3 +x4ap +xas5 +x3c2 +x1e +xe4cu1to +xe1cu +xe3c4ut +x2ed +xer4i +x2e5ro +x1h +xhi2 +xh4il5 +xhu4 +x3i +x2i5a +xi5c +xi5di +x2id +x4ime +xi5m2iz +xim1i +x3o +x4ob +x3p +xp4an4d +x1pa +xpa2n +xpec1to5 +xpe2c +xpe2c1t +x2p2e3d +x1t2 +x3ti +x1u +xu3a +xx4 +y5ac +3y2a2r4 +y5at +y1b +y1c +y2ce +yc5er +y3ch +ych4e2 +ycom4 +y1co +ycot4 +y1d +y5ee +y1er +y4er1f +yes4 +ye4t +y5gi +4y3h +y1i +y3la +ylla5b2l2 +yl1l +y3lo +y5lu +ymbol5 +y4m1b +yme4 +ym1pa3 +y4m1p +yn3c4hr4 +yn2ch +yn5d +yn5g +yn5ic +5ynx +y1o4 +yo5d +y4o5g +yom4 +yo5net +yo2n +y4o2n3s2 +y4os +y4p2ed +yper5 +yp3i +y3po +y4po4c +yp2ta +y2p1t +y5pu +yra5m +yr5i3a +y3ro +yr4r4 +ys4c +y3s2e +ys3i1ca +y1s3io +3y1sis +y4so +y2ss4 +ys1t +ys3ta +ysu2r4 +y1su +y3thin +yt3ic +y1w +za1 +z5a2b +z2a2r2 +4zb +2ze +ze4n +ze4p +z1er +z2e3ro +zet4 +2z1i +z4il +z4is +5zl +4zm +1zo +zo4m +zo5ol +zoo2 +zte4 +4z1z2 +z4zy +.as9s8o9c8i8a8te. +.as1so +.asso1ci +.asso3c2i1a +.as9s8o9c8i8a8t8es. +.de8c9l8i9n8a9t8i8on. +.de1c4l4 +.decl4i1na +.declin2at +.declina1t2io +.declinatio2n +.ob8l8i8g9a9t8o8ry. +.ob2l2 +.obl2ig +.obli1ga +.obliga1to +.obligato1ry +.ph8i8l9a8n9t8h8r8o8p8ic. +.ph4il2 +.phi1la +.phila2n +.philan1t +.philant4hr4 +.philanthrop3ic +.pr8e8s8e8nt. +.p3re1s2e +.presen1t +.pr8e8s8e8n8ts. +.presen4t4s2 +.pr8o8j8e8ct. +.pro5j +.pro1je +.proje2c1t +.pr8o8j8e8c8ts. +.projec4t1s2 +.re8c9i9p8r8o8c9i9t8y. +.re1c2i +.rec2ip +.recipr2 +.recipr2oc +.re1cipro1ci +.recipro2c1it +.reciproci1ty +.re9c8o8g9n8i9z8a8n8ce. +.re1co +.re2cog +.rec3ogniz +.recog1ni +.recogniza1 +.recogniza2n +.re8f9o8r9m8a9t8i8on. +.re1f +.re1fo +.refo2r +.refor1m +.refor1ma +.reforma1t2io +.reformatio2n +.re8t9r8i9b8u9t8i8on. +.re3tri +.retr4ib +.retri3bu1t2io +.retrib4u1t2i +.retributio2n +.ta9b8le. +.2tab +.tab2l2 +.ac8a8d9e9m8y. +.a1ca +.aca2d +.acad4em +.acade3my +.ac8a8d9e9m8i8e8s. +.academ2i4e4 +.ac9c8u9s8a9t8i8v8e. +.ac3c +.ac1c2us +.accusa2 +.accusa1t2iv +.ac8r8o9n8y8m. +.acro2n +.acronym4 +.ac8r8y8l9a8m8i8d8e. +.acry3la +.acrylam2id +.ac8r8y8l9a8m8i8d8e8s. +.acrylamide4s2 +.ac8r8y8l9a8l8d8e9h8y8d8e. +.acryla2ld +.acrylal2de +.acrylalde1h4 +.acrylaldehy1d +.ad8d9a9b8l8e. +.ad1d2a +.ad2d3a4b +.addab2l2 +.ad8d9i9b8l8e. +.addi1b2l2 +.ad8r8e8n9a9l8i8n8e. +.a1dr +.adre4 +.a5dren +.adre1na +.adrena4l1i +.adrena1l4ine +.ae8r8o9s8p8a8c8e. +.ae4r +.a2ero +.aero2s4pa +.aerospa4ce +.af9t8e8r9t8h8o8u8g8h8t. +.afterthou2 +.af9t8e8r9t8h8o8u8g8h8t8s. +.afterthough4t1s2 +.ag8r8o8n9o9m8i8s8t. +.a1gr +.ag4ro +.agro2n +.agronom2is +.ag8r8o8n9o9m8i8s8t8s. +.agronomis4t1s2 +.al9g8e9b8r8a9i9c8a8l9l8y. +.a4l1g4 +.alg2e1b +.alge3br +.algebr2ai2 +.algebrai1ca +.algebraical1l +.algebraical1ly +.am9p8h8e8t9a9m8i8n8e. +.a4m1p +.amphe4t +.amphe1ta +.amphetam1in +.amphetam2ine +.am9p8h8e8t9a9m8i8n8e8s. +.amphetami1nes +.an9a9l8y8s8e. +.3ana1ly +.a1na +.an4a2lys4 +.anal5y3s2e +.an9a9l8y8s8e8d. +.analy4s4ed +.an8a8l8y9s8e8s. +.analys1e4s +.an9i8s8o9t8r8o8p9i8c. +.ani2so +.anisotrop3ic +.an9i8s8o9t8r8o8p9i9c8a8l9l8y. +.anisotropi1ca +.anisotropical1l +.anisotropical1ly +.an9i8s8o8t9r8o9p8i8s8m. +.anisotropi2s1m +.an9i8s8o8t9r8o8p8y. +.anisotropy5 +.an8o8m9a8l8y. +.ano4 +.anoma5l +.ano1ma +.anoma1ly +.an8o8m9a8l8i8e8s. +.anomal1i +.anomal2i4e4 +.an8t8i9d8e8r8i8v9a9t8i8v8e. +.ant2id +.antider1i +.antider2i4v +.antide4ri1va +.antideri3vat +.antider2iva1t2iv +.an8t8i9d8e8r8i8v9a9t8i8v8e8s. +.antiderivativ4e2s +.an8t8i9h8o8l8o9m8o8r9p8h8i8c. +.anti3h +.antiholo1mo +.antiholomo2r +.antiholomor1p +.antiholomorp4h4 +.antiholomorph1ic +.an9t8i8n9o9m8y. +.an2t1in +.ant2i1no +.antino3my +.an9t8i8n9o9m8i8e8s. +.antinom2ie4 +.an9t8i9n8u9c8l8e8a8r. +.antin1u +.antinucle3 +.antinu1c4l4 +.antinucle2a +.antinucle2a2r +.an9t8i9n8u9c8l8e9o8n. +.antinucleo2n +.an9t8i9r8e8v9o9l8u9t8i8o8n9a8r8y. +.ant4ir +.antirev2 +.antirev5olu +.antirevo1lut +.antirevol4u1t2i +.antirevolutio1n5a +.antirevolu1t2io +.antirevolutio2n +.antirevolution2a2r +.ap8o8t8h9e9o9s8e8s. +.ap4ot +.ap4oth +.apoth2e +.apotheos4 +.apotheos1e4s +.ap8o8t8h9e9o9s8i8s. +.apotheo1sis +.ap9p8e8n9d8i8x. +.a4p1p +.ap2pe +.ap3pen +.ar9c8h8i9m8e9d8e8a8n. +.ar1c +.ar2ch +.archi2med +.archimedea2n +.ar9c8h8i9p8e8l9a8g8o. +.arch2i4p +.archipe4 +.archipe4la +.archipela2go +.ar9c8h8i9p8e8l9a9g8o8s. +.ar9c8h8i8v8e. +.arch2i2v +.ar9c8h8i8v8e8s. +.archiv4e2s +.ar9c8h8i8v9i8n8g. +.archiv1in +.archi4ving +.ar9c8h8i8v9i8s8t. +.ar9c8h8i8v9i8s8t8s. +.archivis4t1s2 +.ar9c8h8e9t8y8p9a8l. +.arche2 +.arche4t +.arche1ty +.archety1pa +.archetyp4al +.ar9c8h8e9t8y8p9i9c8a8l. +.archetyp3i +.archetypi1ca +.ar8c9t8a8n9g8e8n8t. +.ar2c1t +.arct5ang +.arc1ta +.arcta2n +.arctan1gen +.arctangen1t +.ar8c9t8a8n9g8e8n8t8s. +.arctangen4t4s2 +.as9s8i8g8n9a9b8l8e. +.as1si +.as4sig1n4a +.ass2ig +.assig2n1a2b +.assignab2l2 +.as9s8i8g8n9o8r. +.assig1no +.as9s8i8g8n9o8r8s. +.assigno4rs2 +.as9s8i8s8t9a8n8t9s8h8i8p. +.as1sis +.assis1ta +.assista2n +.assistan1t +.assistan4t4s2 +.assistants2h4 +.assistant3sh2i4p +.as9s8i8s8t9a8n8t9s8h8i8p8s. +.assistantshi2p1s2 +.as8y8m8p9t8o9m8a8t8i8c. +.as4y +.asy4m1p +.asym2p1t +.asymp1to +.asympto2ma +.asymptomat1ic +.as9y8m8p9t8o8t9i8c. +.as8y8n9c8h8r8o9n8o8u8s. +.asyn3c4hr4 +.asyn2ch +.asynchro2n +.asynchro1nou2 +.asynchrono2us +.at8h9e8r9o9s8c8l8e9r8o9s8i8s. +.4ath +.ath2e +.ath2ero +.atheros2c +.atheroscle5 +.atheros1c4l4 +.ath2eroscl4ero +.atherosclero1sis +.at9m8o8s9p8h8e8r8e. +.a4t1m +.at1mo +.atmos2 +.atmo3sp +.atmos2phe +.atmo3sph4er +.at9m8o8s9p8h8e8r8e8s. +.at9t8r8i8b9u8t8e8d. +.a4t3t2 +.attr4ib +.attribu2t1ed +.at9t8r8i8b9u8t9a8b8l8e. +.attri4bu1ta +.attribu2ta4b +.attributab2l2 +.au9t8o9m8a9t8i8o8n. +.au1to +.auto2ma +.automa1t2io +.automatio2n +.au9t8o8m9a9t8o8n. +.automa1to +.automato2n +.au9t8o8m9a9t8a. +.automa2ta +.au9t8o9n8u8m9b8e8r9i8n8g. +.au5to2n +.auton5um +.autonu4m1b +.autonumber1i +.autonumberin4g +.au9t8o8n9o9m8o8u8s. +.au4tono +.autono4mo +.autono3mo2us +.autonomou2 +.au8t8o9r8o8u8n8d9i8n8g. +.autorou2 +.autoroun2d +.autoround1in +.av9o8i8r9d8u9p8o8i8s. +.avoi4 +.avo4ir +.avoir1du +.avoir4dup +.avoirdupoi2 +.ba8n8d9l8e8a8d8e8r. +.b4and +.ban1dl +.bandle2a +.bandlea2d1 +.ba8n8d9l8e8a8d8e8r8s. +.bandleade4r5s2 +.ba8n8k9r8u8p8t. +.ba4nk2 +.bankru2p1t +.ba8n8k9r8u8p8t9c8y. +.bankrup4tc +.bankrupt1cy +.ba8n8k9r8u8p8t9c8i8e8s. +.bankrupt1ci +.bankruptc2ie4 +.ba8r9o8n8i8e8s. +.b2a2r +.ba5roni +.baro2n +.baron2ie4 +.ba8s8e9l8i8n8e9s8k8i8p. +.basel2i +.base1l4ine +.baseli1nes +.baselinesk2 +.baselinesk1i +.baselinesk2i4p +.ba9t8h8y8m9e9t8r8y. +.1bat +.b4ath +.bathyme4 +.bathym4etr +.bathyme3try +.ba8t8h8y9s8c8a8p8h8e. +.bathy2s +.bathys4c +.bathysca4p +.bathys1ca +.be8a8n9i8e8s. +.bea2n +.bea3nies +.bean2ie4 +.be9h8a8v9i8o8u8r. +.be1h4 +.behav1i +.behavi1ou2 +.behav2io +.behavi4our +.be9h8a8v9i8o8u8r8s. +.behaviou4rs2 +.be8v8i8e8s. +.be1vi +.bev2ie4 +.bi8b9l8i9o8g9r8a9p8h8y9s8t8y8l8e. +.bi2b +.bi1b2l2 +.bib3li +.bibli5og +.bibl2io +.biblio2gr +.biblio4g3ra1phy +.bibliography2s +.bibliographys1t +.bibliographys2ty +.bibliographys2tyl +.bi9d8i8f9f8e8r9e8n9t8i8a8l. +.b2i4d +.bi2di +.bid1if +.bidi4f1f +.bidiffer1 +.bidiffer3en1t +.bidifferent2i +.bidifferen1t2i1a +.bidifferenti2al +.bi8g9g8e8s8t. +.b2ig +.bi4g1g2 +.big2ge +.bi8l8l9a8b8l8e. +.1bil +.bill5ab +.bil1l +.billab2l2 +.bi8o9m8a8t8h9e9m8a8t9i8c8s. +.b2io +.bio4m +.bio1ma +.biom4ath3 +.biomath5em +.biomath2e +.biomathe1ma +.biomathemat1ic +.biomathemati4c3s2 +.bi8o9m8e8d9i9c8a8l. +.bio2me +.bio2med +.biom4edi +.biomed3i1ca +.bi8o9m8e8d9i9c8i8n8e. +.biomed2i1ci +.biomedi2cin +.biomedic2ine +.bi8o9r8h8y8t8h8m8s. +.biorh4 +.biorhyt4h1m +.biorhyth4m1s2 +.bi8t9m8a8p. +.bi2t +.bi4t1m +.bit1ma +.bit4map +.bi8t9m8a8p8s. +.bitma2p1s2 +.bl8a8n8d9e8r. +.b2l2 +.b3l4and +.bla2n +.blan1de +.bl8a8n8d9e8s8t. +.blande4s2 +.bl8i8n8d9e8r. +.bl4ind +.blin1de +.bl8o8n8d8e8s. +.b4lo +.blo2n +.bl2ond +.blon1de +.blondes2 +.bl8u8e9p8r8i8n8t. +.bluepr2 +.blueprin4t3 +.bl8u8e9p8r8i8n8t8s. +.blueprin4t4s2 +.bo9l8o8m9e9t8e8r. +.bolo2me +.bolo4met +.bolome1te +.bo8o8k9s8e8l8l9e8r. +.3boo2 +.bo2o4k +.boo4k1s2 +.booksel1l +.booksel2le +.bo8o8k9s8e8l8l9e8r8s. +.bookselle4r1s2 +.bo8o8l9e8a8n. +.boole2a +.boolea2n +.bo8o8l9e8a8n8s. +.boolea2n1s2 +.bo8r9n8o9l8o8g9i9c8a8l. +.borno4 +.borno3log1ic +.bornologi1ca +.bo8t9u9l8i8s8m. +.bo1tu +.botul2i +.botuli2s1m +.br8u8s8q8u8e8r. +.br2us +.brusqu2 +.brus3quer +.bu8f9f8e8r. +.buf4fer1 +.bu4f1f +.bu8f9f8e8r8s. +.buffe4r1s2 +.bu8s8i8e8r. +.bus5ie4 +.b2us +.bu8s8i8e8s8t. +.busi1est +.bu8s8s8i8n8g. +.bu2ss +.bus1si +.bus2s1in +.buss3ing +.bu8t8t8e8d. +.but2t1ed +.bu8z8z9w8o8r8d. +.bu4z1z2 +.buzz1wo2 +.bu8z8z9w8o8r8d8s. +.buzzwor2d1s2 +.ca9c8o8p8h9o9n8y. +.ca1co +.cac2oph +.cacopho5ny +.cacopho2n +.ca9c8o8p8h9o9n8i8e8s. +.caco5phoni +.cacophon2ie4 +.ca8l8l9e8r. +.cal1l +.cal2le +.ca8l8l9e8r8s. +.calle4r1s2 +.ca8m9e8r8a9m8e8n. +.cam5er1a +.camera1men +.ca8r8t9w8h8e8e8l. +.cartw4 +.ca8r8t9w8h8e8e8l8s. +.cartwhee2l1s2 +.ca9t8a8r8r8h8s. +.ca2ta +.cat2a2r +.catar1r4 +.catarrh4 +.catarr4h1s2 +.ca8t9a9s8t8r8o8p8h9i8c. +.catas1t4r +.catastr2oph +.catastroph1ic +.ca8t9a9s8t8r8o8p8h9i9c8a8l8l8y. +.catastrophi1ca +.catastrophical1l +.catastrophical1ly +.ca8t9e9n8o8i8d. +.cat4eno +.catenoi2 +.cateno2id +.ca8t9e9n8o8i8d8s. +.catenoi2d1s2 +.ca8u9l8i9f8l8o8w9e8r. +.cau4l2 +.caul2i +.cauli4f4l2 +.cauliflow1er +.ch8a8p9a8r9r8a8l. +.chap2a2r4 +.cha1pa +.chapar1r4 +.ch8a8r9t8r8e8u8s8e. +.ch2a2r +.chartr4eu2 +.chartre2us4 +.ch8e8m8o9t8h8e8r9a8p8y. +.che2 +.che1mo +.chem4oth3 +.chemoth2e +.chemoth4er1a +.chemothera3p +.ch8e8m8o9t8h8e8r9a9p8i8e8s. +.chemotherap2ie4 +.ch8l8o8r8o9m8e8t8h9a8n8e. +.c4h1l4 +.ch2lo +.chloro2me +.chloro4met +.chlorometha2n4 +.ch8l8o8r8o9m8e8t8h9a8n8e8s. +.chlorometha1nes +.ch8o9l8e8s9t8e8r8i8c. +.3cho2 +.c3hol4e +.choles2 +.choles1ter1i +.ci8g9a9r8e8t8t8e. +.c2ig +.ci1ga +.cig2a2r +.cigare4t3t2 +.ci8g9a9r8e8t8t8e8s. +.cigaret4tes +.ci8n8q8u8e9f8o8i8l. +.2cin +.cin1q +.cinqu2 +.cinque1f +.cinque1fo +.cinquefoi2 +.co9a8s8s8o9c8i8a9t8i8v8e. +.c4oa +.coa2ss +.coas1so +.coasso1ci +.coasso3c2i1a +.coassoci4a1t2iv +.co9g8n8a8c. +.2cog +.cog1n4a +.co9g8n8a8c8s. +.cogna4c3s2 +.co9k8e8r9n8e8l. +.c2ok +.cok1er +.coker3nel +.co9k8e8r9n8e8l8s. +.cokerne2l1s2 +.co8l9l8i8n9e8a9t8i8o8n. +.col1l +.coll2i +.col2lin4 +.col1l4ine +.collin3ea +.collinea2t +.collinea1t2io +.collineatio2n +.co8l9u8m8n8s. +.colu4m1n +.colum2n1s2 +.co8m9p8a8r9a8n8d. +.co4m1p +.compara5 +.com1pa +.comp2a2r +.compara2n +.compar4and +.co8m9p8a8r9a8n8d8s. +.comparan2d1s2 +.co8m9p8e8n9d8i8u8m. +.compendi1u +.co8m9p8o9n8e8n8t9w8i8s8e. +.compo2n +.compo3nen +.componen1t +.componentw4 +.componentwis4 +.componentwi2 +.component3w4ise +.co8m8p9t8r8o8l9l8e8r. +.comp4tr +.com2p1t +.comptrol1l +.comptrol2le +.co8m8p9t8r8o8l9l8e8r8s. +.comptrolle4r1s2 +.co8n9f8o8r8m9a8b8l8e. +.co2n +.con3f +.con1fo +.confo2r +.confor1m +.confor1ma +.confor2mab +.conformab2l2 +.co8n9f8o8r8m9i8s8t. +.confor2mi +.conform2is +.co8n9f8o8r8m9i8s8t8s. +.conformis4t1s2 +.co8n9f8o8r8m9i8t8y. +.confor3mit +.conformi1ty +.co8n9g8r8e8s8s. +.con3g +.con1gr +.congr2e2ss +.co8n9g8r8e8s8s8e8s. +.congress1e4s +.co8n9t8r8i8b9u8t8e. +.con5t +.contr4ib +.co8n9t8r8i8b9u8t8e8s. +.co8n9t8r8i8b9u8t8e8d. +.contribu2t1ed +.co9r8e9l8a9t8i8o8n. +.core1la +.corela1t2io +.corelatio2n +.co9r8e9l8a9t8i8o8n8s. +.corelatio2n3s2 +.co9r8e9l8i9g8i8o8n9i8s8t. +.core1l2i +.corel2ig +.corel4igi +.coreli5g2io +.coreligion3i +.coreligio2n +.coreligion1is +.co9r8e9l8i9g8i8o8n9i8s8t8s. +.coreligionis4t1s2 +.co9r8e9o8p9s8i8s. +.core1o +.coreo2p1s2 +.coreop1sis +.co9r8e9s8p8o8n9d8e8n8t. +.core1sp +.cores4po2n +.coresp2ond +.corespon1de +.corespon1den +.coresponden1t +.co9r8e9s8p8o8n9d8e8n8t8s. +.coresponden4t4s2 +.co9s8e9c8a8n8t. +.cos4e +.cose1ca +.coseca2n +.cosecan1t +.co9t8a8n9g8e8n8t. +.co4ta2n +.co1ta +.cot2ang +.cotan1gen +.cotangen1t +.co8u8r9s8e8s. +.cou2 +.cou4rs2 +.cour2se +.cours3e4s +.co9w8o8r8k9e8r. +.co4wo2 +.cowork1er +.co9w8o8r8k9e8r8s. +.coworke4r1s2 +.cr8a8n8k9c8a8s8e. +.cra2n +.cra4nk2 +.crank1ca +.cr8a8n8k9s8h8a8f8t. +.cran4k1s2 +.cranks2h +.cranksha2f +.cranksha2ft +.cr8o8c9o9d8i8l8e. +.cr2oc +.cro4cod +.cro1co +.cr8o8c9o9d8i8l8e8s. +.crocodiles2 +.cr8o8s8s9h8a8t8c8h. +.cro2s4s +.cross2h +.crossha4tc +.crosshat4ch +.cr8o8s8s9h8a8t8c8h8e8d. +.crosshatche2 +.crosshat4ch4ed +.cr8o8s8s9o8v8e8r. +.cros1so +.cros4sov +.cr8y8p9t8o9g8r8a8m. +.cry2p1t +.cryp1to +.crypto2gr +.cr8y8p9t8o9g8r8a8m8s. +.cryptogra4m1s2 +.cu8f8f9l8i8n8k. +.c4uf +.cu4f1f +.cuff4l2 +.cufflin4 +.cuffl4i4nk2 +.cu8f8f9l8i8n8k8s. +.cufflin4k1s2 +.cu9n8e8i9f8o8r8m. +.3cun +.cu2ne +.cunei2 +.cunei1fo +.cuneifo2r +.cuneifor1m +.cu8s9t8o8m9i8z9a9b8l8e. +.1c2us +.cus1to +.custom2iz +.customiza1 +.customiz5a2b +.customizab2l2 +.cu8s9t8o8m9i8z8e. +.customi2ze +.cu8s9t8o8m9i8z8e8s. +.cu8s9t8o8m9i8z8e8d. +.da8c8h8s9h8u8n8d. +.1d2a +.da2ch4 +.dac4h1s2 +.dach4s2h +.da8m9s8e8l9f8l8y. +.da2m2 +.da4m1s2 +.dam5se2l2f +.damself4l2 +.damself2ly5 +.da8m9s8e8l9f8l8i8e8s. +.damselfl2ie4 +.da8c8t8y8l9o9g8r8a8m. +.da2c1t +.dac1ty +.dac2tyl +.dacty3lo +.dactylo1gr +.da8c8t8y8l9o9g8r8a8p8h. +.da8t8a9b8a8s8e. +.3dat +.da2ta +.da2tab +.da8t8a9b8a8s8e8s. +.databas1e4s +.da8t8a9p8a8t8h. +.dat5ap +.datap5at +.data1pa +.datap4ath +.da8t8a9p8a8t8h8s. +.datapa2t4h1s2 +.da8t8e9s8t8a8m8p. +.dat3est +.dates1ta +.datesta4m1p +.da8t8e9s8t8a8m8p8s. +.datestam2p1s2 +.de9c8l8a8r9a8b8l8e. +.de4cl2a2r +.decla2rab +.declarab2l2 +.de9f8i8n9i9t8i8v8e. +.de1f +.de1fi +.de2fin +.def2ini +.defin2it +.defini1ti +.defini1t2iv +.de9l8e8c9t8a9b8l8e. +.d5elec +.dele2c1t +.delec2ta4b +.delec1ta +.delectab2l2 +.de8m8i9s8e8m8i9q8u8a9v8e8r. +.de4m2is +.dem4ise +.demisemi3qua +.demisemiqu2 +.demisemiqua5v4 +.de8m8i9s8e8m8i9q8u8a9v8e8r8s. +.demisemiquave4r1s2 +.de9m8o8c9r8a9t8i8s8m. +.de4mocr +.democrati2s4m +.de8m8o8s. +.demos2 +.de9r8i8v9a9t8i8v8e. +.der2i4v +.de4ri1va +.deri3vat +.der2iva1t2iv +.de9r8i8v9a9t8i8v8e8s. +.derivativ4e2s +.di8a9l8e8c9t8i8c. +.1d4i3a +.di2al +.di2ale +.diale2c1t +.di8a9l8e8c9t8i8c8s. +.dialecti4c3s2 +.di8a9l8e8c9t8i9c8i8a8n. +.dialect2i1ci +.d2i1alecti3c2i1a +.dialectici2a2n +.di8a9l8e8c9t8i9c8i8a8n8s. +.dialecticia2n1s2 +.di9c8h8l8o8r8o9m8e8t8h9a8n8e. +.d4i2ch +.dic4h1l4 +.dich2lo +.dichloro2me +.dichloro4met +.dichlorometha2n4 +.di8f9f8r8a8c8t. +.d1if +.dif4fr +.di4f1f +.diffra2c1t +.di8f9f8r8a8c8t8s. +.diffrac4t1s2 +.di8f9f8r8a8c9t8i8o8n. +.diffrac1t2io +.diffractio2n +.di8f9f8r8a8c9t8i8o8n8s. +.diffractio2n3s2 +.di8r8e8r. +.d4ir2 +.di1re +.dir1er4 +.di8r8e9n8e8s8s. +.dire1nes +.diren2e2ss +.di8s9p8a8r9a8n8d. +.dis1 +.dis1p +.di2s1pa +.disp2a2r +.dispara2n +.dispar4and +.di8s9p8a8r9a8n8d8s. +.disparan2d1s2 +.di8s9t8r8a8u8g8h8t9l8y. +.d4is3t +.dist4r +.dis1tra +.distraugh3 +.distraugh2tl +.distraught1ly +.di8s9t8r8i8b9u8t8e. +.distr4ib +.di8s9t8r8i8b9u8t8e8s. +.di8s9t8r8i8b9u8t8e8d. +.distribu2t1ed +.do8u9b8l8e9s8p8a8c8e. +.dou2 +.dou3b2l2 +.dou5ble1sp +.doubles2 +.double2s1pa +.doublespa4ce +.do8u9b8l8e9s8p8a8c9i8n8g. +.doublesp4a2ci +.doublespa2c1in +.doublespac1ing +.do8l8l9i8s8h. +.dol1l +.doll2i +.dollis2h +.dr8i8f8t9a8g8e. +.1dr +.dr4i2ft +.drif1ta +.dr8i8v9e8r8s. +.dr2iv +.drive4r1s2 +.dr8o8m9e9d8a8r8y. +.dro2me +.dro2med +.drom2e2d2a +.drome4dary +.dromed2a2r +.dr8o8m9e9d8a8r8i8e8s. +.dromedar1i +.dromedar2ie4 +.du9o8p9o9l8i8s8t. +.duopol2i +.du9o8p9o9l8i8s8t8s. +.duopolis4t1s2 +.du9o8p9o8l8y. +.duopo2ly +.dy8s9l8e8x8i8a. +.d2y +.dys1l2 +.dys2le +.dyslex3i +.dyslex2i5a +.dy8s9l8e8c9t8i8c. +.dysle2c1t +.ea8s8t9e8n8d9e8r8s. +.east3 +.eas3ten +.eas3tend +.easten1de +.eastende4r5s2 +.ec8o9n8o8m9i8c8s. +.e1co +.eco2n +.eco3nomic +.economi4c3s2 +.ec8o8n9o9m8i8s8t. +.econom2is +.ec8o8n9o9m8i8s8t8s. +.economis4t1s2 +.ei9g8e8n9c8l8a8s8s. +.ei2 +.e2ig2 +.ei1gen +.eigen1c4l4 +.eigencla2ss +.ei9g8e8n9c8l8a8s8s8e8s. +.eigenclass1e4s +.ei9g8e8n9v8a8l9u8e. +.eigen1v2 +.eigen1va +.eigenval1u +.ei9g8e8n9v8a8l9u8e8s. +.el8e8c8t8r8o9m8e8c8h8a8n9i9c8a8l. +.5elec +.ele2c1t +.electro2me +.electrome2ch +.electrome5cha4n1ic +.electromecha2n +.electromechani1ca +.el8e8c8t8r8o9m8e8c8h8a8n8o9a8c8o8u8s8t8i8c. +.electromechano4 +.electromechan4oa +.electromechanoa1co +.electromechanoacou2 +.electromechanoaco2us +.electromechanoacoust2i +.electromechanoacous1tic +.el8i8t9i8s8t. +.el2i +.el1it +.eli1ti +.el4itis +.el8i8t9i8s8t8s. +.elitis4t1s2 +.en9t8r8e9p8r8e9n8e8u8r. +.en1t +.entrepr2 +.entrepren4eu +.en9t8r8e9p8r8e9n8e8u8r9i8a8l. +.entrepreneur2i3a +.entrepreneuri2al +.ep9i9n8e8p8h9r8i8n8e. +.epi2n +.ep2ine +.epinep4hr4 +.ep2inephr2in4e +.eq8u8i9v8a8r8i9a8n8t. +.equ2iv3 +.equi1va +.equiv2a2r +.equivar1i +.equivar3i2a2n +.equivar2i3a +.equivar4ian4t +.eq8u8i9v8a8r8i9a8n8c8e. +.equivar4ianc +.et8h9a8n8e. +.etha2n4 +.et8h9y8l9e8n8e. +.ev8e8r9s8i9b8l8e. +.ev1er +.eve4r1s2 +.ever1si +.ever4si4b +.eversi1b2l2 +.ev8e8r8t. +.ev8e8r8t8s. +.ever4t1s2 +.ev8e8r8t9e8d. +.ever2t1ed +.ev8e8r8t9i8n8g. +.ever1ti +.ever2t1in +.ex9q8u8i8s9i8t8e. +.exqu2 +.exq2ui2 +.exquis2ite +.ex9t8r8a9o8r9d8i9n8a8r8y. +.ex1t2 +.ex1tra +.extr4ao +.extraord2i +.extraord1in4 +.extraor1di1na +.extraordin2a2r +.fa8l8l9i8n8g. +.1fa +.fal1l +.fall2i +.fal2lin4 +.fe8r8m8i9o8n8s. +.fer1 +.fer3m4 +.fer4m2io +.fermio2n +.fermio2n3s2 +.fi9n8i8t8e9l8y. +.1fi +.2fin +.f2ini +.fin2it +.fin2ite +.finite1ly +.fl8a9g8e8l9l8u8m. +.f4l2 +.flag5el1l +.fl8a9g8e8l9l8a. +.flag4ella +.fl8a8m9m8a9b8l8e8s. +.flam1m +.flam1ma +.flam2mab +.flammab2l2 +.flammables2 +.fl8e8d8g9l8i8n8g. +.fledgl2 +.fl8o8w9c8h8a8r8t. +.flow2ch +.flowch2a2r +.fl8o8w9c8h8a8r8t8s. +.flowchar4t1s2 +.fl8u8o8r8o9c8a8r9b8o8n. +.flu3o +.fluo3r +.fluor2oc +.fluoro1ca +.fluoroc2a2r +.fluorocar1b +.fluorocarb4o +.fluorocarbo2n +.fo8r9m8i9d8a9b8l8e. +.for2mi +.formi1d4a +.form2id +.formi2d3a4b +.formidab2l2 +.fo8r9m8i9d8a9b8l8y. +.formidab1ly +.fo8r9s8y8t8h9i8a. +.fo4rs2 +.fors4y +.forsyth2i1a +.fo8r8t8h9r8i8g8h8t. +.fort4hr4 +.forthr2ig +.fr8e8e9l8o8a8d8e8r. +.freel4oa +.freeloa2d3 +.fr8e8e9l8o8a8d8e8r8s. +.freeloade4r5s2 +.fr8i8e8n8d9l8i8e8r. +.fri2 +.fr2ie4 +.friendl2ie4 +.fr8i9v8o8l9i8t8y. +.fr2iv +.frivol2i +.frivol1it +.frivoli1ty +.fr8i9v8o8l9i9t8i8e8s. +.frivoli1ti +.frivolit2ie4 +.fr8i8v9o9l8o8u8s. +.frivolou2 +.frivolo2us +.ga9l8a8c9t8i8c. +.gala2c1t +.ga8l9a8x8y. +.ga8l9a8x9i8e8s. +.galax3i +.galax2ie4 +.ga8s9o8m9e9t8e8r. +.ga1so +.ga3som +.gaso2me +.gaso4met +.gasome1te +.ge9o9d8e8s9i8c. +.geodes2 +.geode1si +.geode2sic +.ge9o9d8e8t9i8c. +.geode1t +.geodet1ic +.ge8o9m8e8t9r8i8c. +.ge3om +.geo2me +.geo4met +.geom4etr +.geo5met3ric +.ge8o9m8e8t9r8i8c8s. +.geome4tri4c3s2 +.ge9o9s8t8r8o8p8h8i8c. +.geos4 +.geost4r +.geostr2oph +.geostroph1ic +.ge8o9t8h8e8r9m8a8l. +.ge4ot +.ge4oth +.geoth2e +.geother3m4 +.geother1ma +.ge9o8t9r8o9p8i8s8m. +.geotropi2s1m +.gn8o9m8o8n. +.g1no +.gno4mo +.gno4mo2n +.gn8o9m8o8n8s. +.gnomo2n3s2 +.gr8a8n8d9u8n8c8l8e. +.1gr +.gra2n2 +.gr4and +.gran1du +.grandu4n +.grandun1c4l4 +.gr8a8n8d9u8n8c8l8e8s. +.granduncles2 +.gr8i8e8v9a8n8c8e. +.gr2ie4 +.grie1va +.grieva2n +.gr8i8e8v9a8n8c8e8s. +.gr8i8e8v9o8u8s. +.grievou2 +.grievo2us +.gr8i8e8v9o8u8s9l8y. +.grievous1l2 +.grievous1ly +.ha8i8r9s8t8y8l8e. +.hai2 +.ha4ir +.hai4rs2 +.hairs2ty +.hairs2tyl +.ha8i8r9s8t8y8l8e8s. +.hairstyles2 +.ha8i8r9s8t8y8l9i8s8t. +.ha8i8r9s8t8y8l9i8s8t8s. +.hairstylis4t1s2 +.ha8l8f9s8p8a8c8e. +.ha2lf +.hal2f3s +.half2s1pa +.halfspa4ce +.ha8l8f9s8p8a8c8e8s. +.ha8l8f9w8a8y. +.ha8r9b8i8n9g8e8r. +.h2a2r +.har1b +.harbi2 +.har2bin +.harb4inge +.ha8r9b8i8n9g8e8r8s. +.harbinge4r1s2 +.ha8r9l8e9q8u8i8n. +.har4le4 +.har1l +.harle1q +.harlequ2 +.harleq2ui2 +.harlequi4n +.ha8r9l8e9q8u8i8n8s. +.harlequ2i2n1s2 +.ha8t8c8h9e8r8i8e8s. +.ha4tc +.hat4ch +.hatche2 +.hatcher1i +.hatcher2ie4 +.he8m8i9d8e8m8i9s8e8m8i9q8u8a9v8e8r. +.hem2id +.hemid4em +.hemide4m2is +.hemidem4ise +.hemidemisemi3qua +.hemidemisemiqu2 +.hemidemisemiqua5v4 +.he8m8i9d8e8m8i9s8e8m8i9q8u8a9v8e8r8s. +.hemidemisemiquave4r1s2 +.he9m8o9g8l8o9b8i8n. +.hemo4g +.he1mo +.hemo4gl2 +.hemo3glo +.hemoglo1bi +.hemoglo2bin +.he9m8o9p8h8i8l9i8a. +.hem2oph +.hemoph4il2 +.hemophil1i +.hemophil3i1a +.he9m8o9p8h8i8l9i8a8c. +.he9m8o9p8h8i8l9i8a8c8s. +.hemophilia4c3s2 +.he8m8o9r8h8e9o8l9o8g8y. +.hemo2r +.hemorh4 +.hemorhe3ol +.hemorheol1o1gy +.he9p8a8t9i8c. +.hep5 +.he2pa +.hepat1ic +.he8r9m8a8p8h9r8o9d8i8t8e. +.her3m4 +.her1ma +.her4map +.hermap4hr4 +.hermaphrod2ite +.he8r9m8a8p8h9r8o9d8i8t9i8c. +.hermaphrod2i1ti +.hermaphrod4i2tic +.he9r8o8e8s. +.hero4e +.he8x8a9d8e8c9i9m8a8l. +.hex1a +.hexa2d +.hexade1c2i +.hexade2c3im +.hexadeci1ma +.ho9l8o9n8o9m8y. +.holo2n +.holon3o3my +.ho9m8e8o9m8o8r9p8h8i8c. +.ho2me3 +.homeo1mo +.homeomo2r +.homeomor1p +.homeomorp4h4 +.homeomorph1ic +.ho9m8e8o9m8o8r9p8h8i8s8m. +.homeomorphi2s1m +.ho9m8o9t8h8e8t8i8c. +.ho1mo +.hom4oth3 +.homoth2e +.homo3the4t +.homothet1ic +.ho8r8s8e9r8a8d9i8s8h. +.hor4se +.ho4rs2 +.horser1a +.horsera2d +.horser2adi +.horseradis1 +.horseradis2h +.ho8t9b8e8d. +.ho2t1b +.hot4be2d +.ho8t9b8e8d8s. +.hotbe2d1s2 +.hy9d8r8o9t8h8e8r9m8a8l. +.hy1d +.hy1dr +.hydro4th2e +.hydr4oth +.hydrother3m4 +.hydrother1ma +.hy9p8o9t8h8a8l9a9m8u8s. +.hy3po +.hyp4ot +.hyp4oth +.hypotha3la +.hypothala3m +.hypothala1mu +.hypothalam2us +.id8e8a8l8s. +.ide3a4l +.idea2l1s2 +.id8e8o9g8r8a8p8h8s. +.ideo2g +.ideo1gr +.ideogra4p4h1s2 +.id8i8o9s8y8n9c8r8a8s8y. +.i2di +.i1d3io +.idi4os +.idios4y +.idiosyn1cr +.idiosyncr2as +.idiosyncras4y +.id8i8o9s8y8n9c8r8a9s8i8e8s. +.idiosyncras2ie4 +.id8i8o9s8y8n9c8r8a8t8i8c. +.idiosyn5crat1ic +.id8i8o9s8y8n9c8r8a8t9i9c8a8l9l8y. +.idiosyncrati1ca +.idiosyncratical1l +.idiosyncratical1ly +.ig9n8i8t9e8r. +.2ig +.ig1ni +.ign2it +.ign2ite +.ig9n8i8t9e8r8s. +.ignite4r1s2 +.ig9n8i9t8o8r. +.ign3itor +.igni1to +.ig8n8o8r8e9s8p8a8c8e8s. +.ig1no +.ignore1sp +.ignore2s1pa +.ignorespa4ce +.im9p8e8d9a8n8c8e. +.im2p2ed +.imp2e2d2a +.impeda2n +.im9p8e8d9a8n8c8e8s. +.in9d8u9b8i9t8a9b8l8e. +.4ind +.in1du +.indu1b4i +.indubi2t +.indubi1ta +.indubi2tab +.indubitab2l2 +.in9f8i8n9i8t8e9l8y. +.in3f +.in1fi +.in2fin +.inf2ini +.infin2it +.infin2ite +.infinite1ly +.in9f8i8n9i9t8e8s9i9m8a8l. +.infinit4es +.infinite1si +.infinite2s5im +.infinitesi1ma +.in9f8r8a9s8t8r8u8c9t8u8r8e. +.infr2as +.infras1t4r +.infrastru2c1t +.infrastructu4r +.infrastruc1tu +.infrastruc3ture +.in9f8r8a9s8t8r8u8c9t8u8r8e8s. +.in9s8t8a8l8l9e8r. +.ins2ta2l +.ins1ta +.instal1l +.instal2le +.in9s8t8a8l8l9e8r8s. +.installe4r1s2 +.in9t8e8r9d8i8s9c8i9p8l8i9n8a8r8y. +.in1t +.in5ter3d +.interd2i +.interdis1 +.interd2is1c +.interdis1ci +.interdisc2ip +.interdisci1p2l2 +.interdiscipli4n +.interdiscipl4i1na +.interdisciplin2a2r +.in9t8e8r9g8a9l8a8c9t8i8c. +.interg2 +.inter1ga +.intergala2c1t +.in9u8t8i8l8e. +.in1u +.in4u1t2i +.in9u8t8i8l9i9t8y. +.inutil1i +.inut2il1it +.inutili1ty +.ir9r8e9d8u8c9i8b8l8e. +.ir2r2ed +.irre1du +.irredu2c +.irreduci4b +.irredu1ci +.irreduci1b2l2 +.ir9r8e9d8u8c9i8b8l8y. +.irreducib1ly +.ir9r8e8v9o9c8a9b8l8e. +.irrev2 +.irre5voc +.irrevo1ca +.irrevoca1b2l2 +.is8o8t9r8o8p8y. +.i2so +.isotropy5 +.is8o9t8r8o8p9i8c. +.isotrop3ic +.it8i8n9e8r9a8r8y. +.i1ti +.i2t1in +.it2ine +.itin4er4a2r +.itin1er +.itiner1a +.it8i8n9e8r9a8r9i8e8s. +.itinerar1i +.itinerar2ie4 +.je9r8e9m8i9a8d8s. +.1je +.jerem2i3a +.jeremia2d +.jeremia2d1s2 +.ke8y9n8o8t8e. +.ke8y9n8o8t8e8s. +.keyno4tes +.ke8y9s8t8r8o8k8e. +.keys4 +.keys1t +.keyst4r +.keystr2ok2 +.ke8y9s8t8r8o8k8e8s. +.keystrokes4 +.ki8l8n9i8n8g. +.k1i +.k4i2l1n2 +.kiln1in +.kilnin4g +.la8c9i9e8s8t. +.l4a2ci4 +.la3c2ie4 +.laci1est +.la8m9e8n9t8a9b8l8e. +.la1men +.la3men1t +.lamen2ta4b +.lamen1ta +.lamentab2l2 +.la8n8d9s8c8a8p9e8r. +.3l4and +.la2n +.lan2d1s2 +.landsca4p +.lands1ca +.landsca5per +.la8n8d9s8c8a8p9e8r8s. +.landscape4r1s2 +.la8r9c8e9n8y. +.l2a2r +.lar1c +.lar2ce +.lar1cen4 +.la8r9c8e9n9i8s8t. +.lar4ceni +.le8a8f9h8o8p9p8e8r. +.le2a +.lea2f +.lea4fh +.leafho4p1p +.leafhop2pe +.leafhop3per +.le8a8f9h8o8p9p8e8r8s. +.leafhoppe4r1s2 +.le8t9t8e8r9s8p8a8c9i8n8g. +.le4t3t2 +.lette4r1s2 +.letter1sp +.letter2s1pa +.lettersp4a2ci +.letterspa2c1in +.letterspac1ing +.li8f8e9s8p8a8n. +.life1sp +.life2s1pa +.lifespa4n +.li8f8e9s8p8a8n8s. +.lifespa2n1s2 +.li8f8e9s8t8y8l8e. +.lifes2ty +.lifes2tyl +.li8f8e9s8t8y8l8e8s. +.lifestyles2 +.li8g8h8t9w8e8i8g8h8t. +.3ligh +.lightw4 +.lightwei2 +.l2ightwe2ig2 +.li8m9o8u9s8i8n8e8s. +.li4mo +.li3mo2us +.limou2 +.limou2s1in +.limous2ine +.limousi1nes +.li8n8e9b8a8c8k8e8r. +.1l4ine +.lin2e2b +.lineback1 +.lineback1er +.li8n8e9s8p8a8c8i8n8g. +.li1nes +.li4ne1sp +.line2s1pa +.linesp4a2ci +.linespa2c1in +.linespac1ing +.li9o8n9e8s8s. +.lio2n +.lio1nes +.lion2e2ss +.li8t8h9o9g8r8a8p8h8e8d. +.l2ith +.litho4g +.litho1gr +.lithograph4ed +.li8t8h9o9g8r8a8p8h8s. +.lithogra4p4h1s2 +.lo9b8o8t9o8m8y. +.lobo4to +.loboto3my +.lo9b8o8t9o8m9i8z8e. +.lobotom2iz +.lobotomi2ze +.lo8g8e8s. +.lo1ge +.lo8n8g9e8s8t. +.5long +.lo2n +.lo9q8u8a8c9i8t8y. +.lo1q +.loqu2 +.loquac4 +.loqu4a2ci +.loqua2c1it +.loquaci1ty +.lo8v8e9s8t8r8u8c8k. +.4lov +.lov4e2s +.lov2est4r +.lovestruc5 +.lovestruck1 +.ma8c8r8o9e8c8o9n8o8m8i8c8s. +.macro4e +.macroe1co +.macroeco2n +.macroeco3nomic +.macroeconomi4c3s2 +.ma8l9a9p8r8o8p9i8s8m. +.malapr2 +.malapropi2s1m +.ma8l9a9p8r8o8p9i8s8m8s. +.malaprop4is4m1s2 +.ma8n9s8l8a8u8g8h9t8e8r. +.ma2n1s2 +.man2s1l2 +.manslaugh3 +.ma8n9u9s8c8r8i8p8t. +.man2us +.manusc2 +.manuscri2 +.manuscr2ip +.manuscri2p1t +.ma8r9g8i8n9a8l. +.marg2 +.margi4n +.margi1na +.ma8t8h9e9m8a9t8i9c8i8a8n. +.m4ath3 +.math5em +.math2e +.1mathe1ma +.mathemat1ic +.mathemat2i1ci +.mathemati3c2i1a +.mathematici2a2n +.ma8t8h9e9m8a9t8i9c8i8a8n8s. +.mathematicia2n1s2 +.ma8t8t8e8s. +.mat5te +.ma4t3t2 +.mat4tes +.me8d9i8c9a8i8d. +.2med +.m4edi +.med3i1ca +.medicai2 +.medica2id +.me8d8i9o8c8r8e. +.me1d2io +.mediocre3 +.me8d8i9o8c9r8i9t8i8e8s. +.medi5ocrit +.mediocri2 +.medio5cri1ti +.mediocrit2ie4 +.me8g8a9l8i8t8h. +.me2g +.m4egal +.me1ga +.me3gal1i +.megal1it +.megal2ith +.me8g8a9l8i8t8h8s. +.megali2t4h1s2 +.me8t8a9b8o8l9i8c. +.me4ta +.me2ta4b +.metabol3ic +.metabol2i +.me9t8a8b9o9l8i8s8m. +.metaboli2s1m +.me9t8a8b9o9l8i8s8m8s. +.metabol4is4m1s2 +.me9t8a8b9o9l8i8t8e. +.metabo5l2ite +.metabol1it +.me9t8a8b9o9l8i8t8e8s. +.metabolit4es +.me8t8a9l8a8n9g8u8a8g8e. +.met3a2l +.meta5la +.metala2n +.metal2ang +.metalan1gu +.metalangu4a +.me8t8a9l8a8n9g8u8a8g8e8s. +.me8t8a9p8h8o8r9i8c. +.metapho4r +.me8t8h9a8n8e. +.metha2n4 +.me9t8r8o8p9o9l8i8s. +.m4etr +.metropol2i +.me9t8r8o8p9o9l8i8s8e8s. +.metropol4ise +.metropolis1e4s +.me8t9r8o9p8o8l9i9t8a8n. +.metropol1it +.metropoli3ta2n +.metropoli1ta +.me8t9r8o9p8o8l9i9t8a8n8s. +.metropolita2n1s2 +.mi8c8r8o9e8c8o9n8o8m8i8c8s. +.m4i1cr +.micro4e +.microe1co +.microeco2n +.microeco3nomic +.microeconomi4c3s2 +.mi9c8r8o9f8i8c8h8e. +.micro2fi +.microf4i2ch +.microfiche2 +.mi9c8r8o9f8i8c8h8e8s. +.microfich1es +.mi8c8r8o9o8r8g8a8n9i8s8m. +.microo2 +.microorg2 +.microor1ga +.microorgan5is +.microorga2n +.microorgani2s1m +.mi8c8r8o9o8r8g8a8n9i8s8m8s. +.microorgan4is4m1s2 +.mi8l8l9a8g8e. +.m4il1l +.mi8l9l8i9l8i8t8e8r. +.mill2i +.mil4l4i4l +.millil1i +.mill2il1it +.millil2ite +.mi8m8e8o9g8r8a8p8h8e8d. +.mimeo2g +.mimeo1gr +.mimeograph4ed +.mi8m8e8o9g8r8a8p8h8s. +.mimeogra4p4h1s2 +.mi8m9i8c9r8i8e8s. +.mim1i +.mim4i1cr +.mimicri2 +.mimicr2ie4 +.mi8n9i8s. +.m2ini +.min1is +.mi8n8i9s8y8m9p8o9s8i8u8m. +.minis4y +.minisy4m1p +.minisym1pos +.minisympo5si4u +.mi8n8i9s8y8m9p8o9s8i8a. +.minisympos2i1a +.mi9n8u8t9e8r. +.m4in1u +.mi9n8u8t9e8s8t. +.mi8s9c8h8i8e9v8o8u8s9l8y. +.m2is1c +.mis3ch2 +.misch2ie4 +.mischievou2 +.mischievo2us +.mischievous1l2 +.mischievous1ly +.mi9s8e8r8s. +.m4ise +.mis3er +.mise4r1s2 +.mi9s8o8g9a9m8y. +.mi2so +.miso1ga +.miso2gam +.mo8d9e8l9l8i8n8g. +.mo2d1 +.model1l +.modell2i +.model2lin4 +.mo8l9e9c8u8l8e. +.mole1cu +.mole4cul +.molecul4e +.mo8l9e9c8u8l8e8s. +.molecules2 +.mo8n9a8r8c8h8s. +.mo1n1a +.monar3c +.mon2a2r +.monar2ch +.monarc4h1s2 +.mo8n8e8y9l8e8n9d8e8r. +.moneylen1de +.mo8n8e8y9l8e8n9d8e8r8s. +.moneylende4r5s2 +.mo8n8o9c8h8r8o8m8e. +.mono2ch4 +.monoc4hr4 +.monochro2me +.mo8n8o9e8n9e8r9g8e8t8i8c. +.mo3noe +.monoen1er +.monoenerg2 +.monoener3get +.monoenerget1ic +.mo8n9o8i8d. +.monoi2 +.mono2id +.mo8n8o9p8o8l8e. +.mo4nop +.mo8n8o9p8o8l8e8s. +.monopoles2 +.mo9n8o8p9o8l8y. +.monopo2ly +.mo8n8o9s8p8l8i8n8e. +.monos1p2l2 +.monospli4n +.monosp1l4ine +.mo8n8o9s8p8l8i8n8e8s. +.monospli1nes +.mo8n8o9s8t8r8o8f8i8c. +.monos5t +.monost4r +.monostro2fi +.mo9n8o8t9o9n8i8e8s. +.mono1to +.mo2noto2n +.monoton2ie4 +.mo9n8o8t9o9n8o8u8s. +.mono4tono +.monoto1nou2 +.monotono2us +.mo9r8o8n9i8s8m. +.moro5n4is +.moro2n +.moroni2s1m +.mo8s9q8u8i9t8o. +.mos2 +.mosqu2 +.mosq2ui2 +.mosqui1to +.mo8s9q8u8i9t8o8s. +.mosquitos2 +.mo8s9q8u8i9t8o8e8s. +.mu8d9r8o8o8m. +.mu1dr +.mud1room +.mudroo2 +.mu8d9r8o8o8m8s. +.mudroo4m1s2 +.mu8l9t8i9f8a8c9e8t8e8d. +.5mu4lt +.mul1ti3 +.multif2 +.multi1fa +.multifa4ce +.multifacet4 +.multiface2t1ed +.mu8l9t8i9p8l8i8c9a8b8l8e. +.mult2ip +.multi1p2l2 +.multipli1ca +.multiplica1b2l2 +.mu8l8t8i9u8s8e8r. +.multi4u +.multi2us +.ne8o9f8i8e8l8d8s. +.3neo +.ne5of +.neo2fi +.neof2ie4 +.neofie2ld3 +.neofiel2d1s2 +.ne8o9n8a8z8i. +.neo2n +.neo1n1a +.neona2z1i +.ne8o9n8a8z8i8s. +.neonaz4is +.ne8p8h9e8w8s. +.nephe4 +.ne8p8h9r8i8t8e. +.nep4hr4 +.nephr2ite +.ne8p8h9r8i8t8i8c. +.nephr4i2t3ic +.nephri1ti +.ne8w9e8s8t. +.ne4w +.newest3 +.ne8w8s9l8e8t9t8e8r. +.news4l2 +.news2le +.newsle4t3t2 +.ne8w8s9l8e8t9t8e8r8s. +.newslette4r1s2 +.ni8t8r8o9m8e8t8h9a8n8e. +.n2it +.ni3tr +.nitro2me +.nitro4met +.nitrometha2n4 +.no9n8a8m8e. +.no4n +.no1n1a +.no8n9a8r9i8t8h9m8e8t9i8c. +.nonar3i +.non2a2r +.nonar2ith +.nonarit4h1m +.nonarithmet4 +.nonarithmet1ic +.no8n9e8m8e8r9g8e8n8c8y. +.none1me +.nonemerg2 +.nonemer1gen +.nonemergen1cy +.no8n9e8q8u8i9v8a8r8i9a8n8c8e. +.none2q +.nonequ2 +.noneq2ui2 +.nonequ2iv3 +.nonequi1va +.nonequiv2a2r +.nonequivar1i +.nonequivar3i2a2n +.nonequivar2i3a +.nonequivar4ianc +.no8n8e9t8h8e9l8e8s8s. +.noneth2e +.nonethe1les2 +.nonethe3l2e2ss +.no8n9e8u8c8l8i8d9e8a8n. +.non4eu +.noneu1c4l4 +.noneucl2id +.noneuclidea2n +.no8n9i8s8o9m8o8r9p8h8i8c. +.non5i +.non1is +.noni2so +.noni3som +.noniso1mo +.nonisomo2r +.nonisomor1p +.nonisomorp4h4 +.nonisomorph1ic +.no8n9p8s8e8u8d8o9c8o8m9p8a8c8t. +.non1p4 +.non2p1s2 +.nonp2se +.nonps4eu +.nonpseu1do +.nonpseudo1co +.nonpseudoco4m1p +.nonpseudocom1pa +.nonpseudocompa2c4t +.no8n9s8m8o8o8t8h. +.no2n3s2 +.non2s3m +.nons1mo +.nonsmoo2 +.nonsmo4oth +.no8n9u8n8i9f8o8r8m. +.no3nu4n +.nonu1ni +.nonuni1fo +.nonunifo2r +.nonunifor1m +.no8n9u8n8i9f8o8r8m9l8y. +.nonunifor4m1l +.nonuniform1ly +.no8r9e8p9i9n8e8p8h9r8i8n8e. +.nore5pi2n +.norep2ine +.norepinep4hr4 +.norep2inephr2in4e +.no8t9w8i8t8h9s8t8a8n8d9i8n8g. +.notw4 +.notwi2 +.notw2ith3 +.notwi2t4h1s2 +.notwith5st4and +.notwiths1ta +.notwithsta2n +.notwithstand1in +.nu9c8l8e8o9t8i8d8e. +.nucle3 +.nu1c4l4 +.nucle4ot +.nucleot2id +.nu9c8l8e8o9t8i8d8e8s. +.nucleotide4s2 +.nu8t9c8r8a8c8k9e8r. +.nu4tc +.nutcrack1 +.nutcrack1er +.nu8t9c8r8a8c8k9e8r8s. +.nutcracke4r1s2 +.oe8r9s8t8e8d8s. +.o3er +.oe4r1s2 +.oers4t1ed +.oerste2d1s2 +.of8f9l8i8n8e. +.o4f1f +.off4l2 +.offlin4 +.off1l4ine +.of8f9l8o8a8d. +.offl4oa +.offloa2d3 +.of8f9l8o8a8d8s. +.offloa2d1s2 +.of8f9l8o8a8d8e8d. +.offloa2d1ed +.ol8i9g8o8p9o9l8i8s8t. +.ol2i +.ol2ig +.oli2go +.ol2igopol2i +.ol8i9g8o8p9o9l8i8s8t8s. +.oligopolis4t1s2 +.ol8i9g8o8p9o8l8y. +.oligopo2ly +.ol8i9g8o8p9o8l9i8e8s. +.oligopol2ie4 +.op9e8r9a8n8d. +.op1er +.3oper1a +.op4er4and +.opera2n +.op9e8r9a8n8d8s. +.operan2d1s2 +.or8a8n8g9u8t8a8n. +.ora2n +.or2ang +.oran1gu +.oran4gu4t +.orangu1ta +.ora2nguta2n +.or8a8n8g9u8t8a8n8s. +.oranguta2n1s2 +.or9t8h8o9d8o8n9t8i8s8t. +.ortho2do4 +.orthodo2n +.orthodon3t4i +.orthodon1t +.or9t8h8o9d8o8n9t8i8s8t8s. +.orthodontis4t1s2 +.or9t8h8o9k8e8r9a9t8o8l9o8g8y. +.orth2ok +.orthok1er +.orthoker1a +.orthokera1to +.orthokeratol1o1gy +.or8t8h8o9n8i8t8r8o9t8o8l8u8e8n8e. +.ortho2n +.orthon2it +.orthoni3tr +.orthonitro1to +.orthonitrotolu3en +.orthonitrotolu4ene +.ov8e8r9v8i8e8w. +.overv2ie4 +.ov8e8r9v8i8e8w8s. +.ox9i8d9i8c. +.ox3i +.oxi5di +.ox2id +.pa8d9d8i8n8g. +.1pa +.p4a2d +.pad4d1in +.pad1d4 +.pa8i8n9l8e8s8s9l8y. +.p4ai2 +.pa4i4n4 +.pa4i4n1l +.painles2 +.pain3l2e2ss +.painles4s1l2 +.painless1ly +.pa8l9e8t8t8e. +.p4al +.p2ale +.pale4t3t2 +.pa8l9e8t8t8e8s. +.palet4tes +.pa8r9a9b8o8l8a. +.p2a2r +.pa2rab +.parabo1la +.pa8r9a9b8o8l9i8c. +.parabol3ic +.parabol2i +.pa9r8a8b9o9l8o8i8d. +.paraboloi2 +.parabolo2id +.pa8r9a9d8i8g8m. +.para2d +.par2adi +.parad2ig +.paradig1m +.pa8r9a9d8i8g8m8s. +.paradig4m1s2 +.pa8r8a9c8h8u8t8e. +.para2ch +.parachu4t +.pa8r8a9c8h8u8t8e8s. +.pa8r8a9d8i9m8e8t8h8y8l9b8e8n8z8e8n8e. +.parad4imet +.paradimethy2l1b +.paradimethylb4e4n3z +.paradimethylben2ze +.paradimethylbenze4n +.pa8r8a9f8l8u8o8r8o9t8o8l8u8e8n8e. +.para2f +.paraf4l2 +.paraflu3o +.parafluo3r +.parafluoro1to +.parafluorotolu3en +.parafluorotolu4ene +.pa8r8a9g8r8a8p8h9e8r. +.para1gr +.parag5ra3ph4er +.pa8r8a9l8e9g8a8l. +.par3al +.par2ale +.paral4egal +.parale1ga +.pa8r9a8l9l8e8l9i8s8m. +.paral1l +.paral2le +.paral3lel +.parallel2i +.paralle2lis +.paralleli2s1m +.pa8r8a9m8a8g9n8e8t9i8s8m. +.par4a1ma +.param3ag +.para5mag1n +.paramagneti2s4m +.pa8r8a9m8e8d8i8c. +.para2med +.param4edi +.pa8r8a9m8e8t8h8y8l9a8n8i8s8o8l8e. +.param3et +.paramethy3la +.paramethyla2n +.paramethylani2so +.pa9r8a8m9e9t8r8i8z8e. +.param4etr +.parametri2ze +.pa8r8a9m8i8l9i9t8a8r8y. +.par2ami +.paramil1i +.param2il1it +.paramili1ta +.paramilit2a2r +.pa8r8a9m8o8u8n8t. +.para2mo +.paramou2 +.paramoun1t +.pa8t8h9o9g8e8n9i8c. +.p4ath +.pat4ho +.patho4g +.patho1ge4 +.patho1gen +.pe8e8v9i8s8h. +.p4ee +.pee1vi +.peevis2h +.pe8e8v9i8s8h9n8e8s8s. +.peevis2h1n +.peevish1nes +.peevishn2e2ss +.pe8n9t8a9g8o8n. +.pen1t +.pen1ta +.penta2go +.pentago2n2 +.pe8n9t8a9g8o8n8s. +.pentago2n3s2 +.pe9t8r8o9l8e9u8m. +.petrol4eu +.ph8e9n8o8m9e9n8o8n. +.ph4e3no +.phe2n +.pheno2me +.pheno1men +.phenom4eno +.phenomeno4n +.ph8e8n8y8l9a8l8a9n8i8n8e. +.pheny3la +.phenylala2n +.phenylala5n2ine +.phenylalan1in +.ph8i9l8a8t9e9l8i8s8t. +.phi4latel2i4 +.philate2lis +.ph8i9l8a8t9e9l8i8s8t8s. +.philatelis4t1s2 +.ph8o9n8e8m8e. +.3phone +.pho2n +.phone1me +.ph8o9n8e8m8e8s. +.phone2mes +.ph8o9n8e9m8i8c. +.phone5mi +.ph8o8s9p8h8o8r9i8c. +.phos1p +.phospho5 +.phospho4r +.ph8o9t8o9g8r8a8p8h8s. +.pho1to +.photo2gr +.photogra4p4h1s2 +.ph8o9t8o9o8f8f9s8e8t. +.photoo2 +.photoo4f1f +.photoof2f3s +.pi8c9a9d8o8r. +.pi1ca +.pica2d +.pica1do +.picad4or +.pi8c9a9d8o8r8s. +.picado4rs2 +.pi8p8e9l8i8n8e. +.p2ip +.pipe4 +.pipel2i +.pipe1l4ine +.pi8p8e9l8i8n8e8s. +.pipeli1nes +.pi8p8e9l8i8n9i8n8g. +.pipel2in3i +.pipelin1in +.pipelinin4g +.pi9r8a9n8h8a8s. +.p4ir +.pi1ra +.pira2n +.pira4n1h4 +.piranha4 +.pl8a8c8a9b8l8e. +.1p2l2 +.pla1ca +.placa1b2l2 +.pl8a8n8t9h8o8p9p8e8r. +.3pla2n +.plan1t +.plantho4p1p +.planthop2pe +.planthop3per +.pl8a8n8t9h8o8p9p8e8r8s. +.planthoppe4r1s2 +.pl8e8a8s9a8n8c8e. +.ple2a +.pleasa2 +.plea3sanc +.pleasa2n +.pl8u8g9i8n. +.plug5in +.pl8u8g9i8n8s. +.plu5g4i2n1s2 +.po8l9t8e8r9g8e8i8s8t. +.po4l2t +.pol1te +.polterg2 +.poltergei2 +.po8l8y9e8n8e. +.po2ly +.po8l8y9e8t8h9y8l9e8n8e. +.polye4t +.po9l8y8g9a9m8i8s8t. +.poly1ga +.poly2gam +.polygam2is +.po9l8y8g9a9m8i8s8t8s. +.polygamis4t1s2 +.po8l8y8g9o8n9i9z8a9t8i8o8n. +.poly1go +.polygo2n2 +.polygo3ni +.polygoniza1 +.polygoniza1t2io +.polygonizatio2n +.po9l8y8p8h9o9n8o8u8s. +.polypho2n +.polypho1nou2 +.polyphono2us +.po8l8y9s8t8y8r8e8n8e. +.po2lys4 +.polys1t +.polys2ty +.po8m8e9g8r8a8n9a8t8e. +.po2me +.pome2g +.pome1gr +.pomegra2n2 +.pomegra1na +.pomegran2at +.po8r8o9e8l8a8s9t8i8c. +.1p4or +.poro4e +.poro4el +.poroe1la +.poroelast2i +.poroelas1tic +.po8r9o8u8s. +.porou2 +.poro2us +.po8r9t8a9b8l8e. +.por1ta +.por2tab +.portab2l2 +.po8s8t9a8m9b8l8e. +.1pos +.pos2ta +.posta4m1b +.postamb2l2 +.po8s8t9a8m9b8l8e8s. +.postambles2 +.po8s8t9h8u9m8o8u8s. +.posthu1mo +.posthu3mo2us +.posthumou2 +.po8s8t9s8c8r8i8p8t. +.pos4t1s2 +.post4sc +.postscri2 +.postscr2ip +.postscri2p1t +.po8s8t9s8c8r8i8p8t8s. +.postscrip4t1s2 +.po8s9t8u8r9a8l. +.pos1tu +.postu1ra +.pr8e9a8m9b8l8e. +.prea4m1b +.preamb2l2 +.pr8e9a8m9b8l8e8s. +.preambles2 +.pr8e9l8o8a8d8e8d. +.prel4oa +.preloa2d3 +.preloa2d1ed +.pr8e9p8a8r9i8n8g. +.pre2pa +.prep4a4r1i +.prep2a2r +.preparin4g +.pr8e9p8r8i8n8t. +.pr2epr2 +.preprin4t3 +.pr8e9p8r8i8n8t8s. +.preprin4t4s2 +.pr8e9p8r8o8c8e8s9s8o8r. +.pre3pro +.prepr2oc +.prepro1ce +.preproc2e2ss +.preproces1so +.pr8e9p8r8o8c8e8s9s8o8r8s. +.preprocesso4rs2 +.pr8e9s8p8l8i8t9t8i8n8g. +.pre1sp +.pres1p2l2 +.prespl1it +.prespl4i4t3t2 +.presplit2t1in +.pr8e9w8r8a8p. +.prewra4 +.pr8e9w8r8a8p8p8e8d. +.prewra4p1p +.prewrap2pe +.prewrap4p2ed +.pr8i8e8s8t9e8s8s8e8s. +.5pr2i4e4 +.pri1est +.pries4t2e2ss +.priestess1e4s +.pr8e8t9t8y9p8r8i8n9t8e8r. +.pre4t3t2 +.pret1ty +.pr2ettypr2 +.prettyprin4t3 +.pr8e8t9t8y9p8r8i8n9t8i8n8g. +.prettyprint2i +.prettyprin4t3ing +.prettyprin2t1in +.pr8o9c8e9d8u8r9a8l. +.pr2oc +.pro1ce +.proce1du +.procedu1ra +.pr8o8c8e8s8s. +.proc2e2ss +.pr8o9c8u8r9a8n8c8e. +.procu1ra +.procura2n +.pr8o8g9e9n8i8e8s. +.pro1ge +.pro1gen +.proge5n2ie4 +.pr8o8g9e9n8y. +.pro4geny +.pr8o9g8r8a8m9m8a8b8l8e. +.pro1gr +.program1m +.program1ma +.program2mab +.programmab2l2 +.pr8o8m9i9n8e8n8t. +.prom4i +.prom1in +.prom2ine +.promi1nen +.prominen1t +.pr8o9m8i8s9c8u9o8u8s. +.prom2is +.prom2is1c +.promis1cu +.promiscu1ou2 +.promiscuo2us +.pr8o8m9i8s9s8o8r8y. +.prom4i2s1s +.promis1so +.promisso1ry +.pr8o8m9i8s8e. +.prom4ise +.pr8o8m9i8s8e8s. +.promis1e4s +.pr8o9p8e8l9l8e8r. +.pro3pel +.propel1l +.propel2le +.pr8o9p8e8l9l8e8r8s. +.propelle4r1s2 +.pr8o9p8e8l9l8i8n8g. +.propell2i +.propel2lin4 +.pr8o9h8i8b9i9t8i8v8e. +.pro1h2 +.prohibi2t +.prohibi1ti +.prohibi1t2iv +.pr8o9h8i8b9i9t8i8v8e9l8y. +.prohibitiv4e1ly +.pr8o9s8c8i8u8t9t8o. +.pros2c +.pros1ci +.prosci1u +.prosciu4t3t2 +.prosciut5to +.pr8o9t8e8s8t9e8r. +.pro1t +.pro4tes +.pr8o9t8e8s8t9e8r8s. +.proteste4r1s2 +.pr8o9t8e8s9t8o8r. +.prot4es2to +.pr8o9t8e8s9t8o8r8s. +.protesto4rs2 +.pr8o9t8o9l8a8n9g8u8a8g8e. +.pro1to +.proto1la +.proto4la2n +.protol2ang +.protolan1gu +.protolangu4a +.pr8o9t8o9t8y8p9a8l. +.proto1ty +.prototy1pa +.prototyp4al +.pr8o8v9i8n8c8e. +.prov1in +.prov2inc +.pr8o8v9i8n8c8e8s. +.pr8o9v8i8n9c8i8a8l. +.provin1ci +.provin3c2i1a +.provinci2al +.pr8o8w9e8s8s. +.prow2e2ss +.ps8e8u9d8o9d8i8f9f8e8r9e8n9t8i8a8l. +.2p1s2 +.p2se +.ps4eu +.pseu1do +.pseudod1if +.pseudodi4f1f +.pseudodiffer1 +.pseudodiffer3en1t +.pseudodifferent2i +.pseudodifferen1t2i1a +.pseudodifferenti2al +.ps8e8u9d8o9f8i9n8i8t8e. +.pseu2d5of +.pseudo2fi +.pseudo2fin +.pseudof2ini +.pseudofin2it +.pseudofin2ite +.ps8e8u9d8o9f8i9n8i8t8e9l8y. +.pseudofinite1ly +.ps8e8u9d8o9f8o8r8c8e8s. +.pseudo1fo +.pseudofo2r +.pseudofor1c +.pseudofor2ce +.ps8e8u9d8o8g9r8a9p8h8e8r. +.pseud4og +.pseudo1gr +.pseudog5ra3ph4er +.ps8e8u9d8o9g8r8o8u8p. +.pseudo4g4ro +.pseudogrou2 +.ps8e8u9d8o9g8r8o8u8p8s. +.pseudogrou2p1s2 +.ps8e8u9d8o9n8y8m. +.pseu4do2n +.pseudonym4 +.ps8e8u9d8o9n8y8m8s. +.pseudony4m1s2 +.ps8e8u9d8o9w8o8r8d. +.pseudo4wo2 +.ps8e8u9d8o9w8o8r8d8s. +.pseudowor2d1s2 +.ps8y9c8h8e9d8e8l9i8c. +.ps4y +.p4sy1c +.psy3ch +.psych4e2 +.psy4ch4ed +.psychedel2i +.ps8y8c8h8s. +.psyc4h1s2 +.pu9b8e8s9c8e8n8c8e. +.pub3 +.pub4e +.pu4bes4 +.pubes2c +.pubes1cen +.pubes3cenc +.qu8a8d9d8i8n8g. +.qu2 +.qua2d +.quad4d1in +.quad1d4 +.qu8a9d8r8a8t9i8c. +.qua1dr +.quadrat1ic +.qu8a9d8r8a8t9i8c8s. +.quadrati4c3s2 +.qu8a8d9r8a9t8u8r8e. +.quadra2tu +.quadra3ture +.qu8a8d9r8i9p8l8e8g9i8c. +.quadri2p2l2 +.quadr2ip +.quadripleg4ic +.qu8a8i8n8t9e8r. +.quai2 +.qua4i4n +.quain1t +.qu8a8i8n8t9e8s8t. +.qu8a9s8i9e8q8u8i8v9a9l8e8n8c8e. +.quas2ie4 +.quasie1q +.qu2asiequ2 +.quasieq2ui2 +.quasiequ2iv3 +.quasiequi1va +.quasiequiv2ale +.quasiequiva3lenc +.qu8a9s8i9e8q8u8i8v9a9l8e8n8c8e8s. +.qu8a9s8i9e8q8u8i8v9a9l8e8n8t. +.quasiequiva1len1t +.qu8a9s8i9h8y9p8o9n8o8r9m8a8l. +.quasi3h +.quasihy3po +.quasihypo2n +.quasihyponor1m +.quasihyponor1ma +.qu8a9s8i9r8a8d9i9c8a8l. +.quas4i2r +.quasi1r5a +.quasira2d +.quasir2adi +.quasirad3i1ca +.qu8a9s8i9r8e8s8i8d9u8a8l. +.quasi4res +.quasire1si +.quasire2s2id +.quasiresi2du +.quasiresid1u1a +.qu8a9s8i9s8m8o8o8t8h. +.qua1sis +.quasi2s1m +.quasis1mo +.quasismoo2 +.quasismo4oth +.qu8a9s8i9s8t8a9t8i8o8n9a8r8y. +.quasis1ta +.quasistation5a2r +.quasista1t2io +.quasistatio2n +.quasistatio1n1a +.qu8a9s8i9t8o8p8o8s. +.qu5a5si4t +.quasi1to +.quasito1pos +.qu8a9s8i9t8r8i9a8n9g8u9l8a8r. +.quasi5tr2i3a +.quasitri2a2n +.quasitri2ang +.quasitrian1gu +.quasitriangu1la +.quasitriangul2a2r +.qu8a9s8i9t8r8i8v9i8a8l. +.quasitr2i4v +.quasitriv3i +.quasitriv2i1a +.quasitrivi2al +.qu8i8n9t8e8s9s8e8n8c8e. +.q2ui2 +.qui4n +.quin1t +.quin4t2e2ss +.quintes4senc +.qu8i8n9t8e8s9s8e8n8c8e8s. +.qu8i8n9t8e8s9s8e8n9t8i8a8l. +.quintessen1t +.quintessent2i +.quintessen1t2i1a +.quintessenti2al +.ra8b9b8i8t9r8y. +.2rab +.ra2b1b +.rabbi2t +.rabbi3tr +.rabbit5ry +.ra9d8i9o8g9r8a9p8h8y. +.ra2d +.r2adi +.ra3d2io +.radio5g +.radio2gr +.radio4g3ra1phy +.ra8f8f9i8s8h. +.raf5fi +.ra2f +.ra4f1f4 +.raf2f5is +.raffis2h +.ra8f8f9i8s8h9l8y. +.raffis4h1l4 +.raffish1ly +.ra8m9s8h8a8c8k8l8e. +.ra4m1s2 +.ram4s2h +.ramshack1 +.ramshack1l +.ra8v9e8n9o8u8s. +.rav4e4no +.rave1nou2 +.raveno2us +.re9a8r8r8a8n8g8e9m8e8n8t. +.re5ar1r4 +.re2a2r +.rearran4ge +.rearra2n +.rearr2ang +.rearrange1me +.rearrange1men +.rearrange3men1t +.re9a8r8r8a8n8g8e9m8e8n8t8s. +.rearrangemen4t4s2 +.re8c9i9p8r8o8c9i9t8i8e8s. +.reciproci1ti +.reciprocit2ie4 +.re8c9t8a8n9g8l8e. +.rec4ta2n +.re2c1t +.rect5ang +.rec1ta +.rectan1gl2 +.rectan1gle +.re8c9t8a8n9g8l8e8s. +.rectangles2 +.re8c9t8a8n9g8u9l8a8r. +.rectan1gu +.rectangu1la +.rectangul2a2r +.re9d8i9r8e8c8t. +.2r2ed +.r4edi +.red4ir2 +.redi1re +.redire2c1t +.re9d8i9r8e8c8t9i8o8n. +.redirec1t2io +.redirectio2n +.re9d8u8c9i8b8l8e. +.re1du +.redu2c +.reduci4b +.redu1ci +.reduci1b2l2 +.re9e8c8h8o. +.ree2c +.ree2ch +.ree3cho2 +.re9p8h8r8a8s8e. +.rep4hr4 +.rephr2as +.re9p8h8r8a8s8e8s. +.rephras1e4s +.re9p8h8r8a8s8e8d. +.rephra4s4ed +.re9p8o9s8i9t8i8o8n. +.re4posi +.re1po +.re1pos +.repo3s2i1t2io +.reposi1ti +.repositio2n +.re9p8o9s8i9t8i8o8n8s. +.repositio2n3s2 +.re9p8r8i8n8t. +.repr2 +.reprin4t3 +.re9p8r8i8n8t8s. +.reprin4t4s2 +.re9s8t8o8r9a8b8l8e. +.r4es2to +.resto2ra +.resto2rab +.restorab2l2 +.re8t8r8o9f8i8t. +.retro2fi +.re8t8r8o9f8i8t9t8e8d. +.retrof4i4t4t2 +.retrofit2t1ed +.re9u8s9a8b8l8e. +.r4eu2 +.re2us4 +.reusa2 +.reu2s1ab +.reusab2l2 +.re9u8s8e. +.re9w8i8r8e. +.rewi2 +.rew4ir4 +.re9w8r8a8p. +.rewra4 +.re9w8r8a8p8p8e8d. +.rewra4p1p +.rewrap2pe +.rewrap4p2ed +.re9w8r8i8t8e. +.rewri4 +.rewr2ite +.rh8i9n8o8c9e8r9o8s. +.rh4 +.rh2i1no +.rhi4no4c +.rhino1ce +.rhinoc2ero +.ri8g8h8t9e8o8u8s. +.righ1teo +.righteou2 +.righteo2us +.ri8g8h8t9e8o8u8s9n8e8s8s. +.righteous1n4 +.righteous1nes +.righteousn2e2ss +.ri8n8g9l8e8a8d8e8r. +.rin4g +.ringl2 +.rin1gle +.ringle2a +.ringlea2d1 +.ri8n8g9l8e8a8d8e8r8s. +.ringleade4r5s2 +.ro9b8o8t. +.ro9b8o8t8s. +.robo4t1s2 +.ro9b8o8t8i8c. +.ro9b8o8t9i8c8s. +.roboti4c3s2 +.ro8u8n8d9t8a8b8l8e. +.rou2 +.roun2d +.round1ta +.round2tab +.roundtab2l2 +.ro8u8n8d9t8a8b8l8e8s. +.roundta5bles2 +.sa8l8e8s9c8l8e8r8k. +.sa2 +.s2ale +.sales2 +.sales2c +.salescle5 +.sales1c4l4 +.sa8l8e8s9c8l8e8r8k8s. +.salescler4k1s2 +.sa8l8e8s9w8o8m8a8n. +.sales4w2 +.sale4s1wo2 +.saleswom1 +.saleswo1ma +.saleswoma2n +.sa8l8e8s9w8o8m8e8n. +.saleswo2me +.saleswo1men +.sa8l9m8o9n8e8l9l8a. +.s4a2l4m +.salmo2n4 +.sal1mo +.salmon4ella +.salmonel1l +.sa8l9t8a9t8i8o8n. +.sa4l4t +.sal1ta +.salta1t2io +.saltatio2n +.sa8r9s8a9p8a8r9i8l9l8a. +.s2a2r +.sa2r4sa2 +.sa4rs2 +.sars1ap +.s2a2rsap2a2r4 +.sarsa1pa +.sarsap4a4r1i +.sarsaparil1l +.sa8u8e8r9k8r8a8u8t. +.sau4 +.sauerkrau4t +.sc8a8t9o9l8o8g9i9c8a8l. +.s1ca +.sca1to +.scato3log1ic +.scatologi1ca +.sc8h8e8d9u8l9i8n8g. +.s2ch2 +.sche2 +.s4ch4ed +.sche4dul +.sche1du +.schedul2i +.schedul3ing +.sc8h8i8z9o9p8h8r8e8n8i8c. +.schi2z +.schi1zo +.schiz2oph +.schizop4hr4 +.sc8h8n8a8u9z8e8r. +.sc2h1n +.sch1na +.schn2au +.schnau2z4e +.schnauz1er +.sc8h8o8o8l9c8h8i8l8d. +.s4cho2 +.schoo2 +.schoo4l1c2 +.s2chool2ch +.schoolch4il2 +.schoolchi2ld +.sc8h8o8o8l9c8h8i8l8d9r8e8n. +.schoolchil3dr +.schoolchildre4 +.schoolchil5dren +.sc8h8o8o8l9t8e8a8c8h8e8r. +.schoo4l2t +.school1te +.s2chooltea2ch +.schoolteache2 +.sc8h8o8o8l9t8e8a8c8h9e8r8s. +.schoolteach3e4r1s2 +.sc8r8u9t8i9n8y. +.scru2t1i5n +.scr4u1t2i +.scrut4iny +.sc8y8t8h9i8n8g. +.s1cy +.scy3thin +.se8l8l9e8r. +.sel2le +.se8l8l9e8r8s. +.selle4r1s2 +.se8c9r8e9t8a8r9i8a8t. +.se1cr +.se4c3re1ta +.secret2a2r +.secretar1i +.secretar2i3a +.se8c9r8e9t8a8r9i8a8t8s. +.secretaria4t1s2 +.se8m9a9p8h8o8r8e. +.se1ma +.se4map +.semapho4r +.se8m9a9p8h8o8r8e8s. +.se9m8e8s9t8e8r. +.4se1me +.se2mes +.se8m8i9d8e8f9i9n8i8t8e. +.sem2id +.semide1f +.semidef5i5n2ite +.semide1fi +.semide2fin +.semidef2ini +.semidefin2it +.se8m8i9d8i9r8e8c8t. +.semi2di +.semid4ir2 +.semidi1re +.semidire2c1t +.se8m8i9h8o9m8o9t8h8e8t9i8c. +.semi3h +.semiho1mo +.semihom4oth3 +.semihomoth2e +.semihomo3the4t +.semihomothet1ic +.se8m8i9r8i8n8g. +.sem4ir +.semir1i +.semirin4g +.se8m8i9r8i8n8g8s. +.semirings2 +.se8m8i9s8i8m9p8l8e. +.se4m2is +.semisi4m1p +.semisim1p2l2 +.se8m8i9s8k8i8l8l8e8d. +.sem4is4k2 +.semisk1i +.semisk4il1l +.semiskil2le +.se8r8o9e8p8i9d8e9m8i9o9l8o8g9i9c8a8l. +.s2er4o +.sero4e +.seroep4id +.seroepi3de +.seroepid4em +.seroepidem2io +.seroepidemi1ol +.seroepidemio3log1ic +.seroepidemiologi1ca +.se8r9v8o9m8e8c8h9a8n8i8s8m. +.4ser3vo +.servo2me +.servome2ch +.servomech5a5nis +.servomecha2n +.servomechani2s1m +.se8r9v8o9m8e8c8h9a8n8i8s8m8s. +.servomechan4is4m1s2 +.se8s9q8u8i9p8e9d8a9l8i8a8n. +.s1e4s +.sesqu2 +.sesq2ui2 +.sesqu2ip +.sesquipe4 +.sesqui2p2ed +.sesquip2e2d2a +.sesquipedal1i +.sesquipedal2i1a +.sesquipedali2a2n +.se8t9u8p. +.se1tu +.se8t9u8p8s. +.setu2p1s2 +.se9v8e8r8e9l8y. +.5sev +.sev1er +.sev4erel +.severe1ly +.sh8a8p8e9a8b8l8e. +.sha3pe4a +.shape1a4b +.shapeab2l2 +.sh8o8e9s8t8r8i8n8g. +.sho4 +.sho2est4r +.shoestrin4g +.sh8o8e9s8t8r8i8n8g8s. +.shoestrings2 +.si8d8e9s8t8e8p. +.5side4s2 +.s2id +.sideste4p +.si8d8e9s8t8e8p8s. +.sideste2p1s2 +.si8d8e9s8w8i8p8e. +.sides4w2 +.sideswi2 +.sidesw2ip +.sideswipe4 +.sk8y9s8c8r8a8p8e8r. +.sk2 +.skys4c +.skyscrap3er +.sk8y9s8c8r8a8p8e8r8s. +.skyscrape4r1s2 +.sm8o8k8e9s8t8a8c8k. +.2s1m +.s1mo +.s4m2ok +.smokes4 +.smokes1ta +.smokestack1 +.sm8o8k8e9s8t8a8c8k8s. +.smokestac4k1s2 +.sn8o8r9k8e8l9i8n8g. +.s1n4 +.snorke5l2i +.snorke4l3ing +.so9l8e9n8o8i8d. +.1so +.sol4eno +.solenoi2 +.soleno2id +.so9l8e9n8o8i8d8s. +.solenoi2d1s2 +.so8l8u8t8e. +.so1lut +.so8l8u8t8e8s. +.so8v9e8r9e8i8g8n. +.4sov +.soverei2 +.sovere2ig2 +.so8v9e8r9e8i8g8n8s. +.sovereig2n1s2 +.sp8a9c8e8s. +.2s1pa +.spa4ce +.sp8e9c8i8o8u8s. +.spe2c +.spe1c2i +.spec2io +.speciou2 +.specio2us +.sp8e8l8l9e8r. +.spel1l +.spel2le +.sp8e8l8l9e8r8s. +.spelle4r1s2 +.sp8e8l8l9i8n8g. +.spell2i +.spel2lin4 +.sp8e9l8u8n8k9e8r. +.spelu4nk2 +.spelunk1er +.sp8e8n8d9t8h8r8i8f8t. +.spen4d +.spend2th +.spendt4hr4 +.spendthr4i2ft +.sp8h8e8r9o8i8d. +.s2phe +.3sph4er +.sph2ero +.spheroi2 +.sphero2id +.sp8h8e8r9o8i8d9a8l. +.spheroi1d2a +.sp8h8i8n9g8e8s. +.sph5ing +.sph4inge +.sp8i8c9i9l8y. +.sp2i1ci +.spici1ly +.sp8i8n9o8r8s. +.spi2n +.sp4i1no +.spino4rs2 +.sp8o8k8e8s9w8o8m8a8n. +.sp2ok +.spokes4 +.spokes4w2 +.spoke4s1wo2 +.spokeswom1 +.spokeswo1ma +.spokeswoma2n +.sp8o8k8e8s9w8o8m8e8n. +.spokeswo2me +.spokeswo1men +.sp8o8r8t8s9c8a8s8t. +.s1p4or4 +.spor4t1s2 +.sport4sc +.sports1ca +.sp8o8r8t8s9c8a8s8t9e8r. +.sportscast5er +.sp8o8r9t8i8v8e9l8y. +.spor1ti +.spor4t2iv +.sportiv4e1ly +.sp8o8r8t8s9w8e8a8r. +.sport4sw2 +.sportswe2a2r +.sp8o8r8t8s9w8r8i8t8e8r. +.sportswri4 +.sportswr2ite +.sp8o8r8t8s9w8r8i8t8e8r8s. +.sportswrit5e4r1s2 +.sp8r8i8g8h8t9l8i8e8r. +.spr2 +.spr2ig +.sprigh2tl +.sprightl2ie4 +.sq8u8e8a9m8i8s8h. +.squ2 +.squeam2is +.squeamis2h +.st8a8n8d9a8l8o8n8e. +.5st4and +.sta2n +.stan1d2a +.standalo2n +.st8a8r9t8l8i8n8g. +.st2a2r +.star2tl +.st8a8r9t8l8i8n8g9l8y. +.startlingl2 +.startling1ly +.st8a9t8i8s9t8i8c8s. +.statis1t2i +.statis1tic +.statisti4c3s2 +.st8e8a8l8t8h9i8l8y. +.stea4l +.stea4lt +.stealth3i +.steal4th4il2 +.stealthi1ly +.st8e8e8p8l8e9c8h8a8s8e. +.s1tee +.stee4p1 +.stee1p2l2 +.steeple2ch +.st8e8r8e8o9g8r8a8p8h9i8c. +.stere1o +.stereo2g +.stereo1gr +.stereo5graph1ic +.stereogr4aphi +.st8o9c8h8a8s9t8i8c. +.s1to +.sto2ch4 +.stochast2i +.stochas1tic +.st8r8a8n8g8e9n8e8s8s. +.st4r +.s1tra +.stran4ge +.stra2n +.str2ang +.strange4n4e +.stran1gen +.strange1nes +.strangen2e2ss +.st8r8a8p9h8a8n8g8e8r. +.straph2an4g +.straphang5er +.strapha2n +.st8r8a8t9a9g8e8m. +.stra2ta +.st8r8a8t9a9g8e8m8s. +.stratage4m1s2 +.st8r8e8t8c8h9i9e8r. +.stre4tc +.stret4ch +.stretch2ie4 +.st8r8i8p9t8e8a8s8e. +.str2ip +.stri2p1t +.strip2te +.st8r8o8n8g9h8o8l8d. +.stro2n +.strongho2l2d +.st8r8o8n8g9e8s8t. +.st8u9p8i8d9e8r. +.s1tu +.stup4id +.stupi3de +.st8u9p8i8d9e8s8t. +.stupide4s2 +.su8b9d8i8f9f8e8r9e8n9t8i8a8l. +.1su +.su4b3 +.su4b1d +.subd1if +.subdi4f1f +.subdiffer1 +.subdiffer3en1t +.subdifferent2i +.subdifferen1t2i1a +.subdifferenti2al +.su8b9e8x9p8r8e8s9s8i8o8n. +.sub4e +.sub1ex3p +.subexpr2 +.subex3pr2e2ss +.subexpres1si +.subexpres1s2io +.subexpres5sio2n +.su8b9e8x9p8r8e8s9s8i8o8n8s. +.subexpressio2n3s2 +.su8m9m8a9b8l8e. +.su2m +.sum1m +.sum1ma +.sum2mab +.summab2l2 +.su8p8e8r9e8g8o. +.su1pe +.supere1go +.su8p8e8r9e8g8o8s. +.supere4gos +.su9p8r8e8m9a9c8i8s8t. +.supr2 +.supre4mac +.supre1ma +.suprem4a2ci +.su9p8r8e8m9a9c8i8s8t8s. +.supremacis4t1s2 +.su8r9v8e8i8l9l8a8n8c8e. +.su2r +.surv4e +.survei2 +.surveil1l +.surveilla2n +.sw8i8m9m8i8n8g9l8y. +.sw2 +.swi2 +.swim1m +.swimm4ingl2 +.swimm5ing1ly +.sy8m8p9t8o9m8a8t8i8c. +.sy4m1p +.sym2p1t +.symp1to +.sympto2ma +.symptomat1ic +.sy8n9c8h8r8o9m8e8s8h. +.syn3c4hr4 +.syn2ch +.synchro2me +.synchro2mes +.synchrom4es2h +.sy8n9c8h8r8o9n8o8u8s. +.synchro2n +.synchro1nou2 +.synchrono2us +.sy8n9c8h8r8o9t8r8o8n. +.synchrotro2n +.ta8f8f9r8a8i8l. +.4ta2f4 +.ta4f1f4 +.taffr2ai2 +.ta8l8k9a9t8i8v8e. +.ta2l +.4talk +.talka3 +.talka4t +.talka1t2iv +.ta9p8e8s9t8r8y. +.tap2est4r +.tape4stry +.ta9p8e8s9t8r8i8e8s. +.tapestr2ie4 +.ta8r9p8a8u9l8i8n. +.t2a2r +.tar2p +.tar1pa +.tarpau4l2 +.tarpaul2i +.ta8r9p8a8u9l8i8n8s. +.tarpaul2i2n1s2 +.te9l8e8g9r8a9p8h8e8r. +.tele1gr +.teleg5ra3ph4er +.te9l8e8g9r8a9p8h8e8r8s. +.telegraphe4r1s2 +.te8l8e9k8i9n8e8t9i8c. +.teleki4n +.telek1i +.telek2ine +.teleki3net1ic +.te8l8e9k8i9n8e8t9i8c8s. +.telekineti4c3s2 +.te8l8e9r8o9b8o8t9i8c8s. +.te4l1er +.tel4ero +.teler5ob +.teleroboti4c3s2 +.te8l8l9e8r. +.tel1l +.tel2le +.te8l8l9e8r8s. +.telle4r1s2 +.te8m9p8o9r8a8r9i8l8y. +.te4m1p +.tem1p4or +.tempo1ra +.tempo4raril +.tempor2a2r +.temporar1i +.temporari1ly +.te8n9u8r8e. +.te8s8t9b8e8d. +.tes2t1b +.test4be2d +.te8x8t9w8i8d8t8h. +.3tex +.tex1t2 +.textw4 +.textwi2 +.textw2id +.textwid2th +.th8a8l9a9m8u8s. +.tha3la +.thala3m +.thala1mu +.thalam2us +.th8e8r9m8o9e8l8a8s9t8i8c. +.th2e +.ther3m4 +.ther1mo +.thermo4el +.thermoe1la +.thermoelast2i +.thermoelas1tic +.ti8m8e9s8t8a8m8p. +.ti2mes +.times1ta +.timesta4m1p +.ti8m8e9s8t8a8m8p8s. +.timestam2p1s2 +.to8o8l9k8i8t. +.too2 +.toolk1i +.to8o8l9k8i8t8s. +.toolki4t1s2 +.to8p8o9g8r8a8p8h9i9c8a8l. +.to5po4g +.topo1gr +.topo5graph1ic +.topogr4aphi +.topographi1ca +.to8q8u8e8s. +.to1q +.toqu2 +.tr8a8i9t8o8r9o8u8s. +.1tra +.tr2ai2 +.trai1to +.traitorou2 +.traitoro2us +.tr8a8n8s9c8e8i8v8e8r. +.tra2n +.tra2n1s2 +.trans4c +.tran4s3cei2 +.transce2iv +.tr8a8n8s9c8e8i8v8e8r8s. +.transceive4r1s2 +.tr8a8n8s9g8r8e8s8s. +.tran2s3g +.trans1gr +.transgr2e2ss +.tr8a8n8s9v8e8r9s8a8l. +.tran4sv +.transve4r1s2 +.transver1sa2 +.tr8a8n8s9v8e8r9s8a8l8s. +.transversa2l1s2 +.tr8a8n8s9v8e8s9t8i8t8e. +.transv4e2s +.transvest2i +.transvest2ite +.tr8a8n8s9v8e8s9t8i8t8e8s. +.transvestit4es +.tr8a9v8e8r8s9a9b8l8e. +.trave4r1s2 +.traver1sa2 +.traver2s1ab +.traversab2l2 +.tr8a9v8e8r9s8a8l. +.tr8a9v8e8r9s8a8l8s. +.traversa2l1s2 +.tr8i9e8t8h8y8l9a8m8i8n8e. +.tri5et +.tr2ie4 +.triethy3la +.triethylam1in +.triethylam2ine +.tr8e8a8c8h9e8r8i8e8s. +.trea2ch +.treache2 +.treacher1i +.treacher2ie4 +.tr8o8u9b8a9d8o8u8r. +.trou2 +.trouba2d +.trouba1do +.troubadou2 +.tu8r9k8e8y. +.1tu +.tu8r9k8e8y8s. +.turkeys4 +.tu8r8n9a8r8o8u8n8d. +.tur4n2a2r +.tur1na +.turnarou2 +.turnaroun2d +.tu8r8n9a8r8o8u8n8d8s. +.turnaroun2d1s2 +.ty8p9a8l. +.1ty +.ty1pa +.typ4al +.un9a8t9t8a8c8h8e8d. +.un2at4 +.una4t3t2 +.unat1ta +.unatta2ch +.unattache2 +.unatta4ch4ed +.un9e8r8r9i8n8g9l8y. +.un4er +.uner4r4 +.unerrin4g +.unerringl2 +.unerring1ly +.un9f8r8i8e8n8d9l8y. +.un3f +.unfri2 +.unfr2ie4 +.unfrien2d1ly +.un9f8r8i8e8n8d9l8i9e8r. +.unfriendl2ie4 +.va8g8u8e8r. +.1va +.vag4 +.va5guer +.va2gue +.va8u8d8e9v8i8l8l8e. +.vaude1v4 +.vaude2v3i4l +.vaude1vi +.vaudevil1l +.vaudevil2le +.vi8c9a8r8s. +.v4ic2a2r +.vi1ca +.vica4rs2 +.vi8l9l8a8i8n9e8s8s. +.2vil +.vil1l +.villai2 +.villa4i4n +.villa2ine +.villai5n2e2ss +.villai1nes +.vi8s9u8a8l. +.vi3su +.visu1al +.vi8s9u8a8l9l8y. +.visual1l +.visual1ly +.vi9v8i8p9a9r8o8u8s. +.3v2iv +.viv2i4p +.vivi1pa +.vivip2a2r +.viviparou2 +.viviparo2us +.vo8i8c8e9p8r8i8n8t. +.voi4 +.voi3cep +.voicepr2 +.voiceprin4t3 +.vs8p8a8c8e. +.v2s1pa +.vspa4ce +.wa8d9d8i8n8g. +.wa2d +.wad4d1in +.wad1d4 +.wa8l8l9f8l8o8w8e8r. +.wal1l +.wal2lf +.wallf4l2 +.wallflow1er +.wa8l8l9f8l8o8w9e8r8s. +.wallflowe4r1s2 +.wa8r8m9e8s8t. +.w2a2r +.war1m +.war2me +.war2mes +.wa8s8t8e9w8a8t8e8r. +.was4t +.waste2w +.waste1w5a +.wastewa1te +.wa8v8e9g8u8i8d8e. +.waveg3 +.waveg2ui2 +.wavegu2id +.wa8v8e9g8u8i8d8e8s. +.waveguide4s2 +.wa8v8e9l8e8t. +.wa8v8e9l8e8t8s. +.wavele4t1s2 +.we8b9l8i8k8e. +.w2e1b +.web2l2 +.web3l4ik +.we8e8k9n8i8g8h8t. +.weekn2ig +.we8e8k9n8i8g8h8t8s. +.weeknigh4t1s2 +.wh8e8e8l9c8h8a8i8r. +.whee4l1c2 +.wheel2ch +.wheelchai2 +.wheelcha4ir +.wh8e8e8l9c8h8a8i8r8s. +.wheelchai4rs2 +.wh8i8c8h9e8v8e8r. +.whi4 +.wh4i2ch +.whiche2 +.whichev1er +.wh8i8t8e9s8i8d8e8d. +.wh2ite +.whit4es +.white1si +.white2s2id +.whitesi2d1ed +.wh8i8t8e9s8p8a8c8e. +.white1sp +.white2s1pa +.whitespa4ce +.wh8i8t8e9s8p8a8c8e8s. +.wi8d8e9s8p8r8e8a8d. +.w2id +.wide4s2 +.wide1sp +.wides4pre +.widespr2 +.widesprea2d1 +.wi8n8g9s8p8a8n. +.win4g +.wings2 +.wing2s1pa +.wingspa4n +.wi8n8g9s8p8a8n8s. +.wingspa2n1s2 +.wi8n8g9s8p8r8e8a8d. +.wingspr2 +.wingsprea2d1 +.wi8t8c8h9c8r8a8f8t. +.wi4tc +.wit4ch +.witchcra2f4t +.witchcra2f +.wo8r8d9s8p8a8c9i8n8g. +.1wo2 +.wor2d1s2 +.words4p +.word2s1pa +.wordsp4a2ci +.wordspa2c1in +.wordspac1ing +.wo8r8k9a8r8o8u8n8d. +.work2a2r +.workarou2 +.workaroun2d +.wo8r8k9a8r8o8u8n8d8s. +.workaroun2d1s2 +.wo8r8k9h8o8r8s8e. +.workh4 +.workhor4se +.workho4rs2 +.wo8r8k9h8o8r8s8e8s. +.workhors3e4s +.wr8a8p9a8r8o8u8n8d. +.wra4 +.wrap2a2r4 +.wra1pa +.wraparou2 +.wraparoun2d +.wr8e8t8c8h9e8d. +.wre4tc +.wret4ch +.wretche2 +.wret4ch4ed +.wr8e8t8c8h9e8d9l8y. +.wretche2d1ly +.ye8s9t8e8r9y8e8a8r. +.yes4 +.yesterye2a2r +.al9g8e9b8r8a8i9s8c8h8e. +.algebra2is1c +.algebrais3ch2 +.algebraische2 +.al9l8e9g8h8e9n8y. +.al1l +.al2le +.al3leg +.alleghe2n +.ar9k8a8n9s8a8s. +.arka2n +.arkan2sa2 +.arka2n1s2 +.at8p9a8s8e. +.a4t1p +.at1pa +.at8p9a8s8e8s. +.atpas1e4s +.au8s9t8r8a8l9a8s8i8a8n. +.a2us +.aus1t4r +.aus1tra +.australas2i1a +.australasi2a2n +.au8t8o9m8a8t8i9s8i8e8r9t8e8r. +.automa3tis +.automatis2ie4 +.automatisiert3er +.be9d8i8e9n8u8n8g. +.4be2d +.b4e3di +.be5di3en +.bed2ie4 +.bedie3nu4n +.be8m8b8o. +.4be5m +.be4m5b +.bi8b9l8i9o9g8r8a9p8h8i9s8c8h8e. +.bibliogr4aphi +.bibliograph2is1c +.bibliographis3ch2 +.bibliographische2 +.bo8s9t8o8n. +.5bos4 +.bos1to +.bosto2n +.br8o8w8n9i8a8n. +.brown5i +.brow3n4i1a +.browni3a2n +.br8u8n8s9w8i8c8k. +.bru2n +.bru2n3s4 +.brun4sw2 +.brunswi2 +.brunswick1 +.bu9d8a9p8e8s8t. +.bu1d2a +.ca8r9i8b9b8e8a8n. +.car1i +.car4ib +.cari2b1b +.carib2be +.caribbea2n +.ch8a8r8l8e8s9t8o8n. +.char4le4 +.char1l +.charles2 +.charl4es2to +.charle3sto2n +.ch8a8r9l8o8t8t8e8s9v8i8l8l8e. +.char3lo4 +.charlo4t3t2 +.charlot4tes +.charlotte4sv +.charlottes2vil +.charlottesvil1l +.charlottesvil2le +.co9l8u8m9b8i8a. +.colum4bi +.colu4m1b +.columb2i1a +.cz8e8c8h8o9s8l8o9v8a9k8i8a. +.c2ze4 +.cze2ch +.cze3cho2 +.czechos4l2 +.czechos4lov +.czechoslo1va +.czechoslovak1i +.czechoslovak2i1a +.de8l9a9w8a8r8e. +.de1la +.de4law +.delaw2a2r +.di8j8k9s8t8r8a. +.di3j +.dij4k1s2 +.dijkst4r +.dijks1tra +.du8a8n8e. +.d1u1a +.dua2n +.dy9n8a9m8i9s8c8h8e. +.5dyn +.dy1na +.dynam2is +.dynam2is1c +.dynamis3ch2 +.dynamische2 +.en8g9l8i8s8h. +.engl2 +.englis2h +.eu8l8e8r9i8a8n. +.eul4e +.eu3l4er1i +.eule1r2i3a4 +.euleri2a2n +.ev8a8n9s8t8o8n. +.e1va +.eva2n +.evan4st +.eva2n1s2 +.evans1to +.evansto2n +.fe8b9r8u9a8r8y. +.f2e4b +.fe3br +.febru3a +.febru2a2r +.fe8s8t9s8c8h8r8i8f8t. +.fes4t1s2 +.fest4sc +.fests2ch2 +.festsc4hr4 +.festschr4i2ft +.fl8o8r9i9d8a. +.flor2id +.flori1d2a +.fl8o8r9i9d9i8a8n. +.flori2di +.florid5i2a2n +.flori1d4i3a +.fo8r9s8c8h8u8n8g8s9i8n9s8t8i9t8u8t. +.fors4c +.fors2ch2 +.forschungs2 +.forschung2s1in +.forschungs2i2n1s2 +.forschungsinst2i +.forschungsinsti1tu +.fr8e8e9b8s8d. +.fre2e1b +.free2b5s2 +.freeb4s5d +.fu8n8k9t8s8i8o8n8a8l. +.3fu +.fu4nk2 +.funk5t +.funk4t1s2 +.funkt1s2io +.funkt5sio2n +.funktsio1n5a +.ga8u8s8s9i8a8n. +.ga2us +.gau2ss +.gaus1si +.gauss2i1a +.gaussi2a2n +.gh8o8s8t9s8c8r8i8p8t. +.ghos4t1s2 +.ghost4sc +.ghostscri2 +.ghostscr2ip +.ghostscri2p1t +.gh8o8s8t9v8i8e8w. +.ghos4tv +.ghostv2ie4 +.gr8a8s8s9m8a8n8n9i8a8n. +.gr2as +.gra2ss +.gras2s1m +.grass3ma +.grassma2n3 +.grassma4n1n2 +.grassman3n4i1a +.grassma2nni3a2n +.gr8e8i8f8s9w8a8l8d. +.grei2 +.grei2f3s +.greifsw2 +.greifswa2ld +.gr8o8t8h8e8n9d8i8e8c8k. +.g4ro +.gro4th2e +.gr4oth +.grothe2n +.grothend2ie4 +.grothendieck1 +.gr8u8n8d9l8e8h9r8e8n. +.gru2n +.grundle1h4 +.grundle4hr4 +.ha9d8a9m8a8r8d. +.ha2d +.ha1d2a +.hada2m2 +.had4a1ma +.hadam2a2r +.ha8i9f8a. +.hai1fa +.ha8m8i8l9t8o8n9i8a8n. +.ha4m +.hami4lt +.hamil1to +.hamilto2n +.hamilto3n4i1a +.hamiltoni3a2n +.he8l9s8i8n8k8i. +.he2l1s2 +.hel2s1in +.hels4i4nk2 +.helsink1i +.he8r9m8i8t9i8a8n. +.her3mit +.hermi1ti +.herm4i1t2i1a +.hermiti2a2n +.hi8b8b8s. +.hi2b1b +.hib2b5s2 +.ho8k9k8a8i9d8o. +.h2ok +.hokk4 +.hokkai2 +.hokka2id +.hokkai1do +.ja8c9k8o8w9s8k8i. +.5ja +.jack1 +.jackowsk2 +.jackowsk1i +.ja8n9u9a8r8y. +.ja2n +.jan3u1a +.janu2a2r +.ja9p8a9n8e8s8e. +.ja4p +.ja1pa +.japa2n +.japa1nes +.japane1s2e +.ka8d9o8m9t8s8e8v. +.ka2d +.ka1do +.kado4mt +.kadom4t1s2 +.kadomt5sev +.ka8n9s8a8s. +.ka2n +.kan2sa2 +.ka2n1s2 +.ka8r8l8s9r8u8h8e. +.k2a2r +.kar1l +.kar2l1s2 +.karls1r +.ko8r9t8e9w8e8g. +.ko5r +.kr8i8s8h8n8a. +.kr2is +.kr3is2h +.kris2h1n +.krish1na +.kr8i8s8h9n8a9i8s8m. +.krishnai2 +.krishnai2s1m +.kr8i8s8h9n8a8n. +.krishn2a2n +.la8n9c8a8s9t8e8r. +.lan1ca +.lancast5er +.le9g8e8n8d8r8e. +.le1gen +.legen1dr +.legendre4 +.le8i8c8e8s9t8e8r. +.lei2 +.le5ic +.leices5t +.li8p9s8c8h8i8t8z. +.l2ip +.li2p1s2 +.lips2ch2 +.lips3chit +.lipschi4tz +.li8p9s8c8h8i8t8z9i8a8n. +.lipschit2z1i +.lipschitz2i1a +.lipschitzi2a2n +.lo8j9b8a8n. +.lo5j +.lojba2n +.lo8u9i9s8i9a8n8a. +.lou2 +.lo2ui2 +.louis2i1a +.louisi2a2n +.louisia1na +.ma8c9o8s. +.ma1co +.ma8n9c8h8e8s9t8e8r. +.man2ch +.manche2 +.manch1es +.ma8r9k8o8v9i8a8n. +.marko5vi2a2n +.markov2i1a +.ma8r8k8t9o8b8e8r9d8o8r8f. +.mark5t +.mark1to +.markto3b +.marktober1do +.marktoberd4or +.marktoberdor1f +.ma8s8s9a9c8h8u9s8e8t8t8s. +.ma2ss +.mas1sa2 +.massa2ch +.massach2us +.massachuse4t3t2 +.massachuset4t1s2 +.ma8x9w8e8l8l. +.maxwel4l +.mi9c8r8o9s8o8f8t. +.micro2so +.microso2ft3 +.mi8n9n8e9a8p9o9l8i8s. +.m2i4n1n2 +.minne4 +.minneapol2i +.mi8n9n8e9s8o8t8a. +.min1nes +.minne1so +.minneso1ta +.mo8s9c8o8w. +.mos2c +.mos1co +.na8c8h9r8i8c8h8t8e8n. +.1na +.na2ch +.nac4hr4 +.na2chr4i2ch +.nachricht1en +.na8s8h9v8i8l8l8e. +.n4as +.nas2h +.nash2vil +.nashvil1l +.nashvil2le +.ne8t9b8s8d. +.ne2t1b +.net2b5s2 +.netb4s5d +.ne8t9s8c8a8p8e. +.ne4t1s2 +.net4sc +.netsca4p +.nets1ca +.ni8j9m8e9g8e8n. +.ni3j +.nijme2g +.nijme1gen +.no8e9t8h8e8r9i8a8n. +.3noe +.noeth2e +.noether1i +.noethe1r2i3a4 +.noetheri2a2n +.no8o8r8d9w8i8j8k8e8r9h8o8u8t. +.noo2 +.no3ord +.noord1w +.noordwi2 +.noordwi3j +.noordwijk1er +.noordwijker1h4 +.noordwijkerhou2 +.no9v8e8m9b8e8r. +.nove4m5b +.op8e8n9b8s8d. +.ope4n1b4 +.open2b5s2 +.openb4s5d +.op8e8n9o8f8f8i8c8e. +.op4eno +.openo4f1f +.openof1fi +.pa8l8a9t8i8n8o. +.pala2t1in +.palat2i1no +.pa9l8e8r9m8o. +.paler3m4 +.paler1mo +.pe9t8r8o8v9s8k8i. +.petro3v +.petrovsk2 +.petrovsk1i +.pf8a8f8f9i8a8n. +.4pf +.p1fa +.pfa2f +.pfa4f1f4 +.pfaf1fi +.pfaff2i3a +.pfaffi2a2n +.ph8i8l9a9d8e8l9p8h8i8a. +.phi4l4ade +.phila2d +.philade2lp +.philadel5phi +.philadelph2i1a +.ph8i8l9o9s8o8p8h9i9s8c8h8e. +.philo2so +.philos4op +.philos2oph +.philosoph2is1c +.philosophis3ch2 +.philosophische2 +.po8i8n9c8a8r8e. +.poin2 +.poi2 +.poinc2a2r5 +.poin1ca +.po9t8e8n9t8i8a8l9g8l8e8i9c8h8u8n8g. +.p4ot +.po1ten1t +.potent2i +.poten1t2i1a +.potenti2al +.potentia4l1g4 +.potentialgl2 +.potential1gle +.potentialglei2 +.potentialgle5ic +.potentialgle4i2ch +.ra9d8h8a9k8r8i8s8h9n8a8n. +.rad1h2 +.radhakr2is +.radhakr3is2h +.radhakris2h1n +.radhakrish1na +.radhakrishn2a2n +.ra8t8h8s9k8e8l9l8e8r. +.r4ath +.ra2t4h1s2 +.rathsk2 +.rath4ske +.rathskel1l +.rathskel2le +.ri8e9m8a8n8n9i8a8n. +.r2ie4 +.rie5ma2n +.rie1ma +.riema4n1n2 +.rieman3n4i1a +.riema2nni3a2n +.ry8d9b8e8r8g. +.ry1d +.ryd1b +.rydberg2 +.sc8h8o8t9t8i8s8c8h8e. +.scho4t3t2 +.schott2is1c +.s2ch2ottis3ch2 +.schottische2 +.sc8h8r8o9d8i8n8g9e8r. +.sc4hr4 +.schrod1in +.schrod4inge +.sc8h8w8a9b8a9c8h8e8r. +.sch1w +.schwaba2ch +.schwabache2 +.sc8h8w8a8r8z9s8c8h8i8l8d. +.schw2a2r +.s2chwarzs2ch2 +.schwarzsch4il2 +.schwarzschi2ld +.se8p9t8e8m9b8e8r. +.se2p1t +.sep2te +.septe4m5b +.st8o8k8e8s9s8c8h8e. +.st2ok +.stokes4 +.stok2e2ss +.stokes2s5c +.stokess2ch2 +.stokessche2 +.st8u8t8t9g8a8r8t. +.stu4t3t2 +.stut4t1g +.stutt1ga +.stuttg2a2r +.su8s9q8u8e9h8a8n9n8a. +.s2us +.susqu2 +.susque1h4 +.susqueha2n +.susqueha4n1n2 +.susquehan1na +.ta8u9b8e8r9i8a8n. +.tau4b +.taub4e +.tau3ber +.tauber1i +.taube1r2i3a4 +.tauberi2a2n +.te8c8h9n8i9s8c8h8e. +.te2ch +.tec2h1n +.techn2is1c +.te2chnis3ch2 +.technische2 +.te8n9n8e8s9s8e8e. +.t4e4n1n2 +.tenne4 +.ten1nes +.tenn2e2ss +.to9m8a9s8z8e8w9s8k8i. +.to2ma +.tomas2ze +.tomaszewsk2 +.tomaszewsk1i +.ty9p8o9g8r8a8p8h8i8q8u8e. +.ty3po +.ty5po4g +.typo1gr +.typogr4aphi +.typographiqu2 +.uk8r8a8i8n9i8a8n. +.4uk +.ukr2ai2 +.ukra4i4n +.ukra2ini +.ukrai4n4i1a +.ukraini3a2n +.ve8r9a8l8l9g8e9m8e8i8n9e8r8t8e. +.veral1l +.veral4l1g4 +.verallge1me +.verallgemei2 +.verallgeme2ine +.verallgemein1er +.ve8r9e8i8n9i9g8u8n8g. +.vere3in +.verei2 +.vere2ini +.verein2ig +.vereini3gun +.ve8r9t8e8i9l8u8n9g8e8n. +.vertei2 +.verteilun1gen +.vi8i8i8t8h. +.v4i5i4 +.vi4i5i4 +.vii2ith +.vi8i8t8h. +.vi2ith +.wa8h8r9s8c8h8e8i8n9l8i8c8h9k8e8i8t8s9t8h8e8o9r8i8e. +.wa4hr4 +.wah4rs2 +.wahrs4c +.wahrs2ch2 +.wahrsche2 +.wahrschei2 +.wahrsche4i4n1l +.wahrs2cheinl4i2ch +.wahrscheinlic4hk +.wahrscheinlichkei2 +.wahrscheinlichkei4t1s2 +.wahrscheinlichkeits3th2e +.wahrscheinlichkeitsthe1o5r +.wahrscheinlichkeitstheor2ie4 +.we8r9n8e8r. +.w1er +.wer4n1er +.we8r9t8h8e8r9i8a8n. +.werth2e +.werther1i +.werthe1r2i3a4 +.wertheri2a2n +.wi8n9c8h8e8s9t8e8r. +.win2ch +.winche2 +.winch1es +.wi8r8t9s8c8h8a8f8t. +.w4ir4 +.wir4t1s2 +.wirt4sc +.wirts2ch2 +.wirtscha2f +.wirtscha2ft +.wi8s9s8e8n9s8c8h8a8f8t9l8i8c8h. +.w4i2s1s +.wissen4 +.wisse2n1s2 +.wissens4c +.wissens2ch2 +.wissenscha2f +.wissenscha2ft +.wissenschaf2tl +.wissens2chaftl4i2ch +.xv8i8i8i8t8h. +.xv4i5i4 +.xvi4i5i4 +.xvii2ith +.xv8i8i8t8h. +.xvi2ith +.xx8i8i8i8r8d. +.xx4 +.xx3i +.xx4i5i4 +.xxi4i5i4 +.xxii4ir +.xx8i8i8n8d. +.xxi4ind +.yi8n8g9y8o8n8g. +.y1i +.yin2gy +.yingy1o4 +.yingyo2n +.sh8u9x8u8e. +.shux1u3 +.ji9s8u8a8n. +.ji2su +.jisua2n +.ze8a9l8a8n8d. +.2ze +.zea4l +.zea3l4and +.zeala2n +.ze8i8t9s8c8h8r8i8f8t. +.zei2 +.zei4t1s2 +.zeit4sc +.zeits2ch2 +.zeitsc4hr4 +.zeitschr4i2ft diff --git a/core/res/res/drawable-hdpi/combobox_disabled.png b/core/res/res/drawable-hdpi/combobox_disabled.png Binary files differnew file mode 100644 index 0000000..50eb45e --- /dev/null +++ b/core/res/res/drawable-hdpi/combobox_disabled.png diff --git a/core/res/res/drawable-hdpi/combobox_nohighlight.png b/core/res/res/drawable-hdpi/combobox_nohighlight.png Binary files differnew file mode 100644 index 0000000..9d60301 --- /dev/null +++ b/core/res/res/drawable-hdpi/combobox_nohighlight.png diff --git a/core/res/res/drawable-hdpi/cursor_controller.png b/core/res/res/drawable-hdpi/cursor_controller.png Binary files differnew file mode 100644 index 0000000..720aded --- /dev/null +++ b/core/res/res/drawable-hdpi/cursor_controller.png diff --git a/core/res/res/drawable-hdpi/ic_aggregated.png b/core/res/res/drawable-hdpi/ic_aggregated.png Binary files differdeleted file mode 100644 index 7ca15b1..0000000 --- a/core/res/res/drawable-hdpi/ic_aggregated.png +++ /dev/null diff --git a/core/res/res/drawable-hdpi/quickcontact_nobadge.9.png b/core/res/res/drawable-hdpi/quickcontact_nobadge.9.png Binary files differnew file mode 100644 index 0000000..68d43c4 --- /dev/null +++ b/core/res/res/drawable-hdpi/quickcontact_nobadge.9.png diff --git a/core/res/res/drawable-hdpi/selection_end_handle.png b/core/res/res/drawable-hdpi/selection_end_handle.png Binary files differnew file mode 100644 index 0000000..624ab58 --- /dev/null +++ b/core/res/res/drawable-hdpi/selection_end_handle.png diff --git a/core/res/res/drawable-hdpi/selection_start_handle.png b/core/res/res/drawable-hdpi/selection_start_handle.png Binary files differnew file mode 100644 index 0000000..7d6f24c --- /dev/null +++ b/core/res/res/drawable-hdpi/selection_start_handle.png diff --git a/core/res/res/drawable-hdpi/sym_action_call.png b/core/res/res/drawable-hdpi/sym_action_call.png Binary files differindex 105f7d0..da8afee 100644 --- a/core/res/res/drawable-hdpi/sym_action_call.png +++ b/core/res/res/drawable-hdpi/sym_action_call.png diff --git a/core/res/res/drawable-mdpi/combobox_disabled.png b/core/res/res/drawable-mdpi/combobox_disabled.png Binary files differnew file mode 100644 index 0000000..c32db7e --- /dev/null +++ b/core/res/res/drawable-mdpi/combobox_disabled.png diff --git a/core/res/res/drawable-mdpi/combobox_nohighlight.png b/core/res/res/drawable-mdpi/combobox_nohighlight.png Binary files differnew file mode 100644 index 0000000..1963316 --- /dev/null +++ b/core/res/res/drawable-mdpi/combobox_nohighlight.png diff --git a/core/res/res/drawable-mdpi/cursor_controller.png b/core/res/res/drawable-mdpi/cursor_controller.png Binary files differnew file mode 100644 index 0000000..1a8a459 --- /dev/null +++ b/core/res/res/drawable-mdpi/cursor_controller.png diff --git a/core/res/res/drawable-mdpi/ic_aggregated.png b/core/res/res/drawable-mdpi/ic_aggregated.png Binary files differdeleted file mode 100644 index 7c2e2b0..0000000 --- a/core/res/res/drawable-mdpi/ic_aggregated.png +++ /dev/null diff --git a/core/res/res/drawable-mdpi/quickcontact_nobadge.9.png b/core/res/res/drawable-mdpi/quickcontact_nobadge.9.png Binary files differnew file mode 100644 index 0000000..ebe747b --- /dev/null +++ b/core/res/res/drawable-mdpi/quickcontact_nobadge.9.png diff --git a/core/res/res/drawable-mdpi/selection_end_handle.png b/core/res/res/drawable-mdpi/selection_end_handle.png Binary files differnew file mode 100644 index 0000000..7e075eb --- /dev/null +++ b/core/res/res/drawable-mdpi/selection_end_handle.png diff --git a/core/res/res/drawable-mdpi/selection_start_handle.png b/core/res/res/drawable-mdpi/selection_start_handle.png Binary files differnew file mode 100644 index 0000000..d8022f7 --- /dev/null +++ b/core/res/res/drawable-mdpi/selection_start_handle.png diff --git a/core/res/res/drawable-nodpi/loading_tile_android.png b/core/res/res/drawable-nodpi/loading_tile_android.png Binary files differnew file mode 100644 index 0000000..8fde46f --- /dev/null +++ b/core/res/res/drawable-nodpi/loading_tile_android.png diff --git a/core/res/res/drawable-nodpi/no_tile_256.png b/core/res/res/drawable-nodpi/no_tile_256.png Binary files differnew file mode 100644 index 0000000..388234e --- /dev/null +++ b/core/res/res/drawable-nodpi/no_tile_256.png diff --git a/core/res/res/drawable-xlarge/default_wallpaper.jpg b/core/res/res/drawable-xlarge/default_wallpaper.jpg Binary files differnew file mode 100644 index 0000000..0302f00 --- /dev/null +++ b/core/res/res/drawable-xlarge/default_wallpaper.jpg diff --git a/core/res/res/drawable/action_bar_background.xml b/core/res/res/drawable/action_bar_background.xml new file mode 100644 index 0000000..3929d7f --- /dev/null +++ b/core/res/res/drawable/action_bar_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:startColor="#ffd1d2d4" + android:endColor="#ff85878a" + android:angle="270" /> +</shape> diff --git a/core/res/res/drawable/action_bar_context_background.xml b/core/res/res/drawable/action_bar_context_background.xml new file mode 100644 index 0000000..8789898 --- /dev/null +++ b/core/res/res/drawable/action_bar_context_background.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:startColor="#ff000000" + android:centerColor="#ffd1d2d4" + android:endColor="#ff85878a" + android:angle="270" /> +</shape> diff --git a/core/res/res/drawable/action_bar_divider.xml b/core/res/res/drawable/action_bar_divider.xml new file mode 100644 index 0000000..414309f --- /dev/null +++ b/core/res/res/drawable/action_bar_divider.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:startColor="#ffe1e2e4" + android:endColor="#ff95979a" + android:angle="270" /> +</shape> diff --git a/core/res/res/drawable/ic_btn_back.png b/core/res/res/drawable/ic_btn_back.png Binary files differnew file mode 100644 index 0000000..c9bff4c --- /dev/null +++ b/core/res/res/drawable/ic_btn_back.png diff --git a/core/res/res/drawable/ic_btn_next.png b/core/res/res/drawable/ic_btn_next.png Binary files differnew file mode 100755 index 0000000..c6cf436 --- /dev/null +++ b/core/res/res/drawable/ic_btn_next.png diff --git a/core/res/res/drawable/quickcontact_nobadge.xml b/core/res/res/drawable/quickcontact_nobadge.xml deleted file mode 100644 index 922fa0e..0000000 --- a/core/res/res/drawable/quickcontact_nobadge.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* bubble_with_chats.xml -** -** Copyright 2009, Google Inc. -** -** 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. -*/ ---> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:drawable="@drawable/quickcontact_nobadge_pressed" /> - <item android:state_selected="true" android:drawable="@drawable/quickcontact_nobadge_highlight" /> - <item android:state_focused="true" android:drawable="@drawable/quickcontact_nobadge_highlight" /> - <item android:state_enabled="false" android:drawable="@drawable/quickcontact_nobadge_normal" /> - <item android:drawable="@drawable/quickcontact_nobadge_normal" /> -</selector> diff --git a/core/res/res/drawable/quickcontact_nobadge_highlight.9.png b/core/res/res/drawable/quickcontact_nobadge_highlight.9.png Binary files differdeleted file mode 100644 index f0f50b3..0000000 --- a/core/res/res/drawable/quickcontact_nobadge_highlight.9.png +++ /dev/null diff --git a/core/res/res/drawable/quickcontact_nobadge_normal.9.png b/core/res/res/drawable/quickcontact_nobadge_normal.9.png Binary files differdeleted file mode 100644 index 01cc9dc..0000000 --- a/core/res/res/drawable/quickcontact_nobadge_normal.9.png +++ /dev/null diff --git a/core/res/res/drawable/quickcontact_nobadge_pressed.9.png b/core/res/res/drawable/quickcontact_nobadge_pressed.9.png Binary files differdeleted file mode 100644 index 6e22c87..0000000 --- a/core/res/res/drawable/quickcontact_nobadge_pressed.9.png +++ /dev/null diff --git a/core/res/res/layout/action_bar_title_item.xml b/core/res/res/layout/action_bar_title_item.xml new file mode 100644 index 0000000..3c046fe --- /dev/null +++ b/core/res/res/layout/action_bar_title_item.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" > + <TextView android:id="@+id/action_bar_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceMediumInverse" /> + <TextView android:id="@+id/action_bar_subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceSmallInverse" /> +</LinearLayout> diff --git a/core/res/res/layout/action_menu_item_layout.xml b/core/res/res/layout/action_menu_item_layout.xml new file mode 100644 index 0000000..3f06251 --- /dev/null +++ b/core/res/res/layout/action_menu_item_layout.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<com.android.internal.view.menu.ActionMenuItemView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + /> diff --git a/core/res/res/layout/action_menu_layout.xml b/core/res/res/layout/action_menu_layout.xml new file mode 100644 index 0000000..18d5531 --- /dev/null +++ b/core/res/res/layout/action_menu_layout.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<com.android.internal.view.menu.ActionMenuView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> diff --git a/core/res/res/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml index 7ae68f9..b746d28 100644 --- a/core/res/res/layout/alert_dialog.xml +++ b/core/res/res/layout/alert_dialog.xml @@ -112,7 +112,7 @@ android:paddingTop="4dip" android:paddingLeft="2dip" android:paddingRight="2dip" - android:useLargestChild="true"> + android:measureWithLargestChild="true"> <LinearLayout android:id="@+id/leftSpacer" android:layout_weight="0.25" android:layout_width="0dip" diff --git a/core/res/res/layout/contact_header.xml b/core/res/res/layout/contact_header.xml deleted file mode 100644 index bf467d3..0000000 --- a/core/res/res/layout/contact_header.xml +++ /dev/null @@ -1,116 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/banner" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="horizontal" - android:background="@drawable/title_bar_medium" - android:paddingRight="5dip"> - - <android.widget.QuickContactBadge android:id="@+id/photo" - android:layout_gravity="center_vertical" - android:layout_marginRight="8dip" - android:layout_marginLeft="-1dip" - style="@*android:style/Widget.QuickContactBadge.WindowSmall" /> - /> - - <LinearLayout - android:layout_width="0dip" - android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical" - android:layout_gravity="center_vertical" > - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> - - <ImageView - android:id="@+id/aggregate_badge" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingRight="3dip" - android:paddingTop="3dip" - android:src="@drawable/ic_aggregated" - android:visibility="gone" - /> - - <TextView android:id="@+id/name" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:singleLine="true" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textStyle="bold" - android:shadowColor="#BB000000" - android:shadowRadius="2.75" - /> - </LinearLayout> - - <TextView android:id="@+id/phonetic_name" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceSmall" - android:singleLine="true" - android:ellipsize="end" - android:layout_marginTop="-2dip" - android:visibility="gone" - /> - - <TextView android:id="@+id/status" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceSmall" - android:singleLine="true" - android:ellipsize="end" - android:layout_marginTop="-2dip" - android:visibility="gone" - /> - - <TextView android:id="@+id/status_date" - android:layout_width="match_parent" - android:layout_height="0dip" - android:layout_weight="1" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textSize="12sp" - android:layout_marginTop="-2dip" - android:visibility="gone" - /> - </LinearLayout> - - <ImageView - android:id="@+id/presence" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:paddingLeft="3dip" - android:paddingRight="6dip" - android:visibility="gone" - /> - - <CheckBox - android:id="@+id/star" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:visibility="gone" - android:contentDescription="@string/description_star" - style="?android:attr/starStyle" /> - -</LinearLayout> diff --git a/core/res/res/layout/keyguard_screen_unlock_landscape.xml b/core/res/res/layout/keyguard_screen_unlock_landscape.xml index c1b406f..83381a1 100644 --- a/core/res/res/layout/keyguard_screen_unlock_landscape.xml +++ b/core/res/res/layout/keyguard_screen_unlock_landscape.xml @@ -58,18 +58,19 @@ android:ellipsize="marquee" android:gravity="right|bottom" /> + <com.android.internal.widget.DigitalClock android:id="@+id/time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_marginTop="8dip" + android:layout_marginBottom="8dip" > <TextView android:id="@+id/timeDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="bottom" android:singleLine="true" android:ellipsize="none" android:textSize="72sp" @@ -84,8 +85,9 @@ <TextView android:id="@+id/am_pm" android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="bottom" + android:layout_height="wrap_content" + android:layout_toRightOf="@id/timeDisplay" + android:layout_alignBaseline="@id/timeDisplay" android:singleLine="true" android:ellipsize="none" android:textSize="22sp" diff --git a/core/res/res/layout/keyguard_screen_unlock_portrait.xml b/core/res/res/layout/keyguard_screen_unlock_portrait.xml index 74a0eee..8dacfaf 100644 --- a/core/res/res/layout/keyguard_screen_unlock_portrait.xml +++ b/core/res/res/layout/keyguard_screen_unlock_portrait.xml @@ -55,12 +55,12 @@ android:layout_alignParentTop="true" android:layout_marginTop="15dip" android:layout_marginLeft="20dip" + android:layout_marginBottom="8dip" > <TextView android:id="@+id/timeDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="bottom" android:singleLine="true" android:ellipsize="none" android:textSize="56sp" @@ -74,8 +74,9 @@ <TextView android:id="@+id/am_pm" android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="bottom" + android:layout_height="wrap_content" + android:layout_toRightOf="@id/timeDisplay" + android:layout_alignBaseline="@id/timeDisplay" android:singleLine="true" android:ellipsize="none" android:textSize="18sp" diff --git a/core/res/res/layout/list_content.xml b/core/res/res/layout/list_content.xml index 6f9f1e0..1414032 100644 --- a/core/res/res/layout/list_content.xml +++ b/core/res/res/layout/list_content.xml @@ -1,8 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -/* //device/apps/common/assets/res/layout/list_content.xml -** -** Copyright 2006, The Android Open Source Project +/* Copyright 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. @@ -17,8 +15,42 @@ ** limitations under the License. */ --> -<ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:drawSelectorOnTop="false" - /> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout android:id="@+id/progressContainer" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + android:gravity="center"> + + <ProgressBar style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="@string/loading" + android:paddingTop="4dip" + android:singleLine="true" /> + + </LinearLayout> + + <FrameLayout android:id="@+id/listContainer" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ListView android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:drawSelectorOnTop="false" /> + <TextView android:id="@+android:id/internalEmpty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:textAppearance="?android:attr/textAppearanceLarge" /> + </FrameLayout> + +</FrameLayout> diff --git a/core/res/res/layout/list_content_simple.xml b/core/res/res/layout/list_content_simple.xml new file mode 100644 index 0000000..6f9f1e0 --- /dev/null +++ b/core/res/res/layout/list_content_simple.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* //device/apps/common/assets/res/layout/list_content.xml +** +** Copyright 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. +*/ +--> +<ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:drawSelectorOnTop="false" + /> diff --git a/core/res/res/layout/preference_list_content.xml b/core/res/res/layout/preference_list_content.xml index 8f86981..844d338 100644 --- a/core/res/res/layout/preference_list_content.xml +++ b/core/res/res/layout/preference_list_content.xml @@ -4,22 +4,58 @@ ** ** Copyright 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 +** 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 +** 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 +** 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. */ --> -<ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" - android:layout_width="match_parent" + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_height="match_parent" - android:drawSelectorOnTop="false" - android:scrollbarAlwaysDrawVerticalTrack="true" + android:layout_width="match_parent"> + + <ListView android:id="@android:id/list" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1" + android:drawSelectorOnTop="false" + android:scrollbarAlwaysDrawVerticalTrack="true" /> + + <RelativeLayout android:id="@+id/button_bar" + android:layout_height="wrap_content" + android:layout_width="fill_parent" + android:layout_weight="0" + android:background="@android:drawable/bottom_bar" + android:visibility="gone"> + + <Button android:id="@+id/back_button" + android:layout_width="150dip" + android:layout_height="wrap_content" + android:layout_margin="5dip" + android:layout_alignParentLeft="true" + android:drawableLeft="@drawable/ic_btn_back" + android:drawablePadding="3dip" + android:text="@string/back_button_label" + /> + + <Button android:id="@+id/next_button" + android:layout_width="150dip" + android:layout_height="wrap_content" + android:layout_margin="5dip" + android:layout_alignParentRight="true" + android:drawableRight="@drawable/ic_btn_next" + android:drawablePadding="3dip" + android:text="@string/next_button_label" + /> + </RelativeLayout> +</LinearLayout> diff --git a/core/res/res/layout/screen_action_bar.xml b/core/res/res/layout/screen_action_bar.xml new file mode 100644 index 0000000..f39852b --- /dev/null +++ b/core/res/res/layout/screen_action_bar.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<!-- +This is an optimized layout for a screen with the Action Bar enabled. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:fitsSystemWindows="true"> + <ViewAnimator android:id="@+id/action_bar_animator" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inAnimation="@anim/push_down_in" + android:outAnimation="@anim/push_down_out"> + <com.android.internal.widget.ActionBarView + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?android:attr/windowActionBarStyle" /> + <com.android.internal.widget.ActionBarContextView + android:id="@+id/action_context_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </ViewAnimator> + <FrameLayout android:id="@android:id/content" + android:layout_width="match_parent" + android:layout_height="0dip" + android:layout_weight="1" + android:foregroundGravity="fill_horizontal|top" + android:foreground="?android:attr/windowContentOverlay" /> + <LinearLayout android:id="@+id/lower_action_context_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?android:attr/windowActionBarStyle" + android:visibility="gone" /> +</LinearLayout> diff --git a/core/res/res/layout/screen_xlarge_action_bar.xml b/core/res/res/layout/screen_xlarge_action_bar.xml new file mode 100644 index 0000000..c51ca13 --- /dev/null +++ b/core/res/res/layout/screen_xlarge_action_bar.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<!-- +This is an optimized layout for a screen with the Action Bar enabled. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:fitsSystemWindows="true"> + <ViewAnimator android:id="@+id/action_bar_animator" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inAnimation="@anim/push_down_in" + android:outAnimation="@anim/push_down_out"> + <com.android.internal.widget.ActionBarView + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?android:attr/windowActionBarStyle" /> + <com.android.internal.widget.ActionBarContextView + android:id="@+id/action_context_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </ViewAnimator> + <FrameLayout android:id="@android:id/content" + android:layout_width="match_parent" + android:layout_height="0dip" + android:layout_weight="1" + android:foregroundGravity="fill_horizontal|top" + android:foreground="?android:attr/windowContentOverlay" /> +</LinearLayout> diff --git a/core/res/res/layout/web_runtime.xml b/core/res/res/layout/web_runtime.xml new file mode 100644 index 0000000..4ec0964 --- /dev/null +++ b/core/res/res/layout/web_runtime.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > + <WebView + android:id="@+id/webview" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + /> + + <ImageView + android:id="@+id/splashscreen" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:scaleType="fitStart" + /> + +</FrameLayout> + diff --git a/core/res/res/values-xlarge/dimens.xml b/core/res/res/values-xlarge/dimens.xml new file mode 100644 index 0000000..b3fdf46 --- /dev/null +++ b/core/res/res/values-xlarge/dimens.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* //device/apps/common/assets/res/any/dimens.xml +** +** Copyright 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. +*/ +--> +<resources> + <dimen name="status_bar_height">50dip</dimen> + <!-- Height of the status bar --> + <dimen name="status_bar_icon_size">50dip</dimen> + <!-- Margin at the edge of the screen to ignore touch events for in the windowshade. --> +</resources> + diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index d16b91c..67072b6 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -14,8 +14,8 @@ limitations under the License. --> -<!-- Formatting note: terminate all comments with a period, to avoid breaking - the documentation output. To suppress comment lines from the documentation +<!-- Formatting note: terminate all comments with a period, to avoid breaking + the documentation output. To suppress comment lines from the documentation output, insert an eat-comment element after the comment lines. --> @@ -244,6 +244,13 @@ {@link android.R.styleable#WindowAnimation}. --> <attr name="windowAnimationStyle" format="reference" /> + <!-- Flag indicating whether this window should have an Action Bar + in place of the usual title bar. --> + <attr name="windowActionBar" format="boolean" /> + + <!-- Reference to a style for the Action Bar --> + <attr name="windowActionBarStyle" format="reference" /> + <!-- Defines the default soft input state that this window would like when it is displayed. --> <attr name="windowSoftInputMode"> @@ -390,6 +397,12 @@ <attr name="horizontalScrollViewStyle" format="reference" /> <!-- Default Spinner style. --> <attr name="spinnerStyle" format="reference" /> + <!-- Default dropdown Spinner style. --> + <attr name="dropDownSpinnerStyle" format="reference" /> + <!-- Default ActionBar dropdown style. --> + <attr name="actionDropDownStyle" format="reference" /> + <!-- Default action button style. --> + <attr name="actionButtonStyle" format="reference" /> <!-- Default Star style. --> <attr name="starStyle" format="reference" /> <!-- Default TabWidget style. --> @@ -426,6 +439,15 @@ <attr name="quickContactBadgeStyleSmallWindowLarge" format="reference" /> <!-- =================== --> + <!-- Action bar styles --> + <!-- =================== --> + <eat-comment /> + <!-- Default amount of padding to use between action buttons. --> + <attr name="actionButtonPadding" format="dimension" /> + <attr name="actionBarContextBackground" format="reference" /> + <attr name="actionBarCloseContextDrawable" format="reference" /> + + <!-- =================== --> <!-- Preference styles --> <!-- =================== --> <eat-comment /> @@ -963,6 +985,8 @@ <attr name="textColor" /> <attr name="backgroundDimEnabled" /> <attr name="backgroundDimAmount" /> + <attr name="windowActionBar" /> + <attr name="windowActionBarStyle" /> </declare-styleable> <!-- The set of attributes that describe a AlertDialog's theme. --> @@ -989,7 +1013,7 @@ <attr name="windowShowAnimation" format="reference" /> <!-- The animation used when a window is going from VISIBLE to INVISIBLE. --> <attr name="windowHideAnimation" format="reference" /> - + <!-- When opening a new activity, this is the animation that is run on the next activity (which is entering the screen). --> <attr name="activityOpenEnterAnimation" format="reference" /> @@ -1030,7 +1054,7 @@ animation that is run on the top activity of the current task (which is exiting the screen). --> <attr name="taskToBackExitAnimation" format="reference" /> - + <!-- When opening a new activity that shows the wallpaper, while currently not showing the wallpaper, this is the animation that is run on the new wallpaper activity (which is entering the screen). --> @@ -1047,7 +1071,7 @@ currently showing the wallpaper, this is the animation that is run on the old wallpaper activity (which is exiting the screen). --> <attr name="wallpaperCloseExitAnimation" format="reference" /> - + <!-- When opening a new activity that is on top of the wallpaper when the current activity is also on top of the wallpaper, this is the animation that is run on the new activity @@ -1535,6 +1559,8 @@ will use only the number of items in the adapter and the number of items visible on screen to determine the scrollbar's properties. --> <attr name="smoothScrollbar" format="boolean" /> + <!-- A reference to an XML description of the adapter to attach to the list. --> + <attr name="adapter" format="reference" /> </declare-styleable> <declare-styleable name="AbsSpinner"> <!-- Reference to an array resource that will populate the Spinner. For static content, @@ -1730,7 +1756,7 @@ <!-- 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. --> - <attr name="useLargestChild" format="boolean" /> + <attr name="measureWithLargestChild" format="boolean" /> </declare-styleable> <declare-styleable name="ListView"> <!-- Reference to an array resource that will populate the ListView. For static content, @@ -2154,7 +2180,7 @@ <attr name="dropDownAnchor" format="reference" /> <!-- Specifies the basic width of the dropdown. Its value may be a dimension (such as "12dip") for a constant width, - fill_parent or match_parent to match the width of the + fill_parent or match_parent to match the width of the screen, or wrap_content to match the width of the anchored view. --> <attr name="dropDownWidth" format="dimension"> @@ -2190,8 +2216,13 @@ <attr name="popupBackground" format="reference|color" /> </declare-styleable> <declare-styleable name="ViewAnimator"> + <!-- Identifier for the animation to use when a view is shown. --> <attr name="inAnimation" format="reference" /> + <!-- Identifier for the animation to use when a view is hidden. --> <attr name="outAnimation" format="reference" /> + <!-- Defines whether to animate the current View when the ViewAnimation + is first displayed. --> + <attr name="animateFirstView" format="boolean" /> </declare-styleable> <declare-styleable name="ViewFlipper"> <attr name="flipInterval" format="integer" min="0" /> @@ -2211,6 +2242,30 @@ <declare-styleable name="Spinner"> <!-- The prompt to display when the spinner's dialog is shown. --> <attr name="prompt" format="reference" /> + <!-- Display mode for spinner options. --> + <attr name="spinnerMode" format="enum"> + <!-- Spinner options will be presented to the user as a dialog window. --> + <enum name="dialog" value="0" /> + <!-- Spinner options will be presented to the user as an inline dropdown + anchored to the spinner widget itself. --> + <enum name="dropdown" value="1" /> + </attr> + <!-- List selector to use for spinnerMode="dropdown" display. --> + <attr name="dropDownSelector" /> + <!-- Background drawable to use for the dropdown in spinnerMode="dropdown". --> + <attr name="popupBackground" /> + <!-- Vertical offset from the spinner widget for positioning the dropdown in + spinnerMode="dropdown". --> + <attr name="dropDownVerticalOffset" /> + <!-- Horizontal offset from the spinner widget for positioning the dropdown + in spinnerMode="dropdown". --> + <attr name="dropDownHorizontalOffset" /> + <!-- Width of the dropdown in spinnerMode="dropdown". --> + <attr name="dropDownWidth" /> + <!-- Reference to a layout to use for displaying a prompt in the dropdown for + spinnerMode="dropdown". This layout must contain a TextView with the id + @android:id/text1 to be populated with the prompt text. --> + <attr name="popupPromptView" format="reference" /> </declare-styleable> <declare-styleable name="DatePicker"> <!-- The first year (inclusive), for example "1940". --> @@ -2521,6 +2576,10 @@ <attr name="drawable" /> </declare-styleable> + <declare-styleable name="MipmapDrawableItem"> + <attr name="drawable" /> + </declare-styleable> + <declare-styleable name="RotateDrawable"> <attr name="visible" /> <attr name="fromDegrees" format="float" /> @@ -3252,6 +3311,17 @@ <!-- Whether the item is enabled. --> <attr name="enabled" /> + <!-- Name of a method on the Context used to inflate the menu that will be + called when the item is clicked. --> + <attr name="onClick" /> + + <!-- How this item should display in the Action Bar, if present. --> + <attr name="showAsAction" format="enum"> + <enum name="never" value="0" /> + <enum name="ifRoom" value="1" /> + <enum name="always" value="2" /> + </attr> + </declare-styleable> <!-- **************************************************************** --> @@ -3354,6 +3424,16 @@ <attr name="entryValues" format="reference" /> </declare-styleable> + <declare-styleable name="MultiSelectListPreference"> + <!-- The human-readable array to present as a list. Each entry must have a corresponding + index in entryValues. --> + <attr name="entries" /> + <!-- The array to find the value to save for a preference when an entry from + entries is selected. If a user clicks the second item in entries, the + second item in this array will be saved to the preference. --> + <attr name="entryValues" /> + </declare-styleable> + <!-- Base attributes available to RingtonePreference. --> <declare-styleable name="RingtonePreference"> <!-- Which ringtone type(s) to show in the picker. --> @@ -3505,7 +3585,7 @@ <!-- AppWidget package class attributes --> <!-- =============================== --> <eat-comment /> - + <!-- Use <code>appwidget-provider</code> as the root tag of the XML resource that describes an AppWidget provider. See {@link android.appwidget android.appwidget} package for more info. @@ -3522,13 +3602,49 @@ <!-- A class name in the AppWidget's package to be launched to configure. If not supplied, then no activity will be launched. --> <attr name="configure" format="string" /> + <!-- A preview of what the AppWidget will look like after it's configured. + If not supplied, the AppWidget's icon will be used. --> + <attr name="previewImage" format="reference" /> </declare-styleable> <!-- =============================== --> <!-- App package class attributes --> <!-- =============================== --> <eat-comment /> - + + <!-- ============================= --> + <!-- View package class attributes --> + <!-- ============================= --> + <eat-comment /> + + <!-- Attributes that can be used with <code><fragment></code> + tags inside of the layout of an Activity. This instantiates + the given {@link android.app.Fragment} and inserts its content + view into the current location in the layout. --> + <declare-styleable name="Fragment"> + <!-- Supply the name of the fragment class to instantiate. --> + <attr name="name" /> + + <!-- Supply an identifier name for the top-level view, to later retrieve it + with {@link android.view.View#findViewById View.findViewById()} or + {@link android.app.Activity#findViewById Activity.findViewById()}. + This must be a + resource reference; typically you set this using the + <code>@+</code> syntax to create a new ID resources. + For example: <code>android:id="@+id/my_id"</code> which + allows you to later retrieve the view + with <code>findViewById(R.id.my_id)</code>. --> + <attr name="id" /> + + <!-- Supply a tag for the top-level view containing a String, to be retrieved + later with {@link android.view.View#getTag View.getTag()} or + searched for with {@link android.view.View#findViewWithTag + View.findViewWithTag()}. It is generally preferable to use + IDs (through the android:id attribute) instead of tags because + they are faster and allow for compile-time type checking. --> + <attr name="tag" /> + </declare-styleable> + <!-- Use <code>device-admin</code> as the root tag of the XML resource that describes a {@link android.app.admin.DeviceAdminReceiver}, which is @@ -3568,7 +3684,7 @@ <!-- Accounts package class attributes --> <!-- =============================== --> <eat-comment /> - + <!-- Use <code>account-authenticator</code> as the root tag of the XML resource that describes an account authenticator. --> @@ -3589,7 +3705,7 @@ <!-- Accounts package class attributes --> <!-- =============================== --> <eat-comment /> - + <!-- Use <code>account-authenticator</code> as the root tag of the XML resource that describes an account authenticator. --> @@ -3605,7 +3721,7 @@ <!-- Contacts meta-data attributes --> <!-- =============================== --> <eat-comment /> - + <!-- TODO: remove this deprecated styleable. --> <eat-comment /> <declare-styleable name="Icon"> @@ -3631,6 +3747,9 @@ <attr name="detailColumn" format="string" /> <!-- Flag indicating that detail should be built from SocialProvider. --> <attr name="detailSocialSummary" format="boolean" /> + <!-- Resource representing the term "All Contacts" (e.g. "All Friends" or + "All connections"). Optional (Default is "All Contacts"). --> + <attr name="allContactsName" format="string" /> </declare-styleable> <!-- =============================== --> @@ -3662,4 +3781,105 @@ <attr name="settingsActivity" /> </declare-styleable> + <!-- =============================== --> + <!-- Adapters attributes --> + <!-- =============================== --> + <eat-comment /> + + <!-- Adapter used to bind cursors. --> + <declare-styleable name="CursorAdapter"> + <!-- URI to get the cursor from. Optional. --> + <attr name="uri" format="string" /> + <!-- Selection statement for the query. Optional. --> + <attr name="selection" format="string" /> + <!-- Sort order statement for the query. Optional. --> + <attr name="sortOrder" format="string" /> + <!-- Layout resource used to display each row from the cursor. Mandatory. --> + <attr name="layout" /> + </declare-styleable> + + <!-- Attributes used in bind items for XML cursor adapters. --> + <declare-styleable name="CursorAdapter_BindItem"> + <!-- The name of the column to bind from. Mandatory. --> + <attr name="from" format="string" /> + <!-- The resource id of the view to bind to. Mandatory. --> + <attr name="to" format="reference" /> + <!-- The type of binding. If this value is not specified, the type will be + inferred from the type of the "to" target view. Mandatory. + + The type can be one of: + <ul> + <li>string, The content of the column is interpreted as a string.</li> + <li>image, The content of the column is interpreted as a blob describing an image.</li> + <li>image-uri, The content of the column is interpreted as a URI to an image.</li> + <li>drawable, The content of the column is interpreted as a resource id to a drawable.</li> + <li>A fully qualified class name, corresponding to an implementation of + android.widget.Adapters.CursorBinder.</li> + </ul> + --> + <attr name="as" format="string" /> + </declare-styleable> + + <!-- Attributes used in select items for XML cursor adapters. --> + <declare-styleable name="CursorAdapter_SelectItem"> + <!-- The name of the column to select. Mandatory. --> + <attr name="column" format="string" /> + </declare-styleable> + + <!-- Attributes used to map values to new values in XML cursor adapters' bind items. --> + <declare-styleable name="CursorAdapter_MapItem"> + <!-- The original value from the column. Mandatory. --> + <attr name="fromValue" format="string" /> + <!-- The new value from the column. Mandatory. --> + <attr name="toValue" format="string" /> + </declare-styleable> + + <!-- Attributes used to map values to new values in XML cursor adapters' bind items. --> + <declare-styleable name="CursorAdapter_TransformItem"> + <!-- The transformation expression. Mandatory if "withClass" is not specified. --> + <attr name="withExpression" format="string" /> + <!-- The transformation class, an implementation of + android.widget.Adapters.CursorTransformation. Mandatory if "withExpression" + is not specified. --> + <attr name="withClass" format="string" /> + </declare-styleable> + + <!-- Attributes used to style the Action Bar. --> + <declare-styleable name="ActionBar"> + <!-- The type of navigation to use. --> + <attr name="navigationMode"> + <!-- Normal static title text --> + <enum name="normal" value="0" /> + <!-- The action bar will use a drop-down selection in place of title text. --> + <enum name="dropdownList" value="1" /> + <!-- The action bar will use a series of horizontal tabs in place of title text. --> + <enum name="tabBar" value="2" /> + </attr> + <!-- Options affecting how the action bar is displayed. --> + <attr name="displayOptions"> + <flag name="useLogo" value="1" /> + <flag name="hideHome" value="2" /> + </attr> + <!-- Specifies the color used to style the action bar. --> + <attr name="colorFilter" format="color" /> + <!-- Specifies title text used for navigationMode="normal" --> + <attr name="title" /> + <!-- Specifies subtitle text used for navigationMode="normal" --> + <attr name="subtitle" format="string" /> + <!-- Specifies a style to use for title text. --> + <attr name="titleTextStyle" format="reference" /> + <!-- Specifies a style to use for subtitle text. --> + <attr name="subtitleTextStyle" format="reference" /> + <!-- Specifies the drawable used for the application icon. --> + <attr name="icon" /> + <!-- Specifies the drawable used for the application logo. --> + <attr name="logo" /> + <!-- Specifies the drawable used for item dividers. --> + <attr name="divider" /> + <!-- Specifies a background drawable for the action bar. --> + <attr name="background" /> + <!-- Specifies a layout for custom navigation. Overrides navigationMode. --> + <attr name="customNavigationLayout" format="reference" /> + </declare-styleable> + </resources> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index f18d14e..5e8c618 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -235,6 +235,10 @@ the safe mode. --> <attr name="vmSafeMode" format="boolean" /> + <!-- Flag indicating whether the application's rendering should be hardware + accelerated if possible. --> + <attr name="hardwareAccelerated" format="boolean" /> + <!-- Flag indicating whether the given application component is available to other applications. If false, it can only be accessed by applications with its same user id (which usually means only by @@ -734,6 +738,7 @@ <attr name="enabled" /> <attr name="debuggable" /> <attr name="vmSafeMode" /> + <attr name="hardwareAccelerated" /> <!-- Name of activity to be launched for managing the application's space on the device. --> <attr name="manageSpaceActivity" /> <attr name="allowClearUserData" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d565c68..689f73a 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -98,6 +98,9 @@ <item>"0,1"</item> </string-array> + <!-- The maximum duration (in milliseconds) we expect a network transition to take --> + <integer name="config_networkTransitionTimeout">60000</integer> + <!-- List of regexpressions describing the interface (if any) that represent tetherable USB interfaces. If the device doesn't want to support tething over USB this should be empty. An example would be "usb.*" --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 11f3e50..679e642 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -25,6 +25,9 @@ <!-- The standard size (both width and height) of an application icon that will be displayed in the app launcher and elsewhere. --> <dimen name="app_icon_size">48dip</dimen> + <!-- The standard size (both width and height) of an action icon that will + be displayed in application action bars. --> + <dimen name="action_icon_size">48dip</dimen> <dimen name="toast_y_offset">64dip</dimen> <!-- Height of the status bar --> <dimen name="status_bar_height">25dip</dimen> @@ -42,4 +45,6 @@ <dimen name="password_keyboard_key_height">56dip</dimen> <!-- Default correction for the space key in the password keyboard --> <dimen name="password_keyboard_spacebar_vertical_correction">4dip</dimen> + <!-- Distance between the text base line and virtual finger position used to position cursor --> + <dimen name="cursor_controller_vertical_offset">12dp</dimen> </resources> diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml index 8b6af71..e607fad 100644 --- a/core/res/res/values/ids.xml +++ b/core/res/res/values/ids.xml @@ -68,4 +68,5 @@ <item type="id" name="accountPreferences" /> <item type="id" name="smallIcon" /> <item type="id" name="custom" /> + <item type="id" name="home" /> </resources> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index bd65fee..d0be554 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1183,7 +1183,7 @@ <public type="attr" name="colorBackgroundCacheHint" id="0x010102ab" /> <public type="attr" name="dropDownHorizontalOffset" id="0x010102ac" /> <public type="attr" name="dropDownVerticalOffset" id="0x010102ad" /> - + <public type="style" name="Theme.Wallpaper" id="0x0103005e" /> <public type="style" name="Theme.Wallpaper.NoTitleBar" id="0x0103005f" /> <public type="style" name="Theme.Wallpaper.NoTitleBar.Fullscreen" id="0x01030060" /> @@ -1191,7 +1191,7 @@ <public type="style" name="Theme.Light.WallpaperSettings" id="0x01030062" /> <public type="style" name="TextAppearance.SearchResult.Title" id="0x01030063" /> <public type="style" name="TextAppearance.SearchResult.Subtitle" id="0x01030064" /> - + <!-- Semi-transparent background that can be used when placing a dark themed UI on top of some arbitrary background (such as the wallpaper). This darkens the background sufficiently that the UI @@ -1199,7 +1199,7 @@ <public type="drawable" name="screen_background_dark_transparent" id="0x010800a9" /> <public type="drawable" name="screen_background_light_transparent" id="0x010800aa" /> <public type="drawable" name="stat_notify_sdcard_prepare" id="0x010800ab" /> - + <!-- =============================================================== Resources added in version 6 of the platform (Eclair 2.0.1). =============================================================== --> @@ -1211,7 +1211,7 @@ <public type="attr" name="quickContactBadgeStyleSmallWindowSmall" id="0x010102b1" /> <public type="attr" name="quickContactBadgeStyleSmallWindowMedium" id="0x010102b2" /> <public type="attr" name="quickContactBadgeStyleSmallWindowLarge" id="0x010102b3" /> - + <!-- =============================================================== Resources added in version 7 of the platform (Eclair MR1). =============================================================== --> @@ -1220,7 +1220,7 @@ <public type="attr" name="author" id="0x010102b4" /> <public type="attr" name="autoStart" id="0x010102b5" /> - + <!-- =============================================================== Resources added in version 8 of the platform (Eclair MR2). =============================================================== --> @@ -1241,19 +1241,19 @@ <public type="attr" name="tabStripEnabled" id="0x010102bd" /> <public type="id" name="custom" id="0x0102002b" /> - + <public type="anim" name="cycle_interpolator" id="0x010a000c" /> <!-- =============================================================== - Resources introduced in kraken. + Resources introduced in Gingerbread. =============================================================== --> - + <eat-comment /> <public type="attr" name="logo" id="0x010102be" /> <public type="attr" name="xlargeScreens" id="0x010102bf" /> <public type="attr" name="heavyWeight" id="0x010102c0" /> <public type="attr" name="immersive" id="0x010102c1" /> <public-padding type="attr" name="kraken_resource_pad" end="0x01010300" /> - + <public-padding type="id" name="kraken_resource_pad" end="0x01020040" /> <public-padding type="anim" name="kraken_resource_pad" end="0x010a0020" /> @@ -1263,9 +1263,9 @@ <public type="drawable" name="presence_video_online" id="0x010800ae" /> <public type="drawable" name="presence_audio_away" id="0x010800af" /> <public type="drawable" name="presence_audio_busy" id="0x010800b0" /> - <public type="drawable" name="presence_audio_online" id="0x010800b1" /> + <public type="drawable" name="presence_audio_online" id="0x010800b1" /> <public-padding type="drawable" name="kraken_resource_pad" end="0x01080100" /> - + <public-padding type="style" name="kraken_resource_pad" end="0x01030090" /> <public-padding type="string" name="kraken_resource_pad" end="0x01040020" /> <public-padding type="integer" name="kraken_resource_pad" end="0x010e0010" /> @@ -1274,4 +1274,52 @@ <public-padding type="color" name="kraken_resource_pad" end="0x01060020" /> <public-padding type="array" name="kraken_resource_pad" end="0x01070010" /> +<!-- =============================================================== + Resources proposed for Honeycomb. + =============================================================== --> + <eat-comment /> + <public type="attr" name="adapter" /> + <public type="attr" name="selection" /> + <public type="attr" name="sortOrder" /> + <public type="attr" name="uri" /> + <public type="attr" name="from" /> + <public type="attr" name="to" /> + <public type="attr" name="as" /> + <public type="attr" name="fromValue" /> + <public type="attr" name="toValue" /> + <public type="attr" name="column" /> + <public type="attr" name="withExpression" /> + <public type="attr" name="withClass" /> + <public type="attr" name="allContactsName" /> + <public type="attr" name="windowActionBar" /> + <public type="attr" name="windowActionBarStyle" /> + <public type="attr" name="navigationMode" /> + <public type="attr" name="displayOptions" /> + <public type="attr" name="subtitle" /> + <public type="attr" name="customNavigationLayout" /> + <public type="attr" name="hardwareAccelerated" /> + <public type="attr" name="measureWithLargestChild" /> + <public type="attr" name="animateFirstView" /> + <public type="attr" name="dropDownSpinnerStyle" /> + <public type="attr" name="actionDropDownStyle" /> + <public type="attr" name="actionButtonStyle" /> + <public type="attr" name="showAsAction" /> + <public type="attr" name="actionButtonPadding" /> + <public type="attr" name="previewImage" /> + <public type="attr" name="actionBarContextBackground" /> + <public type="attr" name="actionBarCloseContextDrawable" /> + + <public type="id" name="home" /> + + <public type="style" name="Theme.WithActionBar" /> + <public type="style" name="Widget.Spinner.DropDown" /> + <public type="style" name="Widget.ActionButton" /> + + <!-- Standard content view for a {@link android.app.ListFragment}. + If you are implementing a subclass of ListFragment with your + own customized content, you can include this layout in that + content to still retain all of the standard functionality of + the base class. --> + <public type="layout" name="list_content" /> + </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 62fd169..7a80884 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1421,20 +1421,18 @@ <!-- Custom organization type --> <string name="orgTypeCustom">Custom</string> - <!-- Attbution of a contact status update, when the time of update is unknown --> - <string name="contact_status_update_attribution">via <xliff:g id="source" example="Google Talk">%1$s</xliff:g></string> - - <!-- Attbution of a contact status update, when the time of update is known --> - <string name="contact_status_update_attribution_with_date"><xliff:g id="date" example="3 hours ago">%1$s</xliff:g> via <xliff:g id="source" example="Google Talk">%2$s</xliff:g></string> - <!-- Instructions telling the user to enter their SIM PIN to unlock the keyguard. Displayed in one line in a large font. --> <string name="keyguard_password_enter_pin_code">Enter PIN code</string> - <!-- Instructions telling the user to enter their PIN password to unlock the keyguard. + <!-- Instructions telling the user to enter their text password to unlock the keyguard. Displayed in one line in a large font. --> <string name="keyguard_password_enter_password_code">Enter password to unlock</string> + <!-- Instructions telling the user to enter their PIN password to unlock the keyguard. + Displayed in one line in a large font. --> + <string name="keyguard_password_enter_pin_password_code">Enter PIN to unlock</string> + <!-- Instructions telling the user that they entered the wrong pin while trying to unlock the keyguard. Displayed in one line in a large font. --> <string name="keyguard_password_wrong_pin_code">Incorrect PIN code!</string> @@ -1471,6 +1469,8 @@ <string name="lockscreen_pattern_correct">Correct!</string> <!-- On the unlock pattern screen, shown when the user enters the wrong lock pattern and must try again. --> <string name="lockscreen_pattern_wrong">Sorry, try again</string> + <!-- On the unlock password screen, shown when the user enters the wrong lock password and must try again. --> + <string name="lockscreen_password_wrong">Sorry, try again</string> <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, show the current charge %. --> @@ -1514,12 +1514,27 @@ progress dialog in the meantime. this is the emssage. --> <string name="lockscreen_sim_unlock_progress_dialog_message">Unlocking SIM card\u2026</string> - <!-- For the unlock screen, Information message shown in dialog when user has too many failed attempts --> + <!-- For the unlock screen, Information message shown in dialog when user has too many failed attempts at + drawing the unlock pattern --> <string name="lockscreen_too_many_failed_attempts_dialog_message"> You have incorrectly drawn your unlock pattern <xliff:g id="number">%d</xliff:g> times. \n\nPlease try again in <xliff:g id="number">%d</xliff:g> seconds. </string> + <!-- For the unlock screen, Information message shown in dialog when user has too many failed attempts at + entering the password --> + <string name="lockscreen_too_many_failed_password_attempts_dialog_message"> + You have incorrectly entered your password <xliff:g id="number">%d</xliff:g> times. + \n\nPlease try again in <xliff:g id="number">%d</xliff:g> seconds. + </string> + + <!-- For the unlock screen, Information message shown in dialog when user has too many failed attempts at + entering the PIN --> + <string name="lockscreen_too_many_failed_pin_attempts_dialog_message"> + You have incorrectly entered your PIN <xliff:g id="number">%d</xliff:g> times. + \n\nPlease try again in <xliff:g id="number">%d</xliff:g> seconds. + </string> + <!-- For the unlock screen, Information message shown in dialog when user is almost at the limit where they will be locked out and may have to enter an alternate username/password to unlock the phone --> <string name="lockscreen_failed_attempts_almost_glogin"> @@ -1589,8 +1604,8 @@ <string name="factorytest_reboot">Reboot</string> <!-- Do not translate. WebView User Agent string --> - <string name="web_user_agent" translatable="false"><xliff:g id="x">Mozilla/5.0 (Linux; U; Android %s) - AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1</xliff:g></string> + <string name="web_user_agent" translatable="false">Mozilla/5.0 (Linux; U; <xliff:g id="x">Android %s</xliff:g>) + AppleWebKit/534.3 (KHTML, like Gecko) Version/4.0 <xliff:g id="mobile">%s</xliff:g>Safari/534.3</string> <!-- Title for a JavaScript dialog. "The page at <url of current page> says:" --> <string name="js_dialog_title">The page at \'<xliff:g id="title">%s</xliff:g>\' says:</string> @@ -1904,6 +1919,9 @@ combined with setIcon(android.R.drawable.ic_dialog_alert) --> <string name="dialog_alert_title">Attention</string> + <!-- Text shown by list fragment when waiting for data to display. --> + <string name="loading">Loading...</string> + <!-- Default text for a button that can be toggled on and off. --> <string name="capital_on">ON</string> <!-- Default text for a button that can be toggled on and off. --> @@ -2242,17 +2260,13 @@ <!-- Localized strings for WebView --> <!-- Label for button in a WebView that will open a chooser to choose a file to upload --> <string name="upload_file">Choose file</string> + <!-- Label for the file upload control when no file has been chosen yet --> + <string name="no_file_chosen">No file chosen</string> <!-- Label for <input type="reset"> button in html --> <string name="reset">Reset</string> <!-- Label for <input type="submit"> button in html --> <string name="submit">Submit</string> - <!-- String describing the Star/Favorite checkbox - - Used by AccessibilityService to announce the purpose of the view. - --> - <string name="description_star">favorite</string> - <!-- Strings for car mode notification --> <!-- Shown when car mode is enabled --> <string name="car_mode_disable_notification_title">Car mode enabled</string> @@ -2263,6 +2277,10 @@ <string name="tethered_notification_title">Tethering or hotspot active</string> <string name="tethered_notification_message">Touch to configure</string> + <!-- Strings for possible PreferenceActivity Back/Next buttons --> + <string name="back_button_label">Back</string> + <string name="next_button_label">Next</string> + <!-- Strings for throttling notification --> <!-- Shown when the user is in danger of being throttled --> <string name="throttle_warning_notification_title">High mobile data use</string> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 02a601a..3c09a89 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -458,6 +458,18 @@ <style name="Widget.Spinner"> <item name="android:background">@android:drawable/btn_dropdown</item> <item name="android:clickable">true</item> + <item name="android:spinnerMode">dialog</item> + + <item name="android:dropDownSelector">@android:drawable/list_selector_background</item> + <item name="android:popupBackground">@android:drawable/spinner_dropdown_background</item> + <item name="android:dropDownVerticalOffset">-10dip</item> + <item name="android:dropDownHorizontalOffset">0dip</item> + <item name="android:dropDownWidth">wrap_content</item> + <item name="android:popupPromptView">@android:layout/simple_dropdown_hint</item> + </style> + + <style name="Widget.Spinner.DropDown"> + <item name="android:spinnerMode">dropdown</item> </style> <style name="Widget.TextView.PopupMenu"> @@ -574,6 +586,7 @@ <item name="android:background">@android:drawable/quickcontact_badge</item> <item name="android:clickable">true</item> <item name="android:scaleType">fitCenter</item> + <item name="android:src">@android:drawable/ic_contact_picture</item> </style> <style name="Widget.QuickContactBadgeSmall"> @@ -613,7 +626,7 @@ <style name="TextAppearance"> <item name="android:textColor">?textColorPrimary</item> - <item name="android:textColorHighlight">#FFFF9200</item> + <item name="android:textColorHighlight">#D077A14B</item> <item name="android:textColorHint">?textColorHint</item> <item name="android:textColorLink">#5C5CFF</item> <item name="android:textSize">16sp</item> @@ -861,4 +874,13 @@ <item name="android:paddingBottom">1dip</item> <item name="android:background">@android:drawable/bottom_bar</item> </style> + + <style name="ActionBar"> + <item name="android:background">@android:drawable/action_bar_background</item> + <item name="android:displayOptions">useLogo</item> + <item name="android:divider">@android:drawable/action_bar_divider</item> + </style> + + <style name="Widget.ActionButton"> + </style> </resources> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index d585d9e..2dfe9ab 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -115,6 +115,8 @@ <item name="windowTitleBackgroundStyle">@android:style/WindowTitleBackground</item> <item name="android:windowAnimationStyle">@android:style/Animation.Activity</item> <item name="android:windowSoftInputMode">stateUnspecified|adjustUnspecified</item> + <item name="windowActionBar">false</item> + <item name="windowActionBarStyle">@android:style/ActionBar</item> <!-- Dialog attributes --> <item name="alertDialogStyle">@android:style/AlertDialog</item> @@ -167,6 +169,9 @@ <item name="scrollViewStyle">@android:style/Widget.ScrollView</item> <item name="horizontalScrollViewStyle">@android:style/Widget.HorizontalScrollView</item> <item name="spinnerStyle">@android:style/Widget.Spinner</item> + <item name="dropDownSpinnerStyle">@android:style/Widget.Spinner.DropDown</item> + <item name="actionDropDownStyle">@android:style/Widget.Spinner.DropDown</item> + <item name="actionButtonStyle">@android:style/Widget.ActionButton</item> <item name="starStyle">@android:style/Widget.CompoundButton.Star</item> <item name="tabWidgetStyle">@android:style/Widget.TabWidget</item> <item name="textViewStyle">@android:style/Widget.TextView</item> @@ -198,6 +203,11 @@ <!-- Search widget styles --> <item name="searchWidgetCorpusItemBackground">@android:color/search_widget_corpus_item_background</item> + + <!-- Action bar styles --> + <item name="actionButtonPadding">12dip</item> + <item name="actionBarContextBackground">@android:drawable/action_bar_context_background</item> + <item name="actionBarCloseContextDrawable">@android:drawable/ic_menu_close_clear_cancel</item> </style> <!-- Variant of the default (dark) theme with no title bar --> @@ -521,5 +531,9 @@ <item name="android:windowAnimationStyle">@android:style/Animation.RecentApplications</item> <item name="android:textColor">@android:color/secondary_text_nofocus</item> </style> + + <style name="Theme.WithActionBar"> + <item name="android:windowActionBar">true</item> + </style> </resources> diff --git a/core/tests/coretests/res/raw/v21_backslash.vcf b/core/tests/coretests/res/raw/v21_backslash.vcf deleted file mode 100644 index bd3002b..0000000 --- a/core/tests/coretests/res/raw/v21_backslash.vcf +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-N:;A\;B\\;C\\\;;D;\:E;\\\\;
-FN:A;B\C\;D:E\\
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_complicated.vcf b/core/tests/coretests/res/raw/v21_complicated.vcf deleted file mode 100644 index de34e16..0000000 --- a/core/tests/coretests/res/raw/v21_complicated.vcf +++ /dev/null @@ -1,106 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-N:Gump;Forrest;Hoge;Pos;Tao
-FN:Joe Due
-ORG:Gump Shrimp Co.;Sales Dept.\;Manager;Fish keeper
-ROLE:Fish Cake Keeper!
-X-CLASS:PUBLIC
-TITLE:Shrimp Man
-TEL;WORK;VOICE:(111) 555-1212
-TEL;HOME;VOICE:(404) 555-1212
-TEL;CELL:0311111111
-TEL;VIDEO:0322222222
-TEL;VOICE:0333333333
-ADR;WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
-LABEL;WORK;ENCODING=QUOTED-PRINTABLE:100 Waters Edge=0D=0ABaytown, LA 30314=0D=0AUnited States of America
-ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
-LABEL;HOME;ENCODING=QUOTED-PRINTABLE:42 Plantation St.=0D=0A=
-Baytown, LA 30314=0D=0A=
-United States of America
-EMAIL;PREF;INTERNET:forrestgump@walladalla.com
-EMAIL;CELL:cell@example.com
-NOTE:The following note is the example from RFC 2045.
-NOTE;ENCODING=QUOTED-PRINTABLE:Now's the time =
-for all folk to come=
- to the aid of their country.
-
-PHOTO;ENCODING=BASE64;TYPE=JPEG:
- /9j/4QoPRXhpZgAATU0AKgAAAAgADQEOAAIAAAAPAAAAqgEPAAIAAAAHAAAAugEQAAIAAAAG
- AAAAwgESAAMAAAABAAEAAAEaAAUAAAABAAAAyAEbAAUAAAABAAAA0AEoAAMAAAABAAIAAAEx
- AAIAAAAOAAAA2AEyAAIAAAAUAAAA5gITAAMAAAABAAEAAIKYAAIAAAAOAAAA+odpAAQAAAAB
- AAABhMSlAAcAAAB8AAABCAAABB4yMDA4MTAyOTEzNTUzMQAARG9Db01vAABEOTA1aQAAAABI
- AAAAAQAAAEgAAAABRDkwNWkgVmVyMS4wMAAyMDA4OjEwOjI5IDEzOjU1OjQ3ACAgICAgICAg
- ICAgICAAUHJpbnRJTQAwMzAwAAAABgABABQAFAACAQAAAAADAAAANAEABQAAAAEBAQAAAAEQ
- gAAAAAAAEQkAACcQAAAPCwAAJxAAAAWXAAAnEAAACLAAACcQAAAcAQAAJxAAAAJeAAAnEAAA
- AIsAACcQAAADywAAJxAAABvlAAAnEAAogpoABQAAAAEAAANqgp0ABQAAAAEAAANyiCIAAwAA
- AAEAAgAAkAAABwAAAAQwMjIwkAMAAgAAABQAAAN6kAQAAgAAABQAAAOOkQEABwAAAAQBAgMA
- kQIABQAAAAEAAAOikgEACgAAAAEAAAOqkgIABQAAAAEAAAOykgQACgAAAAEAAAO6kgUABQAA
- AAEAAAPCkgcAAwAAAAEAAgAAkggAAwAAAAEAAAAAkgkAAwAAAAEAAAAAkgoABQAAAAEAAAPK
- knwABwAAAAEAAAAAkoYABwAAABYAAAPSoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIAAwAA
- AAEAYAAAoAMAAwAAAAEASAAAoAUABAAAAAEAAAQAog4ABQAAAAEAAAPoog8ABQAAAAEAAAPw
- ohAAAwAAAAEAAgAAohcAAwAAAAEAAgAAowAABwAAAAEDAAAAowEABwAAAAEBAAAApAEAAwAA
- AAEAAAAApAIAAwAAAAEAAAAApAMAAwAAAAEAAAAApAQABQAAAAEAAAP4pAUAAwAAAAEAHQAA
- pAYAAwAAAAEAAAAApAcAAwAAAAEAAAAApAgAAwAAAAEAAAAApAkAAwAAAAEAAAAApAoAAwAA
- AAEAAAAApAwAAwAAAAEAAgAAAAAAAAAAAFMAACcQAAABXgAAAGQyMDA4OjEwOjI5IDEzOjU1
- OjMxADIwMDg6MTA6MjkgMTM6NTU6NDcAAAApiAAAGwAAAAKyAAAAZAAAAV4AAABkAAAAAAAA
- AGQAAAAlAAAACgAADpIAAAPoAAAAAAAAAAAyMDA4MTAyOTEzNTUzMQAAICoAAAAKAAAq4gAA
- AAoAAAAAAAAAAQACAAEAAgAAAARSOTgAAAIABwAAAAQwMTAwAAAAAAAGAQMAAwAAAAEABgAA
- ARoABQAAAAEAAARsARsABQAAAAEAAAR0ASgAAwAAAAEAAgAAAgEABAAAAAEAAAR8AgIABAAA
- AAEAAAWLAAAAAAAAAEgAAAABAAAASAAAAAH/2P/bAIQAIBYYHBgUIBwaHCQiICYwUDQwLCww
- YkZKOlB0Znp4cmZwboCQuJyAiK6KbnCg2qKuvsTO0M58muLy4MjwuMrOxgEiJCQwKjBeNDRe
- xoRwhMbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbG
- /8AAEQgAeACgAwEhAAIRAQMRAf/EAaIAAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKCxAA
- AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK
- FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG
- h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
- 5ufo6erx8vP09fb3+Pn6AQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgsRAAIBAgQEAwQH
- BQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBka
- JicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKT
- lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz
- 9PX29/j5+v/aAAwDAQACEQMRAD8AFFSqKkZIoqRVpgSKKeBTEOApwFADsUYpgIRSEUANIppF
- ICNhUTCgCMio2FICJhULCgC0oqVaAJFFSqKBkgFOApiHCnCgB2KMUCENJQA0imEUDGMKiYUA
- RtUbUgIWqJhQBZSpFoAlWpVoGPFPFMQ7tSK2ODQA4yKO9HmKe9FxAzDHFIOlAAaYaAGNUTUD
- ImqNqQETVE1AE6VKKAJFNSqaAHg08GmANIFFQM5Y5qJMBuT60ZNQIcrkVYSQMKuLGKaaasQx
- qiagZE1RtSAjaomoAkQ1KpoAlU1IpoAkU07OBTArO+5qkV12Y71lfUBmaKkCRSuznrTFba2a
- oCwGyM0E1qIjY1GxoGRNUZNICNqiagByGplNAEimpFNMB4YDvSucpxSYEIU04KazsAu1qArU
- WELtPpTSposBNETt5pxNaoCNjUbGgCNjUZoGRtUTUgFU1KpoAkBqQHigCFnO7rUqOdlZp6gA
- c+tODn1pXAXzD60eYfWncQvmNSGQ07gOMhCVEJGz1ptgS5yKYxqwGE1GxoAiamGkMapqVTQB
- Kpp+eKAICfmqWM/Kaz6gANOBqQFzRmmAuaTNACsfkqMHmm9wJs8U0mtRDGNRsaAI2phpDI1N
- SqaAJFNSA8UCISfmqSM/Kaz6jAHmnA1ICg0uaAFzSZpgKx+SmDrTe4E2eKaTWoiMmmMaAIzT
- DSGRKakU0ASKaeDTERseakjPyms+oxAacDUgOBpc0gFzSZpgOY/KKYv3qrqIlpprQBjGoyaA
- GGmmkMgU1IppgPBqQGgQu0Gn4wvFKwEQpwNZDHZpc0ALmigRKBleaQKBWtgA001QDGqM0gGm
- mGkMrqakBoAepp4NMRIDTwaAE2A008GokgHxjd1pzKFpW0uAg5NSBBTirgOpDWgDTTTQAw0w
- 0gGGmmgZWBp4pASKaeDTEOBp4NADwajbrUyBEkXWnSUdAGr1qeiAMSkNWAhphoAaaYaQDDTT
- SGVRTwaYDxTwaBDwaeDQA4GlK5oauIeo20pGaLaAKqgU6hKwBSGmAhphoAaaYaQxhpppDKgN
- PFMB4p4oEPFOBpgPBp4NAhwpwoAWloAKSgBDTTQMYaYaQDTTTSGA/9n/2wCEAAoHBwgHBgoI
- CAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9
- PjsBCgsLDg0OHBAQHDsoIig7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7
- Ozs7Ozs7Ozs7Ozs7O//AABEIAEgAYAMBIQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAA
- AQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNC
- scEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp
- anN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS
- 09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcI
- CQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVi
- ctEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4
- eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY
- 2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AJ7SLgcVr20I4rNFGvbQAAHFaEUX
- SrQi5HCMdKk8oY6VSJYx4hjpVaWMelAFC4iGDxWPdR8mkxmRdxjBrEvI+tZjN20xtHNbNqAc
- UIDXg6Cr0WKtCY8XKQOyzOB3FKNWsyceZ+lS6sY6NkjvPSdwImBHUmmy4q076oCjOODWPdgc
- 0MpGPdAYNYl4o5rNjNKzkyorZtXxihAa1vIDip7m9Frb7/4jwKcnyxbEzN3ieJppZsyZ4U1H
- urzZau4mWVlNrGk0UuWPVa1YroXEIkHfrXZh5W90RWncAHmsi6bJNdQ0ZNw3BrGuiMGs2Mks
- puBzWzbzdOaEBeOpR2oUtkk9hTru7iuo4m8wgemKyqTi04sBsfkEf68j8KlUQZz9o/SuZRj3
- JYriAji4/Sp7W6htbV2aXcu70ramoxle4gN7HcIXjbis+4k5NdaaauhmVcv1rHuW61DGiG1m
- 6c1s20/TmgAv5vmj57VKk3+ixnPc1xVV70h9CVJuOtSrL71hFgxzScUkkn+iY/2q1i9xDrGT
- 9y31pJ5Otd1L+GhMy7mTrWXO2SapjRn28vTmta3nxjmgGOvJd2w1Kkv+ipz/ABGuOoveYdCe
- ObjrU6y5rlsA8ycUksn+ij/eNaw6iJLNsW59zTJn6816FP4EJmbO+Saz5m602UjIgk4HNadv
- LwKaBl+MpIMOMipp490SCJeF7CoqQvF2JuRqWQ4YEGrSiQJuKnHrXByMpki73GFBNXIoh9n2
- SrnnOK6MPTbd3sSwIVF2qMCqkzHmuy1lYRnTHrVGWpZaMKB+BWlbycYoQM0IZDxzV+GU8c1a
- IYy5Y+dnHatAsfsAHfArmS1mPoh1gT8x9qtk1rQX7tCe5DIapzGtGBQm71SlqGWjnIH6Vowt
- zmhAy/E3vV6F6tEMuxlWIyAfrVxCCAO1VZEEyYA4AApxNGwyJ+lVJRUsaKMw61SlFQzRAP/Z
-
-X-ATTRIBUTE:Some String
-BDAY:19800101
-GEO:35.6563854,139.6994233
-URL:http://www.example.com/
-REV:20080424T195243Z
-END:VCARD
\ No newline at end of file diff --git a/core/tests/coretests/res/raw/v21_invalid_comment_line.vcf b/core/tests/coretests/res/raw/v21_invalid_comment_line.vcf deleted file mode 100644 index f910710..0000000 --- a/core/tests/coretests/res/raw/v21_invalid_comment_line.vcf +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN:vCard
-VERSION:2.1
-UID:357
-N:;Conference Call
-FN:Conference Call
-# This line must be ignored.
-NOTE;ENCODING=QUOTED-PRINTABLE:This is an (sharp ->=
-#<- sharp) example. This message must NOT be ignored.
-# This line must be ignored too.
-END:vCard
diff --git a/core/tests/coretests/res/raw/v21_japanese_1.vcf b/core/tests/coretests/res/raw/v21_japanese_1.vcf deleted file mode 100644 index d05e2ff..0000000 --- a/core/tests/coretests/res/raw/v21_japanese_1.vcf +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh;;;;
-SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ;;;;
-TEL;PREF;VOICE:0300000000
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_japanese_2.vcf b/core/tests/coretests/res/raw/v21_japanese_2.vcf deleted file mode 100644 index fa54acb..0000000 --- a/core/tests/coretests/res/raw/v21_japanese_2.vcf +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-FN;CHARSET=SHIFT_JIS:ˆÀ“¡ ƒƒCƒh 1
-N;CHARSET=SHIFT_JIS:ˆÀ“¡;ƒƒCƒh1;;;
-SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³;Û²ÄÞ1;;;
-ADR;HOME;CHARSET=SHIFT_JIS;ENCODING=QUOTED-PRINTABLE:;=93=8C=8B=9E=93=73=
-=8F=61=92=4A=8B=E6=8D=F7=8B=75=92=AC26-1=83=5A=83=8B=83=8A=83=41=83=93=
-=83=5E=83=8F=81=5B6=8A=4B;;;;150-8512;
-NOTE;CHARSET=SHIFT_JIS;ENCODING=QUOTED-PRINTABLE:=83=81=83=82
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_multiple_entry.vcf b/core/tests/coretests/res/raw/v21_multiple_entry.vcf deleted file mode 100644 index ebbb19a..0000000 --- a/core/tests/coretests/res/raw/v21_multiple_entry.vcf +++ /dev/null @@ -1,33 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh3;;;;
-SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ3;;;;
-TEL;X-NEC-SECRET:9
-TEL;X-NEC-HOTEL:10
-TEL;X-NEC-SCHOOL:11
-TEL;HOME;FAX:12
-END:VCARD
-
-
-BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh4;;;;
-SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ4;;;;
-TEL;MODEM:13
-TEL;PAGER:14
-TEL;X-NEC-FAMILY:15
-TEL;X-NEC-GIRL:16
-END:VCARD
-
-
-BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=SHIFT_JIS:ˆÀ“¡ƒƒCƒh5;;;;
-SOUND;X-IRMC-N;CHARSET=SHIFT_JIS:±ÝÄÞ³Û²ÄÞ5;;;;
-TEL;X-NEC-BOY:17
-TEL;X-NEC-FRIEND:18
-TEL;X-NEC-PHS:19
-TEL;X-NEC-RESTAURANT:20
-END:VCARD
-
-
diff --git a/core/tests/coretests/res/raw/v21_org_before_title.vcf b/core/tests/coretests/res/raw/v21_org_before_title.vcf deleted file mode 100644 index 8ff1190..0000000 --- a/core/tests/coretests/res/raw/v21_org_before_title.vcf +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-FN:Normal Guy
-ORG:Company;Organization;Devision;Room;Sheet No.
-TITLE:Excellent Janitor
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_pref_handling.vcf b/core/tests/coretests/res/raw/v21_pref_handling.vcf deleted file mode 100644 index 5105310..0000000 --- a/core/tests/coretests/res/raw/v21_pref_handling.vcf +++ /dev/null @@ -1,15 +0,0 @@ -BEGIN:VCARD -VERSION:2.1 -FN:Smith -TEL;HOME:1 -TEL;WORK;PREF:2 -TEL;ISDN:3 -EMAIL;PREF;HOME:test@example.com -EMAIL;CELL;PREF:test2@examination.com -ORG:Company -TITLE:Engineer -ORG:Mystery -TITLE:Blogger -ORG:Poetry -TITLE:Poet -END:VCARD diff --git a/core/tests/coretests/res/raw/v21_simple_1.vcf b/core/tests/coretests/res/raw/v21_simple_1.vcf deleted file mode 100644 index 6aabb4c..0000000 --- a/core/tests/coretests/res/raw/v21_simple_1.vcf +++ /dev/null @@ -1,3 +0,0 @@ -BEGIN:VCARD
-N:Ando;Roid;
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_simple_2.vcf b/core/tests/coretests/res/raw/v21_simple_2.vcf deleted file mode 100644 index f0d5ab5..0000000 --- a/core/tests/coretests/res/raw/v21_simple_2.vcf +++ /dev/null @@ -1,3 +0,0 @@ -BEGIN:VCARD
-FN:Ando Roid
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_simple_3.vcf b/core/tests/coretests/res/raw/v21_simple_3.vcf deleted file mode 100644 index beddabb..0000000 --- a/core/tests/coretests/res/raw/v21_simple_3.vcf +++ /dev/null @@ -1,4 +0,0 @@ -BEGIN:VCARD
-N:Ando;Roid;
-FN:Ando Roid
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_title_before_org.vcf b/core/tests/coretests/res/raw/v21_title_before_org.vcf deleted file mode 100644 index 9fdc738..0000000 --- a/core/tests/coretests/res/raw/v21_title_before_org.vcf +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-FN:Nice Guy
-TITLE:Cool Title
-ORG:Marverous;Perfect;Great;Good;Bad;Poor
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v21_winmo_65.vcf b/core/tests/coretests/res/raw/v21_winmo_65.vcf deleted file mode 100644 index f380d0d..0000000 --- a/core/tests/coretests/res/raw/v21_winmo_65.vcf +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN:VCARD
-VERSION:2.1
-N:Example;;;;
-FN:Example
-ANNIVERSARY;VALUE=DATE:20091010
-AGENT:Invalid line which must be handled correctly.
-X-CLASS:PUBLIC
-X-REDUCTION:
-X-NO:
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v30_comma_separated.vcf b/core/tests/coretests/res/raw/v30_comma_separated.vcf deleted file mode 100644 index 98a7f20..0000000 --- a/core/tests/coretests/res/raw/v30_comma_separated.vcf +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN:VCARD
-VERSION:3.0
-N:F;G;M;;
-TEL;TYPE=PAGER,WORK,MSG:6101231234@pagersample.com
-END:VCARD
diff --git a/core/tests/coretests/res/raw/v30_simple.vcf b/core/tests/coretests/res/raw/v30_simple.vcf deleted file mode 100644 index 418661f..0000000 --- a/core/tests/coretests/res/raw/v30_simple.vcf +++ /dev/null @@ -1,13 +0,0 @@ -BEGIN:VCARD
-VERSION:3.0
-FN:And Roid
-N:And;Roid;;;
-ORG:Open;Handset; Alliance
-SORT-STRING:android
-TEL;TYPE=PREF;TYPE=VOICE:0300000000
-CLASS:PUBLIC
-X-GNO:0
-X-GN:group0
-X-REDUCTION:0
-REV:20081031T065854Z
-END:VCARD
diff --git a/core/tests/coretests/src/android/accessibilityservice/AccessibilityTestService.java b/core/tests/coretests/src/android/accessibilityservice/AccessibilityTestService.java deleted file mode 100644 index 2a51eea..0000000 --- a/core/tests/coretests/src/android/accessibilityservice/AccessibilityTestService.java +++ /dev/null @@ -1,167 +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.accessibilityservice; - -import android.accessibilityservice.AccessibilityService; -import android.accessibilityservice.AccessibilityServiceInfo; -import android.app.Notification; -import android.util.Log; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import java.util.Timer; -import java.util.TimerTask; - -/** - * This class text the accessibility framework end to end. - * <p> - * Note: Since accessibility is provided by {@link AccessibilityService}s we create one, - * and it generates an event and an interruption dispatching them through the - * {@link AccessibilityManager}. We verify the received result. To trigger the test - * go to Settings->Accessibility and select the enable accessibility check and then - * select the check for this service (same name as the class). - */ -public class AccessibilityTestService extends AccessibilityService { - - private static final String LOG_TAG = "AccessibilityTestService"; - - private static final String CLASS_NAME = "foo.bar.baz.Test"; - private static final String PACKAGE_NAME = "foo.bar.baz"; - private static final String TEXT = "Some stuff"; - private static final String BEFORE_TEXT = "Some other stuff"; - - private static final String CONTENT_DESCRIPTION = "Content description"; - - private static final int ITEM_COUNT = 10; - private static final int CURRENT_ITEM_INDEX = 1; - private static final int INTERRUPT_INVOCATION_TYPE = 0x00000200; - - private static final int FROM_INDEX = 1; - private static final int ADDED_COUNT = 2; - private static final int REMOVED_COUNT = 1; - - private static final int NOTIFICATION_TIMEOUT_MILLIS = 80; - - private int mReceivedResult; - - private Timer mTimer = new Timer(); - - @Override - public void onServiceConnected() { - AccessibilityServiceInfo info = new AccessibilityServiceInfo(); - info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; - info.feedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE; - info.notificationTimeout = NOTIFICATION_TIMEOUT_MILLIS; - info.flags &= AccessibilityServiceInfo.DEFAULT; - setServiceInfo(info); - - // we need to wait until the system picks our configuration - // otherwise it will not notify us - mTimer.schedule(new TimerTask() { - @Override - public void run() { - try { - testAccessibilityEventDispatching(); - testInterrupt(); - } catch (Exception e) { - Log.e(LOG_TAG, "Error in testing Accessibility feature", e); - } - } - }, 1000); - } - - /** - * Check here if the event we received is actually the one we sent. - */ - @Override - public void onAccessibilityEvent(AccessibilityEvent event) { - assert(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED == event.getEventType()); - assert(event != null); - assert(event.getEventTime() > 0); - assert(CLASS_NAME.equals(event.getClassName())); - assert(PACKAGE_NAME.equals(event.getPackageName())); - assert(1 == event.getText().size()); - assert(TEXT.equals(event.getText().get(0))); - assert(BEFORE_TEXT.equals(event.getBeforeText())); - assert(event.isChecked()); - assert(CONTENT_DESCRIPTION.equals(event.getContentDescription())); - assert(ITEM_COUNT == event.getItemCount()); - assert(CURRENT_ITEM_INDEX == event.getCurrentItemIndex()); - assert(event.isEnabled()); - assert(event.isPassword()); - assert(FROM_INDEX == event.getFromIndex()); - assert(ADDED_COUNT == event.getAddedCount()); - assert(REMOVED_COUNT == event.getRemovedCount()); - assert(event.getParcelableData() != null); - assert(1 == ((Notification) event.getParcelableData()).icon); - - // set the type of the receved request - mReceivedResult = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED; - } - - /** - * Set a flag that we received the interrupt request. - */ - @Override - public void onInterrupt() { - - // set the type of the receved request - mReceivedResult = INTERRUPT_INVOCATION_TYPE; - } - - /** - * If an {@link AccessibilityEvent} is sent and received correctly. - */ - public void testAccessibilityEventDispatching() throws Exception { - AccessibilityEvent event = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); - - assert(event != null); - event.setClassName(CLASS_NAME); - event.setPackageName(PACKAGE_NAME); - event.getText().add(TEXT); - event.setBeforeText(BEFORE_TEXT); - event.setChecked(true); - event.setContentDescription(CONTENT_DESCRIPTION); - event.setItemCount(ITEM_COUNT); - event.setCurrentItemIndex(CURRENT_ITEM_INDEX); - event.setEnabled(true); - event.setPassword(true); - event.setFromIndex(FROM_INDEX); - event.setAddedCount(ADDED_COUNT); - event.setRemovedCount(REMOVED_COUNT); - event.setParcelableData(new Notification(1, "Foo", 1234)); - - AccessibilityManager.getInstance(this).sendAccessibilityEvent(event); - - assert(mReceivedResult == event.getEventType()); - - Log.i(LOG_TAG, "AccessibilityTestService#testAccessibilityEventDispatching: Success"); - } - - /** - * If accessibility feedback interruption is triggered and received correctly. - */ - public void testInterrupt() throws Exception { - AccessibilityManager.getInstance(this).interrupt(); - - assert(INTERRUPT_INVOCATION_TYPE == mReceivedResult); - - Log.i(LOG_TAG, "AccessibilityTestService#testInterrupt: Success"); - } -} - diff --git a/core/tests/coretests/src/android/database/DatabaseCursorTest.java b/core/tests/coretests/src/android/database/DatabaseCursorTest.java index fb5a36f..0265c87 100644 --- a/core/tests/coretests/src/android/database/DatabaseCursorTest.java +++ b/core/tests/coretests/src/android/database/DatabaseCursorTest.java @@ -92,43 +92,6 @@ public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTe } @MediumTest - public void testCursorUpdate() { - mDatabase.execSQL( - "CREATE TABLE test (_id INTEGER PRIMARY KEY, d INTEGER, s INTEGER);"); - for(int i = 0; i < 20; i++) { - mDatabase.execSQL("INSERT INTO test (d, s) VALUES (" + i + - "," + i%2 + ");"); - } - - Cursor c = mDatabase.query("test", null, "s = 0", null, null, null, null); - int dCol = c.getColumnIndexOrThrow("d"); - int sCol = c.getColumnIndexOrThrow("s"); - - int count = 0; - while (c.moveToNext()) { - assertTrue(c.updateInt(dCol, 3)); - count++; - } - assertEquals(10, count); - - assertTrue(c.commitUpdates()); - - assertTrue(c.requery()); - - count = 0; - while (c.moveToNext()) { - assertEquals(3, c.getInt(dCol)); - count++; - } - - assertEquals(10, count); - assertTrue(c.moveToFirst()); - assertTrue(c.deleteRow()); - assertEquals(9, c.getCount()); - c.close(); - } - - @MediumTest public void testBlob() throws Exception { // create table mDatabase.execSQL( @@ -164,24 +127,7 @@ public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTe assertTrue(Arrays.equals(blob, cBlob)); assertEquals(s, c.getString(sCol)); assertEquals((double)d, c.getDouble(dCol)); - assertEquals((long)l, c.getLong(lCol)); - - // new byte[] - byte[] newblob = new byte[1000]; - value = 98; - Arrays.fill(blob, value); - - c.updateBlob(bCol, newblob); - cBlob = c.getBlob(bCol); - assertTrue(Arrays.equals(newblob, cBlob)); - - // commit - assertTrue(c.commitUpdates()); - assertTrue(c.requery()); - c.moveToNext(); - cBlob = c.getBlob(bCol); - assertTrue(Arrays.equals(newblob, cBlob)); - c.close(); + assertEquals((long)l, c.getLong(lCol)); } @MediumTest diff --git a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java index 656029d..e33421a 100644 --- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java +++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java @@ -21,18 +21,20 @@ import static android.database.DatabaseUtils.InsertHelper.TABLE_INFO_PRAGMA_DEFA import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; import android.os.Handler; import android.os.Parcel; import android.test.AndroidTestCase; import android.test.PerformanceTestCase; +import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; +import android.util.Pair; import junit.framework.Assert; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; @@ -467,45 +469,6 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT } @MediumTest - public void testNotificationTest1() throws Exception { - /* - Cursor c = mContentResolver.query(Notes.CONTENT_URI, - new String[] {Notes._ID, Notes.NOTE}, - null, null); - c.registerContentObserver(new MyContentObserver(true)); - int count = c.count(); - - MyContentObserver observer = new MyContentObserver(false); - mContentResolver.registerContentObserver(Notes.CONTENT_URI, true, observer); - - Uri uri; - - HashMap<String, String> values = new HashMap<String, String>(); - values.put(Notes.NOTE, "test note1"); - uri = mContentResolver.insert(Notes.CONTENT_URI, values); - assertEquals(1, mCursorNotificationCount); - assertEquals(1, mNotificationCount); - - c.requery(); - assertEquals(count + 1, c.count()); - c.first(); - assertEquals("test note1", c.getString(c.getColumnIndex(Notes.NOTE))); - c.updateString(c.getColumnIndex(Notes.NOTE), "test note2"); - c.commitUpdates(); - - assertEquals(2, mCursorNotificationCount); - assertEquals(2, mNotificationCount); - - mContentResolver.delete(uri, null); - - assertEquals(3, mCursorNotificationCount); - assertEquals(3, mNotificationCount); - - mContentResolver.unregisterContentObserver(observer); - */ - } - - @MediumTest public void testSelectionArgs() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); ContentValues values = new ContentValues(1); @@ -989,21 +952,6 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT } @MediumTest - public void testDbCloseReleasingAllCachedSql() { - mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + - "num1 INTEGER, num2 INTEGER, image BLOB);"); - final String statement = "DELETE FROM test WHERE _id=?;"; - SQLiteStatement statementDoNotClose = mDatabase.compileStatement(statement); - assertTrue(statementDoNotClose.getUniqueId() > 0); - int nStatement = statementDoNotClose.getUniqueId(); - assertTrue(statementDoNotClose.getUniqueId() == nStatement); - /* do not close statementDoNotClose object. - * That should leave it in SQLiteDatabase.mPrograms. - * mDatabase.close() in tearDown() should release it. - */ - } - - @MediumTest public void testSemicolonsInStatements() throws Exception { mDatabase.execSQL("CREATE TABLE pragma_test (" + "i INTEGER DEFAULT 1234, " + @@ -1023,6 +971,34 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT } } + @MediumTest + public void testUnionsWithBindArgs() { + /* make sure unions with bindargs work http://b/issue?id=1061291 */ + mDatabase.execSQL("CREATE TABLE A (i int);"); + mDatabase.execSQL("create table B (k int);"); + mDatabase.execSQL("create table C (n int);"); + mDatabase.execSQL("insert into A values(1);"); + mDatabase.execSQL("insert into A values(2);"); + mDatabase.execSQL("insert into A values(3);"); + mDatabase.execSQL("insert into B values(201);"); + mDatabase.execSQL("insert into B values(202);"); + mDatabase.execSQL("insert into B values(203);"); + mDatabase.execSQL("insert into C values(901);"); + mDatabase.execSQL("insert into C values(902);"); + String s = "select i from A where i > 2 " + + "UNION select k from B where k > 201 " + + "UNION select n from C where n !=900;"; + Cursor c = mDatabase.rawQuery(s, null); + int n = c.getCount(); + c.close(); + String s1 = "select i from A where i > ? " + + "UNION select k from B where k > ? " + + "UNION select n from C where n != ?;"; + Cursor c1 = mDatabase.rawQuery(s1, new String[]{"2", "201", "900"}); + assertEquals(n, c1.getCount()); + c1.close(); + } + /** * This test is available only when the platform has a locale with the language "ja". * It finishes without failure when it is not available. @@ -1108,5 +1084,113 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT } } } - } + } + + @SmallTest + public void testSetMaxCahesize() { + mDatabase.execSQL("CREATE TABLE test (i int, j int);"); + mDatabase.execSQL("insert into test values(1,1);"); + // set cache size + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + + // try reduce cachesize + try { + mDatabase.setMaxSqlCacheSize(1); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("cannot set cacheSize to a value less than")); + } + } + + @LargeTest + public void testDefaultDatabaseErrorHandler() { + DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler(); + + // close the database. and call corruption handler. + // it should delete the database file. + File dbfile = new File(mDatabase.getPath()); + mDatabase.close(); + assertFalse(mDatabase.isOpen()); + assertTrue(dbfile.exists()); + try { + errorHandler.onCorruption(mDatabase); + assertFalse(dbfile.exists()); + } catch (Exception e) { + fail("unexpected"); + } + + // create an in-memory database. and corruption handler shouldn't try to delete it + SQLiteDatabase memoryDb = SQLiteDatabase.openOrCreateDatabase(":memory:", null); + assertNotNull(memoryDb); + memoryDb.close(); + assertFalse(memoryDb.isOpen()); + try { + errorHandler.onCorruption(memoryDb); + } catch (Exception e) { + fail("unexpected"); + } + + // create a database, keep it open, call corruption handler. database file should be deleted + SQLiteDatabase dbObj = SQLiteDatabase.openOrCreateDatabase(mDatabase.getPath(), null); + assertTrue(dbfile.exists()); + assertNotNull(dbObj); + assertTrue(dbObj.isOpen()); + try { + errorHandler.onCorruption(dbObj); + assertFalse(dbfile.exists()); + } catch (Exception e) { + fail("unexpected"); + } + + // create a database, attach 2 more databases to it + // attached database # 1: ":memory:" + // attached database # 2: mDatabase.getPath() + "1"; + // call corruption handler. database files including the one for attached database # 2 + // should be deleted + String attachedDb1File = mDatabase.getPath() + "1"; + dbObj = SQLiteDatabase.openOrCreateDatabase(mDatabase.getPath(), null); + dbObj.execSQL("ATTACH DATABASE ':memory:' as memoryDb"); + dbObj.execSQL("ATTACH DATABASE '" + attachedDb1File + "' as attachedDb1"); + assertTrue(dbfile.exists()); + assertTrue(new File(attachedDb1File).exists()); + assertNotNull(dbObj); + assertTrue(dbObj.isOpen()); + ArrayList<Pair<String, String>> attachedDbs = dbObj.getAttachedDbs(); + try { + errorHandler.onCorruption(dbObj); + assertFalse(dbfile.exists()); + assertFalse(new File(attachedDb1File).exists()); + } catch (Exception e) { + fail("unexpected"); + } + + // same as above, except this is a bit of stress testing. attach 5 database files + // and make sure they are all removed. + int N = 5; + ArrayList<String> attachedDbFiles = new ArrayList<String>(N); + for (int i = 0; i < N; i++) { + attachedDbFiles.add(mDatabase.getPath() + i); + } + dbObj = SQLiteDatabase.openOrCreateDatabase(mDatabase.getPath(), null); + dbObj.execSQL("ATTACH DATABASE ':memory:' as memoryDb"); + for (int i = 0; i < N; i++) { + dbObj.execSQL("ATTACH DATABASE '" + attachedDbFiles.get(i) + "' as attachedDb" + i); + } + assertTrue(dbfile.exists()); + for (int i = 0; i < N; i++) { + assertTrue(new File(attachedDbFiles.get(i)).exists()); + } + assertNotNull(dbObj); + assertTrue(dbObj.isOpen()); + attachedDbs = dbObj.getAttachedDbs(); + try { + errorHandler.onCorruption(dbObj); + assertFalse(dbfile.exists()); + for (int i = 0; i < N; i++) { + assertFalse(new File(attachedDbFiles.get(i)).exists()); + } + } catch (Exception e) { + fail("unexpected"); + } + } } diff --git a/core/tests/coretests/src/android/database/sqlite/AbstractJDBCDriverTest.java b/core/tests/coretests/src/android/database/sqlite/AbstractJDBCDriverTest.java deleted file mode 100644 index 19c7bcb..0000000 --- a/core/tests/coretests/src/android/database/sqlite/AbstractJDBCDriverTest.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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.database.sqlite; - -import java.io.File; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -import junit.framework.TestCase; -import android.test.suitebuilder.annotation.MediumTest; - -/** - * Tests for the most commonly used methods of sql like creating a connection, - * inserting, selecting, updating. - */ -public abstract class AbstractJDBCDriverTest extends TestCase { - - @MediumTest - public void testJDBCDriver() throws Exception { - Connection firstConnection = null; - Connection secondConnection = null; - File dbFile = getDbFile(); - String connectionURL = getConnectionURL(); - Statement firstStmt = null; - Statement secondStmt = null; - try { - Class.forName(getJDBCDriverClassName()); - firstConnection = DriverManager.getConnection(connectionURL); - secondConnection = DriverManager.getConnection(connectionURL); - - String[] ones = {"hello!", "goodbye"}; - short[] twos = {10, 20}; - String[] onesUpdated = new String[ones.length]; - for (int i = 0; i < ones.length; i++) { - onesUpdated[i] = ones[i] + twos[i]; - } - firstStmt = firstConnection.createStatement(); - firstStmt.execute("create table tbl1(one varchar(10), two smallint)"); - secondStmt = secondConnection.createStatement(); - - autoCommitInsertSelectTest(firstStmt, ones, twos); - updateSelectCommitSelectTest(firstStmt, secondStmt, ones, onesUpdated, twos); - updateSelectRollbackSelectTest(firstStmt, secondStmt, onesUpdated, ones, twos); - } finally { - closeConnections(firstConnection, secondConnection, dbFile, firstStmt, secondStmt); - } - } - - protected abstract String getJDBCDriverClassName(); - protected abstract String getConnectionURL(); - protected abstract File getDbFile(); - - private void closeConnections(Connection firstConnection, Connection secondConnection, - File dbFile, Statement firstStmt, Statement secondStmt) { - String failText = null; - try { - if (firstStmt != null) { - firstStmt.execute("drop table tbl1"); - } - } catch (SQLException e) { - failText = e.getLocalizedMessage(); - } - try { - if (firstStmt != null) { - firstStmt.close(); - } - } catch (SQLException e) { - failText = e.getLocalizedMessage(); - } - try { - if (firstConnection != null) { - firstConnection.close(); - } - } catch (SQLException e) { - failText = e.getLocalizedMessage(); - } - try { - if (secondStmt != null) { - secondStmt.close(); - } - } catch (SQLException e) { - failText = e.getLocalizedMessage(); - } - try { - if (secondConnection != null) { - secondConnection.close(); - } - } catch (SQLException e) { - failText = e.getLocalizedMessage(); - } - dbFile.delete(); - assertNull(failText, failText); - } - - /** - * Inserts the values from 'ones' with the values from 'twos' into 'tbl1' - * @param stmt the statement to use for the inserts. - * @param ones the string values to insert into tbl1. - * @param twos the corresponding numerical values to insert into tbl1. - * @throws SQLException in case of a problem during insert. - */ - private void autoCommitInsertSelectTest(Statement stmt, String[] ones, - short[] twos) throws SQLException { - for (int i = 0; i < ones.length; i++) { - stmt.execute("insert into tbl1 values('" + ones[i] + "'," + twos[i] - + ")"); - } - assertAllFromTbl1(stmt, ones, twos); - } - - /** - * Asserts that all values that where added to tbl1 are actually in tbl1. - * @param stmt the statement to use for the select. - * @param ones the string values that where added. - * @param twos the numerical values that where added. - * @throws SQLException in case of a problem during select. - */ - private void assertAllFromTbl1(Statement stmt, String[] ones, short[] twos) - throws SQLException { - ResultSet rs = stmt.executeQuery("select * from tbl1"); - int i = 0; - for (; rs.next(); i++) { - assertTrue(i < ones.length); - assertEquals(ones[i], rs.getString("one")); - assertEquals(twos[i], rs.getShort("two")); - } - assertEquals(i, ones.length); - } - - /** - * Tests the results of an update followed bz a select on a diffrent statement. - * After that the first statement commits its update. and now the second - * statement should also be able to see the changed values in a select. - * @param firstStmt the statement to use for the update and commit. - * @param secondStmt the statement that should be used to check if the commit works - * @param ones the original string values. - * @param onesUpdated the updated string values. - * @param twos the numerical values. - * @throws SQLException in case of a problem during any of the executed commands. - */ - private void updateSelectCommitSelectTest(Statement firstStmt, - Statement secondStmt, String[] ones, String[] onesUpdated, - short[] twos) throws SQLException { - firstStmt.getConnection().setAutoCommit(false); - try { - updateOnes(firstStmt, onesUpdated, twos); - assertAllFromTbl1(secondStmt, ones, twos); - firstStmt.getConnection().commit(); - assertAllFromTbl1(secondStmt, onesUpdated, twos); - } finally { - firstStmt.getConnection().setAutoCommit(true); - } - } - - /** - * Tests if an update followed by a select works. After that a rollback will - * be made and again a select should show that the rollback worked. - * @param firstStmt the statement to use for the update and the rollback - * @param secondStmt the statement to use for checking if the rollback worked as intended. - * @param ones the original string values. - * @param onesUpdated the updated string values. - * @param twos the nomerical values. - * @throws SQLException in case of a problem during any command. - */ - private void updateSelectRollbackSelectTest(Statement firstStmt, - Statement secondStmt, String[] ones, String[] onesUpdated, - short[] twos) throws SQLException { - firstStmt.getConnection().setAutoCommit(false); - try { - updateOnes(firstStmt, onesUpdated, twos); - assertAllFromTbl1(secondStmt, ones, twos); - firstStmt.getConnection().rollback(); - assertAllFromTbl1(secondStmt, ones, twos); - } finally { - firstStmt.getConnection().setAutoCommit(true); - } - } - - /** - * updates the sring values. the original values are stored in 'ones' - * and the updated values in 'ones_updated' - * @param stmt the statement to use for the update. - * @param onesUpdated the new string values. - * @param twos the numerical values. - * @throws SQLException in case of a problem during update. - */ - private void updateOnes(Statement stmt, String[] onesUpdated, short[] twos) - throws SQLException { - for (int i = 0; i < onesUpdated.length; i++) { - stmt.execute("UPDATE tbl1 SET one = '" + onesUpdated[i] - + "' WHERE two = " + twos[i]); - } - } -} diff --git a/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java b/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java new file mode 100644 index 0000000..bb5e024 --- /dev/null +++ b/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java @@ -0,0 +1,364 @@ +/* + * 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.database.sqlite; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; + +public class DatabaseConnectionPoolTest extends AndroidTestCase { + private static final String TAG = "DatabaseConnectionPoolTest"; + + private static final int MAX_CONN = 5; + private static final String TEST_SQL = "select * from test where i = ? AND j = 1"; + private static final String[] TEST_SQLS = new String[] { + TEST_SQL, TEST_SQL + 1, TEST_SQL + 2, TEST_SQL + 3, TEST_SQL + 4 + }; + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private DatabaseConnectionPool mTestPool; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + mDatabase.execSQL("create table test (i int, j int);"); + mTestPool = new DatabaseConnectionPool(mDatabase); + assertNotNull(mTestPool); + } + + @Override + protected void tearDown() throws Exception { + mTestPool.close(); + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + @SmallTest + public void testGetAndRelease() { + mTestPool.setMaxPoolSize(MAX_CONN); + // connections should be lazily created. + assertEquals(0, mTestPool.getSize()); + // MAX pool size should be set to MAX_CONN + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // get a connection + SQLiteDatabase db = mTestPool.get(TEST_SQL); + // pool size should be one - since only one should be allocated for the above get() + assertEquals(1, mTestPool.getSize()); + // no free connections should be available + assertEquals(0, mTestPool.getFreePoolSize()); + assertFalse(mTestPool.isDatabaseObjFree(db)); + // release the connection + mTestPool.release(db); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(1, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + assertTrue(mTestPool.isDatabaseObjFree(db)); + // release the same object again and expect IllegalStateException + try { + mTestPool.release(db); + fail("illegalStateException expected"); + } catch (IllegalStateException e ) { + // expected. + } + } + + /** + * get all connections from the pool and ask for one more. + * should get one of the connections already got so far. + */ + @SmallTest + public void testGetAllConnAndOneMore() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + ArrayList<SQLiteDatabase> dbObjs = new ArrayList<SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQL); + assertFalse(dbObjs.contains(db)); + dbObjs.add(db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // pool is maxed out and no free connections. ask for one more connection + SQLiteDatabase db1 = mTestPool.get(TEST_SQL); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.contains(db1)); + // pool size should remain at MAX_CONN + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release db1 but since it is allocated 2 times, it should still remain 'busy' + mTestPool.release(db1); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release all connections + for (int i = 0; i < MAX_CONN; i++) { + mTestPool.release(dbObjs.get(i)); + } + // all objects in the pool should be freed now + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + /** + * same as above except that each connection has different SQL statement associated with it. + */ + @SmallTest + public void testConnRetrievalForPreviouslySeenSql() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + assertFalse(dbObjs.values().contains(db)); + dbObjs.put(TEST_SQLS[i], db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // pool is maxed out and no free connections. ask for one more connection + // use a previously seen SQL statement + String testSql = TEST_SQLS[MAX_CONN - 1]; + SQLiteDatabase db1 = mTestPool.get(testSql); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.values().contains(db1)); + assertEquals(db1, dbObjs.get(testSql)); + // do the same again + SQLiteDatabase db2 = mTestPool.get(testSql); + // make sure db1 is one of the existing ones + assertEquals(db2, dbObjs.get(testSql)); + + // pool size should remain at MAX_CONN + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release db1 but since the same connection is allocated 3 times, + // it should still remain 'busy' + mTestPool.release(db1); + assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql))); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release db2 but since the same connection is allocated 2 times, + // it should still remain 'busy' + mTestPool.release(db2); + assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql))); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release all connections + for (int i = 0; i < MAX_CONN; i++) { + mTestPool.release(dbObjs.get(TEST_SQLS[i])); + } + // all objects in the pool should be freed now + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + private void executeSqlOnDatabaseConn(SQLiteDatabase db, String sql) { + // execute the given SQL on the given database connection so that the prepared + // statement for SQL is cached by the given database connection + // this will help DatabaseConenctionPool figure out if a given SQL statement + // is already cached by a database connection. + db.execSQL(sql, new String[]{1+""}); + } + + /** + * get a connection for a SQL statement 'blah'. (connection_s) + * make sure the pool has at least one free connection even after this get(). + * and get a connection for the same SQL again. + * this connection should be different from connection_s. + * even though there is a connection with the given SQL pre-compiled, since is it not free + * AND since the pool has free connections available, should get a new connection. + */ + @SmallTest + public void testGetConnForTheSameSql() { + mTestPool.setMaxPoolSize(MAX_CONN); + + SQLiteDatabase db = mTestPool.get(TEST_SQL); + executeSqlOnDatabaseConn(db, TEST_SQL); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(1, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + assertFalse(mTestPool.isDatabaseObjFree(db)); + + SQLiteDatabase db1 = mTestPool.get(TEST_SQL); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertFalse(db1.equals(db)); + + mTestPool.release(db); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + mTestPool.release(db1); + assertEquals(2, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + /** + * get the same connection N times and release it N times. + * this tests DatabaseConnectionPool.PoolObj.mNumHolders + */ + @SmallTest + public void testGetSameConnNtimesAndReleaseItNtimes() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + assertFalse(dbObjs.values().contains(db)); + dbObjs.put(TEST_SQLS[i], db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // every connection in the pool should have numHolders = 1 + for (int i = 0; i < MAX_CONN; i ++) { + assertEquals(1, mTestPool.getPool().get(i).getNumHolders()); + } + // pool is maxed out and no free connections. ask for one more connection + // use a previously seen SQL statement + String testSql = TEST_SQLS[MAX_CONN - 1]; + SQLiteDatabase db1 = mTestPool.get(testSql); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.values().contains(db1)); + assertEquals(db1, dbObjs.get(testSql)); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + DatabaseConnectionPool.PoolObj poolObj = mTestPool.getPool().get(db1.mConnectionNum - 1); + int numHolders = poolObj.getNumHolders(); + assertEquals(2, numHolders); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // get the same connection N times more + int N = 100; + for (int i = 0; i < N; i++) { + SQLiteDatabase db2 = mTestPool.get(testSql); + assertEquals(db1, db2); + assertFalse(mTestPool.isDatabaseObjFree(db2)); + // numHolders for this object should be now up by 1 + int prev = numHolders; + numHolders = poolObj.getNumHolders(); + assertEquals(prev + 1, numHolders); + } + // release it N times + for (int i = 0; i < N; i++) { + mTestPool.release(db1); + int prev = numHolders; + numHolders = poolObj.getNumHolders(); + assertEquals(prev - 1, numHolders); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + } + // the connection should still have 2 more holders + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertEquals(2, poolObj.getNumHolders()); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release 2 more times + mTestPool.release(db1); + mTestPool.release(db1); + assertEquals(0, poolObj.getNumHolders()); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + assertTrue(mTestPool.isDatabaseObjFree(db1)); + } + + @SmallTest + public void testStressTest() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<SQLiteDatabase, Integer> dbMap = new HashMap<SQLiteDatabase, Integer>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + assertFalse(dbMap.containsKey(db)); + dbMap.put(db, 1); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // ask for lot more connections but since the pool is maxed out, we should start receiving + // connections that we already got so far + for (int i = MAX_CONN; i < 1000; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQL + i); + assertTrue(dbMap.containsKey(db)); + int k = dbMap.get(db); + dbMap.put(db, ++k); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // print the distribution of the database connection handles received, should be uniform. + for (SQLiteDatabase d : dbMap.keySet()) { + Log.i(TAG, "connection # " + d.mConnectionNum + ", numHolders: " + dbMap.get(d)); + } + // print the pool info + Log.i(TAG, mTestPool.toString()); + // release all + for (SQLiteDatabase d : dbMap.keySet()) { + int num = dbMap.get(d); + for (int i = 0; i < num; i++) { + mTestPool.release(d); + } + } + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } +} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java new file mode 100644 index 0000000..91ef0b7 --- /dev/null +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -0,0 +1,521 @@ +/* + * 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.database.sqlite; + +import android.content.Context; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.Suppress; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; + +public class SQLiteDatabaseTest extends AndroidTestCase { + private static final String TAG = "DatabaseGeneralTest"; + + private static final int CURRENT_DATABASE_VERSION = 42; + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + dbSetUp(); + } + + @Override + protected void tearDown() throws Exception { + dbTeardown(); + super.tearDown(); + } + + private void dbTeardown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + } + + private void dbSetUp() throws Exception { + File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, null); + assertNotNull(mDatabase); + mDatabase.setVersion(CURRENT_DATABASE_VERSION); + } + + @SmallTest + public void testEnableWriteAheadLogging() { + mDatabase.disableWriteAheadLogging(); + assertNull(mDatabase.mConnectionPool); + mDatabase.enableWriteAheadLogging(); + DatabaseConnectionPool pool = mDatabase.mConnectionPool; + assertNotNull(pool); + // make the same call again and make sure the pool already setup is not re-created + mDatabase.enableWriteAheadLogging(); + assertEquals(pool, mDatabase.mConnectionPool); + } + + @SmallTest + public void testSetConnectionPoolSize() { + mDatabase.enableWriteAheadLogging(); + // can't set pool size to zero + try { + mDatabase.setConnectionPoolSize(0); + fail("IllegalStateException expected"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("less than the current max value")); + } + // set pool size to a valid value + mDatabase.setConnectionPoolSize(10); + assertEquals(10, mDatabase.mConnectionPool.getMaxPoolSize()); + // can't set pool size to < the value above + try { + mDatabase.setConnectionPoolSize(1); + fail("IllegalStateException expected"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("less than the current max value")); + } + } + + /** + * Test to ensure that readers are able to read the database data (old versions) + * EVEN WHEN the writer is in a transaction on the same database. + *<p> + * This test starts 1 Writer and 2 Readers and sets up connection pool for readers + * by calling the method {@link SQLiteDatabase#enableWriteAheadLogging()}. + * <p> + * Writer does the following in a tight loop + * <pre> + * begin transaction + * insert into table_1 + * insert into table_2 + * commit + * </pre> + * <p> + * As long a the writer is alive, Readers do the following in a tight loop at the same time + * <pre> + * Reader_K does "select count(*) from table_K" where K = 1 or 2 + * </pre> + * <p> + * The test is run for TIME_TO_RUN_WAL_TEST_FOR sec. + * <p> + * The test is repeated for different connection-pool-sizes (1..3) + * <p> + * And at the end of of each test, the following statistics are printed + * <ul> + * <li>connection-pool-size</li> + * <li>number-of-transactions by writer</li> + * <li>number of reads by reader_K while the writer is IN or NOT-IN xaction</li> + * </ul> + */ + @LargeTest + @Suppress // run this test only if you need to collect the numbers from this test + public void testConcurrencyEffectsOfConnPool() throws Exception { + // run the test with sqlite WAL enable + runConnectionPoolTest(true); + + // run the same test WITHOUT sqlite WAL enabled + runConnectionPoolTest(false); + } + + private void runConnectionPoolTest(boolean useWal) throws Exception { + int M = 3; + StringBuilder[] buff = new StringBuilder[M]; + for (int i = 0; i < M; i++) { + if (useWal) { + // set up connection pool + mDatabase.enableWriteAheadLogging(); + mDatabase.setConnectionPoolSize(i + 1); + } + mDatabase.execSQL("CREATE TABLE t1 (i int, j int);"); + mDatabase.execSQL("CREATE TABLE t2 (i int, j int);"); + mDatabase.beginTransaction(); + for (int k = 0; k < 5; k++) { + mDatabase.execSQL("insert into t1 values(?,?);", new String[] {k+"", k+""}); + mDatabase.execSQL("insert into t2 values(?,?);", new String[] {k+"", k+""}); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + // start a writer + Writer w = new Writer(mDatabase); + + // initialize an array of counters to be passed to the readers + Reader r1 = new Reader(mDatabase, "t1", w, 0); + Reader r2 = new Reader(mDatabase, "t2", w, 1); + w.start(); + r1.start(); + r2.start(); + + // wait for all threads to die + w.join(); + r1.join(); + r2.join(); + + // print the stats + int[][] counts = getCounts(); + buff[i] = new StringBuilder(); + buff[i].append("connpool-size = "); + buff[i].append(i + 1); + buff[i].append(", num xacts by writer = "); + buff[i].append(getNumXacts()); + buff[i].append(", num-reads-in-xact/NOT-in-xact by reader1 = "); + buff[i].append(counts[0][1] + "/" + counts[0][0]); + buff[i].append(", by reader2 = "); + buff[i].append(counts[1][1] + "/" + counts[1][0]); + + Log.i(TAG, "done testing for conn-pool-size of " + (i+1)); + + dbTeardown(); + dbSetUp(); + } + Log.i(TAG, "duration of test " + TIME_TO_RUN_WAL_TEST_FOR + " sec"); + for (int i = 0; i < M; i++) { + Log.i(TAG, buff[i].toString()); + } + } + + private boolean inXact = false; + private int numXacts; + private static final int TIME_TO_RUN_WAL_TEST_FOR = 15; // num sec this test should run + private int[][] counts = new int[2][2]; + + private synchronized boolean inXact() { + return inXact; + } + + private synchronized void setInXactFlag(boolean flag) { + inXact = flag; + } + + private synchronized void setCounts(int readerNum, int[] numReads) { + counts[readerNum][0] = numReads[0]; + counts[readerNum][1] = numReads[1]; + } + + private synchronized int[][] getCounts() { + return counts; + } + + private synchronized void setNumXacts(int num) { + numXacts = num; + } + + private synchronized int getNumXacts() { + return numXacts; + } + + private class Writer extends Thread { + private SQLiteDatabase db = null; + public Writer(SQLiteDatabase db) { + this.db = db; + } + @Override public void run() { + // in a loop, for N sec, do the following + // BEGIN transaction + // insert into table t1, t2 + // Commit + long now = System.currentTimeMillis(); + int k; + for (k = 0;(System.currentTimeMillis() - now) / 1000 < TIME_TO_RUN_WAL_TEST_FOR; k++) { + db.beginTransactionNonExclusive(); + setInXactFlag(true); + for (int i = 0; i < 10; i++) { + db.execSQL("insert into t1 values(?,?);", new String[] {i+"", i+""}); + db.execSQL("insert into t2 values(?,?);", new String[] {i+"", i+""}); + } + db.setTransactionSuccessful(); + setInXactFlag(false); + db.endTransaction(); + } + setNumXacts(k); + } + } + + private class Reader extends Thread { + private SQLiteDatabase db = null; + private String table = null; + private Writer w = null; + private int readerNum; + private int[] numReads = new int[2]; + public Reader(SQLiteDatabase db, String table, Writer w, int readerNum) { + this.db = db; + this.table = table; + this.w = w; + this.readerNum = readerNum; + } + @Override public void run() { + // while the write is alive, in a loop do the query on a table + while (w.isAlive()) { + for (int i = 0; i < 10; i++) { + DatabaseUtils.longForQuery(db, "select count(*) from " + this.table, null); + // update count of reads + numReads[inXact() ? 1 : 0] += 1; + } + } + setCounts(readerNum, numReads); + } + } + + private static class ClassToTestSqlCompilationAndCaching extends SQLiteProgram { + private ClassToTestSqlCompilationAndCaching(SQLiteDatabase db, String sql) { + super(db, sql); + } + private static ClassToTestSqlCompilationAndCaching create(SQLiteDatabase db, String sql) { + db.lock(); + try { + return new ClassToTestSqlCompilationAndCaching(db, sql); + } finally { + db.unlock(); + } + } + } + + @SmallTest + public void testLruCachingOfSqliteCompiledSqlObjs() { + mDatabase.disableWriteAheadLogging(); + mDatabase.execSQL("CREATE TABLE test (i int, j int);"); + // set cache size + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + + // do N+1 queries - and when the 0th entry is removed from LRU cache due to the + // insertion of (N+1)th entry, make sure 0th entry is closed + ArrayList<Integer> stmtObjs = new ArrayList<Integer>(); + ArrayList<String> sqlStrings = new ArrayList<String>(); + int stmt0 = 0; + for (int i = 0; i < N+1; i++) { + String s = "insert into test values(" + i + ",?);"; + sqlStrings.add(s); + ClassToTestSqlCompilationAndCaching c = + ClassToTestSqlCompilationAndCaching.create(mDatabase, s); + int n = c.getSqlStatementId(); + stmtObjs.add(i, n); + if (i == 0) { + // save the statementId of this obj. we want to make sure it is thrown out of + // the cache at the end of this test. + stmt0 = n; + } + c.close(); + } + // is 0'th entry out of the cache? it should be in the list of statementIds + // corresponding to the pre-compiled sql statements to be finalized. + assertTrue(mDatabase.getQueuedUpStmtList().contains(stmt0)); + for (int i = 1; i < N+1; i++) { + SQLiteCompiledSql compSql = mDatabase.getCompiledStatementForSql(sqlStrings.get(i)); + assertNotNull(compSql); + assertTrue(stmtObjs.contains(compSql.nStatement)); + } + } + + @MediumTest + public void testDbCloseReleasingAllCachedSql() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + + "num1 INTEGER, num2 INTEGER, image BLOB);"); + final String statement = "DELETE FROM test WHERE _id=?;"; + SQLiteStatement statementDoNotClose = mDatabase.compileStatement(statement); + statementDoNotClose.bindLong(1, 1); + /* do not close statementDoNotClose object. + * That should leave it in SQLiteDatabase.mPrograms. + * mDatabase.close() in tearDown() should release it. + */ + } + + /** + * test to make sure the statement finalizations are not done right away but + * piggy-backed onto the next sql statement execution on the same database. + */ + @SmallTest + public void testStatementClose() { + mDatabase.execSQL("CREATE TABLE test (i int, j int);"); + // fill up statement cache in mDatabase\ + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + SQLiteStatement stmt; + int stmt0Id = 0; + for (int i = 0; i < N; i ++) { + ClassToTestSqlCompilationAndCaching c = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(" + i + ", ?);"); + // keep track of 0th entry + if (i == 0) { + stmt0Id = c.getSqlStatementId(); + } + c.close(); + } + + // add one more to the cache - and the above 'stmt0Id' should fall out of cache + ClassToTestSqlCompilationAndCaching stmt1 = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(100, ?);"); + stmt1.close(); + + // the above close() should have queuedUp the statement for finalization + ArrayList<Integer> statementIds = mDatabase.getQueuedUpStmtList(); + assertTrue(statementIds.contains(stmt0Id)); + + // execute something to see if this statement gets finalized + mDatabase.execSQL("delete from test where i = 10;"); + statementIds = mDatabase.getQueuedUpStmtList(); + assertEquals(0, statementIds.size()); + } + + /** + * same as above - except that the statement to be finalized is from Thread # 1. + * and it is eventually finalized in Thread # 2 when it executes a SQL statement. + * @throws InterruptedException + */ + @LargeTest + public void testStatementCloseDiffThread() throws InterruptedException { + mDatabase.execSQL("CREATE TABLE test (i int, j int);"); + // fill up statement cache in mDatabase in a thread + Thread t1 = new Thread() { + @Override public void run() { + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + SQLiteStatement stmt; + for (int i = 0; i < N; i ++) { + ClassToTestSqlCompilationAndCaching c = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(" + i + ", ?);"); + // keep track of 0th entry + if (i == 0) { + stmt0Id = c.getSqlStatementId(); + } + c.close(); + } + } + }; + t1.start(); + // wait for the thread to finish + t1.join(); + + // add one more to the cache - and the above 'stmt0Id' should fall out of cache + // just for the heck of it, do it in a separate thread + Thread t2 = new Thread() { + @Override public void run() { + ClassToTestSqlCompilationAndCaching stmt1 = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(100, ?);"); + stmt1.close(); + } + }; + t2.start(); + t2.join(); + + // close() in the above thread should have queuedUp the statement for finalization + ArrayList<Integer> statementIds = mDatabase.getQueuedUpStmtList(); + assertTrue(getStmt0Id() > 0); + assertTrue(statementIds.contains(stmt0Id)); + assertEquals(1, statementIds.size()); + + // execute something to see if this statement gets finalized + // again do it in a separate thread + Thread t3 = new Thread() { + @Override public void run() { + mDatabase.execSQL("delete from test where i = 10;"); + } + }; + t3.start(); + t3.join(); + + // is the statement finalized? + statementIds = mDatabase.getQueuedUpStmtList(); + assertEquals(0, statementIds.size()); + } + + private volatile int stmt0Id = 0; + private synchronized void setStmt0Id(int stmt0Id) { + this.stmt0Id = stmt0Id; + } + private synchronized int getStmt0Id() { + return this.stmt0Id; + } + + /** + * same as above - except that the queue of statements to be finalized are finalized + * by database close() operation. + */ + @LargeTest + public void testStatementCloseByDbClose() throws InterruptedException { + mDatabase.execSQL("CREATE TABLE test (i int, j int);"); + // fill up statement cache in mDatabase in a thread + Thread t1 = new Thread() { + @Override public void run() { + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + SQLiteStatement stmt; + for (int i = 0; i < N; i ++) { + ClassToTestSqlCompilationAndCaching c = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(" + i + ", ?);"); + // keep track of 0th entry + if (i == 0) { + stmt0Id = c.getSqlStatementId(); + } + c.close(); + } + } + }; + t1.start(); + // wait for the thread to finish + t1.join(); + + // add one more to the cache - and the above 'stmt0Id' should fall out of cache + // just for the heck of it, do it in a separate thread + Thread t2 = new Thread() { + @Override public void run() { + ClassToTestSqlCompilationAndCaching stmt1 = + ClassToTestSqlCompilationAndCaching.create(mDatabase, + "insert into test values(100, ?);"); + stmt1.bindLong(1, 1); + stmt1.close(); + } + }; + t2.start(); + t2.join(); + + // close() in the above thread should have queuedUp the statement for finalization + ArrayList<Integer> statementIds = mDatabase.getQueuedUpStmtList(); + assertTrue(getStmt0Id() > 0); + assertTrue(statementIds.contains(stmt0Id)); + assertEquals(1, statementIds.size()); + + // close the database. everything from mClosedStatementIds in mDatabase + // should be finalized and cleared from the list + // again do it in a separate thread + Thread t3 = new Thread() { + @Override public void run() { + mDatabase.close(); + } + }; + t3.start(); + t3.join(); + + // check mClosedStatementIds in mDatabase. it should be empty + statementIds = mDatabase.getQueuedUpStmtList(); + assertEquals(0, statementIds.size()); + } +} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteGeneralTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteGeneralTest.java deleted file mode 100644 index af7ccce..0000000 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteGeneralTest.java +++ /dev/null @@ -1,136 +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.database.sqlite; - -import android.content.Context; -import android.test.AndroidTestCase; -import android.test.FlakyTest; -import android.test.suitebuilder.annotation.LargeTest; - -import java.io.File; - -public class SQLiteGeneralTest extends AndroidTestCase { - - private SQLiteDatabase mDatabase; - private File mDatabaseFile; - Boolean exceptionRecvd = false; - - @Override - protected void setUp() throws Exception { - super.setUp(); - exceptionRecvd = false; - File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); - mDatabaseFile = new File(dbDir, "database_test.db"); - if (mDatabaseFile.exists()) { - mDatabaseFile.delete(); - } - mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); - assertNotNull(mDatabase); - } - - @Override - protected void tearDown() throws Exception { - mDatabase.close(); - mDatabaseFile.delete(); - super.tearDown(); - } - - @LargeTest - public void testUseOfSameSqlStatementBy2Threads() throws Exception { - mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);"); - - // thread 1 creates a prepared statement - final String stmt = "SELECT * FROM test_pstmt WHERE i = ?"; - - // start 2 threads to do repeatedly execute "stmt" - // since these 2 threads are executing the same sql, they each should get - // their own copy and - // there SHOULD NOT be an error from sqlite: "prepared statement is busy" - class RunStmtThread extends Thread { - private static final int N = 1000; - @Override public void run() { - int i = 0; - try { - // execute many times - for (i = 0; i < N; i++) { - SQLiteStatement s1 = mDatabase.compileStatement(stmt); - s1.bindLong(1, i); - s1.execute(); - s1.close(); - } - } catch (SQLiteException e) { - fail("SQLiteException: " + e.getMessage()); - return; - } catch (Exception e) { - e.printStackTrace(); - fail("random unexpected exception: " + e.getMessage()); - return; - } - } - } - RunStmtThread t1 = new RunStmtThread(); - t1.start(); - RunStmtThread t2 = new RunStmtThread(); - t2.start(); - while (t1.isAlive() || t2.isAlive()) { - Thread.sleep(1000); - } - } - - @FlakyTest - public void testUseOfSamePreparedStatementBy2Threads() throws Exception { - mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);"); - - // thread 1 creates a prepared statement - final String stmt = "SELECT * FROM test_pstmt WHERE i = ?"; - final SQLiteStatement s1 = mDatabase.compileStatement(stmt); - - // start 2 threads to do repeatedly execute "stmt" - // since these 2 threads are executing the same prepared statement, - // should see an error from sqlite: "prepared statement is busy" - class RunStmtThread extends Thread { - private static final int N = 1000; - @Override public void run() { - int i = 0; - try { - // execute many times - for (i = 0; i < N; i++) { - s1.bindLong(1, i); - s1.execute(); - } - } catch (SQLiteException e) { - // expect it - assertTrue(e.getMessage().contains("library routine called out of sequence:")); - exceptionRecvd = true; - return; - } catch (Exception e) { - e.printStackTrace(); - fail("random unexpected exception: " + e.getMessage()); - return; - } - } - } - RunStmtThread t1 = new RunStmtThread(); - t1.start(); - RunStmtThread t2 = new RunStmtThread(); - t2.start(); - while (t1.isAlive() || t2.isAlive()) { - Thread.sleep(1000); - } - assertTrue(exceptionRecvd); - } -} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteJDBCDriverTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteJDBCDriverTest.java deleted file mode 100644 index 8e677a5..0000000 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteJDBCDriverTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.database.sqlite; - -import java.io.File; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; -import android.test.suitebuilder.annotation.MediumTest; - -/** - * Minimal test for JDBC driver - */ -public class SQLiteJDBCDriverTest extends AbstractJDBCDriverTest { - - private File dbFile; - - @Override - protected void setUp() throws Exception { - super.setUp(); - dbFile = File.createTempFile("sqliteTestDB", null); - } - - @Override - protected void tearDown() throws Exception { - if(dbFile != null) { - dbFile.delete(); - } - super.tearDown(); - } - - @Override - protected String getConnectionURL() { - return "jdbc:sqlite:/" + dbFile; - } - - @Override - protected File getDbFile() { - return dbFile; - } - - @Override - protected String getJDBCDriverClassName() { - return "SQLite.JDBCDriver"; - } - - // Regression test for (Noser) #255: PreparedStatement.executeUpdate results - // in VM crashing with SIGABRT. - @MediumTest - public void test_connection3() throws Exception { - PreparedStatement prst = null; - Statement st = null; - Connection conn = null; - try { - Class.forName("SQLite.JDBCDriver").newInstance(); - if (dbFile.exists()) { - dbFile.delete(); - } - conn = DriverManager.getConnection("jdbc:sqlite:/" - + dbFile.getPath()); - assertNotNull(conn); - - // create table - st = conn.createStatement(); - String sql = "CREATE TABLE zoo (ZID INTEGER NOT NULL, family VARCHAR (20) NOT NULL, name VARCHAR (20) NOT NULL, PRIMARY KEY(ZID) )"; - st.executeUpdate(sql); - - String update = "update zoo set family = ? where name = ?;"; - prst = conn.prepareStatement(update); - prst.setString(1, "cat"); - prst.setString(2, "Yasha"); - // st = conn.createStatement(); - // st.execute("select * from zoo where family = 'cat'"); - // ResultSet rs = st.getResultSet(); - // assertEquals(0, getCount(rs)); - prst.executeUpdate(); - // st.execute("select * from zoo where family = 'cat'"); - // ResultSet rs1 = st.getResultSet(); - // assertEquals(1, getCount(rs1)); - try { - prst = conn.prepareStatement(""); - prst.execute(); - fail("SQLException is not thrown"); - } catch (SQLException e) { - // expected - } - - try { - conn.prepareStatement(null); - fail("NPE is not thrown"); - } catch (Exception e) { - // expected - } - try { - st = conn.createStatement(); - st.execute("drop table if exists zoo"); - - } catch (SQLException e) { - fail("Couldn't drop table: " + e.getMessage()); - } finally { - try { - st.close(); - conn.close(); - } catch (SQLException ee) { - } - } - } finally { - try { - if (prst != null) { - prst.close(); - } - if (st != null) { - st.close(); - } - } catch (SQLException ee) { - } - } - - } - -} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java new file mode 100644 index 0000000..217545f --- /dev/null +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java @@ -0,0 +1,213 @@ +/* + * 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.database.sqlite; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; + +import java.io.File; +import java.util.Random; +import java.util.concurrent.locks.ReentrantLock; + +public class SQLiteStatementTest extends AndroidTestCase { + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + /** + * Start 2 threads to repeatedly execute the above SQL statement. + * Even though 2 threads are executing the same SQL, they each should get their own copy of + * prepared SQL statement id and there SHOULD NOT be an error from sqlite or android. + * @throws InterruptedException thrown if the test threads started by this test are interrupted + */ + @LargeTest + public void testUseOfSameSqlStatementBy2Threads() throws InterruptedException { + mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);"); + final String stmt = "SELECT * FROM test_pstmt WHERE i = ?"; + class RunStmtThread extends Thread { + @Override public void run() { + // do it enough times to make sure there are no corner cases going untested + for (int i = 0; i < 1000; i++) { + SQLiteStatement s1 = mDatabase.compileStatement(stmt); + s1.bindLong(1, i); + s1.execute(); + s1.close(); + } + } + } + RunStmtThread t1 = new RunStmtThread(); + t1.start(); + RunStmtThread t2 = new RunStmtThread(); + t2.start(); + while (t1.isAlive() || t2.isAlive()) { + Thread.sleep(10); + } + } + + /** + * A simple test: start 2 threads to repeatedly execute the same {@link SQLiteStatement}. + * The 2 threads take turns to use the {@link SQLiteStatement}; i.e., it is NOT in use + * by both the threads at the same time. + * + * @throws InterruptedException thrown if the test threads started by this test are interrupted + */ + @LargeTest + public void testUseOfSameSqliteStatementBy2Threads() throws InterruptedException { + mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);"); + final String stmt = "SELECT * FROM test_pstmt WHERE i = ?"; + final SQLiteStatement s1 = mDatabase.compileStatement(stmt); + class RunStmtThread extends Thread { + @Override public void run() { + // do it enough times to make sure there are no corner cases going untested + for (int i = 0; i < 1000; i++) { + lock(); + try { + s1.bindLong(1, i); + s1.execute(); + } finally { + unlock(); + } + Thread.yield(); + } + } + } + RunStmtThread t1 = new RunStmtThread(); + t1.start(); + RunStmtThread t2 = new RunStmtThread(); + t2.start(); + while (t1.isAlive() || t2.isAlive()) { + Thread.sleep(10); + } + } + /** Synchronize on this when accessing the SqliteStatemet in the above */ + private final ReentrantLock mLock = new ReentrantLock(true); + private void lock() { + mLock.lock(); + } + private void unlock() { + mLock.unlock(); + } + + /** + * Tests the following: a {@link SQLiteStatement} object should not refer to a + * pre-compiled SQL statement id except in during the period of binding the arguments + * and executing the SQL statement. + */ + @SmallTest + public void testReferenceToPrecompiledStatementId() { + mDatabase.execSQL("create table t (i int, j text);"); + verifyReferenceToPrecompiledStatementId(false); + verifyReferenceToPrecompiledStatementId(true); + + // a small stress test to make sure there are no side effects of + // the acquire & release of pre-compiled statement id by SQLiteStatement object. + for (int i = 0; i < 100; i++) { + verifyReferenceToPrecompiledStatementId(false); + verifyReferenceToPrecompiledStatementId(true); + } + } + + @SuppressWarnings("deprecation") + private void verifyReferenceToPrecompiledStatementId(boolean wal) { + if (wal) { + mDatabase.enableWriteAheadLogging(); + } else { + mDatabase.disableWriteAheadLogging(); + } + // test with INSERT statement - doesn't use connection pool, if WAL is set + SQLiteStatement stmt = mDatabase.compileStatement("insert into t values(?,?);"); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + // sql statement should not be compiled yet + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + int colValue = new Random().nextInt(); + stmt.bindLong(1, colValue); + // verify that the sql statement is still not compiled + assertEquals(0, stmt.getSqlStatementId()); + // should still be using the mDatabase connection - verify + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + stmt.bindString(2, "blah" + colValue); + // verify that the sql statement is still not compiled + assertEquals(0, stmt.getSqlStatementId()); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + stmt.executeInsert(); + // now that the statement is executed, pre-compiled statement should be released + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + stmt.close(); + // pre-compiled SQL statement should still remain released from this object + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + // but the database handle should still be the same + assertEquals(mDatabase, stmt.mDatabase); + + // test with a SELECT statement - uses connection pool if WAL is set + stmt = mDatabase.compileStatement("select i from t where j=?;"); + // sql statement should not be compiled yet + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + stmt.bindString(1, "blah" + colValue); + // verify that the sql statement is still not compiled + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + // execute the statement + Long l = stmt.simpleQueryForLong(); + assertEquals(colValue, l.intValue()); + // now that the statement is executed, pre-compiled statement should be released + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + stmt.close(); + // pre-compiled SQL statement should still remain released from this object + assertEquals(0, stmt.nStatement); + assertEquals(0, stmt.getSqlStatementId()); + // but the database handle should still remain attached to the statement + assertEquals(mDatabase.mNativeHandle, stmt.nHandle); + assertEquals(mDatabase, stmt.mDatabase); + } +} diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java b/core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java deleted file mode 100644 index b3c0773..0000000 --- a/core/tests/coretests/src/android/pim/vcard/ContentValuesBuilder.java +++ /dev/null @@ -1,81 +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; - -/** - * ContentValues-like class which enables users to chain put() methods and restricts - * the other methods. - */ -/* package */ class ContentValuesBuilder { - private final ContentValues mContentValues; - - public ContentValuesBuilder(final ContentValues contentValues) { - mContentValues = contentValues; - } - - public ContentValuesBuilder put(String key, String value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Byte value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Short value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Integer value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Long value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Float value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Double value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, Boolean value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder put(String key, byte[] value) { - mContentValues.put(key, value); - return this; - } - - public ContentValuesBuilder putNull(String key) { - mContentValues.putNull(key); - return this; - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.java b/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.java deleted file mode 100644 index b9e9875..0000000 --- a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifier.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.VCardConfig; -import android.pim.vcard.VCardEntry; -import android.pim.vcard.VCardEntryConstructor; -import android.pim.vcard.VCardEntryHandler; -import android.pim.vcard.VCardParser; -import android.pim.vcard.VCardParser_V21; -import android.pim.vcard.VCardParser_V30; -import android.pim.vcard.exception.VCardException; -import android.test.AndroidTestCase; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -/* package */ class ContentValuesVerifier implements VCardEntryHandler { - private AndroidTestCase mTestCase; - private List<ContentValuesVerifierElem> mContentValuesVerifierElemList = - new ArrayList<ContentValuesVerifierElem>(); - private int mIndex; - - public ContentValuesVerifierElem addElem(AndroidTestCase androidTestCase) { - mTestCase = androidTestCase; - ContentValuesVerifierElem importVerifier = new ContentValuesVerifierElem(androidTestCase); - mContentValuesVerifierElemList.add(importVerifier); - return importVerifier; - } - - public void verify(int resId, int vCardType) throws IOException, VCardException { - verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType); - } - - public void verify(int resId, int vCardType, final VCardParser vCardParser) - throws IOException, VCardException { - verify(mTestCase.getContext().getResources().openRawResource(resId), - vCardType, vCardParser); - } - - public void verify(InputStream is, int vCardType) throws IOException, VCardException { - final VCardParser vCardParser; - if (VCardConfig.isV30(vCardType)) { - vCardParser = new VCardParser_V30(true); // use StrictParsing - } else { - vCardParser = new VCardParser_V21(); - } - verify(is, vCardType, vCardParser); - } - - public void verify(InputStream is, int vCardType, final VCardParser vCardParser) - throws IOException, VCardException { - VCardEntryConstructor builder = - new VCardEntryConstructor(null, null, false, vCardType, null); - builder.addEntryHandler(this); - try { - vCardParser.parse(is, builder); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } - } - } - - public void onStart() { - for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) { - elem.onParsingStart(); - } - } - - public void onEntryCreated(VCardEntry entry) { - mTestCase.assertTrue(mIndex < mContentValuesVerifierElemList.size()); - mContentValuesVerifierElemList.get(mIndex).onEntryCreated(entry); - mIndex++; - } - - public void onEnd() { - for (ContentValuesVerifierElem elem : mContentValuesVerifierElemList) { - elem.onParsingEnd(); - elem.verifyResolver(); - } - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java b/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java deleted file mode 100644 index 2edbb36..0000000 --- a/core/tests/coretests/src/android/pim/vcard/ContentValuesVerifierElem.java +++ /dev/null @@ -1,95 +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.pim.vcard.VCardConfig; -import android.pim.vcard.VCardEntry; -import android.pim.vcard.VCardEntryCommitter; -import android.pim.vcard.VCardEntryConstructor; -import android.pim.vcard.VCardEntryHandler; -import android.pim.vcard.VCardParser; -import android.pim.vcard.VCardParser_V21; -import android.pim.vcard.VCardParser_V30; -import android.pim.vcard.exception.VCardException; -import android.provider.ContactsContract.Data; -import android.test.AndroidTestCase; - -import java.io.IOException; -import java.io.InputStream; - -/* package */ class ContentValuesVerifierElem { - private final AndroidTestCase mTestCase; - private final ImportTestResolver mResolver; - private final VCardEntryHandler mHandler; - - public ContentValuesVerifierElem(AndroidTestCase androidTestCase) { - mTestCase = androidTestCase; - mResolver = new ImportTestResolver(androidTestCase); - mHandler = new VCardEntryCommitter(mResolver); - } - - public ContentValuesBuilder addExpected(String mimeType) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Data.MIMETYPE, mimeType); - mResolver.addExpectedContentValues(contentValues); - return new ContentValuesBuilder(contentValues); - } - - public void verify(int resId, int vCardType) - throws IOException, VCardException { - verify(mTestCase.getContext().getResources().openRawResource(resId), vCardType); - } - - public void verify(InputStream is, int vCardType) throws IOException, VCardException { - final VCardParser vCardParser; - if (VCardConfig.isV30(vCardType)) { - vCardParser = new VCardParser_V30(true); // use StrictParsing - } else { - vCardParser = new VCardParser_V21(); - } - VCardEntryConstructor builder = - new VCardEntryConstructor(null, null, false, vCardType, null); - builder.addEntryHandler(mHandler); - try { - vCardParser.parse(is, builder); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } - } - verifyResolver(); - } - - public void verifyResolver() { - mResolver.verify(); - } - - public void onParsingStart() { - mHandler.onStart(); - } - - public void onEntryCreated(VCardEntry entry) { - mHandler.onEntryCreated(entry); - } - - public void onParsingEnd() { - mHandler.onEnd(); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java deleted file mode 100644 index 5968e83..0000000 --- a/core/tests/coretests/src/android/pim/vcard/ExportTestResolver.java +++ /dev/null @@ -1,216 +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.Entity; -import android.content.EntityIterator; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.RawContacts; -import android.test.mock.MockContentResolver; -import android.test.mock.MockCursor; - -import junit.framework.TestCase; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/* package */ public class ExportTestResolver extends MockContentResolver { - ExportTestProvider mProvider; - public ExportTestResolver(TestCase testCase) { - mProvider = new ExportTestProvider(testCase); - addProvider(VCardComposer.VCARD_TEST_AUTHORITY, mProvider); - addProvider(RawContacts.CONTENT_URI.getAuthority(), mProvider); - } - - public ContactEntry addInputContactEntry() { - return mProvider.buildInputEntry(); - } -} - -/* package */ class MockEntityIterator implements EntityIterator { - List<Entity> mEntityList; - Iterator<Entity> mIterator; - - public MockEntityIterator(List<ContentValues> contentValuesList) { - mEntityList = new ArrayList<Entity>(); - Entity entity = new Entity(new ContentValues()); - for (ContentValues contentValues : contentValuesList) { - entity.addSubValue(Data.CONTENT_URI, contentValues); - } - mEntityList.add(entity); - mIterator = mEntityList.iterator(); - } - - public boolean hasNext() { - return mIterator.hasNext(); - } - - public Entity next() { - return mIterator.next(); - } - - public void remove() { - throw new UnsupportedOperationException("remove not supported"); - } - - public void reset() { - mIterator = mEntityList.iterator(); - } - - public void close() { - } -} - -/** - * Represents one contact, which should contain multiple ContentValues like - * StructuredName, Email, etc. - */ -/* package */ class ContactEntry { - private final List<ContentValues> mContentValuesList = new ArrayList<ContentValues>(); - - public ContentValuesBuilder addContentValues(String mimeType) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Data.MIMETYPE, mimeType); - mContentValuesList.add(contentValues); - return new ContentValuesBuilder(contentValues); - } - - public List<ContentValues> getList() { - return mContentValuesList; - } -} - -/* package */ class ExportTestProvider extends MockContentProvider { - final private TestCase mTestCase; - final private ArrayList<ContactEntry> mContactEntryList = new ArrayList<ContactEntry>(); - - public ExportTestProvider(TestCase testCase) { - mTestCase = testCase; - } - - public ContactEntry buildInputEntry() { - ContactEntry contactEntry = new ContactEntry(); - mContactEntryList.add(contactEntry); - return contactEntry; - } - - /** - * <p> - * An old method which had existed but was removed from ContentResolver. - * </p> - * <p> - * We still keep using this method since we don't have a propeer way to know - * which value in the ContentValue corresponds to the entry in Contacts database. - * </p> - * <p> - * Detail: - * There's an easy way to know which index "family name" corresponds to, via - * {@link android.provider.ContactsContract}. - * FAMILY_NAME equals DATA3, so the corresponding index - * for "family name" should be 2 (note that index is 0-origin). - * However, we cannot know what the index 2 corresponds to; it may be "family name", - * "label" for now, but may be the other some column in the future. We don't have - * convenient way to know the original data structure. - * </p> - */ - public EntityIterator queryEntities(Uri uri, - String selection, String[] selectionArgs, String sortOrder) { - mTestCase.assertTrue(uri != null); - mTestCase.assertTrue(ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())); - final String authority = uri.getAuthority(); - mTestCase.assertTrue(RawContacts.CONTENT_URI.getAuthority().equals(authority)); - mTestCase.assertTrue((Data.CONTACT_ID + "=?").equals(selection)); - mTestCase.assertEquals(1, selectionArgs.length); - final int id = Integer.parseInt(selectionArgs[0]); - mTestCase.assertTrue(id >= 0 && id < mContactEntryList.size()); - - return new MockEntityIterator(mContactEntryList.get(id).getList()); - } - - @Override - public Cursor query(Uri uri,String[] projection, - String selection, String[] selectionArgs, String sortOrder) { - mTestCase.assertTrue(VCardComposer.CONTACTS_TEST_CONTENT_URI.equals(uri)); - // In this test, following arguments are not supported. - mTestCase.assertNull(selection); - mTestCase.assertNull(selectionArgs); - mTestCase.assertNull(sortOrder); - - return new MockCursor() { - int mCurrentPosition = -1; - - @Override - public int getCount() { - return mContactEntryList.size(); - } - - @Override - public boolean moveToFirst() { - mCurrentPosition = 0; - return true; - } - - @Override - public boolean moveToNext() { - if (mCurrentPosition < mContactEntryList.size()) { - mCurrentPosition++; - return true; - } else { - return false; - } - } - - @Override - public boolean isBeforeFirst() { - return mCurrentPosition < 0; - } - - @Override - public boolean isAfterLast() { - return mCurrentPosition >= mContactEntryList.size(); - } - - @Override - public int getColumnIndex(String columnName) { - mTestCase.assertEquals(Contacts._ID, columnName); - return 0; - } - - @Override - public int getInt(int columnIndex) { - mTestCase.assertEquals(0, columnIndex); - mTestCase.assertTrue(mCurrentPosition >= 0 - && mCurrentPosition < mContactEntryList.size()); - return mCurrentPosition; - } - - @Override - public String getString(int columnIndex) { - return String.valueOf(getInt(columnIndex)); - } - - @Override - public void close() { - } - }; - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java b/core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java deleted file mode 100644 index c3f6f79..0000000 --- a/core/tests/coretests/src/android/pim/vcard/ImportTestResolver.java +++ /dev/null @@ -1,299 +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.content.ContentProviderResult; -import android.content.ContentValues; -import android.net.Uri; -import android.provider.ContactsContract.Data; -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.Relation; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; -import android.provider.ContactsContract.CommonDataKinds.Website; -import android.test.mock.MockContentResolver; -import android.text.TextUtils; - -import junit.framework.TestCase; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.Map.Entry; - -/* package */ class ImportTestResolver extends MockContentResolver { - final ImportTestProvider mProvider; - - public ImportTestResolver(TestCase testCase) { - mProvider = new ImportTestProvider(testCase); - } - - @Override - public ContentProviderResult[] applyBatch(String authority, - ArrayList<ContentProviderOperation> operations) { - equalsString(authority, RawContacts.CONTENT_URI.toString()); - return mProvider.applyBatch(operations); - } - - public void addExpectedContentValues(ContentValues expectedContentValues) { - mProvider.addExpectedContentValues(expectedContentValues); - } - - public void verify() { - mProvider.verify(); - } - - private static boolean equalsString(String a, String b) { - if (a == null || a.length() == 0) { - return b == null || b.length() == 0; - } else { - return a.equals(b); - } - } -} - -/* package */ class ImportTestProvider extends MockContentProvider { - private static final Set<String> sKnownMimeTypeSet = - new HashSet<String>(Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, - Nickname.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, - Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, - Im.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, - Event.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE, - Note.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, - Relation.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, - GroupMembership.CONTENT_ITEM_TYPE)); - - final Map<String, Collection<ContentValues>> mMimeTypeToExpectedContentValues; - - private final TestCase mTestCase; - - public ImportTestProvider(TestCase testCase) { - mTestCase = testCase; - mMimeTypeToExpectedContentValues = - new HashMap<String, Collection<ContentValues>>(); - for (String acceptanbleMimeType : sKnownMimeTypeSet) { - // Do not use HashSet since the current implementation changes the content of - // ContentValues after the insertion, which make the result of hashCode() - // changes... - mMimeTypeToExpectedContentValues.put( - acceptanbleMimeType, new ArrayList<ContentValues>()); - } - } - - public void addExpectedContentValues(ContentValues expectedContentValues) { - final String mimeType = expectedContentValues.getAsString(Data.MIMETYPE); - if (!sKnownMimeTypeSet.contains(mimeType)) { - mTestCase.fail(String.format( - "Unknow MimeType %s in the test code. Test code should be broken.", - mimeType)); - } - - final Collection<ContentValues> contentValuesCollection = - mMimeTypeToExpectedContentValues.get(mimeType); - contentValuesCollection.add(expectedContentValues); - } - - @Override - public ContentProviderResult[] applyBatch( - ArrayList<ContentProviderOperation> operations) { - if (operations == null) { - mTestCase.fail("There is no operation."); - } - - final int size = operations.size(); - ContentProviderResult[] fakeResultArray = new ContentProviderResult[size]; - for (int i = 0; i < size; i++) { - Uri uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, String.valueOf(i)); - fakeResultArray[i] = new ContentProviderResult(uri); - } - - for (int i = 0; i < size; i++) { - ContentProviderOperation operation = operations.get(i); - ContentValues contentValues = operation.resolveValueBackReferences( - fakeResultArray, i); - } - for (int i = 0; i < size; i++) { - ContentProviderOperation operation = operations.get(i); - ContentValues actualContentValues = operation.resolveValueBackReferences( - fakeResultArray, i); - final Uri uri = operation.getUri(); - if (uri.equals(RawContacts.CONTENT_URI)) { - mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME)); - mTestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE)); - } else if (uri.equals(Data.CONTENT_URI)) { - final String mimeType = actualContentValues.getAsString(Data.MIMETYPE); - if (!sKnownMimeTypeSet.contains(mimeType)) { - mTestCase.fail(String.format( - "Unknown MimeType %s. Probably added after developing this test", - mimeType)); - } - // Remove data meaningless in this unit tests. - // Specifically, Data.DATA1 - DATA7 are set to null or empty String - // regardless of the input, but it may change depending on how - // resolver-related code handles it. - // Here, we ignore these implementation-dependent specs and - // just check whether vCard importer correctly inserts rellevent data. - Set<String> keyToBeRemoved = new HashSet<String>(); - for (Entry<String, Object> entry : actualContentValues.valueSet()) { - Object value = entry.getValue(); - if (value == null || TextUtils.isEmpty(value.toString())) { - keyToBeRemoved.add(entry.getKey()); - } - } - for (String key: keyToBeRemoved) { - actualContentValues.remove(key); - } - /* for testing - Log.d("@@@", - String.format("MimeType: %s, data: %s", - mimeType, actualContentValues.toString())); */ - // Remove RAW_CONTACT_ID entry just for safety, since we do not care - // how resolver-related code handles the entry in this unit test, - if (actualContentValues.containsKey(Data.RAW_CONTACT_ID)) { - actualContentValues.remove(Data.RAW_CONTACT_ID); - } - final Collection<ContentValues> contentValuesCollection = - mMimeTypeToExpectedContentValues.get(mimeType); - if (contentValuesCollection.isEmpty()) { - mTestCase.fail("ContentValues for MimeType " + mimeType - + " is not expected at all (" + actualContentValues + ")"); - } - boolean checked = false; - for (ContentValues expectedContentValues : contentValuesCollection) { - /*for testing - Log.d("@@@", "expected: " - + convertToEasilyReadableString(expectedContentValues)); - Log.d("@@@", "actual : " - + convertToEasilyReadableString(actualContentValues));*/ - if (equalsForContentValues(expectedContentValues, - actualContentValues)) { - mTestCase.assertTrue(contentValuesCollection.remove(expectedContentValues)); - checked = true; - break; - } - } - if (!checked) { - final StringBuilder builder = new StringBuilder(); - builder.append("Unexpected: "); - builder.append(convertToEasilyReadableString(actualContentValues)); - builder.append("\nExpected: "); - for (ContentValues expectedContentValues : contentValuesCollection) { - builder.append(convertToEasilyReadableString(expectedContentValues)); - } - mTestCase.fail(builder.toString()); - } - } else { - mTestCase.fail("Unexpected Uri has come: " + uri); - } - } // for (int i = 0; i < size; i++) { - return fakeResultArray; - } - - public void verify() { - StringBuilder builder = new StringBuilder(); - for (Collection<ContentValues> contentValuesCollection : - mMimeTypeToExpectedContentValues.values()) { - for (ContentValues expectedContentValues: contentValuesCollection) { - builder.append(convertToEasilyReadableString(expectedContentValues)); - builder.append("\n"); - } - } - if (builder.length() > 0) { - final String failMsg = - "There is(are) remaining expected ContentValues instance(s): \n" - + builder.toString(); - mTestCase.fail(failMsg); - } - } - - /** - * Utility method to print ContentValues whose content is printed with sorted keys. - */ - private String convertToEasilyReadableString(ContentValues contentValues) { - if (contentValues == null) { - return "null"; - } - String mimeTypeValue = ""; - SortedMap<String, String> sortedMap = new TreeMap<String, String>(); - for (Entry<String, Object> entry : contentValues.valueSet()) { - final String key = entry.getKey(); - final Object value = entry.getValue(); - final String valueString = (value != null ? value.toString() : null); - if (Data.MIMETYPE.equals(key)) { - mimeTypeValue = valueString; - } else { - mTestCase.assertNotNull(key); - sortedMap.put(key, valueString); - } - } - StringBuilder builder = new StringBuilder(); - builder.append(Data.MIMETYPE); - builder.append('='); - builder.append(mimeTypeValue); - for (Entry<String, String> entry : sortedMap.entrySet()) { - final String key = entry.getKey(); - final String value = entry.getValue(); - builder.append(' '); - builder.append(key); - builder.append("=\""); - builder.append(value); - builder.append('"'); - } - return builder.toString(); - } - - private static boolean equalsForContentValues( - ContentValues expected, ContentValues actual) { - if (expected == actual) { - return true; - } else if (expected == null || actual == null || expected.size() != actual.size()) { - return false; - } - - for (Entry<String, Object> entry : expected.valueSet()) { - final String key = entry.getKey(); - final Object value = entry.getValue(); - if (!actual.containsKey(key)) { - return false; - } - if (value instanceof byte[]) { - Object actualValue = actual.get(key); - if (!Arrays.equals((byte[])value, (byte[])actualValue)) { - return false; - } - } else if (!value.equals(actual.get(key))) { - return false; - } - } - return true; - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/LineVerifier.java b/core/tests/coretests/src/android/pim/vcard/LineVerifier.java deleted file mode 100644 index cef15fd..0000000 --- a/core/tests/coretests/src/android/pim/vcard/LineVerifier.java +++ /dev/null @@ -1,65 +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.Context; -import android.pim.vcard.VCardComposer; - -import junit.framework.TestCase; - -import java.util.ArrayList; - -class LineVerifier implements VCardComposer.OneEntryHandler { - private final TestCase mTestCase; - private final ArrayList<LineVerifierElem> mLineVerifierElemList; - private int mVCardType; - private int index; - - public LineVerifier(TestCase testCase, int vcardType) { - mTestCase = testCase; - mLineVerifierElemList = new ArrayList<LineVerifierElem>(); - mVCardType = vcardType; - } - - public LineVerifierElem addLineVerifierElem() { - LineVerifierElem lineVerifier = new LineVerifierElem(mTestCase, mVCardType); - mLineVerifierElemList.add(lineVerifier); - return lineVerifier; - } - - public void verify(String vcard) { - if (index >= mLineVerifierElemList.size()) { - mTestCase.fail("Insufficient number of LineVerifier (" + index + ")"); - } - - LineVerifierElem lineVerifier = mLineVerifierElemList.get(index); - lineVerifier.verify(vcard); - - index++; - } - - public boolean onEntryCreated(String vcard) { - verify(vcard); - return true; - } - - public boolean onInit(Context context) { - return true; - } - - public void onTerminate() { - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java b/core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java deleted file mode 100644 index b23b29b..0000000 --- a/core/tests/coretests/src/android/pim/vcard/LineVerifierElem.java +++ /dev/null @@ -1,105 +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.VCardConfig; -import android.text.TextUtils; - -import junit.framework.TestCase; - -import java.util.ArrayList; -import java.util.List; - -class LineVerifierElem { - private final TestCase mTestCase; - private final List<String> mExpectedLineList = new ArrayList<String>(); - private final boolean mIsV30; - - public LineVerifierElem(TestCase testCase, int vcardType) { - mTestCase = testCase; - mIsV30 = VCardConfig.isV30(vcardType); - } - - public LineVerifierElem addExpected(final String line) { - if (!TextUtils.isEmpty(line)) { - mExpectedLineList.add(line); - } - return this; - } - - public void verify(final String vcard) { - final String[] lineArray = vcard.split("\\r?\\n"); - final int length = lineArray.length; - boolean beginExists = false; - boolean endExists = false; - boolean versionExists = false; - - for (int i = 0; i < length; i++) { - final String line = lineArray[i]; - if (TextUtils.isEmpty(line)) { - continue; - } - - if ("BEGIN:VCARD".equalsIgnoreCase(line)) { - if (beginExists) { - mTestCase.fail("Multiple \"BEGIN:VCARD\" line found"); - } else { - beginExists = true; - continue; - } - } else if ("END:VCARD".equalsIgnoreCase(line)) { - if (endExists) { - mTestCase.fail("Multiple \"END:VCARD\" line found"); - } else { - endExists = true; - continue; - } - } else if ((mIsV30 ? "VERSION:3.0" : "VERSION:2.1").equalsIgnoreCase(line)) { - if (versionExists) { - mTestCase.fail("Multiple VERSION line + found"); - } else { - versionExists = true; - continue; - } - } - - if (!beginExists) { - mTestCase.fail("Property other than BEGIN came before BEGIN property: " - + line); - } else if (endExists) { - mTestCase.fail("Property other than END came after END property: " - + line); - } - - final int index = mExpectedLineList.indexOf(line); - if (index >= 0) { - mExpectedLineList.remove(index); - } else { - mTestCase.fail("Unexpected line: " + line); - } - } - - if (!mExpectedLineList.isEmpty()) { - StringBuffer buffer = new StringBuffer(); - for (String expectedLine : mExpectedLineList) { - buffer.append(expectedLine); - buffer.append("\n"); - } - - mTestCase.fail("Expected line(s) not found:" + buffer.toString()); - } - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/PropertyNode.java b/core/tests/coretests/src/android/pim/vcard/PropertyNode.java deleted file mode 100644 index 2c1f6d2..0000000 --- a/core/tests/coretests/src/android/pim/vcard/PropertyNode.java +++ /dev/null @@ -1,198 +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.pim.vcard.VCardEntry; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Previously used in main vCard handling code but now exists only for testing. - * - * Especially useful for testing parser code (VCardParser), since all properties can be - * checked via this class unlike {@link VCardEntry}, which only emits the result of - * interpretation of the content of each vCard. We cannot know whether vCard parser or - * ContactStruct is wrong withouth this class. - */ -public class PropertyNode { - public String propName; - public String propValue; - public List<String> propValue_vector; - - /** Store value as byte[],after decode. - * Used when propValue is encoded by something like BASE64, QUOTED-PRINTABLE, etc. - */ - public byte[] propValue_bytes; - - /** param store: key=paramType, value=paramValue - * Note that currently PropertyNode class does not support multiple param-values - * defined in vCard 3.0 (See also RFC 2426). multiple-values are stored as - * one String value like "A,B", not ["A", "B"]... - * TODO: fix this. - */ - public ContentValues paramMap; - - /** Only for TYPE=??? param store. */ - public Set<String> paramMap_TYPE; - - /** Store group values. Used only in VCard. */ - public Set<String> propGroupSet; - - public PropertyNode() { - propName = ""; - propValue = ""; - propValue_vector = new ArrayList<String>(); - paramMap = new ContentValues(); - paramMap_TYPE = new HashSet<String>(); - propGroupSet = new HashSet<String>(); - } - - public PropertyNode( - String propName, String propValue, List<String> propValue_vector, - byte[] propValue_bytes, ContentValues paramMap, Set<String> paramMap_TYPE, - Set<String> propGroupSet) { - if (propName != null) { - this.propName = propName; - } else { - this.propName = ""; - } - if (propValue != null) { - this.propValue = propValue; - } else { - this.propValue = ""; - } - if (propValue_vector != null) { - this.propValue_vector = propValue_vector; - } else { - this.propValue_vector = new ArrayList<String>(); - } - this.propValue_bytes = propValue_bytes; - if (paramMap != null) { - this.paramMap = paramMap; - } else { - this.paramMap = new ContentValues(); - } - if (paramMap_TYPE != null) { - this.paramMap_TYPE = paramMap_TYPE; - } else { - this.paramMap_TYPE = new HashSet<String>(); - } - if (propGroupSet != null) { - this.propGroupSet = propGroupSet; - } else { - this.propGroupSet = new HashSet<String>(); - } - } - - @Override - public int hashCode() { - // vCard may contain more than one same line in one entry, while HashSet or any other - // library which utilize hashCode() does not honor that, so intentionally throw an - // Exception. - throw new UnsupportedOperationException( - "PropertyNode does not provide hashCode() implementation intentionally."); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof PropertyNode)) { - return false; - } - - PropertyNode node = (PropertyNode)obj; - - if (propName == null || !propName.equals(node.propName)) { - return false; - } else if (!paramMap_TYPE.equals(node.paramMap_TYPE)) { - return false; - } else if (!paramMap_TYPE.equals(node.paramMap_TYPE)) { - return false; - } else if (!propGroupSet.equals(node.propGroupSet)) { - return false; - } - - if (propValue_bytes != null && Arrays.equals(propValue_bytes, node.propValue_bytes)) { - return true; - } else { - if (!propValue.equals(node.propValue)) { - return false; - } - - // The value in propValue_vector is not decoded even if it should be - // decoded by BASE64 or QUOTED-PRINTABLE. When the size of propValue_vector - // is 1, the encoded value is stored in propValue, so we do not have to - // check it. - return (propValue_vector.equals(node.propValue_vector) || - propValue_vector.size() == 1 || - node.propValue_vector.size() == 1); - } - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("propName: "); - builder.append(propName); - builder.append(", paramMap: "); - builder.append(paramMap.toString()); - builder.append(", paramMap_TYPE: ["); - boolean first = true; - for (String elem : paramMap_TYPE) { - if (first) { - first = false; - } else { - builder.append(", "); - } - builder.append('"'); - builder.append(elem); - builder.append('"'); - } - builder.append("]"); - if (!propGroupSet.isEmpty()) { - builder.append(", propGroupSet: ["); - first = true; - for (String elem : propGroupSet) { - if (first) { - first = false; - } else { - builder.append(", "); - } - builder.append('"'); - builder.append(elem); - builder.append('"'); - } - builder.append("]"); - } - if (propValue_vector != null && propValue_vector.size() > 1) { - builder.append(", propValue_vector size: "); - builder.append(propValue_vector.size()); - } - if (propValue_bytes != null) { - builder.append(", propValue_bytes size: "); - builder.append(propValue_bytes.length); - } - builder.append(", propValue: \""); - builder.append(propValue); - builder.append("\""); - return builder.toString(); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java b/core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java deleted file mode 100644 index cfdd074..0000000 --- a/core/tests/coretests/src/android/pim/vcard/PropertyNodesVerifier.java +++ /dev/null @@ -1,386 +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.pim.vcard.VCardConfig; -import android.pim.vcard.VCardParser; -import android.pim.vcard.VCardParser_V21; -import android.pim.vcard.VCardParser_V30; -import android.pim.vcard.exception.VCardException; -import android.test.AndroidTestCase; - -import junit.framework.TestCase; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; - -/* package */ class PropertyNodesVerifier extends VNodeBuilder { - private final List<PropertyNodesVerifierElem> mPropertyNodesVerifierElemList; - private final AndroidTestCase mAndroidTestCase; - private int mIndex; - - public PropertyNodesVerifier(AndroidTestCase testCase) { - mPropertyNodesVerifierElemList = new ArrayList<PropertyNodesVerifierElem>(); - mAndroidTestCase = testCase; - } - - public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { - PropertyNodesVerifierElem elem = new PropertyNodesVerifierElem(mAndroidTestCase); - mPropertyNodesVerifierElemList.add(elem); - return elem; - } - - public void verify(int resId, int vCardType) - throws IOException, VCardException { - verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), vCardType); - } - - public void verify(int resId, int vCardType, final VCardParser vCardParser) - throws IOException, VCardException { - verify(mAndroidTestCase.getContext().getResources().openRawResource(resId), - vCardType, vCardParser); - } - - public void verify(InputStream is, int vCardType) throws IOException, VCardException { - final VCardParser vCardParser; - if (VCardConfig.isV30(vCardType)) { - vCardParser = new VCardParser_V30(true); // Use StrictParsing. - } else { - vCardParser = new VCardParser_V21(); - } - verify(is, vCardType, vCardParser); - } - - public void verify(InputStream is, int vCardType, final VCardParser vCardParser) - throws IOException, VCardException { - try { - vCardParser.parse(is, this); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } - } - } - - @Override - public void endEntry() { - super.endEntry(); - mAndroidTestCase.assertTrue(mIndex < mPropertyNodesVerifierElemList.size()); - mAndroidTestCase.assertTrue(mIndex < vNodeList.size()); - mPropertyNodesVerifierElemList.get(mIndex).verify(vNodeList.get(mIndex)); - mIndex++; - } -} - -/** - * Utility class which verifies input VNode. - * - * This class first checks whether each propertyNode in the VNode is in the - * "ordered expected property list". - * If the node does not exist in the "ordered list", the class refers to - * "unorderd expected property set" and checks the node is expected somewhere. - */ -/* package */ class PropertyNodesVerifierElem { - public static class TypeSet extends HashSet<String> { - public TypeSet(String ... array) { - super(Arrays.asList(array)); - } - } - - public static class GroupSet extends HashSet<String> { - public GroupSet(String ... array) { - super(Arrays.asList(array)); - } - } - - private final HashMap<String, List<PropertyNode>> mOrderedNodeMap; - // Intentionally use ArrayList instead of Set, assuming there may be more than one - // exactly same objects. - private final ArrayList<PropertyNode> mUnorderedNodeList; - private final TestCase mTestCase; - - public PropertyNodesVerifierElem(TestCase testCase) { - mOrderedNodeMap = new HashMap<String, List<PropertyNode>>(); - mUnorderedNodeList = new ArrayList<PropertyNode>(); - mTestCase = testCase; - } - - // WithOrder - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue) { - return addExpectedNodeWithOrder(propName, propValue, null, null, null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder( - String propName, String propValue, ContentValues contentValues) { - return addExpectedNodeWithOrder(propName, propValue, null, - null, contentValues, null, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder( - String propName, List<String> propValueList, ContentValues contentValues) { - return addExpectedNodeWithOrder(propName, null, propValueList, - null, contentValues, null, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder( - String propName, String propValue, List<String> propValueList) { - return addExpectedNodeWithOrder(propName, propValue, propValueList, null, - null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder( - String propName, List<String> propValueList) { - final String propValue = concatinateListWithSemiColon(propValueList); - return addExpectedNodeWithOrder(propName, propValue.toString(), propValueList, - null, null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, - TypeSet paramMap_TYPE) { - return addExpectedNodeWithOrder(propName, propValue, null, - null, null, paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, - List<String> propValueList, TypeSet paramMap_TYPE) { - return addExpectedNodeWithOrder(propName, null, propValueList, null, null, - paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, - ContentValues paramMap, TypeSet paramMap_TYPE) { - return addExpectedNodeWithOrder(propName, propValue, null, null, - paramMap, paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, - List<String> propValueList, TypeSet paramMap_TYPE) { - return addExpectedNodeWithOrder(propName, propValue, propValueList, null, null, - paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNodeWithOrder(String propName, String propValue, - List<String> propValueList, byte[] propValue_bytes, - ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { - if (propValue == null && propValueList != null) { - propValue = concatinateListWithSemiColon(propValueList); - } - PropertyNode propertyNode = new PropertyNode(propName, - propValue, propValueList, propValue_bytes, - paramMap, paramMap_TYPE, propGroupSet); - List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName); - if (expectedNodeList == null) { - expectedNodeList = new ArrayList<PropertyNode>(); - mOrderedNodeMap.put(propName, expectedNodeList); - } - expectedNodeList.add(propertyNode); - return this; - } - - // WithoutOrder - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue) { - return addExpectedNode(propName, propValue, null, null, null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - ContentValues contentValues) { - return addExpectedNode(propName, propValue, null, null, contentValues, null, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, - List<String> propValueList, ContentValues contentValues) { - return addExpectedNode(propName, null, - propValueList, null, contentValues, null, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - List<String> propValueList) { - return addExpectedNode(propName, propValue, propValueList, null, null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, - List<String> propValueList) { - return addExpectedNode(propName, null, propValueList, - null, null, null, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - TypeSet paramMap_TYPE) { - return addExpectedNode(propName, propValue, null, null, null, paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, - List<String> propValueList, TypeSet paramMap_TYPE) { - final String propValue = concatinateListWithSemiColon(propValueList); - return addExpectedNode(propName, propValue, propValueList, null, null, - paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - List<String> propValueList, TypeSet paramMap_TYPE) { - return addExpectedNode(propName, propValue, propValueList, null, null, - paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - ContentValues paramMap, TypeSet paramMap_TYPE) { - return addExpectedNode(propName, propValue, null, null, - paramMap, paramMap_TYPE, null); - } - - public PropertyNodesVerifierElem addExpectedNode(String propName, String propValue, - List<String> propValueList, byte[] propValue_bytes, - ContentValues paramMap, TypeSet paramMap_TYPE, GroupSet propGroupSet) { - if (propValue == null && propValueList != null) { - propValue = concatinateListWithSemiColon(propValueList); - } - mUnorderedNodeList.add(new PropertyNode(propName, propValue, - propValueList, propValue_bytes, paramMap, paramMap_TYPE, propGroupSet)); - return this; - } - - public void verify(VNode vnode) { - for (PropertyNode actualNode : vnode.propList) { - verifyNode(actualNode.propName, actualNode); - } - if (!mOrderedNodeMap.isEmpty() || !mUnorderedNodeList.isEmpty()) { - List<String> expectedProps = new ArrayList<String>(); - for (List<PropertyNode> nodes : mOrderedNodeMap.values()) { - for (PropertyNode node : nodes) { - if (!expectedProps.contains(node.propName)) { - expectedProps.add(node.propName); - } - } - } - for (PropertyNode node : mUnorderedNodeList) { - if (!expectedProps.contains(node.propName)) { - expectedProps.add(node.propName); - } - } - mTestCase.fail("Expected property " + Arrays.toString(expectedProps.toArray()) - + " was not found."); - } - } - - private void verifyNode(final String propName, final PropertyNode actualNode) { - List<PropertyNode> expectedNodeList = mOrderedNodeMap.get(propName); - final int size = (expectedNodeList != null ? expectedNodeList.size() : 0); - if (size > 0) { - for (int i = 0; i < size; i++) { - PropertyNode expectedNode = expectedNodeList.get(i); - List<PropertyNode> expectedButDifferentValueList = new ArrayList<PropertyNode>(); - if (expectedNode.propName.equals(propName)) { - if (expectedNode.equals(actualNode)) { - expectedNodeList.remove(i); - if (expectedNodeList.size() == 0) { - mOrderedNodeMap.remove(propName); - } - return; - } else { - expectedButDifferentValueList.add(expectedNode); - } - } - - // "actualNode" is not in ordered expected list. - // Try looking over unordered expected list. - if (tryFoundExpectedNodeFromUnorderedList(actualNode, - expectedButDifferentValueList)) { - return; - } - - if (!expectedButDifferentValueList.isEmpty()) { - // Same propName exists but with different value(s). - failWithExpectedNodeList(propName, actualNode, - expectedButDifferentValueList); - } else { - // There's no expected node with same propName. - mTestCase.fail("Unexpected property \"" + propName + "\" exists."); - } - } - } else { - List<PropertyNode> expectedButDifferentValueList = - new ArrayList<PropertyNode>(); - if (tryFoundExpectedNodeFromUnorderedList(actualNode, expectedButDifferentValueList)) { - return; - } else { - if (!expectedButDifferentValueList.isEmpty()) { - // Same propName exists but with different value(s). - failWithExpectedNodeList(propName, actualNode, - expectedButDifferentValueList); - } else { - // There's no expected node with same propName. - mTestCase.fail("Unexpected property \"" + propName + "\" exists."); - } - } - } - } - - private String concatinateListWithSemiColon(List<String> array) { - StringBuffer buffer = new StringBuffer(); - boolean first = true; - for (String propValueElem : array) { - if (first) { - first = false; - } else { - buffer.append(';'); - } - buffer.append(propValueElem); - } - - return buffer.toString(); - } - - private boolean tryFoundExpectedNodeFromUnorderedList(PropertyNode actualNode, - List<PropertyNode> expectedButDifferentValueList) { - final String propName = actualNode.propName; - int unorderedListSize = mUnorderedNodeList.size(); - for (int i = 0; i < unorderedListSize; i++) { - PropertyNode unorderedExpectedNode = mUnorderedNodeList.get(i); - if (unorderedExpectedNode.propName.equals(propName)) { - if (unorderedExpectedNode.equals(actualNode)) { - mUnorderedNodeList.remove(i); - return true; - } - expectedButDifferentValueList.add(unorderedExpectedNode); - } - } - return false; - } - - private void failWithExpectedNodeList(String propName, PropertyNode actualNode, - List<PropertyNode> expectedNodeList) { - StringBuilder builder = new StringBuilder(); - for (PropertyNode expectedNode : expectedNodeList) { - builder.append("expected: "); - builder.append(expectedNode.toString()); - builder.append("\n"); - } - mTestCase.fail("Property \"" + propName + "\" has wrong value.\n" - + builder.toString() - + " actual: " + actualNode.toString()); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java b/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java deleted file mode 100644 index 2de0464..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardExporterTests.java +++ /dev/null @@ -1,969 +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.pim.vcard.VCardConfig; -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.pim.vcard.PropertyNodesVerifierElem.TypeSet; - -import java.util.Arrays; - -/** - * Tests for the code related to vCard exporter, inculding vCard composer. - * This test class depends on vCard importer code, so if tests for vCard importer fail, - * the result of this class will not be reliable. - */ -public class VCardExporterTests extends VCardTestsBase { - private static final byte[] sPhotoByteArray = - VCardImporterTests.sPhotoByteArrayForComplicatedCase; - - public void testSimpleV21() { - mVerifier.initForExportTest(V21); - mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Ando") - .put(StructuredName.GIVEN_NAME, "Roid"); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("FN", "Roid Ando") - .addExpectedNode("N", "Ando;Roid;;;", - Arrays.asList("Ando", "Roid", "", "", "")); - } - - private void testStructuredNameBasic(int vcardType) { - final boolean isV30 = VCardConfig.isV30(vcardType); - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") - .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") - .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") - .put(StructuredName.PREFIX, "AppropriatePrefix") - .put(StructuredName.SUFFIX, "AppropriateSuffix") - .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") - .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle"); - - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("N", - "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" - + "AppropriatePrefix;AppropriateSuffix", - Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", - "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) - .addExpectedNodeWithOrder("FN", - "AppropriatePrefix AppropriateGivenName " - + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") - .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") - .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") - .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); - - if (isV30) { - elem.addExpectedNode("SORT-STRING", - "AppropriatePhoneticGiven AppropriatePhoneticMiddle " - + "AppropriatePhoneticFamily"); - } - } - - public void testStructuredNameBasicV21() { - testStructuredNameBasic(V21); - } - - public void testStructuredNameBasicV30() { - testStructuredNameBasic(V30); - } - - /** - * Test that only "primary" StructuredName is emitted, so that our vCard file - * will not confuse the external importer, assuming there may be some importer - * which presume that there's only one property toward each of "N", "FN", etc. - * Note that more than one "N", "FN", etc. properties are acceptable in vCard spec. - */ - private void testStructuredNameUsePrimaryCommon(int vcardType) { - final boolean isV30 = (vcardType == V30); - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") - .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") - .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") - .put(StructuredName.PREFIX, "DoNotEmitPrefix1") - .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") - .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") - .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); - - // With "IS_PRIMARY=1". This is what we should use. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") - .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") - .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") - .put(StructuredName.PREFIX, "AppropriatePrefix") - .put(StructuredName.SUFFIX, "AppropriateSuffix") - .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") - .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") - .put(StructuredName.IS_PRIMARY, 1); - - // With "IS_PRIMARY=1", but we should ignore this time, since this is second, not first. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") - .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") - .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") - .put(StructuredName.PREFIX, "DoNotEmitPrefix2") - .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") - .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") - .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") - .put(StructuredName.IS_PRIMARY, 1); - - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("N", - "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" - + "AppropriatePrefix;AppropriateSuffix", - Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", - "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) - .addExpectedNodeWithOrder("FN", - "AppropriatePrefix AppropriateGivenName " - + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") - .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") - .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") - .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); - - if (isV30) { - elem.addExpectedNode("SORT-STRING", - "AppropriatePhoneticGiven AppropriatePhoneticMiddle " - + "AppropriatePhoneticFamily"); - } - } - - public void testStructuredNameUsePrimaryV21() { - testStructuredNameUsePrimaryCommon(V21); - } - - public void testStructuredNameUsePrimaryV30() { - testStructuredNameUsePrimaryCommon(V30); - } - - /** - * Tests that only "super primary" StructuredName is emitted. - * See also the comment in {@link #testStructuredNameUsePrimaryCommon(int)}. - */ - private void testStructuredNameUseSuperPrimaryCommon(int vcardType) { - final boolean isV30 = (vcardType == V30); - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName1") - .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName1") - .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName1") - .put(StructuredName.PREFIX, "DoNotEmitPrefix1") - .put(StructuredName.SUFFIX, "DoNotEmitSuffix1") - .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily1") - .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven1") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle1"); - - // With "IS_PRIMARY=1", but we should ignore this time. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName2") - .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName2") - .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName2") - .put(StructuredName.PREFIX, "DoNotEmitPrefix2") - .put(StructuredName.SUFFIX, "DoNotEmitSuffix2") - .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily2") - .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven2") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") - .put(StructuredName.IS_PRIMARY, 1); - - // With "IS_SUPER_PRIMARY=1". This is what we should use. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "AppropriateFamilyName") - .put(StructuredName.GIVEN_NAME, "AppropriateGivenName") - .put(StructuredName.MIDDLE_NAME, "AppropriateMiddleName") - .put(StructuredName.PREFIX, "AppropriatePrefix") - .put(StructuredName.SUFFIX, "AppropriateSuffix") - .put(StructuredName.PHONETIC_FAMILY_NAME, "AppropriatePhoneticFamily") - .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle") - .put(StructuredName.IS_SUPER_PRIMARY, 1); - - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "DoNotEmitFamilyName3") - .put(StructuredName.GIVEN_NAME, "DoNotEmitGivenName3") - .put(StructuredName.MIDDLE_NAME, "DoNotEmitMiddleName3") - .put(StructuredName.PREFIX, "DoNotEmitPrefix3") - .put(StructuredName.SUFFIX, "DoNotEmitSuffix3") - .put(StructuredName.PHONETIC_FAMILY_NAME, "DoNotEmitPhoneticFamily3") - .put(StructuredName.PHONETIC_GIVEN_NAME, "DoNotEmitPhoneticGiven3") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle3") - .put(StructuredName.IS_PRIMARY, 1); - - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("N", - "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" - + "AppropriatePrefix;AppropriateSuffix", - Arrays.asList("AppropriateFamilyName", "AppropriateGivenName", - "AppropriateMiddleName", "AppropriatePrefix", "AppropriateSuffix")) - .addExpectedNodeWithOrder("FN", - "AppropriatePrefix AppropriateGivenName " - + "AppropriateMiddleName AppropriateFamilyName AppropriateSuffix") - .addExpectedNode("X-PHONETIC-FIRST-NAME", "AppropriatePhoneticGiven") - .addExpectedNode("X-PHONETIC-MIDDLE-NAME", "AppropriatePhoneticMiddle") - .addExpectedNode("X-PHONETIC-LAST-NAME", "AppropriatePhoneticFamily"); - - if (isV30) { - elem.addExpectedNode("SORT-STRING", - "AppropriatePhoneticGiven AppropriatePhoneticMiddle" - + " AppropriatePhoneticFamily"); - } - } - - public void testStructuredNameUseSuperPrimaryV21() { - testStructuredNameUseSuperPrimaryCommon(V21); - } - - public void testStructuredNameUseSuperPrimaryV30() { - testStructuredNameUseSuperPrimaryCommon(V30); - } - - public void testNickNameV30() { - mVerifier.initForExportTest(V30); - mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) - .put(Nickname.NAME, "Nicky"); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNodeWithOrder("NICKNAME", "Nicky"); - } - - private void testPhoneBasicCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "1") - .put(Phone.TYPE, Phone.TYPE_HOME); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "1", new TypeSet("HOME")); - } - - public void testPhoneBasicV21() { - testPhoneBasicCommon(V21); - } - - public void testPhoneBasicV30() { - testPhoneBasicCommon(V30); - } - - public void testPhoneRefrainFormatting() { - mVerifier.initForExportTest(V21 | VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING); - mVerifier.addInputEntry().addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "1234567890(abcdefghijklmnopqrstuvwxyz)") - .put(Phone.TYPE, Phone.TYPE_HOME); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "1234567890(abcdefghijklmnopqrstuvwxyz)", - new TypeSet("HOME")); - } - - /** - * Tests that vCard composer emits corresponding type param which we expect. - */ - private void testPhoneVariousTypeSupport(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "10") - .put(Phone.TYPE, Phone.TYPE_HOME); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "20") - .put(Phone.TYPE, Phone.TYPE_WORK); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "30") - .put(Phone.TYPE, Phone.TYPE_FAX_HOME); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "40") - .put(Phone.TYPE, Phone.TYPE_FAX_WORK); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "50") - .put(Phone.TYPE, Phone.TYPE_MOBILE); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "60") - .put(Phone.TYPE, Phone.TYPE_PAGER); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "70") - .put(Phone.TYPE, Phone.TYPE_OTHER); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "80") - .put(Phone.TYPE, Phone.TYPE_CAR); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "90") - .put(Phone.TYPE, Phone.TYPE_COMPANY_MAIN); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "100") - .put(Phone.TYPE, Phone.TYPE_ISDN); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "110") - .put(Phone.TYPE, Phone.TYPE_MAIN); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "120") - .put(Phone.TYPE, Phone.TYPE_OTHER_FAX); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "130") - .put(Phone.TYPE, Phone.TYPE_TELEX); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "140") - .put(Phone.TYPE, Phone.TYPE_WORK_MOBILE); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "150") - .put(Phone.TYPE, Phone.TYPE_WORK_PAGER); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "160") - .put(Phone.TYPE, Phone.TYPE_MMS); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "10", new TypeSet("HOME")) - .addExpectedNode("TEL", "20", new TypeSet("WORK")) - .addExpectedNode("TEL", "30", new TypeSet("HOME", "FAX")) - .addExpectedNode("TEL", "40", new TypeSet("WORK", "FAX")) - .addExpectedNode("TEL", "50", new TypeSet("CELL")) - .addExpectedNode("TEL", "60", new TypeSet("PAGER")) - .addExpectedNode("TEL", "70", new TypeSet("VOICE")) - .addExpectedNode("TEL", "80", new TypeSet("CAR")) - .addExpectedNode("TEL", "90", new TypeSet("WORK", "PREF")) - .addExpectedNode("TEL", "100", new TypeSet("ISDN")) - .addExpectedNode("TEL", "110", new TypeSet("PREF")) - .addExpectedNode("TEL", "120", new TypeSet("FAX")) - .addExpectedNode("TEL", "130", new TypeSet("TLX")) - .addExpectedNode("TEL", "140", new TypeSet("WORK", "CELL")) - .addExpectedNode("TEL", "150", new TypeSet("WORK", "PAGER")) - .addExpectedNode("TEL", "160", new TypeSet("MSG")); - } - - public void testPhoneVariousTypeSupportV21() { - testPhoneVariousTypeSupport(V21); - } - - public void testPhoneVariousTypeSupportV30() { - testPhoneVariousTypeSupport(V30); - } - - /** - * Tests that "PREF"s are emitted appropriately. - */ - private void testPhonePrefHandlingCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "1") - .put(Phone.TYPE, Phone.TYPE_HOME); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "2") - .put(Phone.TYPE, Phone.TYPE_WORK) - .put(Phone.IS_PRIMARY, 1); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "3") - .put(Phone.TYPE, Phone.TYPE_FAX_HOME) - .put(Phone.IS_PRIMARY, 1); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "4") - .put(Phone.TYPE, Phone.TYPE_FAX_WORK); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "4", new TypeSet("WORK", "FAX")) - .addExpectedNode("TEL", "3", new TypeSet("HOME", "FAX", "PREF")) - .addExpectedNode("TEL", "2", new TypeSet("WORK", "PREF")) - .addExpectedNode("TEL", "1", new TypeSet("HOME")); - } - - public void testPhonePrefHandlingV21() { - testPhonePrefHandlingCommon(V21); - } - - public void testPhonePrefHandlingV30() { - testPhonePrefHandlingCommon(V30); - } - - private void testMiscPhoneTypeHandling(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "1") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "Modem"); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "2") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "MSG"); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "3") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "BBS"); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "4") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "VIDEO"); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "5") - .put(Phone.TYPE, Phone.TYPE_CUSTOM); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "6") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "_AUTO_CELL"); // The old indicator for the type mobile. - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "7") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "\u643A\u5E2F"); // Mobile phone in Japanese Kanji - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "8") - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "invalid"); - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); - elem.addExpectedNode("TEL", "1", new TypeSet("MODEM")) - .addExpectedNode("TEL", "2", new TypeSet("MSG")) - .addExpectedNode("TEL", "3", new TypeSet("BBS")) - .addExpectedNode("TEL", "4", new TypeSet("VIDEO")) - .addExpectedNode("TEL", "5", new TypeSet("VOICE")) - .addExpectedNode("TEL", "6", new TypeSet("CELL")) - .addExpectedNode("TEL", "7", new TypeSet("CELL")) - .addExpectedNode("TEL", "8", new TypeSet("X-invalid")); - } - - public void testPhoneTypeHandlingV21() { - testMiscPhoneTypeHandling(V21); - } - - public void testPhoneTypeHandlingV30() { - testMiscPhoneTypeHandling(V30); - } - - private void testEmailBasicCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "sample@example.com"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("EMAIL", "sample@example.com"); - } - - public void testEmailBasicV21() { - testEmailBasicCommon(V21); - } - - public void testEmailBasicV30() { - testEmailBasicCommon(V30); - } - - private void testEmailVariousTypeSupportCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_home@example.com") - .put(Email.TYPE, Email.TYPE_HOME); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_work@example.com") - .put(Email.TYPE, Email.TYPE_WORK); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_mobile@example.com") - .put(Email.TYPE, Email.TYPE_MOBILE); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_other@example.com") - .put(Email.TYPE, Email.TYPE_OTHER); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("EMAIL", "type_home@example.com", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "type_work@example.com", new TypeSet("WORK")) - .addExpectedNode("EMAIL", "type_mobile@example.com", new TypeSet("CELL")) - .addExpectedNode("EMAIL", "type_other@example.com"); - } - - public void testEmailVariousTypeSupportV21() { - testEmailVariousTypeSupportCommon(V21); - } - - public void testEmailVariousTypeSupportV30() { - testEmailVariousTypeSupportCommon(V30); - } - - private void testEmailPrefHandlingCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_home@example.com") - .put(Email.TYPE, Email.TYPE_HOME) - .put(Email.IS_PRIMARY, 1); - entry.addContentValues(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "type_notype@example.com") - .put(Email.IS_PRIMARY, 1); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("EMAIL", "type_notype@example.com", new TypeSet("PREF")) - .addExpectedNode("EMAIL", "type_home@example.com", new TypeSet("HOME", "PREF")); - } - - public void testEmailPrefHandlingV21() { - testEmailPrefHandlingCommon(V21); - } - - public void testEmailPrefHandlingV30() { - testEmailPrefHandlingCommon(V30); - } - - private void testPostalAddressCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "Pobox") - .put(StructuredPostal.NEIGHBORHOOD, "Neighborhood") - .put(StructuredPostal.STREET, "Street") - .put(StructuredPostal.CITY, "City") - .put(StructuredPostal.REGION, "Region") - .put(StructuredPostal.POSTCODE, "100") - .put(StructuredPostal.COUNTRY, "Country") - .put(StructuredPostal.FORMATTED_ADDRESS, "Formatted Address") - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK); - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, - // ; Country Name - // - // The NEIGHBORHOOD field is appended after the CITY field. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("ADR", - Arrays.asList("Pobox", "", "Street", "City Neighborhood", - "Region", "100", "Country"), new TypeSet("WORK")); - } - - public void testPostalAddressV21() { - testPostalAddressCommon(V21); - } - - public void testPostalAddressV30() { - testPostalAddressCommon(V30); - } - - private void testPostalAddressNonNeighborhood(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.CITY, "City"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("ADR", - Arrays.asList("", "", "", "City", "", "", ""), new TypeSet("HOME")); - } - - public void testPostalAddressNonNeighborhoodV21() { - testPostalAddressNonNeighborhood(V21); - } - - public void testPostalAddressNonNeighborhoodV30() { - testPostalAddressNonNeighborhood(V30); - } - - private void testPostalAddressNonCity(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.NEIGHBORHOOD, "Neighborhood"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("ADR", - Arrays.asList("", "", "", "Neighborhood", "", "", ""), new TypeSet("HOME")); - } - - public void testPostalAddressNonCityV21() { - testPostalAddressNonCity(V21); - } - - public void testPostalAddressNonCityV30() { - testPostalAddressNonCity(V30); - } - - private void testPostalOnlyWithFormattedAddressCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.REGION, "") // Must be ignored. - .put(StructuredPostal.FORMATTED_ADDRESS, - "Formatted address CA 123-334 United Statue"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNodeWithOrder("ADR", ";Formatted address CA 123-334 United Statue;;;;;", - Arrays.asList("", "Formatted address CA 123-334 United Statue", - "", "", "", "", ""), new TypeSet("HOME")); - } - - public void testPostalOnlyWithFormattedAddressV21() { - testPostalOnlyWithFormattedAddressCommon(V21); - } - - public void testPostalOnlyWithFormattedAddressV30() { - testPostalOnlyWithFormattedAddressCommon(V30); - } - - /** - * Tests that the vCard composer honors formatted data when it is available - * even when it is partial. - */ - private void testPostalWithBothStructuredAndFormattedCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "Pobox") - .put(StructuredPostal.COUNTRY, "Country") - .put(StructuredPostal.FORMATTED_ADDRESS, - "Formatted address CA 123-334 United Statue"); // Should be ignored - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("ADR", "Pobox;;;;;;Country", - Arrays.asList("Pobox", "", "", "", "", "", "Country"), - new TypeSet("HOME")); - } - - public void testPostalWithBothStructuredAndFormattedV21() { - testPostalWithBothStructuredAndFormattedCommon(V21); - } - - public void testPostalWithBothStructuredAndFormattedV30() { - testPostalWithBothStructuredAndFormattedCommon(V30); - } - - private void testOrganizationCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "CompanyX") - .put(Organization.DEPARTMENT, "DepartmentY") - .put(Organization.TITLE, "TitleZ") - .put(Organization.JOB_DESCRIPTION, "Description Rambda") // Ignored. - .put(Organization.OFFICE_LOCATION, "Mountain View") // Ignored. - .put(Organization.PHONETIC_NAME, "PhoneticName!") // Ignored - .put(Organization.SYMBOL, "(^o^)/~~"); // Ignore him (her). - entry.addContentValues(Organization.CONTENT_ITEM_TYPE) - .putNull(Organization.COMPANY) - .put(Organization.DEPARTMENT, "DepartmentXX") - .putNull(Organization.TITLE); - entry.addContentValues(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "CompanyXYZ") - .putNull(Organization.DEPARTMENT) - .put(Organization.TITLE, "TitleXYZYX"); - // Currently we do not use group but depend on the order. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNodeWithOrder("ORG", "CompanyX;DepartmentY", - Arrays.asList("CompanyX", "DepartmentY")) - .addExpectedNodeWithOrder("TITLE", "TitleZ") - .addExpectedNodeWithOrder("ORG", "DepartmentXX") - .addExpectedNodeWithOrder("ORG", "CompanyXYZ") - .addExpectedNodeWithOrder("TITLE", "TitleXYZYX"); - } - - public void testOrganizationV21() { - testOrganizationCommon(V21); - } - - public void testOrganizationV30() { - testOrganizationCommon(V30); - } - - private void testImVariousTypeSupportCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_AIM) - .put(Im.DATA, "aim"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_MSN) - .put(Im.DATA, "msn"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_YAHOO) - .put(Im.DATA, "yahoo"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_SKYPE) - .put(Im.DATA, "skype"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_QQ) - .put(Im.DATA, "qq"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK) - .put(Im.DATA, "google talk"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_ICQ) - .put(Im.DATA, "icq"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_JABBER) - .put(Im.DATA, "jabber"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_NETMEETING) - .put(Im.DATA, "netmeeting"); - - // No determined way to express unknown type... - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("X-JABBER", "jabber") - .addExpectedNode("X-ICQ", "icq") - .addExpectedNode("X-GOOGLE-TALK", "google talk") - .addExpectedNode("X-QQ", "qq") - .addExpectedNode("X-SKYPE-USERNAME", "skype") - .addExpectedNode("X-YAHOO", "yahoo") - .addExpectedNode("X-MSN", "msn") - .addExpectedNode("X-NETMEETING", "netmeeting") - .addExpectedNode("X-AIM", "aim"); - } - - public void testImBasiV21() { - testImVariousTypeSupportCommon(V21); - } - - public void testImBasicV30() { - testImVariousTypeSupportCommon(V30); - } - - private void testImPrefHandlingCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_AIM) - .put(Im.DATA, "aim1"); - entry.addContentValues(Im.CONTENT_ITEM_TYPE) - .put(Im.PROTOCOL, Im.PROTOCOL_AIM) - .put(Im.DATA, "aim2") - .put(Im.TYPE, Im.TYPE_HOME) - .put(Im.IS_PRIMARY, 1); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("X-AIM", "aim1") - .addExpectedNode("X-AIM", "aim2", new TypeSet("HOME", "PREF")); - } - - public void testImPrefHandlingV21() { - testImPrefHandlingCommon(V21); - } - - public void testImPrefHandlingV30() { - testImPrefHandlingCommon(V30); - } - - private void testWebsiteCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Website.CONTENT_ITEM_TYPE) - .put(Website.URL, "http://website.example.android.com/index.html") - .put(Website.TYPE, Website.TYPE_BLOG); - entry.addContentValues(Website.CONTENT_ITEM_TYPE) - .put(Website.URL, "ftp://ftp.example.android.com/index.html") - .put(Website.TYPE, Website.TYPE_FTP); - - // We drop TYPE information since vCard (especially 3.0) does not allow us to emit it. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("URL", "ftp://ftp.example.android.com/index.html") - .addExpectedNode("URL", "http://website.example.android.com/index.html"); - } - - public void testWebsiteV21() { - testWebsiteCommon(V21); - } - - public void testWebsiteV30() { - testWebsiteCommon(V30); - } - - private String getAndroidPropValue(final String mimeType, String value, Integer type) { - return getAndroidPropValue(mimeType, value, type, null); - } - - private String getAndroidPropValue(final String mimeType, String value, - Integer type, String label) { - return (mimeType + ";" + value + ";" - + (type != null ? type : "") + ";" - + (label != null ? label : "") + ";;;;;;;;;;;;"); - } - - private void testEventCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Event.CONTENT_ITEM_TYPE) - .put(Event.TYPE, Event.TYPE_ANNIVERSARY) - .put(Event.START_DATE, "1982-06-16"); - entry.addContentValues(Event.CONTENT_ITEM_TYPE) - .put(Event.TYPE, Event.TYPE_BIRTHDAY) - .put(Event.START_DATE, "2008-10-22"); - entry.addContentValues(Event.CONTENT_ITEM_TYPE) - .put(Event.TYPE, Event.TYPE_OTHER) - .put(Event.START_DATE, "2018-03-12"); - entry.addContentValues(Event.CONTENT_ITEM_TYPE) - .put(Event.TYPE, Event.TYPE_CUSTOM) - .put(Event.LABEL, "The last day") - .put(Event.START_DATE, "When the Tower of Hanoi with 64 rings is completed."); - entry.addContentValues(Event.CONTENT_ITEM_TYPE) - .put(Event.TYPE, Event.TYPE_BIRTHDAY) - .put(Event.START_DATE, "2009-05-19"); // Should be ignored. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("BDAY", "2008-10-22") - .addExpectedNode("X-ANDROID-CUSTOM", - getAndroidPropValue( - Event.CONTENT_ITEM_TYPE, "1982-06-16", Event.TYPE_ANNIVERSARY)) - .addExpectedNode("X-ANDROID-CUSTOM", - getAndroidPropValue( - Event.CONTENT_ITEM_TYPE, "2018-03-12", Event.TYPE_OTHER)) - .addExpectedNode("X-ANDROID-CUSTOM", - getAndroidPropValue( - Event.CONTENT_ITEM_TYPE, - "When the Tower of Hanoi with 64 rings is completed.", - Event.TYPE_CUSTOM, "The last day")); - } - - public void testEventV21() { - testEventCommon(V21); - } - - public void testEventV30() { - testEventCommon(V30); - } - - private void testNoteCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "note1"); - entry.addContentValues(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "note2") - .put(Note.IS_PRIMARY, 1); // Just ignored. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNodeWithOrder("NOTE", "note1") - .addExpectedNodeWithOrder("NOTE", "note2"); - } - - public void testNoteV21() { - testNoteCommon(V21); - } - - public void testNoteV30() { - testNoteCommon(V30); - } - - private void testPhotoCommon(int vcardType) { - final boolean isV30 = vcardType == V30; - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "PhotoTest"); - entry.addContentValues(Photo.CONTENT_ITEM_TYPE) - .put(Photo.PHOTO, sPhotoByteArray); - - ContentValues contentValuesForPhoto = new ContentValues(); - contentValuesForPhoto.put("ENCODING", (isV30 ? "b" : "BASE64")); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("FN", "PhotoTest") - .addExpectedNode("N", "PhotoTest;;;;", - Arrays.asList("PhotoTest", "", "", "", "")) - .addExpectedNodeWithOrder("PHOTO", null, null, sPhotoByteArray, - contentValuesForPhoto, new TypeSet("JPEG"), null); - } - - public void testPhotoV21() { - testPhotoCommon(V21); - } - - public void testPhotoV30() { - testPhotoCommon(V30); - } - - private void testRelationCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - mVerifier.addInputEntry().addContentValues(Relation.CONTENT_ITEM_TYPE) - .put(Relation.TYPE, Relation.TYPE_MOTHER) - .put(Relation.NAME, "Ms. Mother"); - mVerifier.addContentValuesVerifierElem().addExpected(Relation.CONTENT_ITEM_TYPE) - .put(Relation.TYPE, Relation.TYPE_MOTHER) - .put(Relation.NAME, "Ms. Mother"); - } - - public void testRelationV21() { - testRelationCommon(V21); - } - - public void testRelationV30() { - testRelationCommon(V30); - } - - public void testV30HandleEscape() { - mVerifier.initForExportTest(V30); - mVerifier.addInputEntry().addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\\") - .put(StructuredName.GIVEN_NAME, ";") - .put(StructuredName.MIDDLE_NAME, ",") - .put(StructuredName.PREFIX, "\n") - .put(StructuredName.DISPLAY_NAME, "[<{Unescaped:Asciis}>]"); - // Verifies the vCard String correctly escapes each character which must be escaped. - mVerifier.addLineVerifierElem() - .addExpected("N:\\\\;\\;;\\,;\\n;") - .addExpected("FN:[<{Unescaped:Asciis}>]"); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("FN", "[<{Unescaped:Asciis}>]") - .addExpectedNode("N", Arrays.asList("\\", ";", ",", "\n", "")); - } - - /** - * There's no "NICKNAME" property in vCard 2.1, while there is in vCard 3.0. - * We use Android-specific "X-ANDROID-CUSTOM" property. - * This test verifies the functionality. - */ - public void testNickNameV21() { - mVerifier.initForExportTest(V21); - mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) - .put(Nickname.NAME, "Nicky"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("X-ANDROID-CUSTOM", - Nickname.CONTENT_ITEM_TYPE + ";Nicky;;;;;;;;;;;;;;"); - mVerifier.addContentValuesVerifierElem().addExpected(Nickname.CONTENT_ITEM_TYPE) - .put(Nickname.NAME, "Nicky"); - } - - public void testTolerateBrokenPhoneNumberEntryV21() { - mVerifier.initForExportTest(V21); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_HOME) - .put(Phone.NUMBER, "111-222-3333 (Miami)\n444-5555-666 (Tokyo);" - + "777-888-9999 (Chicago);111-222-3333 (Miami)"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "111-222-3333", new TypeSet("HOME")) - .addExpectedNode("TEL", "444-555-5666", new TypeSet("HOME")) - .addExpectedNode("TEL", "777-888-9999", new TypeSet("HOME")); - } - - private void testPickUpNonEmptyContentValuesCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.IS_PRIMARY, 1); // Empty name. Should be ignored. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "family1"); // Not primary. Should be ignored. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.IS_PRIMARY, 1) - .put(StructuredName.FAMILY_NAME, "family2"); // This entry is what we want. - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.IS_PRIMARY, 1) - .put(StructuredName.FAMILY_NAME, "family3"); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "family4"); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("N", Arrays.asList("family2", "", "", "", "")) - .addExpectedNode("FN", "family2"); - } - - public void testPickUpNonEmptyContentValuesV21() { - testPickUpNonEmptyContentValuesCommon(V21); - } - - public void testPickUpNonEmptyContentValuesV30() { - testPickUpNonEmptyContentValuesCommon(V30); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java b/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java deleted file mode 100644 index 21f2254..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java +++ /dev/null @@ -1,1011 +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.pim.vcard.VCardConfig; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Event; -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 com.android.frameworks.coretests.R; -import android.pim.vcard.PropertyNodesVerifierElem.TypeSet; - -import java.util.Arrays; - -public class VCardImporterTests extends VCardTestsBase { - // Push data into int array at first since values like 0x80 are - // interpreted as int by the compiler and casting all of them is - // cumbersome... - private static final int[] sPhotoIntArrayForComplicatedCase = { - 0xff, 0xd8, 0xff, 0xe1, 0x0a, 0x0f, 0x45, 0x78, 0x69, 0x66, 0x00, - 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0d, - 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, - 0xaa, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0xba, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0xc2, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x28, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x32, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x13, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x82, - 0x98, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xfa, - 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x84, 0xc4, 0xa5, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, - 0x01, 0x08, 0x00, 0x00, 0x04, 0x1e, 0x32, 0x30, 0x30, 0x38, 0x31, - 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, 0x00, 0x00, - 0x44, 0x6f, 0x43, 0x6f, 0x4d, 0x6f, 0x00, 0x00, 0x44, 0x39, 0x30, - 0x35, 0x69, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x44, 0x39, 0x30, - 0x35, 0x69, 0x20, 0x56, 0x65, 0x72, 0x31, 0x2e, 0x30, 0x30, 0x00, - 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, - 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x00, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x49, 0x4d, 0x00, 0x30, 0x33, - 0x30, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x14, 0x00, - 0x14, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x34, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x11, 0x09, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x0f, 0x0b, 0x00, - 0x00, 0x27, 0x10, 0x00, 0x00, 0x05, 0x97, 0x00, 0x00, 0x27, 0x10, - 0x00, 0x00, 0x08, 0xb0, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1c, - 0x01, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x02, 0x5e, 0x00, 0x00, - 0x27, 0x10, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x27, 0x10, 0x00, - 0x00, 0x03, 0xcb, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x1b, 0xe5, - 0x00, 0x00, 0x27, 0x10, 0x00, 0x28, 0x82, 0x9a, 0x00, 0x05, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x6a, 0x82, 0x9d, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x72, 0x88, 0x22, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x90, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, - 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, 0x7a, - 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x03, - 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, - 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x03, 0xa2, 0x92, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x03, 0xaa, 0x92, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x03, 0xb2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x03, 0xba, 0x92, 0x05, 0x00, 0x05, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xc2, 0x92, 0x07, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x92, 0x08, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, - 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xca, - 0x92, 0x7c, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x92, 0x86, 0x00, 0x07, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, - 0x03, 0xd2, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, - 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x60, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x00, 0xa2, 0x0e, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xe8, 0xa2, 0x0f, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0xf0, 0xa2, 0x10, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0xa2, - 0x17, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, - 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, - 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, - 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x05, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x03, 0xf8, 0xa4, 0x05, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x1d, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x07, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, - 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0xa4, 0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, - 0x00, 0x27, 0x10, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, - 0x32, 0x30, 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, - 0x31, 0x33, 0x3a, 0x35, 0x35, 0x3a, 0x33, 0x31, 0x00, 0x32, 0x30, - 0x30, 0x38, 0x3a, 0x31, 0x30, 0x3a, 0x32, 0x39, 0x20, 0x31, 0x33, - 0x3a, 0x35, 0x35, 0x3a, 0x34, 0x37, 0x00, 0x00, 0x00, 0x29, 0x88, - 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0xb2, 0x00, 0x00, 0x00, - 0x64, 0x00, 0x00, 0x01, 0x5e, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x25, 0x00, - 0x00, 0x00, 0x0a, 0x00, 0x00, 0x0e, 0x92, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x30, 0x30, - 0x38, 0x31, 0x30, 0x32, 0x39, 0x31, 0x33, 0x35, 0x35, 0x33, 0x31, - 0x00, 0x00, 0x20, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x2a, - 0xe2, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x04, 0x52, 0x39, 0x38, 0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, - 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, - 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x04, 0x6c, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x04, 0x74, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x04, 0x7c, 0x02, 0x02, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x8b, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, - 0x00, 0x20, 0x16, 0x18, 0x1c, 0x18, 0x14, 0x20, 0x1c, 0x1a, 0x1c, - 0x24, 0x22, 0x20, 0x26, 0x30, 0x50, 0x34, 0x30, 0x2c, 0x2c, 0x30, - 0x62, 0x46, 0x4a, 0x3a, 0x50, 0x74, 0x66, 0x7a, 0x78, 0x72, 0x66, - 0x70, 0x6e, 0x80, 0x90, 0xb8, 0x9c, 0x80, 0x88, 0xae, 0x8a, 0x6e, - 0x70, 0xa0, 0xda, 0xa2, 0xae, 0xbe, 0xc4, 0xce, 0xd0, 0xce, 0x7c, - 0x9a, 0xe2, 0xf2, 0xe0, 0xc8, 0xf0, 0xb8, 0xca, 0xce, 0xc6, 0x01, - 0x22, 0x24, 0x24, 0x30, 0x2a, 0x30, 0x5e, 0x34, 0x34, 0x5e, 0xc6, - 0x84, 0x70, 0x84, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, - 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xff, 0xc0, - 0x00, 0x11, 0x08, 0x00, 0x78, 0x00, 0xa0, 0x03, 0x01, 0x21, 0x00, - 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, - 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, - 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, - 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, - 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, - 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, - 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, - 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, - 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, - 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, - 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, - 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, - 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, - 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, - 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, - 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, - 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, - 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, - 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, - 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, - 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, - 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, - 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, - 0x14, 0x54, 0xaa, 0x2a, 0x46, 0x48, 0xa2, 0xa4, 0x55, 0xa6, 0x04, - 0x8a, 0x29, 0xe0, 0x53, 0x10, 0xe0, 0x29, 0xc0, 0x50, 0x03, 0xb1, - 0x46, 0x29, 0x80, 0x84, 0x52, 0x11, 0x40, 0x0d, 0x22, 0x9a, 0x45, - 0x20, 0x23, 0x61, 0x51, 0x30, 0xa0, 0x08, 0xc8, 0xa8, 0xd8, 0x52, - 0x02, 0x26, 0x15, 0x0b, 0x0a, 0x00, 0xb4, 0xa2, 0xa5, 0x5a, 0x00, - 0x91, 0x45, 0x4a, 0xa2, 0x81, 0x92, 0x01, 0x4e, 0x02, 0x98, 0x87, - 0x0a, 0x70, 0xa0, 0x07, 0x62, 0x8c, 0x50, 0x21, 0x0d, 0x25, 0x00, - 0x34, 0x8a, 0x61, 0x14, 0x0c, 0x63, 0x0a, 0x89, 0x85, 0x00, 0x46, - 0xd5, 0x1b, 0x52, 0x02, 0x16, 0xa8, 0x98, 0x50, 0x05, 0x94, 0xa9, - 0x16, 0x80, 0x25, 0x5a, 0x95, 0x68, 0x18, 0xf1, 0x4f, 0x14, 0xc4, - 0x3b, 0xb5, 0x22, 0xb6, 0x38, 0x34, 0x00, 0xe3, 0x22, 0x8e, 0xf4, - 0x79, 0x8a, 0x7b, 0xd1, 0x71, 0x03, 0x30, 0xc7, 0x14, 0x83, 0xa5, - 0x00, 0x06, 0x98, 0x68, 0x01, 0x8d, 0x51, 0x35, 0x03, 0x22, 0x6a, - 0x8d, 0xa9, 0x01, 0x13, 0x54, 0x4d, 0x40, 0x13, 0xa5, 0x4a, 0x28, - 0x02, 0x45, 0x35, 0x2a, 0x9a, 0x00, 0x78, 0x34, 0xf0, 0x69, 0x80, - 0x34, 0x81, 0x45, 0x40, 0xce, 0x58, 0xe6, 0xa2, 0x4c, 0x06, 0xe4, - 0xfa, 0xd1, 0x93, 0x50, 0x21, 0xca, 0xe4, 0x55, 0x84, 0x90, 0x30, - 0xab, 0x8b, 0x18, 0xa6, 0x9a, 0x6a, 0xc4, 0x31, 0xaa, 0x26, 0xa0, - 0x64, 0x4d, 0x51, 0xb5, 0x20, 0x23, 0x6a, 0x89, 0xa8, 0x02, 0x44, - 0x35, 0x2a, 0x9a, 0x00, 0x95, 0x4d, 0x48, 0xa6, 0x80, 0x24, 0x53, - 0x4e, 0xce, 0x05, 0x30, 0x2b, 0x3b, 0xee, 0x6a, 0x91, 0x5d, 0x76, - 0x63, 0xbd, 0x65, 0x7d, 0x40, 0x66, 0x68, 0xa9, 0x02, 0x45, 0x2b, - 0xb3, 0x9e, 0xb4, 0xc5, 0x6d, 0xad, 0x9a, 0xa0, 0x2c, 0x06, 0xc8, - 0xcd, 0x04, 0xd6, 0xa2, 0x23, 0x63, 0x51, 0xb1, 0xa0, 0x64, 0x4d, - 0x51, 0x93, 0x48, 0x08, 0xda, 0xa2, 0x6a, 0x00, 0x72, 0x1a, 0x99, - 0x4d, 0x00, 0x48, 0xa6, 0xa4, 0x53, 0x4c, 0x07, 0x86, 0x03, 0xbd, - 0x2b, 0x9c, 0xa7, 0x14, 0x98, 0x10, 0x85, 0x34, 0xe0, 0xa6, 0xb3, - 0xb0, 0x0b, 0xb5, 0xa8, 0x0a, 0xd4, 0x58, 0x42, 0xed, 0x3e, 0x94, - 0xd2, 0xa6, 0x8b, 0x01, 0x34, 0x44, 0xed, 0xe6, 0x9c, 0x4d, 0x6a, - 0x80, 0x8d, 0x8d, 0x46, 0xc6, 0x80, 0x23, 0x63, 0x51, 0x9a, 0x06, - 0x46, 0xd5, 0x13, 0x52, 0x01, 0x54, 0xd4, 0xaa, 0x68, 0x02, 0x40, - 0x6a, 0x40, 0x78, 0xa0, 0x08, 0x59, 0xce, 0xee, 0xb5, 0x2a, 0x39, - 0xd9, 0x59, 0xa7, 0xa8, 0x00, 0x73, 0xeb, 0x4e, 0x0e, 0x7d, 0x69, - 0x5c, 0x05, 0xf3, 0x0f, 0xad, 0x1e, 0x61, 0xf5, 0xa7, 0x71, 0x0b, - 0xe6, 0x35, 0x21, 0x90, 0xd3, 0xb8, 0x0e, 0x32, 0x10, 0x95, 0x10, - 0x91, 0xb3, 0xd6, 0x9b, 0x60, 0x4b, 0x9c, 0x8a, 0x63, 0x1a, 0xb0, - 0x18, 0x4d, 0x46, 0xc6, 0x80, 0x22, 0x6a, 0x61, 0xa4, 0x31, 0xaa, - 0x6a, 0x55, 0x34, 0x01, 0x2a, 0x9a, 0x7e, 0x78, 0xa0, 0x08, 0x09, - 0xf9, 0xaa, 0x58, 0xcf, 0xca, 0x6b, 0x3e, 0xa0, 0x00, 0xd3, 0x81, - 0xa9, 0x01, 0x73, 0x46, 0x69, 0x80, 0xb9, 0xa4, 0xcd, 0x00, 0x2b, - 0x1f, 0x92, 0xa3, 0x07, 0x9a, 0x6f, 0x70, 0x26, 0xcf, 0x14, 0xd2, - 0x6b, 0x51, 0x0c, 0x63, 0x51, 0xb1, 0xa0, 0x08, 0xda, 0x98, 0x69, - 0x0c, 0x8d, 0x4d, 0x4a, 0xa6, 0x80, 0x24, 0x53, 0x52, 0x03, 0xc5, - 0x02, 0x21, 0x27, 0xe6, 0xa9, 0x23, 0x3f, 0x29, 0xac, 0xfa, 0x8c, - 0x01, 0xe6, 0x9c, 0x0d, 0x48, 0x0a, 0x0d, 0x2e, 0x68, 0x01, 0x73, - 0x49, 0x9a, 0x60, 0x2b, 0x1f, 0x92, 0x98, 0x3a, 0xd3, 0x7b, 0x81, - 0x36, 0x78, 0xa6, 0x93, 0x5a, 0x88, 0x8c, 0x9a, 0x63, 0x1a, 0x00, - 0x8c, 0xd3, 0x0d, 0x21, 0x91, 0x29, 0xa9, 0x14, 0xd0, 0x04, 0x8a, - 0x69, 0xe0, 0xd3, 0x11, 0x1b, 0x1e, 0x6a, 0x48, 0xcf, 0xca, 0x6b, - 0x3e, 0xa3, 0x10, 0x1a, 0x70, 0x35, 0x20, 0x38, 0x1a, 0x5c, 0xd2, - 0x01, 0x73, 0x49, 0x9a, 0x60, 0x39, 0x8f, 0xca, 0x29, 0x8b, 0xf7, - 0xaa, 0xba, 0x88, 0x96, 0x9a, 0x6b, 0x40, 0x18, 0xc6, 0xa3, 0x26, - 0x80, 0x18, 0x69, 0xa6, 0x90, 0xc8, 0x14, 0xd4, 0x8a, 0x69, 0x80, - 0xf0, 0x6a, 0x40, 0x68, 0x10, 0xbb, 0x41, 0xa7, 0xe3, 0x0b, 0xc5, - 0x2b, 0x01, 0x10, 0xa7, 0x03, 0x59, 0x0c, 0x76, 0x69, 0x73, 0x40, - 0x0b, 0x9a, 0x28, 0x11, 0x28, 0x19, 0x5e, 0x69, 0x02, 0x81, 0x5a, - 0xd8, 0x00, 0xd3, 0x4d, 0x50, 0x0c, 0x6a, 0x8c, 0xd2, 0x01, 0xa6, - 0x98, 0x69, 0x0c, 0xae, 0xa6, 0xa4, 0x06, 0x80, 0x1e, 0xa6, 0x9e, - 0x0d, 0x31, 0x12, 0x03, 0x4f, 0x06, 0x80, 0x13, 0x60, 0x34, 0xd3, - 0xc1, 0xa8, 0x92, 0x01, 0xf1, 0x8d, 0xdd, 0x69, 0xcc, 0xa1, 0x69, - 0x5b, 0x4b, 0x80, 0x83, 0x93, 0x52, 0x04, 0x14, 0xe2, 0xae, 0x03, - 0xa9, 0x0d, 0x68, 0x03, 0x4d, 0x34, 0xd0, 0x03, 0x0d, 0x30, 0xd2, - 0x01, 0x86, 0x9a, 0x68, 0x19, 0x58, 0x1a, 0x78, 0xa4, 0x04, 0x8a, - 0x69, 0xe0, 0xd3, 0x10, 0xe0, 0x69, 0xe0, 0xd0, 0x03, 0xc1, 0xa8, - 0xdb, 0xad, 0x4c, 0x81, 0x12, 0x45, 0xd6, 0x9d, 0x25, 0x1d, 0x00, - 0x6a, 0xf5, 0xa9, 0xe8, 0x80, 0x31, 0x29, 0x0d, 0x58, 0x08, 0x69, - 0x86, 0x80, 0x1a, 0x69, 0x86, 0x90, 0x0c, 0x34, 0xd3, 0x48, 0x65, - 0x51, 0x4f, 0x06, 0x98, 0x0f, 0x14, 0xf0, 0x68, 0x10, 0xf0, 0x69, - 0xe0, 0xd0, 0x03, 0x81, 0xa5, 0x2b, 0x9a, 0x1a, 0xb8, 0x87, 0xa8, - 0xdb, 0x4a, 0x46, 0x68, 0xb6, 0x80, 0x2a, 0xa8, 0x14, 0xea, 0x12, - 0xb0, 0x05, 0x21, 0xa6, 0x02, 0x1a, 0x61, 0xa0, 0x06, 0x9a, 0x61, - 0xa4, 0x31, 0x86, 0x9a, 0x69, 0x0c, 0xa8, 0x0d, 0x3c, 0x53, 0x01, - 0xe2, 0x9e, 0x28, 0x10, 0xf1, 0x4e, 0x06, 0x98, 0x0f, 0x06, 0x9e, - 0x0d, 0x02, 0x1c, 0x29, 0xc2, 0x80, 0x16, 0x96, 0x80, 0x0a, 0x4a, - 0x00, 0x43, 0x4d, 0x34, 0x0c, 0x61, 0xa6, 0x1a, 0x40, 0x34, 0xd3, - 0x4d, 0x21, 0x80, 0xff, 0xd9, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0a, - 0x07, 0x07, 0x08, 0x07, 0x06, 0x0a, 0x08, 0x08, 0x08, 0x0b, 0x0a, - 0x0a, 0x0b, 0x0e, 0x18, 0x10, 0x0e, 0x0d, 0x0d, 0x0e, 0x1d, 0x15, - 0x16, 0x11, 0x18, 0x23, 0x1f, 0x25, 0x24, 0x22, 0x1f, 0x22, 0x21, - 0x26, 0x2b, 0x37, 0x2f, 0x26, 0x29, 0x34, 0x29, 0x21, 0x22, 0x30, - 0x41, 0x31, 0x34, 0x39, 0x3b, 0x3e, 0x3e, 0x3e, 0x25, 0x2e, 0x44, - 0x49, 0x43, 0x3c, 0x48, 0x37, 0x3d, 0x3e, 0x3b, 0x01, 0x0a, 0x0b, - 0x0b, 0x0e, 0x0d, 0x0e, 0x1c, 0x10, 0x10, 0x1c, 0x3b, 0x28, 0x22, - 0x28, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, - 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0xff, 0xc0, 0x00, 0x11, - 0x08, 0x00, 0x48, 0x00, 0x60, 0x03, 0x01, 0x21, 0x00, 0x02, 0x11, - 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x01, - 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, - 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, - 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, - 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, - 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, - 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, - 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, - 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, - 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, - 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, - 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, - 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0x01, 0x00, 0x03, 0x01, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, - 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, - 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, - 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, - 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, - 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, - 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, - 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, - 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, - 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, - 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, - 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, - 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, - 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, - 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9e, 0xd2, - 0x2e, 0x07, 0x15, 0xaf, 0x6d, 0x08, 0xe2, 0xb3, 0x45, 0x1a, 0xf6, - 0xd0, 0x00, 0x01, 0xc5, 0x68, 0x45, 0x17, 0x4a, 0xb4, 0x22, 0xe4, - 0x70, 0x8c, 0x74, 0xa9, 0x3c, 0xa1, 0x8e, 0x95, 0x48, 0x96, 0x31, - 0xe2, 0x18, 0xe9, 0x55, 0xa5, 0x8c, 0x7a, 0x50, 0x05, 0x0b, 0x88, - 0x86, 0x0f, 0x15, 0x8f, 0x75, 0x1f, 0x26, 0x93, 0x19, 0x91, 0x77, - 0x18, 0xc1, 0xac, 0x4b, 0xc8, 0xfa, 0xd6, 0x63, 0x37, 0x6d, 0x31, - 0xb4, 0x73, 0x5b, 0x36, 0xa0, 0x1c, 0x50, 0x80, 0xd7, 0x83, 0xa0, - 0xab, 0xd1, 0x62, 0xad, 0x09, 0x8f, 0x17, 0x29, 0x03, 0xb2, 0xcc, - 0xe0, 0x77, 0x14, 0xa3, 0x56, 0xb3, 0x27, 0x1e, 0x67, 0xe9, 0x52, - 0xea, 0xc6, 0x3a, 0x36, 0x48, 0xef, 0x3d, 0x27, 0x70, 0x22, 0x60, - 0x47, 0x52, 0x69, 0xb2, 0xe2, 0xad, 0x3b, 0xea, 0x80, 0xa3, 0x38, - 0xe0, 0xd6, 0x3d, 0xd8, 0x1c, 0xd0, 0xca, 0x46, 0x3d, 0xd0, 0x18, - 0x35, 0x89, 0x78, 0xa3, 0x9a, 0xcd, 0x8c, 0xd2, 0xb3, 0x93, 0x2a, - 0x2b, 0x66, 0xd5, 0xf1, 0x8a, 0x10, 0x1a, 0xd6, 0xf2, 0x03, 0x8a, - 0x9e, 0xe6, 0xf4, 0x5a, 0xdb, 0xef, 0xfe, 0x23, 0xc0, 0xa7, 0x27, - 0xcb, 0x16, 0xc4, 0xcc, 0xdd, 0xe2, 0x78, 0x9a, 0x69, 0x66, 0xcc, - 0x99, 0xe1, 0x4d, 0x47, 0xba, 0xbc, 0xd9, 0x6a, 0xee, 0x26, 0x59, - 0x59, 0x4d, 0xac, 0x69, 0x34, 0x52, 0xe5, 0x8f, 0x55, 0xad, 0x58, - 0xae, 0x85, 0xc4, 0x22, 0x41, 0xdf, 0xad, 0x76, 0x61, 0xe5, 0x6f, - 0x74, 0x45, 0x69, 0xdc, 0x00, 0x79, 0xac, 0x8b, 0xa6, 0xc9, 0x35, - 0xd4, 0x34, 0x64, 0xdc, 0x37, 0x06, 0xb1, 0xae, 0x88, 0xc1, 0xac, - 0xd8, 0xc9, 0x2c, 0xa6, 0xe0, 0x73, 0x5b, 0x36, 0xf3, 0x74, 0xe6, - 0x84, 0x05, 0xe3, 0xa9, 0x47, 0x6a, 0x14, 0xb6, 0x49, 0x3d, 0x85, - 0x3a, 0xee, 0xee, 0x2b, 0xa8, 0xe2, 0x6f, 0x30, 0x81, 0xe9, 0x8a, - 0xca, 0xa4, 0xe2, 0xd3, 0x8b, 0x01, 0xb1, 0xf9, 0x04, 0x7f, 0xaf, - 0x23, 0xf0, 0xa9, 0x54, 0x41, 0x9c, 0xfd, 0xa3, 0xf4, 0xae, 0x65, - 0x18, 0xf7, 0x25, 0x8a, 0xe2, 0x02, 0x38, 0xb8, 0xfd, 0x2a, 0x7b, - 0x5b, 0xa8, 0x6d, 0x6d, 0x5d, 0x9a, 0x5d, 0xcb, 0xbb, 0xd2, 0xb6, - 0xa6, 0xa3, 0x19, 0x5e, 0xe2, 0x03, 0x7b, 0x1d, 0xc2, 0x17, 0x8d, - 0xb8, 0xac, 0xfb, 0x89, 0x39, 0x35, 0xd6, 0x9a, 0x6a, 0xe8, 0x66, - 0x55, 0xcb, 0xf5, 0xac, 0x7b, 0x96, 0xeb, 0x50, 0xc6, 0x88, 0x6d, - 0x66, 0xe9, 0xcd, 0x6c, 0xdb, 0x4f, 0xd3, 0x9a, 0x00, 0x2f, 0xe6, - 0xf9, 0xa3, 0xe7, 0xb5, 0x4a, 0x93, 0x7f, 0xa2, 0xc6, 0x73, 0xdc, - 0xd7, 0x15, 0x55, 0xef, 0x48, 0x7d, 0x09, 0x52, 0x6e, 0x3a, 0xd4, - 0xab, 0x2f, 0xbd, 0x61, 0x16, 0x0c, 0x73, 0x49, 0xc5, 0x24, 0x92, - 0x7f, 0xa2, 0x63, 0xfd, 0xaa, 0xd6, 0x2f, 0x71, 0x0e, 0xb1, 0x93, - 0xf7, 0x2d, 0xf5, 0xa4, 0x9e, 0x4e, 0xb5, 0xdd, 0x4b, 0xf8, 0x68, - 0x4c, 0xcb, 0xb9, 0x93, 0xad, 0x65, 0xce, 0xd9, 0x26, 0xa9, 0x8d, - 0x19, 0xf6, 0xf2, 0xf4, 0xe6, 0xb5, 0xad, 0xe7, 0xc6, 0x39, 0xa0, - 0x18, 0xeb, 0xc9, 0x77, 0x6c, 0x35, 0x2a, 0x4b, 0xfe, 0x8a, 0x9c, - 0xff, 0x00, 0x11, 0xae, 0x3a, 0x8b, 0xde, 0x61, 0xd0, 0x9e, 0x39, - 0xb8, 0xeb, 0x53, 0xac, 0xb9, 0xae, 0x5b, 0x00, 0xf3, 0x27, 0x14, - 0x92, 0xc9, 0xfe, 0x8a, 0x3f, 0xde, 0x35, 0xac, 0x3a, 0x88, 0x92, - 0xcd, 0xb1, 0x6e, 0x7d, 0xcd, 0x32, 0x67, 0xeb, 0xcd, 0x7a, 0x14, - 0xfe, 0x04, 0x26, 0x66, 0xce, 0xf9, 0x26, 0xb3, 0xe6, 0x6e, 0xb4, - 0xd9, 0x48, 0xc8, 0x82, 0x4e, 0x07, 0x35, 0xa7, 0x6f, 0x2f, 0x02, - 0x9a, 0x06, 0x5f, 0x8c, 0xa4, 0x83, 0x0e, 0x32, 0x2a, 0x69, 0xe3, - 0xdd, 0x12, 0x08, 0x97, 0x85, 0xec, 0x2a, 0x2a, 0x42, 0xf1, 0x76, - 0x26, 0xe4, 0x6a, 0x59, 0x0e, 0x18, 0x10, 0x6a, 0xd2, 0x89, 0x02, - 0x6e, 0x2a, 0x71, 0xeb, 0x5c, 0x1c, 0x8c, 0xa6, 0x48, 0xbb, 0xdc, - 0x61, 0x41, 0x35, 0x72, 0x28, 0x87, 0xd9, 0xf6, 0x4a, 0xb9, 0xe7, - 0x38, 0xae, 0x8c, 0x3d, 0x36, 0xdd, 0xde, 0xc4, 0xb0, 0x21, 0x51, - 0x76, 0xa8, 0xc0, 0xaa, 0x93, 0x31, 0xe6, 0xbb, 0x2d, 0x65, 0x61, - 0x19, 0xd3, 0x1e, 0xb5, 0x46, 0x5a, 0x96, 0x5a, 0x30, 0xa0, 0x7e, - 0x05, 0x69, 0x5b, 0xc9, 0xc6, 0x28, 0x40, 0xcd, 0x08, 0x64, 0x3c, - 0x73, 0x57, 0xe1, 0x94, 0xf1, 0xcd, 0x5a, 0x21, 0x8c, 0xb9, 0x63, - 0xe7, 0x67, 0x1d, 0xab, 0x40, 0xb1, 0xfb, 0x00, 0x1d, 0xf0, 0x2b, - 0x99, 0x2d, 0x66, 0x3e, 0x88, 0x75, 0x81, 0x3f, 0x31, 0xf6, 0xab, - 0x64, 0xd6, 0xb4, 0x17, 0xee, 0xd0, 0x9e, 0xe4, 0x32, 0x1a, 0xa7, - 0x31, 0xad, 0x18, 0x14, 0x26, 0xef, 0x54, 0xa5, 0xa8, 0x65, 0xa3, - 0x9c, 0x81, 0xfa, 0x56, 0x8c, 0x2d, 0xce, 0x68, 0x40, 0xcb, 0xf1, - 0x37, 0xbd, 0x5e, 0x85, 0xea, 0xd1, 0x0c, 0xbb, 0x19, 0x56, 0x23, - 0x20, 0x1f, 0xad, 0x5c, 0x42, 0x08, 0x03, 0xb5, 0x55, 0x91, 0x04, - 0xc9, 0x80, 0x38, 0x00, 0x0a, 0x71, 0x34, 0x6c, 0x32, 0x27, 0xe9, - 0x55, 0x25, 0x15, 0x2c, 0x68, 0xa3, 0x30, 0xeb, 0x54, 0xa5, 0x15, - 0x0c, 0xd1, 0x00, 0xff, 0xd9}; - - /* package */ static final byte[] sPhotoByteArrayForComplicatedCase; - - static { - final int length = sPhotoIntArrayForComplicatedCase.length; - sPhotoByteArrayForComplicatedCase = new byte[length]; - for (int i = 0; i < length; i++) { - sPhotoByteArrayForComplicatedCase[i] = (byte)sPhotoIntArrayForComplicatedCase[i]; - } - } - - public void testV21SimpleCase1_Parsing() { - mVerifier.initForImportTest(V21, R.raw.v21_simple_1); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("N", "Ando;Roid;", Arrays.asList("Ando", "Roid", "")); - } - - public void testV21SimpleCase1_Type_Generic() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, R.raw.v21_simple_1); - mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Ando") - .put(StructuredName.GIVEN_NAME, "Roid") - .put(StructuredName.DISPLAY_NAME, "Roid Ando"); - } - - public void testV21SimpleCase1_Type_Japanese() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, R.raw.v21_simple_1); - mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Ando") - .put(StructuredName.GIVEN_NAME, "Roid") - // If name-related strings only contains printable Ascii, - // the order is remained to be US's: - // "Prefix Given Middle Family Suffix" - .put(StructuredName.DISPLAY_NAME, "Roid Ando"); - } - - public void testV21SimpleCase2() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, R.raw.v21_simple_2); - mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.DISPLAY_NAME, "Ando Roid"); - } - - public void testV21SimpleCase3() { - mVerifier.initForImportTest(V21, R.raw.v21_simple_3); - mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Ando") - .put(StructuredName.GIVEN_NAME, "Roid") - // "FN" field should be prefered since it should contain the original - // order intended by the author of the file. - .put(StructuredName.DISPLAY_NAME, "Ando Roid"); - } - - /** - * Tests ';' is properly handled by VCardParser implementation. - */ - public void testV21BackslashCase_Parsing() { - mVerifier.initForImportTest(V21, R.raw.v21_backslash); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", ";A;B\\;C\\;;D;:E;\\\\;", - Arrays.asList("", "A;B\\", "C\\;", "D", ":E", "\\\\", "")) - .addExpectedNodeWithOrder("FN", "A;B\\C\\;D:E\\\\"); - - } - - /** - * Tests ContactStruct correctly ignores redundant fields in "N" property values and - * inserts name related data. - */ - public void testV21BackslashCase() { - mVerifier.initForImportTest(V21, R.raw.v21_backslash); - mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE) - // FAMILY_NAME is empty and removed in this test... - .put(StructuredName.GIVEN_NAME, "A;B\\") - .put(StructuredName.MIDDLE_NAME, "C\\;") - .put(StructuredName.PREFIX, "D") - .put(StructuredName.SUFFIX, ":E") - .put(StructuredName.DISPLAY_NAME, "A;B\\C\\;D:E\\\\"); - } - - public void testOrgBeforTitle() { - mVerifier.initForImportTest(V21, R.raw.v21_org_before_title); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.DISPLAY_NAME, "Normal Guy"); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Company") - .put(Organization.DEPARTMENT, "Organization Devision Room Sheet No.") - .put(Organization.TITLE, "Excellent Janitor") - .put(Organization.TYPE, Organization.TYPE_WORK); - } - - public void testTitleBeforOrg() { - mVerifier.initForImportTest(V21, R.raw.v21_title_before_org); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.DISPLAY_NAME, "Nice Guy"); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Marverous") - .put(Organization.DEPARTMENT, "Perfect Great Good Bad Poor") - .put(Organization.TITLE, "Cool Title") - .put(Organization.TYPE, Organization.TYPE_WORK); - } - - /** - * Verifies that vCard importer correctly interpret "PREF" attribute to IS_PRIMARY. - * The data contain three cases: one "PREF", no "PREF" and multiple "PREF", in each type. - */ - public void testV21PrefToIsPrimary() { - mVerifier.initForImportTest(V21, R.raw.v21_pref_handling); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.DISPLAY_NAME, "Smith"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "1") - .put(Phone.TYPE, Phone.TYPE_HOME); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "2") - .put(Phone.TYPE, Phone.TYPE_WORK) - .put(Phone.IS_PRIMARY, 1); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "3") - .put(Phone.TYPE, Phone.TYPE_ISDN); - elem.addExpected(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "test@example.com") - .put(Email.TYPE, Email.TYPE_HOME) - .put(Email.IS_PRIMARY, 1); - elem.addExpected(Email.CONTENT_ITEM_TYPE) - .put(Email.DATA, "test2@examination.com") - .put(Email.TYPE, Email.TYPE_MOBILE) - .put(Email.IS_PRIMARY, 1); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Company") - .put(Organization.TITLE, "Engineer") - .put(Organization.TYPE, Organization.TYPE_WORK); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Mystery") - .put(Organization.TITLE, "Blogger") - .put(Organization.TYPE, Organization.TYPE_WORK); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Poetry") - .put(Organization.TITLE, "Poet") - .put(Organization.TYPE, Organization.TYPE_WORK); - } - - /** - * Tests all the properties in a complicated vCard are correctly parsed by the VCardParser. - */ - public void testV21ComplicatedCase_Parsing() { - mVerifier.initForImportTest(V21, R.raw.v21_complicated); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", "Gump;Forrest;Hoge;Pos;Tao", - Arrays.asList("Gump", "Forrest", "Hoge", "Pos", "Tao")) - .addExpectedNodeWithOrder("FN", "Joe Due") - .addExpectedNodeWithOrder("ORG", "Gump Shrimp Co.;Sales Dept.;Manager;Fish keeper", - Arrays.asList("Gump Shrimp Co.", "Sales Dept.;Manager", "Fish keeper")) - .addExpectedNodeWithOrder("ROLE", "Fish Cake Keeper!") - .addExpectedNodeWithOrder("TITLE", "Shrimp Man") - .addExpectedNodeWithOrder("X-CLASS", "PUBLIC") - .addExpectedNodeWithOrder("TEL", "(111) 555-1212", new TypeSet("WORK", "VOICE")) - .addExpectedNodeWithOrder("TEL", "(404) 555-1212", new TypeSet("HOME", "VOICE")) - .addExpectedNodeWithOrder("TEL", "0311111111", new TypeSet("CELL")) - .addExpectedNodeWithOrder("TEL", "0322222222", new TypeSet("VIDEO")) - .addExpectedNodeWithOrder("TEL", "0333333333", new TypeSet("VOICE")) - .addExpectedNodeWithOrder("ADR", - ";;100 Waters Edge;Baytown;LA;30314;United States of America", - Arrays.asList("", "", "100 Waters Edge", "Baytown", - "LA", "30314", "United States of America"), - null, null, new TypeSet("WORK"), null) - .addExpectedNodeWithOrder("LABEL", - "100 Waters Edge\r\nBaytown, LA 30314\r\nUnited States of America", - null, null, mContentValuesForQP, new TypeSet("WORK"), null) - .addExpectedNodeWithOrder("ADR", - ";;42 Plantation St.;Baytown;LA;30314;United States of America", - Arrays.asList("", "", "42 Plantation St.", "Baytown", - "LA", "30314", "United States of America"), null, null, - new TypeSet("HOME"), null) - .addExpectedNodeWithOrder("LABEL", - "42 Plantation St.\r\nBaytown, LA 30314\r\nUnited States of America", - null, null, mContentValuesForQP, - new TypeSet("HOME"), null) - .addExpectedNodeWithOrder("EMAIL", "forrestgump@walladalla.com", - new TypeSet("PREF", "INTERNET")) - .addExpectedNodeWithOrder("EMAIL", "cell@example.com", new TypeSet("CELL")) - .addExpectedNodeWithOrder("NOTE", "The following note is the example from RFC 2045.") - .addExpectedNodeWithOrder("NOTE", - "Now's the time for all folk to come to the aid of their country.", - null, null, mContentValuesForQP, null, null) - .addExpectedNodeWithOrder("PHOTO", null, - null, sPhotoByteArrayForComplicatedCase, mContentValuesForBase64V21, - new TypeSet("JPEG"), null) - .addExpectedNodeWithOrder("X-ATTRIBUTE", "Some String") - .addExpectedNodeWithOrder("BDAY", "19800101") - .addExpectedNodeWithOrder("GEO", "35.6563854,139.6994233") - .addExpectedNodeWithOrder("URL", "http://www.example.com/") - .addExpectedNodeWithOrder("REV", "20080424T195243Z"); - } - - /** - * Checks ContactStruct correctly inserts values in a complicated vCard - * into ContentResolver. - */ - public void testV21ComplicatedCase() { - mVerifier.initForImportTest(V21, R.raw.v21_complicated); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Gump") - .put(StructuredName.GIVEN_NAME, "Forrest") - .put(StructuredName.MIDDLE_NAME, "Hoge") - .put(StructuredName.PREFIX, "Pos") - .put(StructuredName.SUFFIX, "Tao") - .put(StructuredName.DISPLAY_NAME, "Joe Due"); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.TYPE, Organization.TYPE_WORK) - .put(Organization.COMPANY, "Gump Shrimp Co.") - .put(Organization.DEPARTMENT, "Sales Dept.;Manager Fish keeper") - .put(Organization.TITLE, "Shrimp Man"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_WORK) - // Phone number is expected to be formated with NAMP format in default. - .put(Phone.NUMBER, "111-555-1212"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_HOME) - .put(Phone.NUMBER, "404-555-1212"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_MOBILE) - .put(Phone.NUMBER, "031-111-1111"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "VIDEO") - .put(Phone.NUMBER, "032-222-2222"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "VOICE") - .put(Phone.NUMBER, "033-333-3333"); - elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) - .put(StructuredPostal.COUNTRY, "United States of America") - .put(StructuredPostal.POSTCODE, "30314") - .put(StructuredPostal.REGION, "LA") - .put(StructuredPostal.CITY, "Baytown") - .put(StructuredPostal.STREET, "100 Waters Edge") - .put(StructuredPostal.FORMATTED_ADDRESS, - "100 Waters Edge Baytown LA 30314 United States of America"); - elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) - .put(StructuredPostal.COUNTRY, "United States of America") - .put(StructuredPostal.POSTCODE, "30314") - .put(StructuredPostal.REGION, "LA") - .put(StructuredPostal.CITY, "Baytown") - .put(StructuredPostal.STREET, "42 Plantation St.") - .put(StructuredPostal.FORMATTED_ADDRESS, - "42 Plantation St. Baytown LA 30314 United States of America"); - elem.addExpected(Email.CONTENT_ITEM_TYPE) - // "TYPE=INTERNET" -> TYPE_CUSTOM + the label "INTERNET" - .put(Email.TYPE, Email.TYPE_CUSTOM) - .put(Email.LABEL, "INTERNET") - .put(Email.DATA, "forrestgump@walladalla.com") - .put(Email.IS_PRIMARY, 1); - elem.addExpected(Email.CONTENT_ITEM_TYPE) - .put(Email.TYPE, Email.TYPE_MOBILE) - .put(Email.DATA, "cell@example.com"); - elem.addExpected(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "The following note is the example from RFC 2045."); - elem.addExpected(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, - "Now's the time for all folk to come to the aid of their country."); - elem.addExpected(Photo.CONTENT_ITEM_TYPE) - // No information about its image format can be inserted. - .put(Photo.PHOTO, sPhotoByteArrayForComplicatedCase); - elem.addExpected(Event.CONTENT_ITEM_TYPE) - .put(Event.START_DATE, "19800101") - .put(Event.TYPE, Event.TYPE_BIRTHDAY); - elem.addExpected(Website.CONTENT_ITEM_TYPE) - .put(Website.URL, "http://www.example.com/") - .put(Website.TYPE, Website.TYPE_HOMEPAGE); - } - - public void testV30Simple_Parsing() { - mVerifier.initForImportTest(V30, R.raw.v30_simple); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "3.0") - .addExpectedNodeWithOrder("FN", "And Roid") - .addExpectedNodeWithOrder("N", "And;Roid;;;", Arrays.asList("And", "Roid", "", "", "")) - .addExpectedNodeWithOrder("ORG", "Open;Handset; Alliance", - Arrays.asList("Open", "Handset", " Alliance")) - .addExpectedNodeWithOrder("SORT-STRING", "android") - .addExpectedNodeWithOrder("TEL", "0300000000", new TypeSet("PREF", "VOICE")) - .addExpectedNodeWithOrder("CLASS", "PUBLIC") - .addExpectedNodeWithOrder("X-GNO", "0") - .addExpectedNodeWithOrder("X-GN", "group0") - .addExpectedNodeWithOrder("X-REDUCTION", "0") - .addExpectedNodeWithOrder("REV", "20081031T065854Z"); - } - - public void testV30Simple() { - mVerifier.initForImportTest(V30, R.raw.v30_simple); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "And") - .put(StructuredName.GIVEN_NAME, "Roid") - .put(StructuredName.DISPLAY_NAME, "And Roid") - .put(StructuredName.PHONETIC_GIVEN_NAME, "android"); - elem.addExpected(Organization.CONTENT_ITEM_TYPE) - .put(Organization.COMPANY, "Open") - .put(Organization.DEPARTMENT, "Handset Alliance") - .put(Organization.TYPE, Organization.TYPE_WORK); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "VOICE") - .put(Phone.NUMBER, "030-000-0000") - .put(Phone.IS_PRIMARY, 1); - } - - public void testV21Japanese1_Parsing() { - // Though Japanese careers append ";;;;" at the end of the value of "SOUND", - // vCard 2.1/3.0 specification does not allow multiple values. - // Do not need to handle it as multiple values. - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, - R.raw.v21_japanese_1); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1", null, null, null, null, null) - .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9", "", "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E;;;;", - null, null, mContentValuesForSJis, - new TypeSet("X-IRMC-N"), null) - .addExpectedNodeWithOrder("TEL", "0300000000", null, null, null, - new TypeSet("VOICE", "PREF"), null); - } - - private void testV21Japanese1Common(int resId, int vcardType, boolean japanese) { - mVerifier.initForImportTest(vcardType, resId); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") - .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9") - // While vCard parser does not split "SOUND" property values, - // ContactStruct care it. - .put(StructuredName.PHONETIC_GIVEN_NAME, - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - // Phone number formatting is different. - .put(Phone.NUMBER, (japanese ? "03-0000-0000" : "030-000-0000")) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "VOICE") - .put(Phone.IS_PRIMARY, 1); - } - - /** - * Verifies vCard with Japanese can be parsed correctly with - * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_GENERIC_UTF8}. - */ - public void testV21Japanese1_Type_Generic_Utf8() { - testV21Japanese1Common( - R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, false); - } - - /** - * Verifies vCard with Japanese can be parsed correctly with - * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_SJIS}. - */ - public void testV21Japanese1_Type_Japanese_Sjis() { - testV21Japanese1Common( - R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, true); - } - - /** - * Verifies vCard with Japanese can be parsed correctly with - * {@link android.pim.vcard.VCardConfig#VCARD_TYPE_V21_JAPANESE_UTF8}. - * since vCard 2.1 specifies the charset of each line if it contains non-Ascii. - */ - public void testV21Japanese1_Type_Japanese_Utf8() { - testV21Japanese1Common( - R.raw.v21_japanese_1, VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8, true); - } - - public void testV21Japanese2_Parsing() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, - R.raw.v21_japanese_2); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", "\u5B89\u85E4;\u30ED\u30A4\u30C9\u0031;;;", - Arrays.asList("\u5B89\u85E4", "\u30ED\u30A4\u30C9\u0031", - "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("FN", "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031", - null, null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73;\uFF9B\uFF72\uFF84\uFF9E\u0031;;;", - null, null, mContentValuesForSJis, - new TypeSet("X-IRMC-N"), null) - .addExpectedNodeWithOrder("ADR", - ";\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC\u0036" + - "\u968E;;;;150-8512;", - Arrays.asList("", - "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + - "\u0036\u968E", "", "", "", "150-8512", ""), - null, mContentValuesForQPAndSJis, new TypeSet("HOME"), null) - .addExpectedNodeWithOrder("NOTE", "\u30E1\u30E2", null, null, - mContentValuesForQPAndSJis, null, null); - } - - public void testV21Japanese2_Type_Generic_Utf8() { - mVerifier.initForImportTest(V21, R.raw.v21_japanese_2); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4") - .put(StructuredName.GIVEN_NAME, "\u30ED\u30A4\u30C9\u0031") - .put(StructuredName.DISPLAY_NAME, - "\u5B89\u85E4\u0020\u30ED\u30A4\u30C9\u0020\u0031") - // ContactStruct should correctly split "SOUND" property into several elements, - // even though VCardParser side does not care it. - .put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF71\uFF9D\uFF84\uFF9E\uFF73") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF9B\uFF72\uFF84\uFF9E\u0031"); - elem.addExpected(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POSTCODE, "150-8512") - .put(StructuredPostal.STREET, - "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + - "\u0036\u968E") - .put(StructuredPostal.FORMATTED_ADDRESS, - "\u6771\u4EAC\u90FD\u6E0B\u8C37\u533A\u685C" + - "\u4E18\u753A\u0032\u0036\u002D\u0031\u30BB" + - "\u30EB\u30EA\u30A2\u30F3\u30BF\u30EF\u30FC" + - "\u0036\u968E 150-8512") - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME); - elem.addExpected(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "\u30E1\u30E2"); - } - - public void testV21MultipleEntryCase_Parse() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, - R.raw.v21_multiple_entry); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0033", "", "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033;;;;", - null, null, mContentValuesForSJis, - new TypeSet("X-IRMC-N"), null) - .addExpectedNodeWithOrder("TEL", "9", new TypeSet("X-NEC-SECRET")) - .addExpectedNodeWithOrder("TEL", "10", new TypeSet("X-NEC-HOTEL")) - .addExpectedNodeWithOrder("TEL", "11", new TypeSet("X-NEC-SCHOOL")) - .addExpectedNodeWithOrder("TEL", "12", new TypeSet("FAX", "HOME")); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0034", "", "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034;;;;", - null, null, mContentValuesForSJis, - new TypeSet("X-IRMC-N"), null) - .addExpectedNodeWithOrder("TEL", "13", new TypeSet("MODEM")) - .addExpectedNodeWithOrder("TEL", "14", new TypeSet("PAGER")) - .addExpectedNodeWithOrder("TEL", "15", new TypeSet("X-NEC-FAMILY")) - .addExpectedNodeWithOrder("TEL", "16", new TypeSet("X-NEC-GIRL")); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035;;;;", - Arrays.asList("\u5B89\u85E4\u30ED\u30A4\u30C9\u0035", "", "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNodeWithOrder("SOUND", - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035;;;;", - null, null, mContentValuesForSJis, - new TypeSet("X-IRMC-N"), null) - .addExpectedNodeWithOrder("TEL", "17", new TypeSet("X-NEC-BOY")) - .addExpectedNodeWithOrder("TEL", "18", new TypeSet("X-NEC-FRIEND")) - .addExpectedNodeWithOrder("TEL", "19", new TypeSet("X-NEC-PHS")) - .addExpectedNodeWithOrder("TEL", "20", new TypeSet("X-NEC-RESTAURANT")); - } - - public void testV21MultipleEntryCase() { - mVerifier.initForImportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS, - R.raw.v21_multiple_entry); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") - .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0033") - .put(StructuredName.PHONETIC_GIVEN_NAME, - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0033"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-SECRET") - .put(Phone.NUMBER, "9"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-HOTEL") - .put(Phone.NUMBER, "10"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-SCHOOL") - .put(Phone.NUMBER, "11"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_FAX_HOME) - .put(Phone.NUMBER, "12"); - - elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") - .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0034") - .put(StructuredName.PHONETIC_GIVEN_NAME, - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0034"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "MODEM") - .put(Phone.NUMBER, "13"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_PAGER) - .put(Phone.NUMBER, "14"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-FAMILY") - .put(Phone.NUMBER, "15"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-GIRL") - .put(Phone.NUMBER, "16"); - - elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") - .put(StructuredName.DISPLAY_NAME, "\u5B89\u85E4\u30ED\u30A4\u30C9\u0035") - .put(StructuredName.PHONETIC_GIVEN_NAME, - "\uFF71\uFF9D\uFF84\uFF9E\uFF73\uFF9B\uFF72\uFF84\uFF9E\u0035"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-BOY") - .put(Phone.NUMBER, "17"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-FRIEND") - .put(Phone.NUMBER, "18"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-PHS") - .put(Phone.NUMBER, "19"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_CUSTOM) - .put(Phone.LABEL, "NEC-RESTAURANT") - .put(Phone.NUMBER, "20"); - } - - public void testIgnoreAgentV21_Parse() { - mVerifier.initForImportTest(V21, R.raw.v21_winmo_65); - ContentValues contentValuesForValue = new ContentValues(); - contentValuesForValue.put("VALUE", "DATE"); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "2.1") - .addExpectedNodeWithOrder("N", Arrays.asList("Example", "", "", "", "")) - .addExpectedNodeWithOrder("FN", "Example") - .addExpectedNodeWithOrder("ANNIVERSARY", "20091010", contentValuesForValue) - .addExpectedNodeWithOrder("AGENT", "") - .addExpectedNodeWithOrder("X-CLASS", "PUBLIC") - .addExpectedNodeWithOrder("X-REDUCTION", "") - .addExpectedNodeWithOrder("X-NO", ""); - } - - public void testIgnoreAgentV21() { - mVerifier.initForImportTest(V21, R.raw.v21_winmo_65); - ContentValuesVerifier verifier = new ContentValuesVerifier(); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "Example") - .put(StructuredName.DISPLAY_NAME, "Example"); - } - - public void testTolerateInvalidCommentLikeLineV21() { - mVerifier.initForImportTest(V21, R.raw.v21_invalid_comment_line); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.GIVEN_NAME, "Conference Call") - .put(StructuredName.DISPLAY_NAME, "Conference Call"); - elem.addExpected(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "This is an (sharp ->#<- sharp) example. " - + "This message must NOT be ignored."); - } - - public void testPagerV30_Parse() { - mVerifier.initForImportTest(V30, R.raw.v30_comma_separated); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNodeWithOrder("VERSION", "3.0") - .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", "")) - .addExpectedNodeWithOrder("TEL", "6101231234@pagersample.com", - new TypeSet("WORK", "MSG", "PAGER")); - } - - public void testPagerV30() { - mVerifier.initForImportTest(V30, R.raw.v30_comma_separated); - ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem(); - elem.addExpected(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "F") - .put(StructuredName.MIDDLE_NAME, "M") - .put(StructuredName.GIVEN_NAME, "G") - .put(StructuredName.DISPLAY_NAME, "G M F"); - elem.addExpected(Phone.CONTENT_ITEM_TYPE) - .put(Phone.TYPE, Phone.TYPE_PAGER) - .put(Phone.NUMBER, "6101231234@pagersample.com"); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java b/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java deleted file mode 100644 index 5b60342..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardJapanizationTests.java +++ /dev/null @@ -1,434 +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.pim.vcard.VCardConfig; -import android.provider.ContactsContract.CommonDataKinds.Nickname; -import android.provider.ContactsContract.CommonDataKinds.Note; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; -import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; - -import android.pim.vcard.PropertyNodesVerifierElem.TypeSet; - -import java.util.Arrays; - -public class VCardJapanizationTests extends VCardTestsBase { - private void testNameUtf8Common(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") - .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") - .put(StructuredName.MIDDLE_NAME, "B") - .put(StructuredName.PREFIX, "Dr.") - .put(StructuredName.SUFFIX, "Ph.D"); - ContentValues contentValues = - (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8); - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", - contentValues) - .addExpectedNode("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", - Arrays.asList( - "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), - null, contentValues, null, null); - } - - public void testNameUtf8V21() { - testNameUtf8Common(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8); - } - - public void testNameUtf8V30() { - testNameUtf8Common(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8); - } - - public void testNameShiftJis() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V30_JAPANESE_SJIS); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") - .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") - .put(StructuredName.MIDDLE_NAME, "B") - .put(StructuredName.PREFIX, "Dr.") - .put(StructuredName.SUFFIX, "Ph.D"); - - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("FN", "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D", - mContentValuesForSJis) - .addExpectedNode("N", "\u3075\u308B\u3069;\u3091\u308A\u304B;B;Dr.;Ph.D", - Arrays.asList( - "\u3075\u308B\u3069", "\u3091\u308A\u304B", "B", "Dr.", "Ph.D"), - null, mContentValuesForSJis, null, null); - } - - /** - * DoCoMo phones require all name elements should be in "family name" field. - */ - public void testNameDoCoMo() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.FAMILY_NAME, "\u3075\u308B\u3069") - .put(StructuredName.GIVEN_NAME, "\u3091\u308A\u304B") - .put(StructuredName.MIDDLE_NAME, "B") - .put(StructuredName.PREFIX, "Dr.") - .put(StructuredName.SUFFIX, "Ph.D"); - - final String fullName = "Dr. \u3075\u308B\u3069 B \u3091\u308A\u304B Ph.D"; - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("N", fullName + ";;;;", - Arrays.asList(fullName, "", "", "", ""), - null, mContentValuesForSJis, null, null) - .addExpectedNode("FN", fullName, mContentValuesForSJis) - .addExpectedNode("SOUND", ";;;;", new TypeSet("X-IRMC-N")) - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("ADR", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", ""); - } - - private void testPhoneticNameCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); - - final ContentValues contentValues = - (VCardConfig.usesShiftJis(vcardType) ? - (VCardConfig.isV30(vcardType) ? mContentValuesForSJis : - mContentValuesForQPAndSJis) : - (VCardConfig.isV30(vcardType) ? null : mContentValuesForQPAndUtf8)); - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); - elem.addExpectedNode("X-PHONETIC-LAST-NAME", "\u3084\u307E\u3060", - contentValues) - .addExpectedNode("X-PHONETIC-MIDDLE-NAME", - "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0", - contentValues) - .addExpectedNode("X-PHONETIC-FIRST-NAME", "\u305F\u308D\u3046", - contentValues); - if (VCardConfig.isV30(vcardType)) { - elem.addExpectedNode("SORT-STRING", - "\u3084\u307E\u3060 \u30DF\u30C9\u30EB\u30CD\u30FC\u30E0 \u305F\u308D\u3046", - contentValues); - } - ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE); - builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046") - .put(StructuredName.DISPLAY_NAME, - "\u3084\u307E\u3060 \u30DF\u30C9\u30EB\u30CD\u30FC\u30E0 " + - "\u305F\u308D\u3046"); - } - - public void testPhoneticNameForJapaneseV21Utf8() { - testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8); - } - - public void testPhoneticNameForJapaneseV21Sjis() { - testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); - } - - public void testPhoneticNameForJapaneseV30Utf8() { - testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8); - } - - public void testPhoneticNameForJapaneseV30SJis() { - testPhoneticNameCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_SJIS); - } - - public void testPhoneticNameForMobileV21_1() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") - .put(StructuredName.PHONETIC_MIDDLE_NAME, "\u30DF\u30C9\u30EB\u30CD\u30FC\u30E0") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); - - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("SOUND", - "\uFF94\uFF8F\uFF80\uFF9E \uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91 " + - "\uFF80\uFF9B\uFF73;;;;", - mContentValuesForSJis, new TypeSet("X-IRMC-N")); - ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE); - builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF94\uFF8F\uFF80\uFF9E") - .put(StructuredName.PHONETIC_MIDDLE_NAME, - "\uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF80\uFF9B\uFF73") - .put(StructuredName.DISPLAY_NAME, - "\uFF94\uFF8F\uFF80\uFF9E \uFF90\uFF84\uFF9E\uFF99\uFF88\uFF70\uFF91 " + - "\uFF80\uFF9B\uFF73"); - } - - public void testPhoneticNameForMobileV21_2() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredName.CONTENT_ITEM_TYPE) - .put(StructuredName.PHONETIC_FAMILY_NAME, "\u3084\u307E\u3060") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\u305F\u308D\u3046"); - - mVerifier.addPropertyNodesVerifierElem() - .addExpectedNode("SOUND", "\uFF94\uFF8F\uFF80\uFF9E \uFF80\uFF9B\uFF73;;;;", - mContentValuesForSJis, new TypeSet("X-IRMC-N")); - ContentValuesBuilder builder = mVerifier.addContentValuesVerifierElem() - .addExpected(StructuredName.CONTENT_ITEM_TYPE); - builder.put(StructuredName.PHONETIC_FAMILY_NAME, "\uFF94\uFF8F\uFF80\uFF9E") - .put(StructuredName.PHONETIC_GIVEN_NAME, "\uFF80\uFF9B\uFF73") - .put(StructuredName.DISPLAY_NAME, "\uFF94\uFF8F\uFF80\uFF9E \uFF80\uFF9B\uFF73"); - } - - private void testPostalAddressWithJapaneseCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "\u79C1\u66F8\u7BB107") - .put(StructuredPostal.STREET, "\u96DB\u898B\u6CA2\u6751") - .put(StructuredPostal.CITY, "\u9E7F\u9AA8\u5E02") - .put(StructuredPostal.REGION, "\u00D7\u00D7\u770C") - .put(StructuredPostal.POSTCODE, "494-1313") - .put(StructuredPostal.COUNTRY, "\u65E5\u672C") - .put(StructuredPostal.FORMATTED_ADDRESS, - "\u3053\u3093\u306A\u3068\u3053\u308D\u3092\u898B" - + "\u308B\u306A\u3093\u3066\u6687\u4EBA\u3067\u3059\u304B\uFF1F") - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) - .put(StructuredPostal.LABEL, "\u304A\u3082\u3061\u304B\u3048\u308A"); - - ContentValues contentValues = (VCardConfig.usesShiftJis(vcardType) ? - (VCardConfig.isV30(vcardType) ? mContentValuesForSJis : - mContentValuesForQPAndSJis) : - (VCardConfig.isV30(vcardType) ? mContentValuesForUtf8 : - mContentValuesForQPAndUtf8)); - - PropertyNodesVerifierElem elem = mVerifier.addPropertyNodesVerifierElemWithEmptyName(); - // LABEL must be ignored in vCard 2.1. As for vCard 3.0, the current behavior is - // same as that in vCard 3.0, which can be changed in the future. - elem.addExpectedNode("ADR", Arrays.asList("\u79C1\u66F8\u7BB107", - "", "\u96DB\u898B\u6CA2\u6751", "\u9E7F\u9AA8\u5E02", "\u00D7\u00D7\u770C", - "494-1313", "\u65E5\u672C"), - contentValues); - mVerifier.addContentValuesVerifierElem().addExpected(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "\u79C1\u66F8\u7BB107") - .put(StructuredPostal.STREET, "\u96DB\u898B\u6CA2\u6751") - .put(StructuredPostal.CITY, "\u9E7F\u9AA8\u5E02") - .put(StructuredPostal.REGION, "\u00D7\u00D7\u770C") - .put(StructuredPostal.POSTCODE, "494-1313") - .put(StructuredPostal.COUNTRY, "\u65E5\u672C") - .put(StructuredPostal.FORMATTED_ADDRESS, - "\u65E5\u672C 494-1313 \u00D7\u00D7\u770C \u9E7F\u9AA8\u5E02 " + - "\u96DB\u898B\u6CA2\u6751 " + "\u79C1\u66F8\u7BB107") - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME); - } - public void testPostalAddresswithJapaneseV21() { - testPostalAddressWithJapaneseCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_SJIS); - } - - /** - * Verifies that only one address field is emitted toward DoCoMo phones. - * Prefered type must (should?) be: HOME > WORK > OTHER > CUSTOM - */ - public void testPostalAdrressForDoCoMo_1() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) - .put(StructuredPostal.POBOX, "1"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) - .put(StructuredPostal.POBOX, "2"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) - .put(StructuredPostal.POBOX, "3"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) - .put(StructuredPostal.LABEL, "custom") - .put(StructuredPostal.POBOX, "4"); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", - Arrays.asList("3", "", "", "", "", "", ""), new TypeSet("HOME")); - } - - public void testPostalAdrressForDoCoMo_2() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) - .put(StructuredPostal.POBOX, "1"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) - .put(StructuredPostal.POBOX, "2"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) - .put(StructuredPostal.LABEL, "custom") - .put(StructuredPostal.POBOX, "3"); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", - Arrays.asList("2", "", "", "", "", "", ""), new TypeSet("WORK")); - } - - public void testPostalAdrressForDoCoMo_3() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) - .put(StructuredPostal.LABEL, "custom1") - .put(StructuredPostal.POBOX, "1"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) - .put(StructuredPostal.POBOX, "2"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_CUSTOM) - .put(StructuredPostal.LABEL, "custom2") - .put(StructuredPostal.POBOX, "3"); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", Arrays.asList("2", "", "", "", "", "", "")); - } - - /** - * Verifies the vCard exporter tolerates null TYPE. - */ - public void testPostalAdrressForDoCoMo_4() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "1"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_OTHER) - .put(StructuredPostal.POBOX, "2"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_HOME) - .put(StructuredPostal.POBOX, "3"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.TYPE, StructuredPostal.TYPE_WORK) - .put(StructuredPostal.POBOX, "4"); - entry.addContentValues(StructuredPostal.CONTENT_ITEM_TYPE) - .put(StructuredPostal.POBOX, "5"); - - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", - Arrays.asList("3", "", "", "", "", "", ""), new TypeSet("HOME")); - } - - private void testJapanesePhoneNumberCommon(int vcardType) { - mVerifier.initForExportTest(vcardType); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "0312341234") - .put(Phone.TYPE, Phone.TYPE_HOME); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "09012341234") - .put(Phone.TYPE, Phone.TYPE_MOBILE); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "03-1234-1234", new TypeSet("HOME")) - .addExpectedNode("TEL", "090-1234-1234", new TypeSet("CELL")); - } - - public void testJapanesePhoneNumberV21_1() { - testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V21_JAPANESE_UTF8); - } - - public void testJapanesePhoneNumberV30() { - testJapanesePhoneNumberCommon(VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8); - } - - public void testJapanesePhoneNumberDoCoMo() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "0312341234") - .put(Phone.TYPE, Phone.TYPE_HOME); - entry.addContentValues(Phone.CONTENT_ITEM_TYPE) - .put(Phone.NUMBER, "09012341234") - .put(Phone.TYPE, Phone.TYPE_MOBILE); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", "", new TypeSet("HOME")) - .addExpectedNode("TEL", "03-1234-1234", new TypeSet("HOME")) - .addExpectedNode("TEL", "090-1234-1234", new TypeSet("CELL")); - } - - public void testNoteDoCoMo() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_DOCOMO); - ContactEntry entry = mVerifier.addInputEntry(); - entry.addContentValues(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "note1"); - entry.addContentValues(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "note2"); - entry.addContentValues(Note.CONTENT_ITEM_TYPE) - .put(Note.NOTE, "note3"); - - // More than one note fields must be aggregated into one note. - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("TEL", "", new TypeSet("HOME")) - .addExpectedNode("EMAIL", "", new TypeSet("HOME")) - .addExpectedNode("X-CLASS", "PUBLIC") - .addExpectedNode("X-REDUCTION", "") - .addExpectedNode("X-NO", "") - .addExpectedNode("X-DCM-HMN-MODE", "") - .addExpectedNode("ADR", "", new TypeSet("HOME")) - .addExpectedNode("NOTE", "note1\nnote2\nnote3", mContentValuesForQP); - } - - public void testAndroidCustomV21() { - mVerifier.initForExportTest(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); - mVerifier.addInputEntry().addContentValues(Nickname.CONTENT_ITEM_TYPE) - .put(Nickname.NAME, "\u304D\u3083\u30FC\u30A8\u30C3\u30C1\u30FC"); - mVerifier.addPropertyNodesVerifierElemWithEmptyName() - .addExpectedNode("X-ANDROID-CUSTOM", - Arrays.asList(Nickname.CONTENT_ITEM_TYPE, - "\u304D\u3083\u30FC\u30A8\u30C3\u30C1\u30FC", - "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - mContentValuesForQPAndUtf8); - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java b/core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java deleted file mode 100644 index 0857e0c..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardTestsBase.java +++ /dev/null @@ -1,169 +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.ContentProvider; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentValues; -import android.content.EntityIterator; -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.database.CursorWindow; -import android.database.IBulkCursor; -import android.database.IContentObserver; -import android.net.Uri; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.pim.vcard.VCardConfig; -import android.test.AndroidTestCase; -import android.util.Log; - -import java.util.ArrayList; - -/** - * Almost a dead copy of android.test.mock.MockContentProvider, but different in that this - * class extends ContentProvider, not implementing IContentProvider, - * so that MockContentResolver is able to accept this class :( - */ -class MockContentProvider extends ContentProvider { - @Override - public boolean onCreate() { - return true; - } - - @Override - public int bulkInsert(Uri url, ContentValues[] initialValues) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @SuppressWarnings("unused") - public IBulkCursor bulkQuery(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder, IContentObserver observer, - CursorWindow window) throws RemoteException { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - @SuppressWarnings("unused") - public int delete(Uri url, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public String getType(Uri url) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public Uri insert(Uri url, ContentValues initialValues) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public ParcelFileDescriptor openFile(Uri url, String mode) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public AssetFileDescriptor openAssetFile(Uri uri, String mode) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - @Override - public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("unimplemented mock method"); - } - - public IBinder asBinder() { - throw new UnsupportedOperationException("unimplemented mock method"); - } -} - -/** - * BaseClass for vCard unit tests with utility classes. - * Please do not add each unit test here. - */ -/* package */ class VCardTestsBase extends AndroidTestCase { - public static final int V21 = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8; - public static final int V30 = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8; - - // Do not modify these during tests. - protected final ContentValues mContentValuesForQP; - protected final ContentValues mContentValuesForSJis; - protected final ContentValues mContentValuesForUtf8; - protected final ContentValues mContentValuesForQPAndSJis; - protected final ContentValues mContentValuesForQPAndUtf8; - protected final ContentValues mContentValuesForBase64V21; - protected final ContentValues mContentValuesForBase64V30; - - protected VCardVerifier mVerifier; - private boolean mSkipVerification; - - public VCardTestsBase() { - super(); - mContentValuesForQP = new ContentValues(); - mContentValuesForQP.put("ENCODING", "QUOTED-PRINTABLE"); - mContentValuesForSJis = new ContentValues(); - mContentValuesForSJis.put("CHARSET", "SHIFT_JIS"); - mContentValuesForUtf8 = new ContentValues(); - mContentValuesForUtf8.put("CHARSET", "UTF-8"); - mContentValuesForQPAndSJis = new ContentValues(); - mContentValuesForQPAndSJis.put("ENCODING", "QUOTED-PRINTABLE"); - mContentValuesForQPAndSJis.put("CHARSET", "SHIFT_JIS"); - mContentValuesForQPAndUtf8 = new ContentValues(); - mContentValuesForQPAndUtf8.put("ENCODING", "QUOTED-PRINTABLE"); - mContentValuesForQPAndUtf8.put("CHARSET", "UTF-8"); - mContentValuesForBase64V21 = new ContentValues(); - mContentValuesForBase64V21.put("ENCODING", "BASE64"); - mContentValuesForBase64V30 = new ContentValues(); - mContentValuesForBase64V30.put("ENCODING", "b"); - } - - @Override - public void testAndroidTestCaseSetupProperly() { - super.testAndroidTestCaseSetupProperly(); - mSkipVerification = true; - } - - @Override - public void setUp() throws Exception{ - super.setUp(); - mVerifier = new VCardVerifier(this); - mSkipVerification = false; - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - if (!mSkipVerification) { - mVerifier.verify(); - } - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java b/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java deleted file mode 100644 index 59299f9..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java +++ /dev/null @@ -1,85 +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.VCardUtils; - -import junit.framework.TestCase; - -import java.util.List; - -public class VCardUtilsTests extends TestCase { - public void testContainsOnlyPrintableAscii() { - assertTrue(VCardUtils.containsOnlyPrintableAscii((String)null)); - assertTrue(VCardUtils.containsOnlyPrintableAscii((String[])null)); - assertTrue(VCardUtils.containsOnlyPrintableAscii((List<String>)null)); - assertTrue(VCardUtils.containsOnlyPrintableAscii("")); - assertTrue(VCardUtils.containsOnlyPrintableAscii("abcdefghijklmnopqrstuvwxyz")); - assertTrue(VCardUtils.containsOnlyPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); - StringBuilder builder = new StringBuilder(); - for (int i = 0x20; i < 0x7F; i++) { - builder.append((char)i); - } - assertTrue(VCardUtils.containsOnlyPrintableAscii(builder.toString())); - assertTrue(VCardUtils.containsOnlyPrintableAscii("\r\n")); - assertFalse(VCardUtils.containsOnlyPrintableAscii("\u0019")); - assertFalse(VCardUtils.containsOnlyPrintableAscii("\u007F")); - } - - public void testContainsOnlyNonCrLfPrintableAscii() { - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((String)null)); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((String[])null)); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii((List<String>)null)); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("")); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("abcdefghijklmnopqrstuvwxyz")); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); - StringBuilder builder = new StringBuilder(); - for (int i = 0x20; i < 0x7F; i++) { - builder.append((char)i); - } - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii(builder.toString())); - assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\u0019")); - assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\u007F")); - assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\r")); - assertFalse(VCardUtils.containsOnlyNonCrLfPrintableAscii("\n")); - } - - public void testContainsOnlyAlphaDigitHyphen() { - assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((String)null)); - assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((String[])null)); - assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen((List<String>)null)); - assertTrue(VCardUtils.containsOnlyAlphaDigitHyphen("")); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("abcdefghijklmnopqrstuvwxyz")); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); - assertTrue(VCardUtils.containsOnlyNonCrLfPrintableAscii("0123456789-")); - for (int i = 0; i < 0x30; i++) { - if (i == 0x2D) { // - - continue; - } - assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); - } - for (int i = 0x3A; i < 0x41; i++) { - assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); - } - for (int i = 0x5B; i < 0x61; i++) { - assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); - } - for (int i = 0x7B; i < 0x100; i++) { - assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i))); - } - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java b/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java deleted file mode 100644 index bfc3158..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VCardVerifier.java +++ /dev/null @@ -1,306 +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.ContentProvider; -import android.content.ContentResolver; -import android.content.Context; -import android.content.EntityIterator; -import android.net.Uri; -import android.pim.vcard.VCardComposer; -import android.pim.vcard.VCardConfig; -import android.pim.vcard.VCardEntryConstructor; -import android.pim.vcard.VCardInterpreter; -import android.pim.vcard.VCardInterpreterCollection; -import android.pim.vcard.VCardParser; -import android.pim.vcard.VCardParser_V21; -import android.pim.vcard.VCardParser_V30; -import android.pim.vcard.exception.VCardException; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Method; -import java.util.Arrays; - -/* package */ class CustomMockContext extends MockContext { - final ContentResolver mResolver; - public CustomMockContext(ContentResolver resolver) { - mResolver = resolver; - } - - @Override - public ContentResolver getContentResolver() { - return mResolver; - } -} - -/* package */ class VCardVerifier { - private class VCardVerifierInternal implements VCardComposer.OneEntryHandler { - public boolean onInit(Context context) { - return true; - } - public boolean onEntryCreated(String vcard) { - verifyOneVCard(vcard); - return true; - } - public void onTerminate() { - } - } - - private final AndroidTestCase mTestCase; - private final VCardVerifierInternal mVCardVerifierInternal; - private int mVCardType; - private boolean mIsV30; - private boolean mIsDoCoMo; - - // Only one of them must be non-empty. - private ExportTestResolver mExportTestResolver; - private InputStream mInputStream; - - // To allow duplication, use list instead of set. - // When null, we don't need to do the verification. - private PropertyNodesVerifier mPropertyNodesVerifier; - private LineVerifier mLineVerifier; - private ContentValuesVerifier mContentValuesVerifier; - private boolean mInitialized; - private boolean mVerified = false; - - public VCardVerifier(AndroidTestCase androidTestCase) { - mTestCase = androidTestCase; - mVCardVerifierInternal = new VCardVerifierInternal(); - mExportTestResolver = null; - mInputStream = null; - mInitialized = false; - mVerified = false; - } - - public void initForExportTest(int vcardType) { - if (mInitialized) { - mTestCase.fail("Already initialized"); - } - mExportTestResolver = new ExportTestResolver(mTestCase); - mVCardType = vcardType; - mIsV30 = VCardConfig.isV30(vcardType); - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mInitialized = true; - } - - public void initForImportTest(int vcardType, int resId) { - if (mInitialized) { - mTestCase.fail("Already initialized"); - } - mVCardType = vcardType; - mIsV30 = VCardConfig.isV30(vcardType); - mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - setInputResourceId(resId); - mInitialized = true; - } - - private void setInputResourceId(int resId) { - InputStream inputStream = mTestCase.getContext().getResources().openRawResource(resId); - if (inputStream == null) { - mTestCase.fail("Wrong resId: " + resId); - } - setInputStream(inputStream); - } - - private void setInputStream(InputStream inputStream) { - if (mExportTestResolver != null) { - mTestCase.fail("addInputEntry() is called."); - } else if (mInputStream != null) { - mTestCase.fail("InputStream is already set"); - } - mInputStream = inputStream; - } - - public ContactEntry addInputEntry() { - if (!mInitialized) { - mTestCase.fail("Not initialized"); - } - if (mInputStream != null) { - mTestCase.fail("setInputStream is called"); - } - return mExportTestResolver.addInputContactEntry(); - } - - public PropertyNodesVerifierElem addPropertyNodesVerifierElem() { - if (!mInitialized) { - mTestCase.fail("Not initialized"); - } - if (mPropertyNodesVerifier == null) { - mPropertyNodesVerifier = new PropertyNodesVerifier(mTestCase); - } - PropertyNodesVerifierElem elem = - mPropertyNodesVerifier.addPropertyNodesVerifierElem(); - elem.addExpectedNodeWithOrder("VERSION", (mIsV30 ? "3.0" : "2.1")); - - return elem; - } - - public PropertyNodesVerifierElem addPropertyNodesVerifierElemWithEmptyName() { - if (!mInitialized) { - mTestCase.fail("Not initialized"); - } - PropertyNodesVerifierElem elem = addPropertyNodesVerifierElem(); - if (mIsV30) { - elem.addExpectedNodeWithOrder("N", "").addExpectedNodeWithOrder("FN", ""); - } else if (mIsDoCoMo) { - elem.addExpectedNodeWithOrder("N", ""); - } - return elem; - } - - public LineVerifierElem addLineVerifierElem() { - if (!mInitialized) { - mTestCase.fail("Not initialized"); - } - if (mLineVerifier == null) { - mLineVerifier = new LineVerifier(mTestCase, mVCardType); - } - return mLineVerifier.addLineVerifierElem(); - } - - public ContentValuesVerifierElem addContentValuesVerifierElem() { - if (!mInitialized) { - mTestCase.fail("Not initialized"); - } - if (mContentValuesVerifier == null) { - mContentValuesVerifier = new ContentValuesVerifier(); - } - - return mContentValuesVerifier.addElem(mTestCase); - } - - private void verifyOneVCard(final String vcard) { - // Log.d("@@@", vcard); - final VCardInterpreter builder; - if (mContentValuesVerifier != null) { - final VNodeBuilder vnodeBuilder = mPropertyNodesVerifier; - final VCardEntryConstructor vcardDataBuilder = - new VCardEntryConstructor(mVCardType); - vcardDataBuilder.addEntryHandler(mContentValuesVerifier); - if (mPropertyNodesVerifier != null) { - builder = new VCardInterpreterCollection(Arrays.asList( - mPropertyNodesVerifier, vcardDataBuilder)); - } else { - builder = vnodeBuilder; - } - } else { - if (mPropertyNodesVerifier != null) { - builder = mPropertyNodesVerifier; - } else { - return; - } - } - - final VCardParser parser = - (mIsV30 ? new VCardParser_V30(true) : new VCardParser_V21()); - InputStream is = null; - try { - String charset = - (VCardConfig.usesShiftJis(mVCardType) ? "SHIFT_JIS" : "UTF-8"); - is = new ByteArrayInputStream(vcard.getBytes(charset)); - mTestCase.assertEquals(true, parser.parse(is, null, builder)); - } catch (IOException e) { - mTestCase.fail("Unexpected IOException: " + e.getMessage()); - } catch (VCardException e) { - mTestCase.fail("Unexpected VCardException: " + e.getMessage()); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } - } - } - - public void verify() { - if (!mInitialized) { - mTestCase.fail("Not initialized."); - } - if (mVerified) { - mTestCase.fail("verify() was called twice."); - } - if (mInputStream != null) { - try { - verifyForImportTest(); - } catch (IOException e) { - mTestCase.fail("IOException was thrown: " + e.getMessage()); - } catch (VCardException e) { - mTestCase.fail("VCardException was thrown: " + e.getMessage()); - } - } else if (mExportTestResolver != null){ - verifyForExportTest(); - } else { - mTestCase.fail("No input is determined"); - } - mVerified = true; - } - - private void verifyForImportTest() throws IOException, VCardException { - if (mLineVerifier != null) { - mTestCase.fail("Not supported now."); - } - if (mContentValuesVerifier != null) { - mContentValuesVerifier.verify(mInputStream, mVCardType); - } - } - - public static EntityIterator mockGetEntityIteratorMethod( - final ContentResolver resolver, - final Uri uri, final String selection, - final String[] selectionArgs, final String sortOrder) { - final ContentProvider provider = - resolver.acquireContentProviderClient(uri).getLocalContentProvider(); - return ((ExportTestProvider)provider).queryEntities( - uri, selection, selectionArgs, sortOrder); - } - - private Method getMockGetEntityIteratorMethod() - throws SecurityException, NoSuchMethodException { - return this.getClass().getMethod("mockGetEntityIteratorMethod", - ContentResolver.class, Uri.class, String.class, String[].class, String.class); - } - - private void verifyForExportTest() { - final VCardComposer composer = - new VCardComposer(new CustomMockContext(mExportTestResolver), mVCardType); - composer.addHandler(mLineVerifier); - composer.addHandler(mVCardVerifierInternal); - if (!composer.init(VCardComposer.CONTACTS_TEST_CONTENT_URI, null, null, null)) { - mTestCase.fail("init() failed. Reason: " + composer.getErrorReason()); - } - mTestCase.assertFalse(composer.isAfterLast()); - try { - while (!composer.isAfterLast()) { - try { - final Method mockGetEntityIteratorMethod = getMockGetEntityIteratorMethod(); - mTestCase.assertTrue( - composer.createOneEntry(getMockGetEntityIteratorMethod())); - } catch (Exception e) { - e.printStackTrace(); - mTestCase.fail(); - } - } - } finally { - composer.terminate(); - } - } -} diff --git a/core/tests/coretests/src/android/pim/vcard/VNode.java b/core/tests/coretests/src/android/pim/vcard/VNode.java deleted file mode 100644 index 79f10dc..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VNode.java +++ /dev/null @@ -1,30 +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.ArrayList; - -/** - * Previously used in main vCard handling code but now exists only for testing. - */ -public class VNode { - public String VName; - - public ArrayList<PropertyNode> propList = new ArrayList<PropertyNode>(); - - /** 0:parse over. 1:parsing. */ - public int parseStatus = 1; -} diff --git a/core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java b/core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java deleted file mode 100644 index 0e6c325..0000000 --- a/core/tests/coretests/src/android/pim/vcard/VNodeBuilder.java +++ /dev/null @@ -1,314 +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.pim.vcard.VCardInterpreter; -import android.pim.vcard.VCardConfig; -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.List; - -/** - * Store the parse result to custom datastruct: VNode, PropertyNode - * Maybe several vcard instance, so use vNodeList to store. - * VNode: standy by a vcard instance. - * PropertyNode: standy by a property line of a card. - * - * Previously used in main vCard handling code but now exists only for testing. - */ -public class VNodeBuilder implements VCardInterpreter { - static private String LOG_TAG = "VNodeBuilder"; - - /** - * If there's no other information available, this class uses this charset for encoding - * byte arrays. - */ - static public String TARGET_CHARSET = "UTF-8"; - - /** type=VNode */ - public List<VNode> vNodeList = new ArrayList<VNode>(); - private int mNodeListPos = 0; - private VNode mCurrentVNode; - private PropertyNode mCurrentPropNode; - private String mCurrentParamType; - - /** - * The charset using which VParser parses the text. - */ - private String mSourceCharset; - - /** - * The charset with which byte array is encoded to String. - */ - private String mTargetCharset; - - private boolean mStrictLineBreakParsing; - - public VNodeBuilder() { - this(VCardConfig.DEFAULT_CHARSET, TARGET_CHARSET, false); - } - - public VNodeBuilder(String charset, boolean strictLineBreakParsing) { - this(null, charset, strictLineBreakParsing); - } - - /** - * @hide sourceCharset is temporal. - */ - public VNodeBuilder(String sourceCharset, String targetCharset, - boolean strictLineBreakParsing) { - if (sourceCharset != null) { - mSourceCharset = sourceCharset; - } else { - mSourceCharset = VCardConfig.DEFAULT_CHARSET; - } - if (targetCharset != null) { - mTargetCharset = targetCharset; - } else { - mTargetCharset = TARGET_CHARSET; - } - mStrictLineBreakParsing = strictLineBreakParsing; - } - - public void start() { - } - - public void end() { - } - - // Note: I guess that this code assumes the Record may nest like this: - // START:VPOS - // ... - // START:VPOS2 - // ... - // END:VPOS2 - // ... - // END:VPOS - // - // However the following code has a bug. - // When error occurs after calling startRecord(), the entry which is probably - // the cause of the error remains to be in vNodeList, while endRecord() is not called. - // - // I leave this code as is since I'm not familiar with vcalendar specification. - // But I believe we should refactor this code in the future. - // Until this, the last entry has to be removed when some error occurs. - public void startEntry() { - VNode vnode = new VNode(); - vnode.parseStatus = 1; - vnode.VName = "VCARD"; - // I feel this should be done in endRecord(), but it cannot be done because of - // the reason above. - vNodeList.add(vnode); - mNodeListPos = vNodeList.size() - 1; - mCurrentVNode = vNodeList.get(mNodeListPos); - } - - public void endEntry() { - VNode endNode = vNodeList.get(mNodeListPos); - endNode.parseStatus = 0; - while(mNodeListPos > 0){ - mNodeListPos--; - if((vNodeList.get(mNodeListPos)).parseStatus == 1) - break; - } - mCurrentVNode = vNodeList.get(mNodeListPos); - } - - public void startProperty() { - mCurrentPropNode = new PropertyNode(); - } - - public void endProperty() { - mCurrentVNode.propList.add(mCurrentPropNode); - } - - public void propertyName(String name) { - mCurrentPropNode.propName = name; - } - - // Used only in VCard. - public void propertyGroup(String group) { - mCurrentPropNode.propGroupSet.add(group); - } - - public void propertyParamType(String type) { - mCurrentParamType = type; - } - - public void propertyParamValue(String value) { - if (mCurrentParamType == null || - mCurrentParamType.equalsIgnoreCase("TYPE")) { - mCurrentPropNode.paramMap_TYPE.add(value); - } else { - mCurrentPropNode.paramMap.put(mCurrentParamType, value); - } - - mCurrentParamType = null; - } - - private String encodeString(String originalString, String targetCharset) { - if (mSourceCharset.equalsIgnoreCase(targetCharset)) { - return originalString; - } - Charset charset = Charset.forName(mSourceCharset); - 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, targetCharset); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); - return null; - } - } - - private String handleOneValue(String value, String targetCharset, String encoding) { - if (encoding != null) { - encoding = encoding.toUpperCase(); - if (encoding.equals("BASE64") || encoding.equals("B")) { - // Assume BASE64 is used only when the number of values is 1. - mCurrentPropNode.propValue_bytes = - Base64.decodeBase64(value.getBytes()); - return value; - } else if (encoding.equals("QUOTED-PRINTABLE")) { - String quotedPrintable = value - .replaceAll("= ", " ").replaceAll("=\t", "\t"); - String[] lines; - if (mStrictLineBreakParsing) { - lines = quotedPrintable.split("\r\n"); - } else { - StringBuilder builder = new StringBuilder(); - int 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]); - } - StringBuilder 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(mSourceCharset); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); - 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, targetCharset); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); - return new String(bytes); - } - } - // Unknown encoding. Fall back to default. - } - return encodeString(value, targetCharset); - } - - public void propertyValues(List<String> values) { - if (values == null || values.size() == 0) { - mCurrentPropNode.propValue_bytes = null; - mCurrentPropNode.propValue_vector.clear(); - mCurrentPropNode.propValue_vector.add(""); - mCurrentPropNode.propValue = ""; - return; - } - - ContentValues paramMap = mCurrentPropNode.paramMap; - - String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); - String encoding = paramMap.getAsString("ENCODING"); - - if (targetCharset == null || targetCharset.length() == 0) { - targetCharset = mTargetCharset; - } - - for (String value : values) { - mCurrentPropNode.propValue_vector.add( - handleOneValue(value, targetCharset, encoding)); - } - - mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector); - } - - private String listToString(List<String> list){ - int size = list.size(); - if (size > 1) { - StringBuilder typeListB = new StringBuilder(); - for (String type : list) { - typeListB.append(type).append(";"); - } - int len = typeListB.length(); - if (len > 0 && typeListB.charAt(len - 1) == ';') { - return typeListB.substring(0, len - 1); - } - return typeListB.toString(); - } else if (size == 1) { - return list.get(0); - } else { - return ""; - } - } - - public String getResult(){ - return null; - } -} diff --git a/core/tests/coretests/src/android/provider/SettingsProviderTest.java b/core/tests/coretests/src/android/provider/SettingsProviderTest.java index 370ae78..b82e698 100644 --- a/core/tests/coretests/src/android/provider/SettingsProviderTest.java +++ b/core/tests/coretests/src/android/provider/SettingsProviderTest.java @@ -144,4 +144,51 @@ public class SettingsProviderTest extends AndroidTestCase { assertEquals(null, Settings.Bookmarks.getIntentForShortcut(r, '*')); } + + @MediumTest + public void testParseProviderList() { + ContentResolver r = getContext().getContentResolver(); + + // Make sure we get out what we put in. + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, + "test1,test2,test3"); + assertEquals(Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED), + "test1,test2,test3"); + + // Test adding a value + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, + ""); + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "+test1"); + assertEquals("test1", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + + // Test adding a second value + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "+test2"); + assertEquals("test1,test2", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + + // Test adding a third value + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "+test3"); + assertEquals("test1,test2,test3", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + + // Test deleting the first value in a 3 item list + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "-test1"); + assertEquals("test2,test3", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + + // Test deleting the middle value in a 3 item list + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, + "test1,test2,test3"); + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "-test2"); + assertEquals("test1,test3", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + + // Test deleting the last value in a 3 item list + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, + "test1,test2,test3"); + Settings.Secure.putString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "-test3"); + assertEquals("test1,test2", + Settings.Secure.getString(r, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)); + } } diff --git a/core/tests/coretests/src/android/text/StaticLayoutBidiTest.java b/core/tests/coretests/src/android/text/StaticLayoutBidiTest.java index 8e7e63e..fb0f0c1 100644 --- a/core/tests/coretests/src/android/text/StaticLayoutBidiTest.java +++ b/core/tests/coretests/src/android/text/StaticLayoutBidiTest.java @@ -22,7 +22,7 @@ import android.util.Log; import junit.framework.TestCase; /** - * Tests StaticLayout bidi implementation. + * Quick check of native bidi implementation. */ public class StaticLayoutBidiTest extends TestCase { @@ -41,73 +41,47 @@ public class StaticLayoutBidiTest extends TestCase { //@SmallTest public void testAllLtr() { - expectBidi(REQ_DL, "a test", "000000", L); + expectNativeBidi(REQ_DL, "a test", "000000", L); } //@SmallTest public void testLtrRtl() { - expectBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L); + expectNativeBidi(REQ_DL, "abc " + ALEF + BET + GIMEL, "0000111", L); } //@SmallTest public void testAllRtl() { - expectBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R); + expectNativeBidi(REQ_DL, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", R); } //@SmallTest public void testRtlLtr() { - expectBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111000", R); + expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R); } //@SmallTest public void testRAllLtr() { - expectBidi(REQ_R, "a test", "000000", R); + expectNativeBidi(REQ_R, "a test", "222222", R); } //@SmallTest public void testRLtrRtl() { - expectBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "0001111", R); + expectNativeBidi(REQ_R, "abc " + ALEF + BET + GIMEL, "2221111", R); } //@SmallTest public void testLAllRtl() { - expectBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L); + expectNativeBidi(REQ_L, ALEF + SP + ALEF + BET + GIMEL + DALET, "111111", L); } //@SmallTest public void testLRtlLtr() { - expectBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L); - } - - private void expectBidi(int dir, String text, - String expectedLevels, int expectedDir) { - char[] chs = text.toCharArray(); - int n = chs.length; - byte[] chInfo = new byte[n]; - - int resultDir = StaticLayout.bidi(dir, chs, chInfo, n, false); - - { - StringBuilder sb = new StringBuilder("info:"); - for (int i = 0; i < n; ++i) { - sb.append(" ").append(String.valueOf(chInfo[i])); - } - Log.i("BIDI", sb.toString()); - } - - char[] resultLevelChars = new char[n]; - for (int i = 0; i < n; ++i) { - resultLevelChars[i] = (char)('0' + chInfo[i]); - } - String resultLevels = new String(resultLevelChars); - assertEquals("direction", expectedDir, resultDir); - assertEquals("levels", expectedLevels, resultLevels); + expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R); } //@SmallTest public void testNativeBidi() { - // native bidi returns levels, not simply directions - expectNativeBidi(REQ_DL, ALEF + BET + GIMEL + " abc", "1111222", R); + expectNativeBidi(REQ_L, ALEF + BET + GIMEL + " abc", "1110000", L); } private void expectNativeBidi(int dir, String text, diff --git a/core/tests/coretests/src/android/text/StaticLayoutDirectionsTest.java b/core/tests/coretests/src/android/text/StaticLayoutDirectionsTest.java new file mode 100644 index 0000000..4fde849 --- /dev/null +++ b/core/tests/coretests/src/android/text/StaticLayoutDirectionsTest.java @@ -0,0 +1,243 @@ +/* + * 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 android.text.Layout.Directions; +import android.text.StaticLayoutTest.LayoutBuilder; + +import java.util.Arrays; +import java.util.Formatter; + +import junit.framework.TestCase; + +public class StaticLayoutDirectionsTest extends TestCase { + private static final char ALEF = '\u05d0'; + + private static Directions dirs(int ... dirs) { + return new Directions(dirs); + } + + // constants from Layout that are package-protected + private static final int RUN_LENGTH_MASK = 0x03ffffff; + private static final int RUN_LEVEL_SHIFT = 26; + private static final int RUN_LEVEL_MASK = 0x3f; + private static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; + + private static final Directions DIRS_ALL_LEFT_TO_RIGHT = + new Directions(new int[] { 0, RUN_LENGTH_MASK }); + private static final Directions DIRS_ALL_RIGHT_TO_LEFT = + new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); + + private static final int LVL1_1 = 1 | (1 << RUN_LEVEL_SHIFT); + private static final int LVL2_1 = 1 | (2 << RUN_LEVEL_SHIFT); + private static final int LVL2_2 = 2 | (2 << RUN_LEVEL_SHIFT); + + private static String[] texts = { + "", + " ", + "a", + "a1", + "aA", + "a1b", + "a1A", + "aA1", + "aAb", + "aA1B", + "aA1B2", + + // rtl + "A", + "A1", + "Aa", + "A1B", + "A1a", + "Aa1", + "AaB" + }; + + // Expected directions are an array of start/length+level pairs, + // in visual order from the leading margin. + private static Directions[] expected = { + DIRS_ALL_LEFT_TO_RIGHT, + DIRS_ALL_LEFT_TO_RIGHT, + DIRS_ALL_LEFT_TO_RIGHT, + DIRS_ALL_LEFT_TO_RIGHT, + dirs(0, 1, 1, LVL1_1), + DIRS_ALL_LEFT_TO_RIGHT, + dirs(0, 2, 2, LVL1_1), + dirs(0, 1, 2, LVL2_1, 1, LVL1_1), + dirs(0, 1, 1, LVL1_1, 2, 1), + dirs(0, 1, 3, LVL1_1, 2, LVL2_1, 1, LVL1_1), + dirs(0, 1, 4, LVL2_1, 3, LVL1_1, 2, LVL2_1, 1, LVL1_1), + + // rtl + DIRS_ALL_RIGHT_TO_LEFT, + dirs(0, LVL1_1, 1, LVL2_1), + dirs(0, LVL1_1, 1, LVL2_1), + dirs(0, LVL1_1, 1, LVL2_1, 2, LVL1_1), + dirs(0, LVL1_1, 1, LVL2_2), + dirs(0, LVL1_1, 1, LVL2_2), + dirs(0, LVL1_1, 1, LVL2_1, 2, LVL1_1), + }; + + private static String pseudoBidiToReal(String src) { + char[] chars = src.toCharArray(); + for (int j = 0; j < chars.length; ++j) { + char c = chars[j]; + if (c >= 'A' && c <= 'D') { + chars[j] = (char)(ALEF + c - 'A'); + } + } + + return new String(chars, 0, chars.length); + } + + // @SmallTest + public void testDirections() { + StringBuilder buf = new StringBuilder("\n"); + Formatter f = new Formatter(buf); + + LayoutBuilder b = StaticLayoutTest.builder(); + for (int i = 0; i < texts.length; ++i) { + b.setText(pseudoBidiToReal(texts[i])); + checkDirections(b.build(), i, b.text, expected, f); + } + if (buf.length() > 1) { + fail(buf.toString()); + } + } + + // @SmallTest + public void testTrailingWhitespace() { + LayoutBuilder b = StaticLayoutTest.builder(); + b.setText(pseudoBidiToReal("Ab c")); + float width = b.paint.measureText(b.text, 0, 5); // exclude 'c' + b.setWidth(Math.round(width)); + Layout l = b.build(); + if (l.getLineCount() != 2) { + throw new RuntimeException("expected 2 lines, got: " + l.getLineCount()); + } + Directions result = l.getLineDirections(0); + Directions expected = dirs(0, LVL1_1, 1, LVL2_1, 2, 3 | (1 << Layout.RUN_LEVEL_SHIFT)); + expectDirections("split line", expected, result); + } + + public void testNextToRightOf() { + LayoutBuilder b = StaticLayoutTest.builder(); + b.setText(pseudoBidiToReal("aA1B2")); + // visual a2B1A positions 04321 + // 0: |a2B1A, strong is sol, after -> 0 + // 1: a|2B1A, strong is a, after ->, 1 + // 2: a2|B1A, strong is B, after -> 4 + // 3: a2B|1A, strong is B, before -> 3 + // 4: a2B1|A, strong is A, after -> 2 + // 5: a2B1A|, strong is eol, before -> 5 + int[] expected = { 0, 1, 4, 3, 2, 5 }; + Layout l = b.build(); + int n = 0; + for (int i = 1; i < expected.length; ++i) { + int t = l.getOffsetToRightOf(n); + if (t != expected[i]) { + fail("offset[" + i + "] to right of: " + n + " expected: " + + expected[i] + " got: " + t); + } + n = t; + } + } + + public void testNextToLeftOf() { + LayoutBuilder b = StaticLayoutTest.builder(); + b.setText(pseudoBidiToReal("aA1B2")); + int[] expected = { 0, 1, 4, 3, 2, 5 }; + Layout l = b.build(); + int n = 5; + for (int i = expected.length - 1; --i >= 0;) { + int t = l.getOffsetToLeftOf(n); + if (t != expected[i]) { + fail("offset[" + i + "] to left of: " + n + " expected: " + + expected[i] + " got: " + t); + } + n = t; + } + } + + // utility, not really a test + /* + public void testMeasureText1() { + LayoutBuilder b = StaticLayoutTest.builder(); + String text = "ABC"; // "abAB" + b.setText(pseudoBidiToReal(text)); + Layout l = b.build(); + Directions directions = l.getLineDirections(0); + + TextPaint workPaint = new TextPaint(); + + int dir = -1; // LEFT_TO_RIGHT + boolean trailing = true; + boolean alt = true; + do { + dir = -dir; + do { + trailing = !trailing; + for (int offset = 0, end = b.text.length(); offset <= end; ++offset) { + float width = Layout.measureText(b.paint, + workPaint, + b.text, + 0, offset, end, + dir, directions, + trailing, false, + null); + Log.i("BIDI", "dir: " + dir + " trail: " + trailing + + " offset: " + offset + " width: " + width); + } + } while (!trailing); + } while (dir > 0); + } + */ + + // utility for displaying arrays in hex + private static String hexArray(int[] array) { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + for (int i : array) { + if (sb.length() > 1) { + sb.append(", "); + } + sb.append(Integer.toHexString(i)); + } + sb.append('}'); + return sb.toString(); + } + + private void checkDirections(Layout l, int i, String text, + Directions[] expectedDirs, Formatter f) { + Directions expected = expectedDirs[i]; + Directions result = l.getLineDirections(0); + if (!Arrays.equals(expected.mDirections, result.mDirections)) { + f.format("%n[%2d] '%s', %s != %s", i, text, + hexArray(expected.mDirections), + hexArray(result.mDirections)); + } + } + + private void expectDirections(String msg, Directions expected, Directions result) { + if (!Arrays.equals(expected.mDirections, result.mDirections)) { + fail("expected: " + hexArray(expected.mDirections) + + " got: " + hexArray(result.mDirections)); + } + } +} diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java index 1f58a2c..d554a50 100644 --- a/core/tests/coretests/src/android/text/StaticLayoutTest.java +++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java @@ -228,11 +228,11 @@ public class StaticLayoutTest extends TestCase { } } - private static LayoutBuilder builder() { + /* package */ static LayoutBuilder builder() { return new LayoutBuilder(); } - private static class LayoutBuilder { + /* package */ static class LayoutBuilder { String text = "This is a test"; TextPaint paint = new TextPaint(); // default int width = 100; diff --git a/core/tests/coretests/src/android/util/ExpandableListScenario.java b/core/tests/coretests/src/android/util/ExpandableListScenario.java deleted file mode 100644 index 4a12b0d..0000000 --- a/core/tests/coretests/src/android/util/ExpandableListScenario.java +++ /dev/null @@ -1,386 +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.util; - -import java.util.ArrayList; -import java.util.List; - -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.ListView; -import android.widget.TextView; - -/** - * Utility base class for creating various Expandable List scenarios. - * <p> - * WARNING: A lot of the features are mixed between ListView's expected position - * (flat list position) and an ExpandableListView's expected position. You must add/change - * features as you need them. - * - * @see ListScenario - */ -public abstract class ExpandableListScenario extends ListScenario { - protected ExpandableListAdapter mAdapter; - protected List<MyGroup> mGroups; - - @Override - protected ListView createListView() { - return new ExpandableListView(this); - } - - @Override - protected Params createParams() { - return new ExpandableParams(); - } - - @Override - protected void setAdapter(ListView listView) { - ((ExpandableListView) listView).setAdapter(mAdapter = createAdapter()); - } - - protected ExpandableListAdapter createAdapter() { - return new MyAdapter(); - } - - @Override - protected void readAndValidateParams(Params params) { - ExpandableParams expandableParams = (ExpandableParams) params; - - int[] numChildren = expandableParams.mNumChildren; - - mGroups = new ArrayList<MyGroup>(numChildren.length); - for (int i = 0; i < numChildren.length; i++) { - mGroups.add(new MyGroup(numChildren[i])); - } - - expandableParams.superSetNumItems(); - - super.readAndValidateParams(params); - } - - /** - * Get the ExpandableListView widget. - * @return The main widget. - */ - public ExpandableListView getExpandableListView() { - return (ExpandableListView) super.getListView(); - } - - public static class ExpandableParams extends Params { - private int[] mNumChildren; - - /** - * Sets the number of children per group. - * - * @param numChildrenPerGroup The number of children per group. - */ - public ExpandableParams setNumChildren(int[] numChildren) { - mNumChildren = numChildren; - return this; - } - - /** - * Sets the number of items on the superclass based on the number of - * groups and children per group. - */ - private ExpandableParams superSetNumItems() { - int numItems = 0; - - if (mNumChildren != null) { - for (int i = mNumChildren.length - 1; i >= 0; i--) { - numItems += mNumChildren[i]; - } - } - - super.setNumItems(numItems); - - return this; - } - - @Override - public Params setNumItems(int numItems) { - throw new IllegalStateException("Use setNumGroups and setNumChildren instead."); - } - - @Override - public ExpandableParams setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) { - return (ExpandableParams) super.setFadingEdgeScreenSizeFactor(fadingEdgeScreenSizeFactor); - } - - @Override - public ExpandableParams setItemScreenSizeFactor(double itemScreenSizeFactor) { - return (ExpandableParams) super.setItemScreenSizeFactor(itemScreenSizeFactor); - } - - @Override - public ExpandableParams setItemsFocusable(boolean itemsFocusable) { - return (ExpandableParams) super.setItemsFocusable(itemsFocusable); - } - - @Override - public ExpandableParams setMustFillScreen(boolean fillScreen) { - return (ExpandableParams) super.setMustFillScreen(fillScreen); - } - - @Override - public ExpandableParams setPositionScreenSizeFactorOverride(int position, double itemScreenSizeFactor) { - return (ExpandableParams) super.setPositionScreenSizeFactorOverride(position, itemScreenSizeFactor); - } - - @Override - public ExpandableParams setPositionUnselectable(int position) { - return (ExpandableParams) super.setPositionUnselectable(position); - } - - @Override - public ExpandableParams setStackFromBottom(boolean stackFromBottom) { - return (ExpandableParams) super.setStackFromBottom(stackFromBottom); - } - - @Override - public ExpandableParams setStartingSelectionPosition(int startingSelectionPosition) { - return (ExpandableParams) super.setStartingSelectionPosition(startingSelectionPosition); - } - - @Override - public ExpandableParams setConnectAdapter(boolean connectAdapter) { - return (ExpandableParams) super.setConnectAdapter(connectAdapter); - } - } - - /** - * Gets a string for the value of some item. - * @param packedPosition The position of the item. - * @return The string. - */ - public final String getValueAtPosition(long packedPosition) { - final int type = ExpandableListView.getPackedPositionType(packedPosition); - - if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { - return mGroups.get(ExpandableListView.getPackedPositionGroup(packedPosition)) - .children.get(ExpandableListView.getPackedPositionChild(packedPosition)) - .name; - } else if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { - return mGroups.get(ExpandableListView.getPackedPositionGroup(packedPosition)) - .name; - } else { - throw new IllegalStateException("packedPosition is not a valid position."); - } - } - - /** - * Whether a particular position is out of bounds. - * - * @param packedPosition The packed position. - * @return Whether it's out of bounds. - */ - private boolean isOutOfBounds(long packedPosition) { - final int type = ExpandableListView.getPackedPositionType(packedPosition); - - if (type == ExpandableListView.PACKED_POSITION_TYPE_NULL) { - throw new IllegalStateException("packedPosition is not a valid position."); - } - - final int group = ExpandableListView.getPackedPositionGroup(packedPosition); - if (group >= mGroups.size() || group < 0) { - return true; - } - - if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { - final int child = ExpandableListView.getPackedPositionChild(packedPosition); - if (child >= mGroups.get(group).children.size() || child < 0) { - return true; - } - } - - return false; - } - - /** - * Gets a view for the packed position, possibly reusing the convertView. - * - * @param packedPosition The position to get a view for. - * @param convertView Optional view to convert. - * @param parent The future parent. - * @return A view. - */ - private View getView(long packedPosition, View convertView, ViewGroup parent) { - if (isOutOfBounds(packedPosition)) { - throw new IllegalStateException("position out of range for adapter!"); - } - - final ExpandableListView elv = getExpandableListView(); - final int flPos = elv.getFlatListPosition(packedPosition); - - if (convertView != null) { - ((TextView) convertView).setText(getValueAtPosition(packedPosition)); - convertView.setId(flPos); - return convertView; - } - - int desiredHeight = getHeightForPosition(flPos); - return createView(packedPosition, flPos, parent, desiredHeight); - } - - /** - * Create a view for a group or child position. - * - * @param packedPosition The packed position (has type, group pos, and optionally child pos). - * @param flPos The flat list position (the position that the ListView goes by). - * @param parent The parent view. - * @param desiredHeight The desired height. - * @return A view. - */ - protected View createView(long packedPosition, int flPos, ViewGroup parent, int desiredHeight) { - TextView result = new TextView(parent.getContext()); - result.setHeight(desiredHeight); - result.setText(getValueAtPosition(packedPosition)); - final ViewGroup.LayoutParams lp = new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - result.setLayoutParams(lp); - result.setGravity(Gravity.CENTER_VERTICAL); - result.setPadding(36, 0, 0, 0); - result.setId(flPos); - return result; - } - - /** - * Returns a group index containing either the number of children or at - * least one child. - * - * @param numChildren The group must have this amount, or -1 if using - * atLeastOneChild. - * @param atLeastOneChild The group must have at least one child, or false - * if using numChildren. - * @return A group index with the requirements. - */ - public int findGroupWithNumChildren(int numChildren, boolean atLeastOneChild) { - final ExpandableListAdapter adapter = mAdapter; - - for (int i = adapter.getGroupCount() - 1; i >= 0; i--) { - final int curNumChildren = adapter.getChildrenCount(i); - - if (numChildren == curNumChildren || atLeastOneChild && curNumChildren > 0) { - return i; - } - } - - return -1; - } - - public List<MyGroup> getGroups() { - return mGroups; - } - - public ExpandableListAdapter getAdapter() { - return mAdapter; - } - - /** - * Simple expandable list adapter. - */ - protected class MyAdapter extends BaseExpandableListAdapter { - public Object getChild(int groupPosition, int childPosition) { - return getValueAtPosition(ExpandableListView.getPackedPositionForChild(groupPosition, - childPosition)); - } - - public long getChildId(int groupPosition, int childPosition) { - return mGroups.get(groupPosition).children.get(childPosition).id; - } - - public int getChildrenCount(int groupPosition) { - return mGroups.get(groupPosition).children.size(); - } - - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - return getView(ExpandableListView.getPackedPositionForChild(groupPosition, - childPosition), convertView, parent); - } - - public Object getGroup(int groupPosition) { - return getValueAtPosition(ExpandableListView.getPackedPositionForGroup(groupPosition)); - } - - public int getGroupCount() { - return mGroups.size(); - } - - public long getGroupId(int groupPosition) { - return mGroups.get(groupPosition).id; - } - - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, - ViewGroup parent) { - return getView(ExpandableListView.getPackedPositionForGroup(groupPosition), - convertView, parent); - } - - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - public boolean hasStableIds() { - return true; - } - - } - - public static class MyGroup { - private static long mNextId = 1000; - - String name; - long id = mNextId++; - List<MyChild> children; - - public MyGroup(int numChildren) { - name = "Group " + id; - children = new ArrayList<MyChild>(numChildren); - for (int i = 0; i < numChildren; i++) { - children.add(new MyChild()); - } - } - } - - public static class MyChild { - private static long mNextId = 2000; - - String name; - long id = mNextId++; - - public MyChild() { - name = "Child " + id; - } - } - - @Override - protected final void init(Params params) { - init((ExpandableParams) params); - } - - /** - * @see ListScenario#init - */ - protected abstract void init(ExpandableParams params); -} diff --git a/core/tests/coretests/src/android/util/PatternsTest.java b/core/tests/coretests/src/android/util/PatternsTest.java index b90c97b..aad3fe1 100644 --- a/core/tests/coretests/src/android/util/PatternsTest.java +++ b/core/tests/coretests/src/android/util/PatternsTest.java @@ -68,6 +68,13 @@ public class PatternsTest extends TestCase { t = Patterns.WEB_URL.matcher("xn--fsqu00a.xn--0zwm56d").matches(); assertTrue("Valid URL", t); + // Url for testing top level Arabic country code domain in Punycode: + // http://xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx + t = Patterns.WEB_URL.matcher("http://xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx").matches(); + assertTrue("Valid URL", t); + t = Patterns.WEB_URL.matcher("xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx").matches(); + assertTrue("Valid URL", t); + // Internationalized URL. t = Patterns.WEB_URL.matcher("http://\uD604\uAE08\uC601\uC218\uC99D.kr").matches(); assertTrue("Valid URL", t); @@ -109,7 +116,7 @@ public class PatternsTest extends TestCase { t = Patterns.DOMAIN_NAME.matcher("mail.example.com").matches(); assertTrue("Valid domain", t); - t = Patterns.WEB_URL.matcher("google.me").matches(); + t = Patterns.DOMAIN_NAME.matcher("google.me").matches(); assertTrue("Valid domain", t); // Internationalized domains. @@ -118,6 +125,14 @@ public class PatternsTest extends TestCase { t = Patterns.DOMAIN_NAME.matcher("__+&42.xer").matches(); assertFalse("Invalid domain", t); + + // Obsolete domain .yu + t = Patterns.DOMAIN_NAME.matcher("test.yu").matches(); + assertFalse("Obsolete country code top level domain", t); + + // Testing top level Arabic country code domain in Punycode: + t = Patterns.DOMAIN_NAME.matcher("xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c").matches(); + assertTrue("Valid domain", t); } @SmallTest diff --git a/core/tests/coretests/src/android/webkit/ZoomManagerTest.java b/core/tests/coretests/src/android/webkit/ZoomManagerTest.java new file mode 100644 index 0000000..1c9defe --- /dev/null +++ b/core/tests/coretests/src/android/webkit/ZoomManagerTest.java @@ -0,0 +1,127 @@ +/* + * 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.test.AndroidTestCase; + +public class ZoomManagerTest extends AndroidTestCase { + + private ZoomManager zoomManager; + + @Override + public void setUp() { + WebView webView = new WebView(this.getContext()); + CallbackProxy callbackProxy = new CallbackProxy(this.getContext(), webView); + zoomManager = new ZoomManager(webView, callbackProxy); + + zoomManager.init(1.00f); + } + + public void testInit() { + testInit(0.01f); + testInit(1.00f); + testInit(1.25f); + } + + private void testInit(float density) { + zoomManager.init(density); + actualScaleTest(density); + defaultScaleTest(density); + assertEquals(zoomManager.getDefaultMaxZoomScale(), zoomManager.getMaxZoomScale()); + assertEquals(zoomManager.getDefaultMinZoomScale(), zoomManager.getMinZoomScale()); + assertEquals(density, zoomManager.getTextWrapScale()); + } + + public void testUpdateDefaultZoomDensity() { + // test the basic case where the actual values are equal to the defaults + testUpdateDefaultZoomDensity(0.01f); + testUpdateDefaultZoomDensity(1.00f); + testUpdateDefaultZoomDensity(1.25f); + } + + private void testUpdateDefaultZoomDensity(float density) { + zoomManager.updateDefaultZoomDensity(density); + defaultScaleTest(density); + } + + public void testUpdateDefaultZoomDensityWithSmallMinZoom() { + // test the case where the minZoomScale has changed to be < the default + float newDefaultScale = 1.50f; + float minZoomScale = ZoomManager.DEFAULT_MIN_ZOOM_SCALE_FACTOR * newDefaultScale; + WebViewCore.ViewState minViewState = new WebViewCore.ViewState(); + minViewState.mMinScale = minZoomScale - 0.1f; + zoomManager.updateZoomRange(minViewState, 0, 0); + zoomManager.updateDefaultZoomDensity(newDefaultScale); + defaultScaleTest(newDefaultScale); + } + + public void testUpdateDefaultZoomDensityWithLargeMinZoom() { + // test the case where the minZoomScale has changed to be > the default + float newDefaultScale = 1.50f; + float minZoomScale = ZoomManager.DEFAULT_MIN_ZOOM_SCALE_FACTOR * newDefaultScale; + WebViewCore.ViewState minViewState = new WebViewCore.ViewState(); + minViewState.mMinScale = minZoomScale + 0.1f; + zoomManager.updateZoomRange(minViewState, 0, 0); + zoomManager.updateDefaultZoomDensity(newDefaultScale); + defaultScaleTest(newDefaultScale); + } + + public void testUpdateDefaultZoomDensityWithSmallMaxZoom() { + // test the case where the maxZoomScale has changed to be < the default + float newDefaultScale = 1.50f; + float maxZoomScale = ZoomManager.DEFAULT_MAX_ZOOM_SCALE_FACTOR * newDefaultScale; + WebViewCore.ViewState maxViewState = new WebViewCore.ViewState(); + maxViewState.mMaxScale = maxZoomScale - 0.1f; + zoomManager.updateZoomRange(maxViewState, 0, 0); + zoomManager.updateDefaultZoomDensity(newDefaultScale); + defaultScaleTest(newDefaultScale); + } + + public void testUpdateDefaultZoomDensityWithLargeMaxZoom() { + // test the case where the maxZoomScale has changed to be > the default + float newDefaultScale = 1.50f; + float maxZoomScale = ZoomManager.DEFAULT_MAX_ZOOM_SCALE_FACTOR * newDefaultScale; + WebViewCore.ViewState maxViewState = new WebViewCore.ViewState(); + maxViewState.mMaxScale = maxZoomScale + 0.1f; + zoomManager.updateZoomRange(maxViewState, 0, 0); + zoomManager.updateDefaultZoomDensity(newDefaultScale); + defaultScaleTest(newDefaultScale); + } + + public void testComputeScaleWithLimits() { + final float maxScale = zoomManager.getMaxZoomScale(); + final float minScale = zoomManager.getMinZoomScale(); + assertTrue(maxScale > minScale); + assertEquals(maxScale, zoomManager.computeScaleWithLimits(maxScale)); + assertEquals(maxScale, zoomManager.computeScaleWithLimits(maxScale + .01f)); + assertEquals(minScale, zoomManager.computeScaleWithLimits(minScale)); + assertEquals(minScale, zoomManager.computeScaleWithLimits(minScale - .01f)); + } + + private void actualScaleTest(float actualScale) { + assertEquals(actualScale, zoomManager.getScale()); + assertEquals(1 / actualScale, zoomManager.getInvScale()); + } + + private void defaultScaleTest(float defaultScale) { + final float maxDefault = ZoomManager.DEFAULT_MAX_ZOOM_SCALE_FACTOR * defaultScale; + final float minDefault = ZoomManager.DEFAULT_MIN_ZOOM_SCALE_FACTOR * defaultScale; + assertEquals(defaultScale, zoomManager.getDefaultScale()); + assertEquals(1 / defaultScale, zoomManager.getInvDefaultScale()); + assertEquals(maxDefault, zoomManager.getDefaultMaxZoomScale()); + assertEquals(minDefault, zoomManager.getDefaultMinZoomScale()); + } +} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListBasicTest.java b/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListBasicTest.java deleted file mode 100644 index e23b516..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListBasicTest.java +++ /dev/null @@ -1,144 +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.widget.expandablelistview; - -import android.test.ActivityInstrumentationTestCase2; -import android.test.suitebuilder.annotation.MediumTest; -import android.util.ExpandableListScenario; -import android.util.ListUtil; -import android.util.ExpandableListScenario.MyGroup; -import android.view.KeyEvent; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; - -import java.util.List; - -public class ExpandableListBasicTest extends ActivityInstrumentationTestCase2<ExpandableListSimple> { - private ExpandableListScenario mActivity; - private ExpandableListView mExpandableListView; - private ExpandableListAdapter mAdapter; - private ListUtil mListUtil; - - public ExpandableListBasicTest() { - super(ExpandableListSimple.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - mActivity = getActivity(); - mExpandableListView = mActivity.getExpandableListView(); - mAdapter = mExpandableListView.getExpandableListAdapter(); - mListUtil = new ListUtil(mExpandableListView, getInstrumentation()); - } - - @MediumTest - public void testPreconditions() { - assertNotNull(mActivity); - assertNotNull(mExpandableListView); - } - - private int expandGroup(int numChildren, boolean atLeastOneChild) { - final int groupPos = mActivity.findGroupWithNumChildren(numChildren, atLeastOneChild); - assertTrue("Could not find group to expand", groupPos >= 0); - - assertFalse("Group is already expanded", mExpandableListView.isGroupExpanded(groupPos)); - mListUtil.arrowScrollToSelectedPosition(groupPos); - getInstrumentation().waitForIdleSync(); - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - getInstrumentation().waitForIdleSync(); - assertTrue("Group did not expand", mExpandableListView.isGroupExpanded(groupPos)); - - return groupPos; - } - - @MediumTest - public void testExpandGroup() { - expandGroup(-1, true); - } - - @MediumTest - public void testCollapseGroup() { - final int groupPos = expandGroup(-1, true); - - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - getInstrumentation().waitForIdleSync(); - assertFalse("Group did not collapse", mExpandableListView.isGroupExpanded(groupPos)); - } - - @MediumTest - public void testExpandedGroupMovement() { - // Expand the first group - mListUtil.arrowScrollToSelectedPosition(0); - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - getInstrumentation().waitForIdleSync(); - - // Ensure it expanded - assertTrue("Group did not expand", mExpandableListView.isGroupExpanded(0)); - - // Wait until that's all good - getInstrumentation().waitForIdleSync(); - - // Make sure it expanded - assertTrue("Group did not expand", mExpandableListView.isGroupExpanded(0)); - - // Insert a collapsed group in front of the one just expanded - List<MyGroup> groups = mActivity.getGroups(); - MyGroup insertedGroup = new MyGroup(1); - groups.add(0, insertedGroup); - - // Notify data change - assertTrue("Adapter is not an instance of the base adapter", - mAdapter instanceof BaseExpandableListAdapter); - final BaseExpandableListAdapter adapter = (BaseExpandableListAdapter) mAdapter; - - mActivity.runOnUiThread(new Runnable() { - public void run() { - adapter.notifyDataSetChanged(); - } - }); - getInstrumentation().waitForIdleSync(); - - // Make sure the right group is expanded - assertTrue("The expanded state didn't stay with the proper group", - mExpandableListView.isGroupExpanded(1)); - assertFalse("The expanded state was given to the inserted group", - mExpandableListView.isGroupExpanded(0)); - } - - @MediumTest - public void testContextMenus() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testContextMenus(); - } - - @MediumTest - public void testConvertionBetweenFlatAndPacked() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testConvertionBetweenFlatAndPackedOnGroups(); - tester.testConvertionBetweenFlatAndPackedOnChildren(); - } - - @MediumTest - public void testSelectedPosition() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testSelectedPositionOnGroups(); - tester.testSelectedPositionOnChildren(); - } -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListSimple.java b/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListSimple.java deleted file mode 100644 index 78db28c..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListSimple.java +++ /dev/null @@ -1,49 +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.widget.expandablelistview; - -import android.view.Menu; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.widget.BaseExpandableListAdapter; - -import android.util.ExpandableListScenario; - -public class ExpandableListSimple extends ExpandableListScenario { - private static final int[] NUM_CHILDREN = {4, 3, 2, 1, 0}; - - @Override - protected void init(ExpandableParams params) { - params.setNumChildren(NUM_CHILDREN) - .setItemScreenSizeFactor(0.14); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - - menu.add("Add item").setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - mGroups.add(0, new MyGroup(2)); - ((BaseExpandableListAdapter) mAdapter).notifyDataSetChanged(); - return true; - } - }); - - return true; - } - -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListTester.java b/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListTester.java deleted file mode 100644 index dfb10fb..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListTester.java +++ /dev/null @@ -1,241 +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.widget.expandablelistview; - -import android.app.Instrumentation; -import android.test.ActivityInstrumentationTestCase2; -import android.util.ExpandableListScenario; -import android.util.ListUtil; -import android.view.KeyEvent; -import android.view.View; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; - -import junit.framework.Assert; - -public class ExpandableListTester { - private final ExpandableListView mExpandableListView; - private final ExpandableListAdapter mAdapter; - private final ListUtil mListUtil; - - private final ActivityInstrumentationTestCase2<? extends ExpandableListScenario> - mActivityInstrumentation; - - Instrumentation mInstrumentation; - - public ExpandableListTester( - ExpandableListView expandableListView, - ActivityInstrumentationTestCase2<? extends ExpandableListScenario> - activityInstrumentation) { - mExpandableListView = expandableListView; - Instrumentation instrumentation = activityInstrumentation.getInstrumentation(); - mListUtil = new ListUtil(mExpandableListView, instrumentation); - mAdapter = mExpandableListView.getExpandableListAdapter(); - mActivityInstrumentation = activityInstrumentation; - mInstrumentation = mActivityInstrumentation.getInstrumentation(); - } - - private void expandGroup(final int groupIndex, int flatPosition) { - Assert.assertFalse("Group is already expanded", mExpandableListView - .isGroupExpanded(groupIndex)); - mListUtil.arrowScrollToSelectedPosition(flatPosition); - mInstrumentation.waitForIdleSync(); - mActivityInstrumentation.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - mActivityInstrumentation.getInstrumentation().waitForIdleSync(); - Assert.assertTrue("Group did not expand " + groupIndex, - mExpandableListView.isGroupExpanded(groupIndex)); - } - - void testContextMenus() { - // Add a position tester ContextMenu listener to the ExpandableListView - PositionTesterContextMenuListener menuListener = new PositionTesterContextMenuListener(); - mExpandableListView.setOnCreateContextMenuListener(menuListener); - - int index = 0; - - // Scrolling on header elements should trigger an AdapterContextMenu - for (int i=0; i<mExpandableListView.getHeaderViewsCount(); i++) { - // Check group index in context menu - menuListener.expectAdapterContextMenu(i); - // Make sure the group is visible so that getChild finds it - mListUtil.arrowScrollToSelectedPosition(index); - View headerChild = mExpandableListView.getChildAt(index - - mExpandableListView.getFirstVisiblePosition()); - mExpandableListView.showContextMenuForChild(headerChild); - mInstrumentation.waitForIdleSync(); - Assert.assertNull(menuListener.getErrorMessage(), menuListener.getErrorMessage()); - index++; - } - - int groupCount = mAdapter.getGroupCount(); - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { - - // Expand group - expandGroup(groupIndex, index); - - // Check group index in context menu - menuListener.expectGroupContextMenu(groupIndex); - // Make sure the group is visible so that getChild finds it - mListUtil.arrowScrollToSelectedPosition(index); - View groupChild = mExpandableListView.getChildAt(index - - mExpandableListView.getFirstVisiblePosition()); - mExpandableListView.showContextMenuForChild(groupChild); - mInstrumentation.waitForIdleSync(); - Assert.assertNull(menuListener.getErrorMessage(), menuListener.getErrorMessage()); - index++; - - final int childrenCount = mAdapter.getChildrenCount(groupIndex); - for (int childIndex = 0; childIndex < childrenCount; childIndex++) { - // Check child index in context menu - mListUtil.arrowScrollToSelectedPosition(index); - menuListener.expectChildContextMenu(groupIndex, childIndex); - View child = mExpandableListView.getChildAt(index - - mExpandableListView.getFirstVisiblePosition()); - mExpandableListView.showContextMenuForChild(child); - mInstrumentation.waitForIdleSync(); - Assert.assertNull(menuListener.getErrorMessage(), menuListener.getErrorMessage()); - index++; - } - } - - // Scrolling on footer elements should trigger an AdapterContextMenu - for (int i=0; i<mExpandableListView.getFooterViewsCount(); i++) { - // Check group index in context menu - menuListener.expectAdapterContextMenu(index); - // Make sure the group is visible so that getChild finds it - mListUtil.arrowScrollToSelectedPosition(index); - View footerChild = mExpandableListView.getChildAt(index - - mExpandableListView.getFirstVisiblePosition()); - mExpandableListView.showContextMenuForChild(footerChild); - mInstrumentation.waitForIdleSync(); - Assert.assertNull(menuListener.getErrorMessage(), menuListener.getErrorMessage()); - index++; - } - - // Cleanup: remove the listener we added. - mExpandableListView.setOnCreateContextMenuListener(null); - } - - private int expandAGroup() { - final int groupIndex = 2; - final int headerCount = mExpandableListView.getHeaderViewsCount(); - Assert.assertTrue("Not enough groups", groupIndex < mAdapter.getGroupCount()); - expandGroup(groupIndex, groupIndex + headerCount); - return groupIndex; - } - - // This method assumes that NO group is expanded when called - void testConvertionBetweenFlatAndPackedOnGroups() { - final int headerCount = mExpandableListView.getHeaderViewsCount(); - - for (int i=0; i<headerCount; i++) { - Assert.assertEquals("Non NULL position for header item", - ExpandableListView.PACKED_POSITION_VALUE_NULL, - mExpandableListView.getExpandableListPosition(i)); - } - - // Test all (non expanded) groups - final int groupCount = mAdapter.getGroupCount(); - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { - int expectedFlatPosition = headerCount + groupIndex; - long packedPositionForGroup = ExpandableListView.getPackedPositionForGroup(groupIndex); - Assert.assertEquals("Group not found at flat position " + expectedFlatPosition, - packedPositionForGroup, - mExpandableListView.getExpandableListPosition(expectedFlatPosition)); - - Assert.assertEquals("Wrong flat position for group " + groupIndex, - expectedFlatPosition, - mExpandableListView.getFlatListPosition(packedPositionForGroup)); - } - - for (int i=0; i<mExpandableListView.getFooterViewsCount(); i++) { - Assert.assertEquals("Non NULL position for header item", - ExpandableListView.PACKED_POSITION_VALUE_NULL, - mExpandableListView.getExpandableListPosition(headerCount + groupCount + i)); - } - } - - // This method assumes that NO group is expanded when called - void testConvertionBetweenFlatAndPackedOnChildren() { - // Test with an expanded group - final int headerCount = mExpandableListView.getHeaderViewsCount(); - final int groupIndex = expandAGroup(); - - final int childrenCount = mAdapter.getChildrenCount(groupIndex); - for (int childIndex = 0; childIndex < childrenCount; childIndex++) { - int expectedFlatPosition = headerCount + groupIndex + 1 + childIndex; - long childPos = ExpandableListView.getPackedPositionForChild(groupIndex, childIndex); - - Assert.assertEquals("Wrong flat position for child ", - childPos, - mExpandableListView.getExpandableListPosition(expectedFlatPosition)); - - Assert.assertEquals("Wrong flat position for child ", - expectedFlatPosition, - mExpandableListView.getFlatListPosition(childPos)); - } - } - - // This method assumes that NO group is expanded when called - void testSelectedPositionOnGroups() { - int index = 0; - - // Scrolling on header elements should not give a valid selected position. - for (int i=0; i<mExpandableListView.getHeaderViewsCount(); i++) { - mListUtil.arrowScrollToSelectedPosition(index); - Assert.assertEquals("Header item is selected", - ExpandableListView.PACKED_POSITION_VALUE_NULL, - mExpandableListView.getSelectedPosition()); - index++; - } - - // Check selection on group items - final int groupCount = mAdapter.getGroupCount(); - for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { - mListUtil.arrowScrollToSelectedPosition(index); - Assert.assertEquals("Group item is not selected", - ExpandableListView.getPackedPositionForGroup(groupIndex), - mExpandableListView.getSelectedPosition()); - index++; - } - - // Scrolling on footer elements should not give a valid selected position. - for (int i=0; i<mExpandableListView.getFooterViewsCount(); i++) { - mListUtil.arrowScrollToSelectedPosition(index); - Assert.assertEquals("Footer item is selected", - ExpandableListView.PACKED_POSITION_VALUE_NULL, - mExpandableListView.getSelectedPosition()); - index++; - } - } - - // This method assumes that NO group is expanded when called - void testSelectedPositionOnChildren() { - // Test with an expanded group - final int headerCount = mExpandableListView.getHeaderViewsCount(); - final int groupIndex = expandAGroup(); - - final int childrenCount = mAdapter.getChildrenCount(groupIndex); - for (int childIndex = 0; childIndex < childrenCount; childIndex++) { - int childFlatPosition = headerCount + groupIndex + 1 + childIndex; - mListUtil.arrowScrollToSelectedPosition(childFlatPosition); - Assert.assertEquals("Group item is not selected", - ExpandableListView.getPackedPositionForChild(groupIndex, childIndex), - mExpandableListView.getSelectedPosition()); - } - } -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeaders.java b/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeaders.java deleted file mode 100644 index 2251c1d..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeaders.java +++ /dev/null @@ -1,67 +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.widget.expandablelistview; - -import android.os.Bundle; -import android.util.ExpandableListScenario; -import android.widget.Button; -import android.widget.ExpandableListView; - -public class ExpandableListWithHeaders extends ExpandableListScenario { - private static final int[] sNumChildren = {1, 4, 3, 2, 6}; - private static final int sNumOfHeadersAndFooters = 12; - - @Override - protected void init(ExpandableParams params) { - params.setStackFromBottom(false) - .setStartingSelectionPosition(-1) - .setNumChildren(sNumChildren) - .setItemScreenSizeFactor(0.14) - .setConnectAdapter(false); - } - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - final ExpandableListView expandableListView = getExpandableListView(); - expandableListView.setItemsCanFocus(true); - - for (int i = 0; i < sNumOfHeadersAndFooters; i++) { - Button header = new Button(this); - header.setText("Header View " + i); - expandableListView.addHeaderView(header); - } - - for (int i = 0; i < sNumOfHeadersAndFooters; i++) { - Button footer = new Button(this); - footer.setText("Footer View " + i); - expandableListView.addFooterView(footer); - } - - // Set adapter here AFTER we set header and footer views - setAdapter(expandableListView); - } - - /** - * @return The number of headers (and the same number of footers) - */ - public int getNumOfHeadersAndFooters() { - return sNumOfHeadersAndFooters; - } - -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeadersTest.java b/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeadersTest.java deleted file mode 100644 index 64a0fff..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/ExpandableListWithHeadersTest.java +++ /dev/null @@ -1,84 +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.widget.expandablelistview; - -import android.test.ActivityInstrumentationTestCase2; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.MediumTest; -import android.util.ListUtil; -import android.view.KeyEvent; -import android.widget.ExpandableListView; - -public class ExpandableListWithHeadersTest extends - ActivityInstrumentationTestCase2<ExpandableListWithHeaders> { - private ExpandableListView mExpandableListView; - private ListUtil mListUtil; - - public ExpandableListWithHeadersTest() { - super(ExpandableListWithHeaders.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - mExpandableListView = getActivity().getExpandableListView(); - mListUtil = new ListUtil(mExpandableListView, getInstrumentation()); - } - - @MediumTest - public void testPreconditions() { - assertNotNull(mExpandableListView); - } - - @MediumTest - public void testExpandOnFirstPosition() { - // Should be a header, and hence the first group should NOT have expanded - mListUtil.arrowScrollToSelectedPosition(0); - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - getInstrumentation().waitForIdleSync(); - assertFalse(mExpandableListView.isGroupExpanded(0)); - } - - @LargeTest - public void testExpandOnFirstGroup() { - mListUtil.arrowScrollToSelectedPosition(getActivity().getNumOfHeadersAndFooters()); - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - getInstrumentation().waitForIdleSync(); - assertTrue(mExpandableListView.isGroupExpanded(0)); - } - - @MediumTest - public void testContextMenus() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testContextMenus(); - } - - @MediumTest - public void testConvertionBetweenFlatAndPacked() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testConvertionBetweenFlatAndPackedOnGroups(); - tester.testConvertionBetweenFlatAndPackedOnChildren(); - } - - @MediumTest - public void testSelectedPosition() { - ExpandableListTester tester = new ExpandableListTester(mExpandableListView, this); - tester.testSelectedPositionOnGroups(); - tester.testSelectedPositionOnChildren(); - } -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/InflatedExpandableListView.java b/core/tests/coretests/src/android/widget/expandablelistview/InflatedExpandableListView.java deleted file mode 100644 index f4c9d56..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/InflatedExpandableListView.java +++ /dev/null @@ -1,115 +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.widget.expandablelistview; - -import com.android.frameworks.coretests.R; - -import android.app.Activity; -import android.os.Bundle; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.TextView; - -public class InflatedExpandableListView extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.inflated_expandablelistview); - - ExpandableListView elv = (ExpandableListView) findViewById(R.id.elv); - elv.setAdapter(new MyExpandableListAdapter()); - } - - public class MyExpandableListAdapter extends BaseExpandableListAdapter { - // Sample data set. children[i] contains the children (String[]) for groups[i]. - private String[] groups = { "People Names", "Dog Names", "Cat Names", "Fish Names" }; - private String[][] children = { - { "Arnold", "Barry", "Chuck", "David" }, - { "Ace", "Bandit", "Cha-Cha", "Deuce" }, - { "Fluffy", "Snuggles" }, - { "Goldy", "Bubbles" } - }; - - public Object getChild(int groupPosition, int childPosition) { - return children[groupPosition][childPosition]; - } - - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - public int getChildrenCount(int groupPosition) { - return children[groupPosition].length; - } - - public TextView getGenericView() { - // Layout parameters for the ExpandableListView - AbsListView.LayoutParams lp = new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 64); - - TextView textView = new TextView(InflatedExpandableListView.this); - textView.setLayoutParams(lp); - // Center the text vertically - textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); - // Set the text starting position - textView.setPadding(36, 0, 0, 0); - return textView; - } - - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - TextView textView = getGenericView(); - textView.setText(getChild(groupPosition, childPosition).toString()); - return textView; - } - - public Object getGroup(int groupPosition) { - return groups[groupPosition]; - } - - public int getGroupCount() { - return groups.length; - } - - public long getGroupId(int groupPosition) { - return groupPosition; - } - - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, - ViewGroup parent) { - TextView textView = getGenericView(); - textView.setText(getGroup(groupPosition).toString()); - return textView; - } - - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } - - public boolean hasStableIds() { - return true; - } - - } - -} diff --git a/core/tests/coretests/src/android/widget/expandablelistview/PositionTesterContextMenuListener.java b/core/tests/coretests/src/android/widget/expandablelistview/PositionTesterContextMenuListener.java deleted file mode 100644 index 2dbdff8..0000000 --- a/core/tests/coretests/src/android/widget/expandablelistview/PositionTesterContextMenuListener.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.widget.expandablelistview; - -import android.view.ContextMenu; -import android.view.View; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View.OnCreateContextMenuListener; -import android.widget.ExpandableListView; -import android.widget.AdapterView.AdapterContextMenuInfo; - -public class PositionTesterContextMenuListener implements OnCreateContextMenuListener { - - private int groupPosition, childPosition; - - // Fake constant to store in testType a test type specific to headers and footers - private static final int ADAPTER_TYPE = -1; - private int testType; // as returned by getPackedPositionType - - // Will be set to null by each call to onCreateContextMenu, unless an error occurred. - private String errorMessage; - - public void expectGroupContextMenu(int groupPosition) { - this.groupPosition = groupPosition; - testType = ExpandableListView.PACKED_POSITION_TYPE_GROUP; - } - - public void expectChildContextMenu(int groupPosition, int childPosition) { - this.groupPosition = groupPosition; - this.childPosition = childPosition; - testType = ExpandableListView.PACKED_POSITION_TYPE_CHILD; - } - - public void expectAdapterContextMenu(int flatPosition) { - this.groupPosition = flatPosition; - testType = ADAPTER_TYPE; - } - - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - errorMessage = null; - if (testType == ADAPTER_TYPE) { - if (!isTrue("MenuInfo is not an AdapterContextMenuInfo", - menuInfo instanceof AdapterContextMenuInfo)) { - return; - } - AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo; - if (!areEqual("Wrong flat position", groupPosition, adapterContextMenuInfo.position)) { - return; - } - } else { - if (!isTrue("MenuInfo is not an ExpandableListContextMenuInfo", - menuInfo instanceof ExpandableListView.ExpandableListContextMenuInfo)) { - return; - } - ExpandableListView.ExpandableListContextMenuInfo elvMenuInfo = - (ExpandableListView.ExpandableListContextMenuInfo) menuInfo; - long packedPosition = elvMenuInfo.packedPosition; - - int packedPositionType = ExpandableListView.getPackedPositionType(packedPosition); - if (!areEqual("Wrong packed position type", testType, packedPositionType)) { - return; - } - - int packedPositionGroup = ExpandableListView.getPackedPositionGroup(packedPosition); - if (!areEqual("Wrong group position", groupPosition, packedPositionGroup)) { - return; - } - - if (testType == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { - int packedPositionChild = ExpandableListView.getPackedPositionChild(packedPosition); - if (!areEqual("Wrong child position", childPosition, packedPositionChild)) { - return; - } - } - } - } - - private boolean areEqual(String message, int expected, int actual) { - if (expected != actual) { - errorMessage = String.format(message + " (%d vs %d", expected, actual); - return false; - } - return true; - } - - private boolean isTrue(String message, boolean value) { - if (!value) { - errorMessage = message; - return false; - } - return true; - } - - public String getErrorMessage() { - return errorMessage; - } -} diff --git a/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java b/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java index 91cbe2f..19c9d26 100644 --- a/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java +++ b/core/tests/hosttests/src/android/content/pm/PackageManagerHostTestUtils.java @@ -124,7 +124,11 @@ public class PackageManagerHostTestUtils extends Assert { RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner( pkgName, mDevice); CollectingTestRunListener listener = new CollectingTestRunListener(); - testRunner.run(listener); + try { + testRunner.run(listener); + } catch (IOException ioe) { + Log.w(LOG_TAG, "encountered IOException " + ioe); + } return listener; } |