diff options
Diffstat (limited to 'core/java/android')
125 files changed, 6164 insertions, 4636 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index ef4adca..a09607a 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -171,6 +171,11 @@ public class AccessibilityServiceInfo implements Parcelable { private boolean mCanRetrieveWindowContent; /** + * Description of the accessibility service. + */ + private String mDescription; + + /** * Creates a new instance. */ public AccessibilityServiceInfo() { @@ -193,8 +198,6 @@ public class AccessibilityServiceInfo implements Parcelable { mId = new ComponentName(serviceInfo.packageName, serviceInfo.name).flattenToShortString(); mResolveInfo = resolveInfo; - String settingsActivityName = null; - boolean retrieveScreenContent = false; XmlResourceParser parser = null; try { @@ -242,6 +245,8 @@ public class AccessibilityServiceInfo implements Parcelable { mCanRetrieveWindowContent = asAttributes.getBoolean( com.android.internal.R.styleable.AccessibilityService_canRetrieveWindowContent, false); + mDescription = asAttributes.getString( + com.android.internal.R.styleable.AccessibilityService_description); asAttributes.recycle(); } catch (NameNotFoundException e) { throw new XmlPullParserException( "Unable to create context for: " @@ -315,6 +320,18 @@ public class AccessibilityServiceInfo implements Parcelable { } /** + * Description of the accessibility service. + * <p> + * <strong>Statically set from + * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * @return The description. + */ + public String getDescription() { + return mDescription; + } + + /** * {@inheritDoc} */ public int describeContents() { @@ -331,6 +348,7 @@ public class AccessibilityServiceInfo implements Parcelable { parcel.writeParcelable(mResolveInfo, 0); parcel.writeString(mSettingsActivityName); parcel.writeInt(mCanRetrieveWindowContent ? 1 : 0); + parcel.writeString(mDescription); } private void initFromParcel(Parcel parcel) { @@ -343,6 +361,7 @@ public class AccessibilityServiceInfo implements Parcelable { mResolveInfo = parcel.readParcelable(null); mSettingsActivityName = parcel.readString(); mCanRetrieveWindowContent = (parcel.readInt() == 1); + mDescription = parcel.readString(); } @Override diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index d77dbdc..e6c2a0f 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -87,11 +87,13 @@ public final class AnimatorSet extends Animator { private AnimatorSetListener mSetListener = null; /** - * Flag indicating that the AnimatorSet has been canceled (by calling cancel() or end()). + * Flag indicating that the AnimatorSet has been manually + * terminated (by calling cancel() or end()). * This flag is used to avoid starting other animations when currently-playing - * child animations of this AnimatorSet end. + * child animations of this AnimatorSet end. It also determines whether cancel/end + * notifications are sent out via the normal AnimatorSetListener mechanism. */ - boolean mCanceled = false; + boolean mTerminated = false; // The amount of time in ms to delay starting the animation after start() is called private long mStartDelay = 0; @@ -271,30 +273,28 @@ public final class AnimatorSet extends Animator { @SuppressWarnings("unchecked") @Override public void cancel() { - mCanceled = true; - if (mListeners != null) { - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } - if (mDelayAnim != null && mDelayAnim.isRunning()) { - // If we're currently in the startDelay period, just cancel that animator and - // send out the end event to all listeners - mDelayAnim.cancel(); + mTerminated = true; + if (isRunning()) { + ArrayList<AnimatorListener> tmpListeners = null; if (mListeners != null) { - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); + tmpListeners = (ArrayList<AnimatorListener>) mListeners.clone(); for (AnimatorListener listener : tmpListeners) { - listener.onAnimationEnd(this); + listener.onAnimationCancel(this); } } - return; - } - if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.cancel(); + if (mDelayAnim != null && mDelayAnim.isRunning()) { + // If we're currently in the startDelay period, just cancel that animator and + // send out the end event to all listeners + mDelayAnim.cancel(); + } else if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.cancel(); + } + } + if (tmpListeners != null) { + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } } } } @@ -307,23 +307,32 @@ public final class AnimatorSet extends Animator { */ @Override public void end() { - mCanceled = true; - if (mSortedNodes.size() != mNodes.size()) { - // hasn't been started yet - sort the nodes now, then end them - sortNodes(); - for (Node node : mSortedNodes) { - if (mSetListener == null) { - mSetListener = new AnimatorSetListener(this); + mTerminated = true; + if (isRunning()) { + if (mSortedNodes.size() != mNodes.size()) { + // hasn't been started yet - sort the nodes now, then end them + sortNodes(); + for (Node node : mSortedNodes) { + if (mSetListener == null) { + mSetListener = new AnimatorSetListener(this); + } + node.animation.addListener(mSetListener); } - node.animation.addListener(mSetListener); } - } - if (mDelayAnim != null) { - mDelayAnim.cancel(); - } - if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.end(); + if (mDelayAnim != null) { + mDelayAnim.cancel(); + } + if (mSortedNodes.size() > 0) { + for (Node node : mSortedNodes) { + node.animation.end(); + } + } + if (mListeners != null) { + ArrayList<AnimatorListener> tmpListeners = + (ArrayList<AnimatorListener>) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationEnd(this); + } } } } @@ -424,7 +433,7 @@ public final class AnimatorSet extends Animator { @SuppressWarnings("unchecked") @Override public void start() { - mCanceled = false; + mTerminated = false; // First, sort the nodes (if necessary). This will ensure that sortedNodes // contains the animation nodes in the correct order. @@ -437,7 +446,8 @@ public final class AnimatorSet extends Animator { ArrayList<AnimatorListener> oldListeners = node.animation.getListeners(); if (oldListeners != null && oldListeners.size() > 0) { for (AnimatorListener listener : oldListeners) { - if (listener instanceof DependencyListener) { + if (listener instanceof DependencyListener || + listener instanceof AnimatorSetListener) { node.animation.removeListener(listener); } } @@ -522,7 +532,7 @@ public final class AnimatorSet extends Animator { * and will populate any appropriate lists, when it is started. */ anim.mNeedsSort = true; - anim.mCanceled = false; + anim.mTerminated = false; anim.mPlayingSet = new ArrayList<Animator>(); anim.mNodeMap = new HashMap<Animator, Node>(); anim.mNodes = new ArrayList<Node>(); @@ -640,7 +650,7 @@ public final class AnimatorSet extends Animator { * @param dependencyAnimation the animation that sent the event. */ private void startIfReady(Animator dependencyAnimation) { - if (mAnimatorSet.mCanceled) { + if (mAnimatorSet.mTerminated) { // if the parent AnimatorSet was canceled, then don't start any dependent anims return; } @@ -676,11 +686,15 @@ public final class AnimatorSet extends Animator { } public void onAnimationCancel(Animator animation) { - if (mPlayingSet.size() == 0) { - if (mListeners != null) { - int numListeners = mListeners.size(); - for (int i = 0; i < numListeners; ++i) { - mListeners.get(i).onAnimationCancel(mAnimatorSet); + if (!mTerminated) { + // Listeners are already notified of the AnimatorSet canceling in cancel(). + // The logic below only kicks in when animations end normally + if (mPlayingSet.size() == 0) { + if (mListeners != null) { + int numListeners = mListeners.size(); + for (int i = 0; i < numListeners; ++i) { + mListeners.get(i).onAnimationCancel(mAnimatorSet); + } } } } @@ -692,24 +706,28 @@ public final class AnimatorSet extends Animator { mPlayingSet.remove(animation); Node animNode = mAnimatorSet.mNodeMap.get(animation); animNode.done = true; - ArrayList<Node> sortedNodes = mAnimatorSet.mSortedNodes; - boolean allDone = true; - int numSortedNodes = sortedNodes.size(); - for (int i = 0; i < numSortedNodes; ++i) { - if (!sortedNodes.get(i).done) { - allDone = false; - break; + if (!mTerminated) { + // Listeners are already notified of the AnimatorSet ending in cancel() or + // end(); the logic below only kicks in when animations end normally + ArrayList<Node> sortedNodes = mAnimatorSet.mSortedNodes; + boolean allDone = true; + int numSortedNodes = sortedNodes.size(); + for (int i = 0; i < numSortedNodes; ++i) { + if (!sortedNodes.get(i).done) { + allDone = false; + break; + } } - } - if (allDone) { - // If this was the last child animation to end, then notify listeners that this - // AnimatorSet has ended - if (mListeners != null) { - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (int i = 0; i < numListeners; ++i) { - tmpListeners.get(i).onAnimationEnd(mAnimatorSet); + if (allDone) { + // If this was the last child animation to end, then notify listeners that this + // AnimatorSet has ended + if (mListeners != null) { + ArrayList<AnimatorListener> tmpListeners = + (ArrayList<AnimatorListener>) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onAnimationEnd(mAnimatorSet); + } } } } @@ -791,6 +809,8 @@ public final class AnimatorSet extends Animator { } } } + // nodes are 'done' by default; they become un-done when started, and done + // again when ended node.done = false; } } diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java index d25de97..06d18ec 100644 --- a/core/java/android/animation/LayoutTransition.java +++ b/core/java/android/animation/LayoutTransition.java @@ -25,6 +25,7 @@ import android.view.animation.DecelerateInterpolator; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; /** @@ -178,14 +179,17 @@ public class LayoutTransition { * the transition. The reason for this is that a further layout event should cause * existing animations to stop where they are prior to starting new animations. So * we cache all of the current animations in this map for possible cancellation on - * another layout event. + * another layout event. LinkedHashMaps are used to preserve the order in which animations + * are inserted, so that we process events (such as setting up start values) in the same order. */ - private final HashMap<View, Animator> pendingAnimations = new HashMap<View, Animator>(); - private final HashMap<View, Animator> currentChangingAnimations = new HashMap<View, Animator>(); - private final HashMap<View, Animator> currentAppearingAnimations = - new HashMap<View, Animator>(); - private final HashMap<View, Animator> currentDisappearingAnimations = + private final HashMap<View, Animator> pendingAnimations = new HashMap<View, Animator>(); + private final LinkedHashMap<View, Animator> currentChangingAnimations = + new LinkedHashMap<View, Animator>(); + private final LinkedHashMap<View, Animator> currentAppearingAnimations = + new LinkedHashMap<View, Animator>(); + private final LinkedHashMap<View, Animator> currentDisappearingAnimations = + new LinkedHashMap<View, Animator>(); /** * This hashmap is used to track the listeners that have been added to the children of @@ -235,7 +239,7 @@ public class LayoutTransition { PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1); PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1); PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1); - defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder(this, + defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object)null, pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY); defaultChangeIn.setDuration(DEFAULT_DURATION); defaultChangeIn.setStartDelay(mChangingAppearingDelay); @@ -244,11 +248,11 @@ public class LayoutTransition { defaultChangeOut.setStartDelay(mChangingDisappearingDelay); defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); - defaultFadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f); + defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); defaultFadeIn.setStartDelay(mAppearingDelay); defaultFadeIn.setInterpolator(mAppearingInterpolator); - defaultFadeOut = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); + defaultFadeOut = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); defaultFadeOut.setDuration(DEFAULT_DURATION); defaultFadeOut.setStartDelay(mDisappearingDelay); defaultFadeOut.setInterpolator(mDisappearingInterpolator); @@ -547,7 +551,7 @@ public class LayoutTransition { } /** - * This function sets up runs animations on all of the views that change during layout. + * This function sets up animations on all of the views that change during layout. * For every child in the parent, we create a change animation of the appropriate * type (appearing or disappearing) and ask it to populate its start values from its * target view. We add layout listeners to all child views and listen for changes. For @@ -821,24 +825,24 @@ public class LayoutTransition { */ public void cancel() { if (currentChangingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentChangingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.cancel(); } currentChangingAnimations.clear(); } if (currentAppearingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentAppearingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentAppearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } currentAppearingAnimations.clear(); } if (currentDisappearingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentDisappearingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentDisappearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } @@ -859,8 +863,8 @@ public class LayoutTransition { case CHANGE_APPEARING: case CHANGE_DISAPPEARING: if (currentChangingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentChangingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.cancel(); } @@ -869,8 +873,8 @@ public class LayoutTransition { break; case APPEARING: if (currentAppearingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentAppearingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentAppearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } @@ -879,8 +883,8 @@ public class LayoutTransition { break; case DISAPPEARING: if (currentDisappearingAnimations.size() > 0) { - HashMap<View, Animator> currentAnimCopy = - (HashMap<View, Animator>) currentDisappearingAnimations.clone(); + LinkedHashMap<View, Animator> currentAnimCopy = + (LinkedHashMap<View, Animator>) currentDisappearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } @@ -1113,4 +1117,4 @@ public class LayoutTransition { View view, int transitionType); } -}
\ No newline at end of file +} diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 1dcaa04..90d676e 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -933,17 +933,17 @@ public class ValueAnimator extends Animator { @Override public void cancel() { - if (mListeners != null) { - ArrayList<AnimatorListener> tmpListeners = - (ArrayList<AnimatorListener>) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } // Only cancel if the animation is actually running or has been started and is about // to run if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) || sDelayedAnims.get().contains(this)) { + if (mListeners != null) { + ArrayList<AnimatorListener> tmpListeners = + (ArrayList<AnimatorListener>) mListeners.clone(); + for (AnimatorListener listener : tmpListeners) { + listener.onAnimationCancel(this); + } + } endAnimation(); } } diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index 3ec5edb..36940c2 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -81,6 +81,9 @@ public abstract class ActionBar { * Set this flag if selecting the 'home' button in the action bar to return * up by a single level in your UI rather than back to the top level or front page. * + * <p>Setting this option will implicitly enable interaction with the home/up + * button. See {@link #setHomeButtonEnabled(boolean)}. + * * @see #setDisplayOptions(int) * @see #setDisplayOptions(int, int) */ @@ -107,18 +110,6 @@ public abstract class ActionBar { public static final int DISPLAY_SHOW_CUSTOM = 0x10; /** - * Disable the 'home' element. This may be combined with - * {@link #DISPLAY_SHOW_HOME} to create a non-focusable/non-clickable - * 'home' element. Useful for a level of your app's navigation hierarchy - * where clicking 'home' doesn't do anything. - * - * @see #setDisplayOptions(int) - * @see #setDisplayOptions(int, int) - * @see #setDisplayDisableHomeEnabled(boolean) - */ - public static final int DISPLAY_DISABLE_HOME = 0x20; - - /** * Set the action bar into custom navigation mode, supplying a view * for custom navigation. * @@ -405,21 +396,6 @@ public abstract class ActionBar { public abstract void setDisplayShowCustomEnabled(boolean showCustom); /** - * Set whether the 'home' affordance on the action bar should be disabled. - * If set, the 'home' element will not be focusable or clickable, useful if - * the user is at the top level of the app's navigation hierarchy. - * - * <p>To set several display options at once, see the setDisplayOptions methods. - * - * @param disableHome true to disable the 'home' element. - * - * @see #setDisplayOptions(int) - * @see #setDisplayOptions(int, int) - * @see #DISPLAY_DISABLE_HOME - */ - public abstract void setDisplayDisableHomeEnabled(boolean disableHome); - - /** * Set the ActionBar's background. * * @param d Background drawable @@ -632,6 +608,22 @@ public abstract class ActionBar { public abstract void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener); /** + * Enable or disable the "home" button in the corner of the action bar. (Note that this + * is the application home/up affordance on the action bar, not the systemwide home + * button.) + * + * <p>This defaults to true for packages targeting < API 14. For packages targeting + * API 14 or greater, the application should call this method to enable interaction + * with the home/up affordance. + * + * <p>Setting the {@link #DISPLAY_HOME_AS_UP} display option will automatically enable + * the home button. + * + * @param enabled true to enable the home button, false to disable the home button. + */ + public abstract void setHomeButtonEnabled(boolean enabled); + + /** * Listener interface for ActionBar navigation events. */ public interface OnNavigationListener { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index a6658cc..d207a0a 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1471,6 +1471,24 @@ public class ActivityManager { } /** + * Returns the usage statistics of each installed package. + * + * @hide + */ + public PkgUsageStats[] getAllPackageUsageStats() { + try { + IUsageStats usageStatsService = IUsageStats.Stub.asInterface( + ServiceManager.getService("usagestats")); + if (usageStatsService != null) { + return usageStatsService.getAllPkgUsageStats(); + } + } catch (RemoteException e) { + Log.w(TAG, "Could not query usage stats", e); + } + return new PkgUsageStats[0]; + } + + /** * @param userid the user's id. Zero indicates the default user * @hide */ diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index eee14fb..c6a746b 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -413,10 +413,10 @@ public final class ActivityThread { native private void dumpGraphicsInfo(FileDescriptor 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 TWO_COUNT_COLUMNS_DB = "%20s %8d %20s %8d"; + private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s"; + private static final String ONE_COUNT_COLUMN = "%21s %8d"; + private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d"; + private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d"; private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s"; // Formatting for checkin service - update version if row format changes @@ -729,12 +729,17 @@ public final class ActivityThread { } @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (args != null && args.length == 1 && args[0].equals("graphics")) { + public Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, String[] args) { + FileOutputStream fout = new FileOutputStream(fd); + PrintWriter pw = new PrintWriter(fout); + try { + return dumpMemInfo(fd, pw, args); + } finally { pw.flush(); - dumpGraphicsInfo(fd); - return; } + } + + private Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, PrintWriter pw, String[] args) { long nativeMax = Debug.getNativeHeapSize() / 1024; long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; long nativeFree = Debug.getNativeHeapFreeSize() / 1024; @@ -742,14 +747,6 @@ public final class ActivityThread { Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); Debug.getMemoryInfo(memInfo); - final int nativeShared = memInfo.nativeSharedDirty; - final int dalvikShared = memInfo.dalvikSharedDirty; - final int otherShared = memInfo.otherSharedDirty; - - final int nativePrivate = memInfo.nativePrivateDirty; - final int dalvikPrivate = memInfo.dalvikPrivateDirty; - final int otherPrivate = memInfo.otherPrivateDirty; - Runtime runtime = Runtime.getRuntime(); long dalvikMax = runtime.totalMemory() / 1024; @@ -813,16 +810,18 @@ public final class ActivityThread { pw.print(memInfo.nativePss + memInfo.dalvikPss + memInfo.otherPss); pw.print(','); // Heap info - shared - pw.print(nativeShared); pw.print(','); - pw.print(dalvikShared); pw.print(','); - pw.print(otherShared); pw.print(','); - pw.print(nativeShared + dalvikShared + otherShared); pw.print(','); + pw.print(memInfo.nativeSharedDirty); pw.print(','); + pw.print(memInfo.dalvikSharedDirty); pw.print(','); + pw.print(memInfo.otherSharedDirty); pw.print(','); + pw.print(memInfo.nativeSharedDirty + memInfo.dalvikSharedDirty + + memInfo.otherSharedDirty); pw.print(','); // Heap info - private - pw.print(nativePrivate); pw.print(','); - pw.print(dalvikPrivate); pw.print(','); - pw.print(otherPrivate); pw.print(','); - pw.print(nativePrivate + dalvikPrivate + otherPrivate); pw.print(','); + pw.print(memInfo.nativePrivateDirty); pw.print(','); + pw.print(memInfo.dalvikPrivateDirty); pw.print(','); + pw.print(memInfo.otherPrivateDirty); pw.print(','); + pw.print(memInfo.nativePrivateDirty + memInfo.dalvikPrivateDirty + + memInfo.otherPrivateDirty); pw.print(','); // Object counts pw.print(viewInstanceCount); pw.print(','); @@ -850,24 +849,38 @@ public final class ActivityThread { pw.print(','); } - return; + return memInfo; } // otherwise, show human-readable format - printRow(pw, HEAP_COLUMN, "", "native", "dalvik", "other", "total"); - printRow(pw, HEAP_COLUMN, "size:", nativeMax, dalvikMax, "N/A", nativeMax + dalvikMax); - printRow(pw, HEAP_COLUMN, "allocated:", nativeAllocated, dalvikAllocated, "N/A", - nativeAllocated + dalvikAllocated); - printRow(pw, HEAP_COLUMN, "free:", nativeFree, dalvikFree, "N/A", - nativeFree + dalvikFree); - - printRow(pw, HEAP_COLUMN, "(Pss):", memInfo.nativePss, memInfo.dalvikPss, - memInfo.otherPss, memInfo.nativePss + memInfo.dalvikPss + memInfo.otherPss); - - printRow(pw, HEAP_COLUMN, "(shared dirty):", nativeShared, dalvikShared, otherShared, - nativeShared + dalvikShared + otherShared); - printRow(pw, HEAP_COLUMN, "(priv dirty):", nativePrivate, dalvikPrivate, otherPrivate, - nativePrivate + dalvikPrivate + otherPrivate); + printRow(pw, HEAP_COLUMN, "", "", "Shared", "Private", "Heap", "Heap", "Heap"); + printRow(pw, HEAP_COLUMN, "", "Pss", "Dirty", "Dirty", "Size", "Alloc", "Free"); + printRow(pw, HEAP_COLUMN, "", "------", "------", "------", "------", "------", + "------"); + printRow(pw, HEAP_COLUMN, "Native", memInfo.nativePss, memInfo.nativeSharedDirty, + memInfo.nativePrivateDirty, nativeMax, nativeAllocated, nativeFree); + printRow(pw, HEAP_COLUMN, "Dalvik", memInfo.dalvikPss, memInfo.dalvikSharedDirty, + memInfo.dalvikPrivateDirty, dalvikMax, dalvikAllocated, dalvikFree); + + int otherPss = memInfo.otherPss; + int otherSharedDirty = memInfo.otherSharedDirty; + int otherPrivateDirty = memInfo.otherPrivateDirty; + + for (int i=0; i<Debug.MemoryInfo.NUM_OTHER_STATS; i++) { + printRow(pw, HEAP_COLUMN, memInfo.getOtherLabel(i), + memInfo.getOtherPss(i), memInfo.getOtherSharedDirty(i), + memInfo.getOtherPrivateDirty(i), "", "", ""); + otherPss -= memInfo.getOtherPss(i); + otherSharedDirty -= memInfo.getOtherSharedDirty(i); + otherPrivateDirty -= memInfo.getOtherPrivateDirty(i); + } + + printRow(pw, HEAP_COLUMN, "Unknown", otherPss, otherSharedDirty, + otherPrivateDirty, "", "", ""); + printRow(pw, HEAP_COLUMN, "TOTAL", memInfo.getTotalPss(), + memInfo.getTotalSharedDirty(), memInfo.getTotalPrivateDirty(), + nativeMax+dalvikMax, nativeAllocated+dalvikAllocated, + nativeFree+dalvikFree); pw.println(" "); pw.println(" Objects"); @@ -916,6 +929,13 @@ public final class ActivityThread { pw.println(" Asset Allocations"); pw.print(assetAlloc); } + + return memInfo; + } + + @Override + public void dumpGfxInfo(FileDescriptor fd, String[] args) { + dumpGraphicsInfo(fd); } private void printRow(PrintWriter pw, String format, Object...objs) { @@ -932,6 +952,10 @@ public final class ActivityThread { ucd.info = info; queueOrSendMessage(H.UPDATE_PACKAGE_COMPATIBILITY_INFO, ucd); } + + public void scheduleTrimMemory(int level) { + queueOrSendMessage(H.TRIM_MEMORY, level); + } } private final class H extends Handler { @@ -975,6 +999,7 @@ public final class ActivityThread { public static final int SLEEPING = 137; public static final int SET_CORE_SETTINGS = 138; public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139; + public static final int TRIM_MEMORY = 140; String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { @@ -1018,6 +1043,7 @@ public final class ActivityThread { case SLEEPING: return "SLEEPING"; case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS"; case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO"; + case TRIM_MEMORY: return "TRIM_MEMORY"; } } return "(unknown)"; @@ -1158,6 +1184,8 @@ public final class ActivityThread { break; case UPDATE_PACKAGE_COMPATIBILITY_INFO: handleUpdatePackageCompatibilityInfo((UpdateCompatibilityData)msg.obj); + case TRIM_MEMORY: + handleTrimMemory(msg.arg1); } if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + msg.what); } @@ -3529,6 +3557,9 @@ public final class ActivityThread { BinderInternal.forceGc("mem"); } + final void handleTrimMemory(int level) { + } + private final void handleBindApplication(AppBindData data) { mBoundApplication = data; mConfiguration = new Configuration(data.config); @@ -4093,11 +4124,6 @@ public final class ActivityThread { }); } - private final void detach() - { - sThreadLocal.set(null); - } - public static final ActivityThread systemMain() { HardwareRenderer.disable(); ActivityThread thread = new ActivityThread(); @@ -4105,10 +4131,9 @@ public final class ActivityThread { return thread; } - public final void installSystemProviders(List providers) { + public final void installSystemProviders(List<ProviderInfo> providers) { if (providers != null) { - installContentProviders(mInitialApplication, - (List<ProviderInfo>)providers); + installContentProviders(mInitialApplication, providers); } } @@ -4147,14 +4172,6 @@ public final class ActivityThread { Looper.loop(); - if (Process.supportsProcesses()) { - throw new RuntimeException("Main thread loop unexpectedly exited"); - } - - thread.detach(); - String name = (thread.mInitialApplication != null) - ? thread.mInitialApplication.getPackageName() - : "<unknown>"; - Slog.i(TAG, "Main thread of " + name + " is now exiting"); + throw new RuntimeException("Main thread loop unexpectedly exited"); } } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index dc0f529..942f245 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -478,6 +478,55 @@ public abstract class ApplicationThreadNative extends Binder updatePackageCompatibilityInfo(pkg, compat); return true; } + + case SCHEDULE_TRIM_MEMORY_TRANSACTION: { + data.enforceInterface(IApplicationThread.descriptor); + int level = data.readInt(); + scheduleTrimMemory(level); + return true; + } + + case DUMP_MEM_INFO_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + ParcelFileDescriptor fd = data.readFileDescriptor(); + String[] args = data.readStringArray(); + Debug.MemoryInfo mi = null; + if (fd != null) { + try { + mi = dumpMemInfo(fd.getFileDescriptor(), args); + } finally { + try { + fd.close(); + } catch (IOException e) { + // swallowed, not propagated back to the caller + } + } + } + reply.writeNoException(); + mi.writeToParcel(reply, 0); + return true; + } + + case DUMP_GFX_INFO_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + ParcelFileDescriptor fd = data.readFileDescriptor(); + String[] args = data.readStringArray(); + if (fd != null) { + try { + dumpGfxInfo(fd.getFileDescriptor(), args); + } finally { + try { + fd.close(); + } catch (IOException e) { + // swallowed, not propagated back to the caller + } + } + } + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -989,4 +1038,36 @@ class ApplicationThreadProxy implements IApplicationThread { mRemote.transact(UPDATE_PACKAGE_COMPATIBILITY_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); } + + public void scheduleTrimMemory(int level) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeInt(level); + mRemote.transact(SCHEDULE_TRIM_MEMORY_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + } + + public Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + mRemote.transact(DUMP_MEM_INFO_TRANSACTION, data, reply, 0); + reply.readException(); + Debug.MemoryInfo info = new Debug.MemoryInfo(); + info.readFromParcel(reply); + data.recycle(); + reply.recycle(); + return info; + } + + public void dumpGfxInfo(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + mRemote.transact(DUMP_GFX_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 8749d3e..d2323e7 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1163,9 +1163,6 @@ class ContextImpl extends Context { throw new IllegalArgumentException("permission is null"); } - if (!Process.supportsProcesses()) { - return PackageManager.PERMISSION_GRANTED; - } try { return ActivityManagerNative.getDefault().checkPermission( permission, pid, uid); @@ -1180,9 +1177,6 @@ class ContextImpl extends Context { throw new IllegalArgumentException("permission is null"); } - if (!Process.supportsProcesses()) { - return PackageManager.PERMISSION_GRANTED; - } int pid = Binder.getCallingPid(); if (pid != Process.myPid()) { return checkPermission(permission, pid, @@ -1263,9 +1257,6 @@ class ContextImpl extends Context { @Override public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { - if (!Process.supportsProcesses()) { - return PackageManager.PERMISSION_GRANTED; - } try { return ActivityManagerNative.getDefault().checkUriPermission( uri, pid, uid, modeFlags); @@ -1276,9 +1267,6 @@ class ContextImpl extends Context { @Override public int checkCallingUriPermission(Uri uri, int modeFlags) { - if (!Process.supportsProcesses()) { - return PackageManager.PERMISSION_GRANTED; - } int pid = Binder.getCallingPid(); if (pid != Process.myPid()) { return checkUriPermission(uri, pid, diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index 285f1c1..c82c9ec 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -1307,6 +1307,7 @@ final class FragmentManagerImpl extends FragmentManager { mExecutingActions = true; for (int i=0; i<numActions; i++) { mTmpActions[i].run(); + mTmpActions[i] = null; } mExecutingActions = false; didSomething = true; diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 05a68a8..9de0bf4 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -119,6 +119,9 @@ public interface IApplicationThread extends IInterface { throws RemoteException; void setCoreSettings(Bundle coreSettings) throws RemoteException; void updatePackageCompatibilityInfo(String pkg, CompatibilityInfo info) throws RemoteException; + void scheduleTrimMemory(int level) throws RemoteException; + Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, String[] args) throws RemoteException; + void dumpGfxInfo(FileDescriptor fd, String[] args) throws RemoteException; String descriptor = "android.app.IApplicationThread"; @@ -162,4 +165,7 @@ public interface IApplicationThread extends IInterface { int SET_HTTP_PROXY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+38; int SET_CORE_SETTINGS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+39; int UPDATE_PACKAGE_COMPATIBILITY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+40; + int SCHEDULE_TRIM_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+41; + int DUMP_MEM_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+42; + int DUMP_GFX_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+43; } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index dce0a97..436fdf8 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -28,6 +28,7 @@ import android.os.RemoteException; import android.util.Log; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; @@ -533,6 +534,16 @@ public abstract class BackupAgent extends ContextWrapper { Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); throw ex; } finally { + // Send the EOD marker indicating that there is no more data + // forthcoming from this agent. + try { + FileOutputStream out = new FileOutputStream(data.getFileDescriptor()); + byte[] buf = new byte[4]; + out.write(buf); + } catch (IOException e) { + Log.e(TAG, "Unable to finalize backup stream!"); + } + Binder.restoreCallingIdentity(ident); try { callbackBinder.opComplete(token); diff --git a/core/java/android/app/backup/IRestoreSession.aidl b/core/java/android/app/backup/IRestoreSession.aidl index 1dddbb0..14731ee 100644 --- a/core/java/android/app/backup/IRestoreSession.aidl +++ b/core/java/android/app/backup/IRestoreSession.aidl @@ -52,6 +52,25 @@ interface IRestoreSession { int restoreAll(long token, IRestoreObserver observer); /** + * Restore select packages from the given set onto the device, replacing the + * current data of any app contained in the set with the data previously + * backed up. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @return Zero on success, nonzero on error. The observer will only receive + * progress callbacks if this method returned zero. + * @param token The token from {@link getAvailableRestoreSets()} corresponding to + * the restore set that should be used. + * @param observer If non-null, this binder points to an object that will receive + * progress callbacks during the restore operation. + * @param packages The set of packages for which to attempt a restore. Regardless of + * the contents of the actual back-end dataset named by {@code token}, only + * applications mentioned in this list will have their data restored. + */ + int restoreSome(long token, IRestoreObserver observer, in String[] packages); + + /** * Restore a single application from backup. The data will be restored from the * current backup dataset if the given package has stored data there, or from * the dataset used during the last full device setup operation if the current diff --git a/core/java/android/app/backup/RestoreSession.java b/core/java/android/app/backup/RestoreSession.java index 24ddb99..7181c61 100644 --- a/core/java/android/app/backup/RestoreSession.java +++ b/core/java/android/app/backup/RestoreSession.java @@ -87,6 +87,40 @@ public class RestoreSession { } /** + * Restore select packages from the given set onto the device, replacing the + * current data of any app contained in the set with the data previously + * backed up. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @return Zero on success, nonzero on error. The observer will only receive + * progress callbacks if this method returned zero. + * @param token The token from {@link getAvailableRestoreSets()} corresponding to + * the restore set that should be used. + * @param observer If non-null, this binder points to an object that will receive + * progress callbacks during the restore operation. + * @param packages The set of packages for which to attempt a restore. Regardless of + * the contents of the actual back-end dataset named by {@code token}, only + * applications mentioned in this list will have their data restored. + * + * @hide + */ + public int restoreSome(long token, RestoreObserver observer, String[] packages) { + int err = -1; + if (mObserver != null) { + Log.d(TAG, "restoreAll() called during active restore"); + return -1; + } + mObserver = new RestoreObserverWrapper(mContext, observer); + try { + err = mBinder.restoreSome(token, mObserver, packages); + } catch (RemoteException e) { + Log.d(TAG, "Can't contact server to restore packages"); + } + return err; + } + + /** * Restore a single application from backup. The data will be restored from the * current backup dataset if the given package has stored data there, or from * the dataset used during the last full device setup operation if the current diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index 61d3707..96f3290 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -423,7 +423,24 @@ public final class BluetoothA2dp implements BluetoothProfile { return false; } - /** + /** + * Allow or disallow incoming connection + * @param device Sink + * @param value True / False + * @return Success or Failure of the binder call. + * @hide + */ + public boolean allowIncomingConnect(BluetoothDevice device, boolean value) { + if (DBG) log("allowIncomingConnect(" + device + ":" + value + ")"); + try { + return mService.allowIncomingConnect(device, value); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** * Helper for converting a state to a string. * * For debug use only - strings are not internationalized. diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index 254e2f8..d9525a3 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -276,6 +276,70 @@ public final class BluetoothDevice implements Parcelable { public static final String ACTION_PAIRING_CANCEL = "android.bluetooth.device.action.PAIRING_CANCEL"; + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_ACCESS_REQUEST = + "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST"; + + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_ACCESS_REPLY = + "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY"; + + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_ACCESS_CANCEL = + "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL"; + + /** + * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intent. + * @hide + */ + public static final String EXTRA_ACCESS_REQUEST_TYPE = + "android.bluetooth.device.extra.ACCESS_REQUEST_TYPE"; + + /**@hide*/ + public static final int REQUEST_TYPE_PROFILE_CONNECTION = 1; + + /**@hide*/ + public static final int REQUEST_TYPE_PHONEBOOK_ACCESS = 2; + + /** + * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents, + * Contains package name to return reply intent to. + * @hide + */ + public static final String EXTRA_PACKAGE_NAME = "android.bluetooth.device.extra.PACKAGE_NAME"; + + /** + * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents, + * Contains class name to return reply intent to. + * @hide + */ + public static final String EXTRA_CLASS_NAME = "android.bluetooth.device.extra.CLASS_NAME"; + + /** + * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intent. + * @hide + */ + public static final String EXTRA_CONNECTION_ACCESS_RESULT = + "android.bluetooth.device.extra.CONNECTION_ACCESS_RESULT"; + + /**@hide*/ + public static final int CONNECTION_ACCESS_YES = 1; + + /**@hide*/ + public static final int CONNECTION_ACCESS_NO = 2; + + /** + * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intents, + * Contains boolean to indicate if the allowed response is once-for-all so that + * next request will be granted without asking user again. + * @hide + */ + public static final String EXTRA_ALWAYS_ALLOWED = + "android.bluetooth.device.extra.ALWAYS_ALLOWED"; + /** * A bond attempt succeeded * @hide diff --git a/core/java/android/bluetooth/BluetoothDeviceProfileState.java b/core/java/android/bluetooth/BluetoothDeviceProfileState.java index 56f236d..ab3a426 100644 --- a/core/java/android/bluetooth/BluetoothDeviceProfileState.java +++ b/core/java/android/bluetooth/BluetoothDeviceProfileState.java @@ -22,9 +22,11 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Message; import android.bluetooth.BluetoothAdapter; +import android.os.PowerManager; import android.server.BluetoothA2dpService; import android.server.BluetoothService; import android.util.Log; +import android.util.Pair; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -81,8 +83,18 @@ public final class BluetoothDeviceProfileState extends StateMachine { public static final int AUTO_CONNECT_PROFILES = 101; public static final int TRANSITION_TO_STABLE = 102; public static final int CONNECT_OTHER_PROFILES = 103; + private static final int CONNECTION_ACCESS_REQUEST_REPLY = 104; + private static final int CONNECTION_ACCESS_REQUEST_EXPIRY = 105; private static final int CONNECT_OTHER_PROFILES_DELAY = 4000; // 4 secs + private static final int CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT = 7000; // 7 secs + private static final int CONNECTION_ACCESS_UNDEFINED = -1; + private static final long INIT_INCOMING_REJECT_TIMER = 1000; // 1 sec + private static final long MAX_INCOMING_REJECT_TIMER = 3600 * 1000 * 4; // 4 hours + + private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; + private static final String ACCESS_AUTHORITY_CLASS = + "com.android.settings.bluetooth.BluetoothPermissionRequest"; private BondedDevice mBondedDevice = new BondedDevice(); private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree(); @@ -98,10 +110,16 @@ public final class BluetoothDeviceProfileState extends StateMachine { private BluetoothHeadset mHeadsetService; private BluetoothPbap mPbapService; private boolean mPbapServiceConnected; + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; private BluetoothDevice mDevice; private int mHeadsetState = BluetoothProfile.STATE_DISCONNECTED; private int mA2dpState = BluetoothProfile.STATE_DISCONNECTED; + private long mIncomingRejectTimer; + private boolean mConnectionAccessReplyReceived = false; + private Pair<Integer, String> mIncomingConnections; + private PowerManager.WakeLock mWakeLock; + private PowerManager mPowerManager; private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -113,6 +131,10 @@ public final class BluetoothDeviceProfileState extends StateMachine { if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); + // We trust this device now + if (newState == BluetoothHeadset.STATE_CONNECTED) { + setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); + } mA2dpState = newState; if (oldState == BluetoothA2dp.STATE_CONNECTED && newState == BluetoothA2dp.STATE_DISCONNECTED) { @@ -125,7 +147,10 @@ public final class BluetoothDeviceProfileState extends StateMachine { } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); - + // We trust this device now + if (newState == BluetoothHeadset.STATE_CONNECTED) { + setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); + } mHeadsetState = newState; if (oldState == BluetoothHeadset.STATE_CONNECTED && newState == BluetoothHeadset.STATE_DISCONNECTED) { @@ -139,7 +164,10 @@ public final class BluetoothDeviceProfileState extends StateMachine { int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); - + // We trust this device now + if (newState == BluetoothHeadset.STATE_CONNECTED) { + setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); + } if (oldState == BluetoothProfile.STATE_CONNECTED && newState == BluetoothProfile.STATE_DISCONNECTED) { sendMessage(DISCONNECT_HID_INCOMING); @@ -152,8 +180,15 @@ public final class BluetoothDeviceProfileState extends StateMachine { // This is technically not needed, but we can get stuck sometimes. // For example, if incoming A2DP fails, we are not informed by Bluez sendMessage(TRANSITION_TO_STABLE); + } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { + mWakeLock.release(); + int val = intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, + BluetoothDevice.CONNECTION_ACCESS_NO); + Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_REPLY); + msg.arg1 = val; + sendMessage(msg); } - } + } }; private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) { @@ -195,6 +230,7 @@ public final class BluetoothDeviceProfileState extends StateMachine { filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); mContext.registerReceiver(mBroadcastReceiver, filter); @@ -203,6 +239,14 @@ public final class BluetoothDeviceProfileState extends StateMachine { BluetoothProfile.HEADSET); // TODO(): Convert PBAP to the new Profile APIs. PbapServiceListener p = new PbapServiceListener(); + + mIncomingConnections = mService.getIncomingState(address); + mIncomingRejectTimer = readTimerValue(); + mPowerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | + PowerManager.ACQUIRE_CAUSES_WAKEUP | + PowerManager.ON_AFTER_RELEASE, TAG); + mWakeLock.setReferenceCounted(false); } private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = @@ -497,6 +541,24 @@ public final class BluetoothDeviceProfileState extends StateMachine { // Ignore Log.e(TAG, "Error: Incoming connection with a pending incoming connection"); break; + case CONNECTION_ACCESS_REQUEST_REPLY: + int val = message.arg1; + mConnectionAccessReplyReceived = true; + boolean value = false; + if (val == BluetoothDevice.CONNECTION_ACCESS_YES) { + value = true; + } + setTrust(val); + + handleIncomingConnection(CONNECT_HFP_INCOMING, value); + break; + case CONNECTION_ACCESS_REQUEST_EXPIRY: + if (!mConnectionAccessReplyReceived) { + handleIncomingConnection(CONNECT_HFP_INCOMING, false); + sendConnectionAccessRemovalIntent(); + sendMessage(TRANSITION_TO_STABLE); + } + break; case CONNECT_A2DP_INCOMING: // Serialize the commands. deferMessage(message); @@ -690,6 +752,25 @@ public final class BluetoothDeviceProfileState extends StateMachine { case CONNECT_A2DP_INCOMING: // ignore break; + case CONNECTION_ACCESS_REQUEST_REPLY: + int val = message.arg1; + mConnectionAccessReplyReceived = true; + boolean value = false; + if (val == BluetoothDevice.CONNECTION_ACCESS_YES) { + value = true; + } + setTrust(val); + handleIncomingConnection(CONNECT_A2DP_INCOMING, value); + break; + case CONNECTION_ACCESS_REQUEST_EXPIRY: + // The check protects the race condition between REQUEST_REPLY + // and the timer expiry. + if (!mConnectionAccessReplyReceived) { + handleIncomingConnection(CONNECT_A2DP_INCOMING, false); + sendConnectionAccessRemovalIntent(); + sendMessage(TRANSITION_TO_STABLE); + } + break; case CONNECT_A2DP_OUTGOING: // Defer message and retry deferMessage(message); @@ -847,6 +928,20 @@ public final class BluetoothDeviceProfileState extends StateMachine { case DISCONNECT_HID_OUTGOING: deferMessage(message); break; + case CONNECTION_ACCESS_REQUEST_REPLY: + mConnectionAccessReplyReceived = true; + int val = message.arg1; + setTrust(val); + handleIncomingConnection(CONNECT_HID_INCOMING, + val == BluetoothDevice.CONNECTION_ACCESS_YES); + break; + case CONNECTION_ACCESS_REQUEST_EXPIRY: + if (!mConnectionAccessReplyReceived) { + handleIncomingConnection(CONNECT_HID_INCOMING, false); + sendConnectionAccessRemovalIntent(); + sendMessage(TRANSITION_TO_STABLE); + } + break; case DISCONNECT_HFP_INCOMING: // Shouldn't happen but if does, we can handle it. // Depends if the headset can handle it. @@ -891,8 +986,150 @@ public final class BluetoothDeviceProfileState extends StateMachine { deferMessage(msg); } + private void updateIncomingAllowedTimer() { + // Not doing a perfect exponential backoff because + // we want two different rates. For all practical + // purposes, this is good enough. + if (mIncomingRejectTimer == 0) mIncomingRejectTimer = INIT_INCOMING_REJECT_TIMER; + + mIncomingRejectTimer *= 5; + if (mIncomingRejectTimer > MAX_INCOMING_REJECT_TIMER) { + mIncomingRejectTimer = MAX_INCOMING_REJECT_TIMER; + } + writeTimerValue(mIncomingRejectTimer); + } + + private boolean handleIncomingConnection(int command, boolean accept) { + boolean ret = false; + Log.i(TAG, "handleIncomingConnection:" + command + ":" + accept); + switch (command) { + case CONNECT_HFP_INCOMING: + if (!accept) { + ret = mHeadsetService.rejectIncomingConnect(mDevice); + sendMessage(TRANSITION_TO_STABLE); + updateIncomingAllowedTimer(); + } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) { + writeTimerValue(0); + ret = mHeadsetService.acceptIncomingConnect(mDevice); + } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) { + writeTimerValue(0); + handleConnectionOfOtherProfiles(command); + ret = mHeadsetService.createIncomingConnect(mDevice); + } + break; + case CONNECT_A2DP_INCOMING: + if (!accept) { + ret = mA2dpService.allowIncomingConnect(mDevice, false); + sendMessage(TRANSITION_TO_STABLE); + updateIncomingAllowedTimer(); + } else { + writeTimerValue(0); + ret = mA2dpService.allowIncomingConnect(mDevice, true); + handleConnectionOfOtherProfiles(command); + } + break; + case CONNECT_HID_INCOMING: + if (!accept) { + ret = mService.allowIncomingHidConnect(mDevice, false); + sendMessage(TRANSITION_TO_STABLE); + updateIncomingAllowedTimer(); + } else { + writeTimerValue(0); + ret = mService.allowIncomingHidConnect(mDevice, true); + } + break; + default: + Log.e(TAG, "Waiting for incoming connection but state changed to:" + command); + break; + } + return ret; + } + + private void sendConnectionAccessIntent() { + mConnectionAccessReplyReceived = false; + + if (!mPowerManager.isScreenOn()) mWakeLock.acquire(); + + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); + intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); + intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, + BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + } + + private void sendConnectionAccessRemovalIntent() { + mWakeLock.release(); + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + } + + private int getTrust() { + String address = mDevice.getAddress(); + if (mIncomingConnections != null) return mIncomingConnections.first; + return CONNECTION_ACCESS_UNDEFINED; + } + + + private String getStringValue(long value) { + StringBuilder sbr = new StringBuilder(); + sbr.append(Long.toString(System.currentTimeMillis())); + sbr.append("-"); + sbr.append(Long.toString(value)); + return sbr.toString(); + } + + private void setTrust(int value) { + String second; + if (mIncomingConnections == null) { + second = getStringValue(INIT_INCOMING_REJECT_TIMER); + } else { + second = mIncomingConnections.second; + } + + mIncomingConnections = new Pair(value, second); + mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections); + } + + private void writeTimerValue(long value) { + Integer first; + if (mIncomingConnections == null) { + first = CONNECTION_ACCESS_UNDEFINED; + } else { + first = mIncomingConnections.first; + } + mIncomingConnections = new Pair(first, getStringValue(value)); + mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections); + } + + private long readTimerValue() { + if (mIncomingConnections == null) + return 0; + String value = mIncomingConnections.second; + String[] splits = value.split("-"); + if (splits != null && splits.length == 2) { + return Long.parseLong(splits[1]); + } + return 0; + } + + private boolean readIncomingAllowedValue() { + if (readTimerValue() == 0) return true; + String value = mIncomingConnections.second; + String[] splits = value.split("-"); + if (splits != null && splits.length == 2) { + long val1 = Long.parseLong(splits[0]); + long val2 = Long.parseLong(splits[1]); + if (val1 + val2 <= System.currentTimeMillis()) { + return true; + } + } + return false; + } + synchronized boolean processCommand(int command) { - Log.i(TAG, "Processing command:" + command); + log("Processing command:" + command); switch(command) { case CONNECT_HFP_OUTGOING: if (mHeadsetService == null) { @@ -904,11 +1141,9 @@ public final class BluetoothDeviceProfileState extends StateMachine { case CONNECT_HFP_INCOMING: if (mHeadsetService == null) { deferProfileServiceMessage(command); - } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) { - return mHeadsetService.acceptIncomingConnect(mDevice); - } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) { - handleConnectionOfOtherProfiles(command); - return mHeadsetService.createIncomingConnect(mDevice); + } else { + processIncomingConnectCommand(command); + return true; } break; case CONNECT_A2DP_OUTGOING: @@ -917,12 +1152,12 @@ public final class BluetoothDeviceProfileState extends StateMachine { } break; case CONNECT_A2DP_INCOMING: - handleConnectionOfOtherProfiles(command); - // ignore, Bluez takes care + processIncomingConnectCommand(command); return true; case CONNECT_HID_OUTGOING: return mService.connectInputDeviceInternal(mDevice); case CONNECT_HID_INCOMING: + processIncomingConnectCommand(command); return true; case DISCONNECT_HFP_OUTGOING: if (mHeadsetService == null) { @@ -972,6 +1207,8 @@ public final class BluetoothDeviceProfileState extends StateMachine { } break; case UNPAIR: + writeTimerValue(INIT_INCOMING_REJECT_TIMER); + setTrust(CONNECTION_ACCESS_UNDEFINED); return mService.removeBondInternal(mDevice.getAddress()); default: Log.e(TAG, "Error: Unknown Command"); @@ -979,6 +1216,22 @@ public final class BluetoothDeviceProfileState extends StateMachine { return false; } + private void processIncomingConnectCommand(int command) { + // Check if device is already trusted + int access = getTrust(); + if (access == BluetoothDevice.CONNECTION_ACCESS_YES) { + handleIncomingConnection(command, true); + } else if (access == BluetoothDevice.CONNECTION_ACCESS_NO && + !readIncomingAllowedValue()) { + handleIncomingConnection(command, false); + } else { + sendConnectionAccessIntent(); + Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_EXPIRY); + sendMessageDelayed(msg, + CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT); + } + } + private void handleConnectionOfOtherProfiles(int command) { // The white paper recommendations mentions that when there is a // link loss, it is the responsibility of the remote device to connect. diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 23724f2..3284361 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -618,6 +618,23 @@ public final class BluetoothHeadset implements BluetoothProfile { } /** + * Reject the incoming connection. + * @hide + */ + public boolean rejectIncomingConnect(BluetoothDevice device) { + if (DBG) log("rejectIncomingConnect"); + if (mService != null) { + try { + return mService.rejectIncomingConnect(device); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); + } + return false; + } + + /** * Connect to a Bluetooth Headset. * Note: This is an internal function and shouldn't be exposed * diff --git a/core/java/android/bluetooth/BluetoothHealth.java b/core/java/android/bluetooth/BluetoothHealth.java index 52efc07..0a01dcf 100644 --- a/core/java/android/bluetooth/BluetoothHealth.java +++ b/core/java/android/bluetooth/BluetoothHealth.java @@ -16,7 +16,6 @@ package android.bluetooth; -import android.annotation.SdkConstant; import android.content.Context; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -67,9 +66,6 @@ public final class BluetoothHealth implements BluetoothProfile { */ public static final int CHANNEL_TYPE_ANY = 12; - private final ArrayList<BluetoothHealthAppConfiguration> mAppConfigs = - new ArrayList<BluetoothHealthAppConfiguration>(); - /** * Register an application configuration that acts as a Health SINK. * This is the configuration that will be used to communicate with health devices @@ -86,7 +82,7 @@ public final class BluetoothHealth implements BluetoothProfile { * @return If true, callback will be called. */ public boolean registerSinkAppConfiguration(String name, int dataType, - IBluetoothHealthCallback callback) { + BluetoothHealthCallback callback) { if (!isEnabled() || name == null) return false; if (DBG) log("registerSinkApplication(" + name + ":" + dataType + ")"); @@ -111,18 +107,18 @@ public final class BluetoothHealth implements BluetoothProfile { * @hide */ public boolean registerAppConfiguration(String name, int dataType, int role, - int channelType, IBluetoothHealthCallback callback) { + int channelType, BluetoothHealthCallback callback) { boolean result = false; if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result; if (DBG) log("registerApplication(" + name + ":" + dataType + ")"); + BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback); BluetoothHealthAppConfiguration config = - new BluetoothHealthAppConfiguration(name, dataType, role, channelType, - callback); + new BluetoothHealthAppConfiguration(name, dataType, role, channelType); if (mService != null) { try { - result = mService.registerAppConfiguration(config); + result = mService.registerAppConfiguration(config, wrapper); } catch (RemoteException e) { Log.e(TAG, e.toString()); } @@ -130,8 +126,6 @@ public final class BluetoothHealth implements BluetoothProfile { Log.w(TAG, "Proxy not attached to service"); if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } - - if (result) mAppConfigs.add(config); return result; } @@ -147,7 +141,7 @@ public final class BluetoothHealth implements BluetoothProfile { */ public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) { boolean result = false; - if (mService != null && isEnabled() && isValidAppConfig(config)) { + if (mService != null && isEnabled() && config != null) { try { result = mService.unregisterAppConfiguration(config); } catch (RemoteException e) { @@ -157,26 +151,26 @@ public final class BluetoothHealth implements BluetoothProfile { Log.w(TAG, "Proxy not attached to service"); if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } - if (result) mAppConfigs.remove(config); + return result; } /** * Connect to a health device which has the {@link #SOURCE_ROLE}. - * This is an asynchrnous call. If this function returns true, the callback + * This is an asynchronous call. If this function returns true, the callback * associated with the application configuration will be called. * * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. * * @param device The remote Bluetooth device. - * @param config The application configuration which has been registed using - * {@link #registerSinkAppConfiguration(String, int, IBluetoothHealthCallback) } + * @param config The application configuration which has been registered using + * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } * @return If true, the callback associated with the application config will be called. */ public boolean connectChannelToSource(BluetoothDevice device, BluetoothHealthAppConfiguration config) { if (mService != null && isEnabled() && isValidDevice(device) && - isValidAppConfig(config)) { + config != null) { try { return mService.connectChannelToSource(device, config); } catch (RemoteException e) { @@ -197,15 +191,15 @@ public final class BluetoothHealth implements BluetoothProfile { *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. * * @param device The remote Bluetooth device. - * @param config The application configuration which has been registed using - * {@link #registerSinkAppConfiguration(String, int, IBluetoothHealthCallback) } + * @param config The application configuration which has been registered using + * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } * @return If true, the callback associated with the application config will be called. * @hide */ public boolean connectChannelToSink(BluetoothDevice device, BluetoothHealthAppConfiguration config, int channelType) { if (mService != null && isEnabled() && isValidDevice(device) && - isValidAppConfig(config)) { + config != null) { try { return mService.connectChannelToSink(device, config, channelType); } catch (RemoteException e) { @@ -226,8 +220,8 @@ public final class BluetoothHealth implements BluetoothProfile { *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. * * @param device The remote Bluetooth device. - * @param config The application configuration which has been registed using - * {@link #registerSinkAppConfiguration(String, int, IBluetoothHealthCallback) } + * @param config The application configuration which has been registered using + * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } * @param fd The file descriptor that was associated with the channel. * @return If true, the callback associated with the application config will be called. * @hide @@ -235,7 +229,7 @@ public final class BluetoothHealth implements BluetoothProfile { public boolean disconnectChannel(BluetoothDevice device, BluetoothHealthAppConfiguration config, ParcelFileDescriptor fd) { if (mService != null && isEnabled() && isValidDevice(device) && - isValidAppConfig(config)) { + config != null) { try { return mService.disconnectChannel(device, config, fd); } catch (RemoteException e) { @@ -262,7 +256,7 @@ public final class BluetoothHealth implements BluetoothProfile { public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device, BluetoothHealthAppConfiguration config) { if (mService != null && isEnabled() && isValidDevice(device) && - isValidAppConfig(config)) { + config != null) { try { return mService.getMainChannelFd(device, config); } catch (RemoteException e) { @@ -290,6 +284,7 @@ public final class BluetoothHealth implements BluetoothProfile { * {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING}, * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING} */ + @Override public int getConnectionState(BluetoothDevice device) { if (mService != null && isEnabled() && isValidDevice(device)) { try { @@ -317,6 +312,7 @@ public final class BluetoothHealth implements BluetoothProfile { * local adapter. * @return List of devices. The list will be empty on error. */ + @Override public List<BluetoothDevice> getConnectedDevices() { if (mService != null && isEnabled()) { try { @@ -348,6 +344,7 @@ public final class BluetoothHealth implements BluetoothProfile { * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}, * @return List of devices. The list will be empty on error. */ + @Override public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { if (mService != null && isEnabled()) { try { @@ -361,6 +358,27 @@ public final class BluetoothHealth implements BluetoothProfile { return new ArrayList<BluetoothDevice>(); } + private static class BluetoothHealthCallbackWrapper extends IBluetoothHealthCallback.Stub { + private BluetoothHealthCallback mCallback; + + public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) { + mCallback = callback; + } + + @Override + public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config, + int status) { + mCallback.onHealthAppConfigurationStatusChange(config, status); + } + + @Override + public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config, + BluetoothDevice device, int prevState, int newState, + ParcelFileDescriptor fd) { + mCallback.onHealthChannelStateChange(config, device, prevState, newState, fd); + } + } + /** Health Channel Connection State - Disconnected */ public static final int STATE_CHANNEL_DISCONNECTED = 0; /** Health Channel Connection State - Connecting */ @@ -379,7 +397,6 @@ public final class BluetoothHealth implements BluetoothProfile { /** Health App Configuration un-registration failure */ public static final int APPLICATION_UNREGISTRATION_FAILURE = 3; - private Context mContext; private ServiceListener mServiceListener; private IBluetooth mService; BluetoothAdapter mAdapter; @@ -420,14 +437,8 @@ public final class BluetoothHealth implements BluetoothProfile { return false; } - private boolean isValidAppConfig(BluetoothHealthAppConfiguration config) { - if (!mAppConfigs.isEmpty() && mAppConfigs.contains(config)) return true; - log("Not a valid config: " + config); - return false; - } - private boolean checkAppParam(String name, int role, int channelType, - IBluetoothHealthCallback callback) { + BluetoothHealthCallback callback) { if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE) || (channelType != CHANNEL_TYPE_RELIABLE && channelType != CHANNEL_TYPE_STREAMING && diff --git a/core/java/android/bluetooth/BluetoothHealthAppConfiguration.java b/core/java/android/bluetooth/BluetoothHealthAppConfiguration.java index b87aea5..7020249 100644 --- a/core/java/android/bluetooth/BluetoothHealthAppConfiguration.java +++ b/core/java/android/bluetooth/BluetoothHealthAppConfiguration.java @@ -17,7 +17,6 @@ package android.bluetooth; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -34,21 +33,18 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { private final int mDataType; private final int mRole; private final int mChannelType; - private final IBluetoothHealthCallback mCallback; /** * Constructor to register the SINK role * * @param name Friendly name associated with the application configuration * @param dataType Data Type of the remote Bluetooth Health device - * @param callback Callback associated with the application configuration. */ - BluetoothHealthAppConfiguration(String name, int dataType, IBluetoothHealthCallback callback) { + BluetoothHealthAppConfiguration(String name, int dataType) { mName = name; mDataType = dataType; mRole = BluetoothHealth.SINK_ROLE; mChannelType = BluetoothHealth.CHANNEL_TYPE_ANY; - mCallback = callback; } /** @@ -56,17 +52,15 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { * * @param name Friendly name associated with the application configuration * @param dataType Data Type of the remote Bluetooth Health device - * @param role {@link BluetoothHealth.SOURCE_ROLE} or - * {@link BluetoothHealth.SINK_ROLE} - * @param callback Callback associated with the application configuration. + * @param role {@link BluetoothHealth#SOURCE_ROLE} or + * {@link BluetoothHealth#SINK_ROLE} */ - BluetoothHealthAppConfiguration(String name, int dataType, int role, int channelType, - IBluetoothHealthCallback callback) { + BluetoothHealthAppConfiguration(String name, int dataType, int role, int + channelType) { mName = name; mDataType = dataType; mRole = role; mChannelType = channelType; - mCallback = callback; } @Override @@ -77,8 +71,7 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { return mName.equals(config.getName()) && mDataType == config.getDataType() && mRole == config.getRole() && - mChannelType == config.getChannelType() && - mCallback.equals(config.getCallback()); + mChannelType == config.getChannelType(); } return false; } @@ -90,7 +83,6 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { result = 31 * result + mDataType; result = 31 * result + mRole; result = 31 * result + mChannelType; - result = 31 * result + (mCallback != null ? mCallback.hashCode() : 0); return result; } @@ -98,9 +90,10 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { public String toString() { return "BluetoothHealthAppConfiguration [mName = " + mName + ",mDataType = " + mDataType + ", mRole = " + mRole + ",mChannelType = " + - mChannelType + ",callback=" + mCallback +"]"; + mChannelType + "]"; } + @Override public int describeContents() { return 0; } @@ -144,37 +137,31 @@ public final class BluetoothHealthAppConfiguration implements Parcelable { return mChannelType; } - /** - * Return the callback associated with this application configuration. - * - * @return IBluetoothHealthCallback - */ - public IBluetoothHealthCallback getCallback() { - return mCallback; - } - public static final Parcelable.Creator<BluetoothHealthAppConfiguration> CREATOR = new Parcelable.Creator<BluetoothHealthAppConfiguration>() { + @Override public BluetoothHealthAppConfiguration createFromParcel(Parcel in) { String name = in.readString(); int type = in.readInt(); int role = in.readInt(); int channelType = in.readInt(); - IBluetoothHealthCallback callback = - IBluetoothHealthCallback.Stub.asInterface(in.readStrongBinder()); - return new BluetoothHealthAppConfiguration(name, type, role, channelType, - callback); + return new BluetoothHealthAppConfiguration(name, type, role, + channelType); } + + @Override public BluetoothHealthAppConfiguration[] newArray(int size) { return new BluetoothHealthAppConfiguration[size]; } }; + @Override public void writeToParcel(Parcel out, int flags) { out.writeString(mName); out.writeInt(mDataType); out.writeInt(mRole); out.writeInt(mChannelType); - out.writeStrongInterface(mCallback); } + + } diff --git a/core/java/android/bluetooth/BluetoothHealthCallback.java b/core/java/android/bluetooth/BluetoothHealthCallback.java new file mode 100644 index 0000000..0d11bb5 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothHealthCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.bluetooth; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +/** + * This class is used for all the {@link BluetoothHealth} callbacks. + * @hide + */ +public abstract class BluetoothHealthCallback { + + private static final String TAG = "BluetoothHealthCallback"; + + public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config, + int status) { + Log.d(TAG, "onHealthAppConfigurationStatusChange: " + config + " Status:" + status); + } + + public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config, + BluetoothDevice device, int prevState, int newState, + ParcelFileDescriptor fd) { + Log.d(TAG, "onHealthChannelStateChange: " + config + " Device:" + device + + "PrevState:" + prevState + "NewState:" + newState + "FileDescriptor:" + fd); + } +} diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java index 282b70a..f6757d9 100644 --- a/core/java/android/bluetooth/BluetoothInputDevice.java +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -308,6 +308,28 @@ public final class BluetoothInputDevice implements BluetoothProfile { return BluetoothProfile.PRIORITY_OFF; } + /** + * Allow or disallow incoming connection + * @param device Input device + * @param allow true / false + * @return Success or Failure of the operation + * @hide + */ + public boolean allowIncomingConnect(BluetoothDevice device, boolean allow) { + if (DBG) log("allowIncomingConnect(" + device + ", " + allow + ")"); + + if (mService == null || !isEnabled() || !isValidDevice(device)) { + return false; + } + try { + mService.allowIncomingHidConnect(device, allow); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + return false; + } + return true; + } + private boolean isEnabled() { if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; return false; diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java index 83e59e2..acce182 100644 --- a/core/java/android/bluetooth/BluetoothServerSocket.java +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -62,6 +62,7 @@ public final class BluetoothServerSocket implements Closeable { /*package*/ final BluetoothSocket mSocket; private Handler mHandler; private int mMessage; + private final int mChannel; /** * Construct a socket for incoming connections. @@ -74,6 +75,7 @@ public final class BluetoothServerSocket implements Closeable { */ /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, int port) throws IOException { + mChannel = port; mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null); } @@ -125,4 +127,12 @@ public final class BluetoothServerSocket implements Closeable { mHandler = handler; mMessage = message; } + + /** + * Returns the channel on which this socket is bound. + * @hide + */ + public int getChannel() { + return mChannel; + } } diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 28b09b6..183772d 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.IBluetoothHealthCallback; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHealthAppConfiguration; import android.os.ParcelUuid; @@ -91,6 +92,7 @@ interface IBluetooth int getInputDeviceConnectionState(in BluetoothDevice device); boolean setInputDevicePriority(in BluetoothDevice device, int priority); int getInputDevicePriority(in BluetoothDevice device); + boolean allowIncomingHidConnect(in BluetoothDevice device, boolean value); boolean isTetheringOn(); void setBluetoothTethering(boolean value); @@ -101,7 +103,8 @@ interface IBluetooth boolean disconnectPanDevice(in BluetoothDevice device); // HDP profile APIs - boolean registerAppConfiguration(in BluetoothHealthAppConfiguration config); + boolean registerAppConfiguration(in BluetoothHealthAppConfiguration config, + in IBluetoothHealthCallback callback); boolean unregisterAppConfiguration(in BluetoothHealthAppConfiguration config); boolean connectChannelToSource(in BluetoothDevice device, in BluetoothHealthAppConfiguration config); boolean connectChannelToSink(in BluetoothDevice device, in BluetoothHealthAppConfiguration config, diff --git a/core/java/android/bluetooth/IBluetoothA2dp.aidl b/core/java/android/bluetooth/IBluetoothA2dp.aidl index b4fc366..444dd1e 100644 --- a/core/java/android/bluetooth/IBluetoothA2dp.aidl +++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl @@ -39,4 +39,6 @@ interface IBluetoothA2dp { boolean resumeSink(in BluetoothDevice device); boolean connectSinkInternal(in BluetoothDevice device); boolean disconnectSinkInternal(in BluetoothDevice device); + boolean allowIncomingConnect(in BluetoothDevice device, boolean value); + } diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl index 273cda7..ec00527 100644 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -42,6 +42,7 @@ interface IBluetoothHeadset { // Internal functions, not be made public boolean createIncomingConnect(in BluetoothDevice device); boolean acceptIncomingConnect(in BluetoothDevice device); + boolean rejectIncomingConnect(in BluetoothDevice device); boolean cancelConnectThread(); boolean connectHeadsetInternal(in BluetoothDevice device); boolean disconnectHeadsetInternal(in BluetoothDevice device); diff --git a/core/java/android/content/ComponentCallbacks.java b/core/java/android/content/ComponentCallbacks.java index dad60b0..92b98fd 100644 --- a/core/java/android/content/ComponentCallbacks.java +++ b/core/java/android/content/ComponentCallbacks.java @@ -51,4 +51,16 @@ public interface ComponentCallbacks { * The system will perform a gc for you after returning from this method. */ void onLowMemory(); + + /** @hide */ + static final int TRIM_MEMORY_COMPLETE = 80; + + /** @hide */ + static final int TRIM_MEMORY_MODERATE = 60; + + /** @hide */ + static final int TRIM_MEMORY_BACKGROUND = 40; + + /** @hide */ + static final int TRIM_MEMORY_INVISIBLE = 20; } diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index a2af558..0e83dc0 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -20,17 +20,21 @@ import android.accounts.Account; import android.database.IContentObserver; import android.database.sqlite.SQLiteException; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; +import android.util.SparseIntArray; import android.Manifest; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -70,6 +74,40 @@ public final class ContentService extends IContentService.Stub { } else { mSyncManager.dump(fd, pw); } + pw.println(); + pw.println("Observer tree:"); + synchronized (mRootNode) { + int[] counts = new int[2]; + final SparseIntArray pidCounts = new SparseIntArray(); + mRootNode.dumpLocked(fd, pw, args, "", " ", counts, pidCounts); + pw.println(); + ArrayList<Integer> sorted = new ArrayList<Integer>(); + for (int i=0; i<pidCounts.size(); i++) { + sorted.add(pidCounts.keyAt(i)); + } + Collections.sort(sorted, new Comparator<Integer>() { + @Override + public int compare(Integer lhs, Integer rhs) { + int lc = pidCounts.get(lhs); + int rc = pidCounts.get(rhs); + if (lc < rc) { + return 1; + } else if (lc > rc) { + return -1; + } + return 0; + } + + }); + for (int i=0; i<sorted.size(); i++) { + int pid = sorted.get(i); + pw.print(" pid "); pw.print(pid); pw.print(": "); + pw.print(pidCounts.get(pid)); pw.println(" observers"); + } + pw.println(); + pw.print(" Total number of nodes: "); pw.println(counts[0]); + pw.print(" Total number of observers: "); pw.println(counts[1]); + } } finally { restoreCallingIdentity(identityToken); } @@ -102,7 +140,8 @@ public final class ContentService extends IContentService.Stub { throw new IllegalArgumentException("You must pass a valid uri and observer"); } synchronized (mRootNode) { - mRootNode.addObserverLocked(uri, observer, notifyForDescendents, mRootNode); + mRootNode.addObserverLocked(uri, observer, notifyForDescendents, mRootNode, + Binder.getCallingUid(), Binder.getCallingPid()); if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri + " with notifyForDescendents " + notifyForDescendents); } @@ -465,12 +504,17 @@ public final class ContentService extends IContentService.Stub { public static final class ObserverNode { private class ObserverEntry implements IBinder.DeathRecipient { public final IContentObserver observer; + public final int uid; + public final int pid; public final boolean notifyForDescendents; private final Object observersLock; - public ObserverEntry(IContentObserver o, boolean n, Object observersLock) { + public ObserverEntry(IContentObserver o, boolean n, Object observersLock, + int _uid, int _pid) { this.observersLock = observersLock; observer = o; + uid = _uid; + pid = _pid; notifyForDescendents = n; try { observer.asBinder().linkToDeath(this, 0); @@ -484,6 +528,16 @@ public final class ContentService extends IContentService.Stub { removeObserverLocked(observer); } } + + public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args, + String name, String prefix, SparseIntArray pidCounts) { + pidCounts.put(pid, pidCounts.get(pid)+1); + pw.print(prefix); pw.print(name); pw.print(": pid="); + pw.print(pid); pw.print(" uid="); + pw.print(uid); pw.print(" target="); + pw.println(Integer.toHexString(System.identityHashCode( + observer != null ? observer.asBinder() : null))); + } } public static final int INSERT_TYPE = 0; @@ -498,6 +552,37 @@ public final class ContentService extends IContentService.Stub { mName = name; } + public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args, + String name, String prefix, int[] counts, SparseIntArray pidCounts) { + String innerName = null; + if (mObservers.size() > 0) { + if ("".equals(name)) { + innerName = mName; + } else { + innerName = name + "/" + mName; + } + for (int i=0; i<mObservers.size(); i++) { + counts[1]++; + mObservers.get(i).dumpLocked(fd, pw, args, innerName, prefix, + pidCounts); + } + } + if (mChildren.size() > 0) { + if (innerName == null) { + if ("".equals(name)) { + innerName = mName; + } else { + innerName = name + "/" + mName; + } + } + for (int i=0; i<mChildren.size(); i++) { + counts[0]++; + mChildren.get(i).dumpLocked(fd, pw, args, innerName, prefix, + counts, pidCounts); + } + } + } + private String getUriSegment(Uri uri, int index) { if (uri != null) { if (index == 0) { @@ -518,15 +603,16 @@ public final class ContentService extends IContentService.Stub { } public void addObserverLocked(Uri uri, IContentObserver observer, - boolean notifyForDescendents, Object observersLock) { - addObserverLocked(uri, 0, observer, notifyForDescendents, observersLock); + boolean notifyForDescendents, Object observersLock, int uid, int pid) { + addObserverLocked(uri, 0, observer, notifyForDescendents, observersLock, uid, pid); } private void addObserverLocked(Uri uri, int index, IContentObserver observer, - boolean notifyForDescendents, Object observersLock) { + boolean notifyForDescendents, Object observersLock, int uid, int pid) { // If this is the leaf node add the observer if (index == countUriSegments(uri)) { - mObservers.add(new ObserverEntry(observer, notifyForDescendents, observersLock)); + mObservers.add(new ObserverEntry(observer, notifyForDescendents, observersLock, + uid, pid)); return; } @@ -539,7 +625,8 @@ public final class ContentService extends IContentService.Stub { for (int i = 0; i < N; i++) { ObserverNode node = mChildren.get(i); if (node.mName.equals(segment)) { - node.addObserverLocked(uri, index + 1, observer, notifyForDescendents, observersLock); + node.addObserverLocked(uri, index + 1, observer, notifyForDescendents, + observersLock, uid, pid); return; } } @@ -547,7 +634,8 @@ public final class ContentService extends IContentService.Stub { // No child found, create one ObserverNode node = new ObserverNode(segment); mChildren.add(node); - node.addObserverLocked(uri, index + 1, observer, notifyForDescendents, observersLock); + node.addObserverLocked(uri, index + 1, observer, notifyForDescendents, + observersLock, uid, pid); } public boolean removeObserverLocked(IContentObserver observer) { diff --git a/core/java/android/content/XmlDocumentProvider.java b/core/java/android/content/XmlDocumentProvider.java deleted file mode 100644 index 76539c7..0000000 --- a/core/java/android/content/XmlDocumentProvider.java +++ /dev/null @@ -1,438 +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.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; - -/** - * @hide -- not yet ready to support, should be provided just as a static lib. - * - * 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/hardware/Camera.java b/core/java/android/hardware/Camera.java index 8a42693..7d67e11 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -720,8 +720,20 @@ public class Camera { * onAutoFocus will be called immediately with a fake value of * <code>success</code> set to <code>true</code>. * + * The auto-focus routine may lock auto-exposure and auto-white balance + * after it completes. To check for the state of these locks, use the + * {@link android.hardware.Camera.Parameters#getAutoExposureLock()} and + * {@link android.hardware.Camera.Parameters#getAutoWhiteBalanceLock()} + * methods. If such locking is undesirable, use + * {@link android.hardware.Camera.Parameters#setAutoExposureLock(boolean)} + * and + * {@link android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean)} + * to release the locks. + * * @param success true if focus was successful, false if otherwise * @param camera the Camera service object + * @see android.hardware.Camera.Parameters#setAutoExposureLock(boolean) + * @see android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean) */ void onAutoFocus(boolean success, Camera camera); }; @@ -747,8 +759,21 @@ public class Camera { * {@link android.hardware.Camera.Parameters#FLASH_MODE_OFF}, flash may be * fired during auto-focus, depending on the driver and camera hardware.<p> * + * The auto-focus routine may lock auto-exposure and auto-white balance + * after it completes. To check for the state of these locks, use the + * {@link android.hardware.Camera.Parameters#getAutoExposureLock()} and + * {@link android.hardware.Camera.Parameters#getAutoWhiteBalanceLock()} + * methods after the {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} + * callback is invoked. If such locking is undesirable, use + * {@link android.hardware.Camera.Parameters#setAutoExposureLock(boolean)} + * and + * {@link android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean)} + * to release the locks. + * * @param cb the callback to run * @see #cancelAutoFocus() + * @see android.hardware.Camera.Parameters#setAutoExposureLock(boolean) + * @see android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean) */ public final void autoFocus(AutoFocusCallback cb) { @@ -763,7 +788,13 @@ public class Camera { * this function will return the focus position to the default. * If the camera does not support auto-focus, this is a no-op. * + * Canceling auto-focus will return the auto-exposure lock and auto-white + * balance lock to their state before {@link #autoFocus(AutoFocusCallback)} + * was called. + * * @see #autoFocus(Camera.AutoFocusCallback) + * @see android.hardware.Camera.Parameters#setAutoExposureLock(boolean) + * @see android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean) */ public final void cancelAutoFocus() { @@ -2562,8 +2593,6 @@ public class Camera { * routine is free to run normally. * * @see #getAutoExposureLock() - * - * @hide */ public void setAutoExposureLock(boolean toggle) { set(KEY_AUTO_EXPOSURE_LOCK, toggle ? TRUE : FALSE); @@ -2583,7 +2612,6 @@ public class Camera { * * @see #setAutoExposureLock(boolean) * - * @hide */ public boolean getAutoExposureLock() { String str = get(KEY_AUTO_EXPOSURE_LOCK); @@ -2598,7 +2626,6 @@ public class Camera { * @return true if auto-exposure lock is supported. * @see #setAutoExposureLock(boolean) * - * @hide */ public boolean isAutoExposureLockSupported() { String str = get(KEY_AUTO_EXPOSURE_LOCK_SUPPORTED); @@ -2645,8 +2672,6 @@ public class Camera { * auto-white balance routine is free to run normally. * * @see #getAutoWhiteBalanceLock() - * - * @hide */ public void setAutoWhiteBalanceLock(boolean toggle) { set(KEY_AUTO_WHITEBALANCE_LOCK, toggle ? TRUE : FALSE); @@ -2668,7 +2693,6 @@ public class Camera { * * @see #setAutoWhiteBalanceLock(boolean) * - * @hide */ public boolean getAutoWhiteBalanceLock() { String str = get(KEY_AUTO_WHITEBALANCE_LOCK); @@ -2683,7 +2707,6 @@ public class Camera { * @return true if auto-white balance lock is supported. * @see #setAutoWhiteBalanceLock(boolean) * - * @hide */ public boolean isAutoWhiteBalanceLockSupported() { String str = get(KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED); diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 3447e76..44e7e52 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -98,11 +98,8 @@ public class ExtractEditText extends EditText { } @Override public boolean onTextContextMenuItem(int id) { - // Horrible hack: select word option has to be handled by original view to work. - if (mIME != null && id != android.R.id.startSelectingText) { - if (mIME.onExtractTextContextMenuItem(id)) { - return true; - } + if (mIME != null && mIME.onExtractTextContextMenuItem(id)) { + return true; } return super.onTextContextMenuItem(id); } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index d6f5643..d95fc8d 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -100,7 +100,7 @@ interface IConnectivityManager void setDataDependency(int networkType, boolean met); - void protectVpn(in ParcelFileDescriptor socket); + boolean protectVpn(in ParcelFileDescriptor socket); boolean prepareVpn(String oldPackage, String newPackage); diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 19894a0..9826bec 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -52,11 +52,26 @@ import java.util.Collections; public class LinkProperties implements Parcelable { String mIfaceName; - private Collection<LinkAddress> mLinkAddresses; - private Collection<InetAddress> mDnses; - private Collection<RouteInfo> mRoutes; + private Collection<LinkAddress> mLinkAddresses = new ArrayList<LinkAddress>(); + private Collection<InetAddress> mDnses = new ArrayList<InetAddress>(); + private Collection<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); private ProxyProperties mHttpProxy; + public static class CompareResult<T> { + public ArrayList<T> removed = new ArrayList<T>(); + public ArrayList<T> added = new ArrayList<T>(); + + @Override + public String toString() { + String retVal = "removed=["; + for (T addr : removed) retVal += addr.toString() + ","; + retVal += "] added=["; + for (T addr : added) retVal += addr.toString() + ","; + retVal += "]"; + return retVal; + } + } + public LinkProperties() { clear(); } @@ -121,9 +136,9 @@ public class LinkProperties implements Parcelable { public void clear() { mIfaceName = null; - mLinkAddresses = new ArrayList<LinkAddress>(); - mDnses = new ArrayList<InetAddress>(); - mRoutes = new ArrayList<RouteInfo>(); + mLinkAddresses.clear(); + mDnses.clear(); + mRoutes.clear(); mHttpProxy = null; } @@ -155,6 +170,63 @@ public class LinkProperties implements Parcelable { return ifaceName + linkAddresses + routes + dns + proxy; } + /** + * Compares this {@code LinkProperties} interface name against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + */ + public boolean isIdenticalInterfaceName(LinkProperties target) { + return TextUtils.equals(getInterfaceName(), target.getInterfaceName()); + } + + /** + * Compares this {@code LinkProperties} interface name against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + */ + public boolean isIdenticalAddresses(LinkProperties target) { + Collection<InetAddress> targetAddresses = target.getAddresses(); + Collection<InetAddress> sourceAddresses = getAddresses(); + return (sourceAddresses.size() == targetAddresses.size()) ? + sourceAddresses.containsAll(targetAddresses) : false; + } + + /** + * Compares this {@code LinkProperties} DNS addresses against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + */ + public boolean isIdenticalDnses(LinkProperties target) { + Collection<InetAddress> targetDnses = target.getDnses(); + return (mDnses.size() == targetDnses.size()) ? + mDnses.containsAll(targetDnses) : false; + } + + /** + * Compares this {@code LinkProperties} Routes against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + */ + public boolean isIdenticalRoutes(LinkProperties target) { + Collection<RouteInfo> targetRoutes = target.getRoutes(); + return (mRoutes.size() == targetRoutes.size()) ? + mRoutes.containsAll(targetRoutes) : false; + } + + /** + * Compares this {@code LinkProperties} HttpProxy against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + */ + public boolean isIdenticalHttpProxy(LinkProperties target) { + return getHttpProxy() == null ? target.getHttpProxy() == null : + getHttpProxy().equals(target.getHttpProxy()); + } @Override /** @@ -176,32 +248,107 @@ public class LinkProperties implements Parcelable { if (!(obj instanceof LinkProperties)) return false; - boolean sameAddresses; - boolean sameDnses; - boolean sameRoutes; - LinkProperties target = (LinkProperties) obj; - Collection<InetAddress> targetAddresses = target.getAddresses(); - Collection<InetAddress> sourceAddresses = getAddresses(); - sameAddresses = (sourceAddresses.size() == targetAddresses.size()) ? - sourceAddresses.containsAll(targetAddresses) : false; + return isIdenticalInterfaceName(target) && + isIdenticalAddresses(target) && + isIdenticalDnses(target) && + isIdenticalRoutes(target) && + isIdenticalHttpProxy(target); + } - Collection<InetAddress> targetDnses = target.getDnses(); - sameDnses = (mDnses.size() == targetDnses.size()) ? - mDnses.containsAll(targetDnses) : false; + /** + * Return two lists, a list of addresses that would be removed from + * mLinkAddresses and a list of addresses that would be added to + * mLinkAddress which would then result in target and mLinkAddresses + * being the same list. + * + * @param target is a LinkProperties with the new list of addresses + * @return the removed and added lists. + */ + public CompareResult<LinkAddress> compareAddresses(LinkProperties target) { + /* + * Duplicate the LinkAddresses into removed, we will be removing + * address which are common between mLinkAddresses and target + * leaving the addresses that are different. And address which + * are in target but not in mLinkAddresses are placed in the + * addedAddresses. + */ + CompareResult<LinkAddress> result = new CompareResult<LinkAddress>(); + result.removed = new ArrayList<LinkAddress>(mLinkAddresses); + result.added.clear(); + if (target != null) { + for (LinkAddress newAddress : target.getLinkAddresses()) { + if (! result.removed.remove(newAddress)) { + result.added.add(newAddress); + } + } + } + return result; + } - Collection<RouteInfo> targetRoutes = target.getRoutes(); - sameRoutes = (mRoutes.size() == targetRoutes.size()) ? - mRoutes.containsAll(targetRoutes) : false; - - return - sameAddresses && sameDnses && sameRoutes - && TextUtils.equals(getInterfaceName(), target.getInterfaceName()) - && (getHttpProxy() == null ? target.getHttpProxy() == null : - getHttpProxy().equals(target.getHttpProxy())); + /** + * Return two lists, a list of dns addresses that would be removed from + * mDnses and a list of addresses that would be added to + * mDnses which would then result in target and mDnses + * being the same list. + * + * @param target is a LinkProperties with the new list of dns addresses + * @return the removed and added lists. + */ + public CompareResult<InetAddress> compareDnses(LinkProperties target) { + /* + * Duplicate the InetAddresses into removed, we will be removing + * dns address which are common between mDnses and target + * leaving the addresses that are different. And dns address which + * are in target but not in mDnses are placed in the + * addedAddresses. + */ + CompareResult<InetAddress> result = new CompareResult<InetAddress>(); + + result.removed = new ArrayList<InetAddress>(mDnses); + result.added.clear(); + if (target != null) { + for (InetAddress newAddress : target.getDnses()) { + if (! result.removed.remove(newAddress)) { + result.added.add(newAddress); + } + } + } + return result; + } + + /** + * Return two lists, a list of routes that would be removed from + * mRoutes and a list of routes that would be added to + * mRoutes which would then result in target and mRoutes + * being the same list. + * + * @param target is a LinkProperties with the new list of routes + * @return the removed and added lists. + */ + public CompareResult<RouteInfo> compareRoutes(LinkProperties target) { + /* + * Duplicate the RouteInfos into removed, we will be removing + * routes which are common between mDnses and target + * leaving the routes that are different. And route address which + * are in target but not in mRoutes are placed in added. + */ + CompareResult<RouteInfo> result = new CompareResult<RouteInfo>(); + + result.removed = new ArrayList<RouteInfo>(mRoutes); + result.added.clear(); + if (target != null) { + for (RouteInfo r : target.getRoutes()) { + if (! result.removed.remove(r)) { + result.added.add(r); + } + } + } + return result; } + @Override /** * generate hashcode based on significant fields diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java index ccef122..aa6400b 100644 --- a/core/java/android/net/NetworkIdentity.java +++ b/core/java/android/net/NetworkIdentity.java @@ -19,6 +19,7 @@ package android.net; import static android.net.ConnectivityManager.isNetworkTypeMobile; import android.content.Context; +import android.os.Build; import android.telephony.TelephonyManager; import com.android.internal.util.Objects; @@ -68,7 +69,7 @@ public class NetworkIdentity { subTypeName = Integer.toString(mSubType); } - final String scrubSubscriberId = mSubscriberId != null ? "valid" : "null"; + final String scrubSubscriberId = scrubSubscriberId(mSubscriberId); final String roaming = mRoaming ? ", ROAMING" : ""; return "[type=" + typeName + ", subType=" + subTypeName + ", subscriberId=" + scrubSubscriberId + roaming + "]"; @@ -91,6 +92,17 @@ public class NetworkIdentity { } /** + * Scrub given IMSI on production builds. + */ + public static String scrubSubscriberId(String subscriberId) { + if ("eng".equals(Build.TYPE)) { + return subscriberId; + } else { + return subscriberId != null ? "valid" : "null"; + } + } + + /** * Build a {@link NetworkIdentity} from the given {@link NetworkState}, * assuming that any mobile networks are using the current IMSI. */ diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 9d40c42..fbff7d8 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -47,13 +47,39 @@ public class NetworkStats implements Parcelable { * {@link SystemClock#elapsedRealtime()} timestamp when this data was * generated. */ - public final long elapsedRealtime; - public int size; - public String[] iface; - public int[] uid; - public int[] tag; - public long[] rx; - public long[] tx; + private final long elapsedRealtime; + private int size; + private String[] iface; + private int[] uid; + private int[] tag; + private long[] rxBytes; + private long[] rxPackets; + private long[] txBytes; + private long[] txPackets; + + public static class Entry { + public String iface; + public int uid; + public int tag; + public long rxBytes; + public long rxPackets; + public long txBytes; + public long txPackets; + + public Entry() { + } + + public Entry(String iface, int uid, int tag, long rxBytes, long rxPackets, long txBytes, + long txPackets) { + this.iface = iface; + this.uid = uid; + this.tag = tag; + this.rxBytes = rxBytes; + this.rxPackets = rxPackets; + this.txBytes = txBytes; + this.txPackets = txPackets; + } + } public NetworkStats(long elapsedRealtime, int initialSize) { this.elapsedRealtime = elapsedRealtime; @@ -61,8 +87,10 @@ public class NetworkStats implements Parcelable { this.iface = new String[initialSize]; this.uid = new int[initialSize]; this.tag = new int[initialSize]; - this.rx = new long[initialSize]; - this.tx = new long[initialSize]; + this.rxBytes = new long[initialSize]; + this.rxPackets = new long[initialSize]; + this.txBytes = new long[initialSize]; + this.txPackets = new long[initialSize]; } public NetworkStats(Parcel parcel) { @@ -71,46 +99,93 @@ public class NetworkStats implements Parcelable { iface = parcel.createStringArray(); uid = parcel.createIntArray(); tag = parcel.createIntArray(); - rx = parcel.createLongArray(); - tx = parcel.createLongArray(); + rxBytes = parcel.createLongArray(); + rxPackets = parcel.createLongArray(); + txBytes = parcel.createLongArray(); + txPackets = parcel.createLongArray(); + } + + public NetworkStats addValues(String iface, int uid, int tag, long rxBytes, long rxPackets, + long txBytes, long txPackets) { + return addValues(new Entry(iface, uid, tag, rxBytes, rxPackets, txBytes, txPackets)); } /** - * Add new stats entry with given values. + * Add new stats entry, copying from given {@link Entry}. The {@link Entry} + * object can be recycled across multiple calls. */ - public NetworkStats addEntry(String iface, int uid, int tag, long rx, long tx) { + public NetworkStats addValues(Entry entry) { if (size >= this.iface.length) { - final int newLength = Math.max(this.iface.length, 10) * 3 / 2; - this.iface = Arrays.copyOf(this.iface, newLength); - this.uid = Arrays.copyOf(this.uid, newLength); - this.tag = Arrays.copyOf(this.tag, newLength); - this.rx = Arrays.copyOf(this.rx, newLength); - this.tx = Arrays.copyOf(this.tx, newLength); + final int newLength = Math.max(iface.length, 10) * 3 / 2; + iface = Arrays.copyOf(iface, newLength); + uid = Arrays.copyOf(uid, newLength); + tag = Arrays.copyOf(tag, newLength); + rxBytes = Arrays.copyOf(rxBytes, newLength); + rxPackets = Arrays.copyOf(rxPackets, newLength); + txBytes = Arrays.copyOf(txBytes, newLength); + txPackets = Arrays.copyOf(txPackets, newLength); } - this.iface[size] = iface; - this.uid[size] = uid; - this.tag[size] = tag; - this.rx[size] = rx; - this.tx[size] = tx; + iface[size] = entry.iface; + uid[size] = entry.uid; + tag[size] = entry.tag; + rxBytes[size] = entry.rxBytes; + rxPackets[size] = entry.rxPackets; + txBytes[size] = entry.txBytes; + txPackets[size] = entry.txPackets; size++; return this; } /** + * Return specific stats entry. + */ + public Entry getValues(int i, Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.iface = iface[i]; + entry.uid = uid[i]; + entry.tag = tag[i]; + entry.rxBytes = rxBytes[i]; + entry.rxPackets = rxPackets[i]; + entry.txBytes = txBytes[i]; + entry.txPackets = txPackets[i]; + return entry; + } + + public long getElapsedRealtime() { + return elapsedRealtime; + } + + public int size() { + return size; + } + + // @VisibleForTesting + public int internalSize() { + return iface.length; + } + + public NetworkStats combineValues(String iface, int uid, int tag, long rxBytes, long rxPackets, + long txBytes, long txPackets) { + return combineValues(new Entry(iface, uid, tag, rxBytes, rxPackets, txBytes, txPackets)); + } + + /** * Combine given values with an existing row, or create a new row if * {@link #findIndex(String, int, int)} is unable to find match. Can also be * used to subtract values from existing rows. */ - public NetworkStats combineEntry(String iface, int uid, int tag, long rx, long tx) { - final int i = findIndex(iface, uid, tag); + public NetworkStats combineValues(Entry entry) { + final int i = findIndex(entry.iface, entry.uid, entry.tag); if (i == -1) { // only create new entry when positive contribution - addEntry(iface, uid, tag, rx, tx); + addValues(entry); } else { - this.rx[i] += rx; - this.tx[i] += tx; + rxBytes[i] += entry.rxBytes; + rxPackets[i] += entry.rxPackets; + txBytes[i] += entry.txBytes; + txPackets[i] += entry.txPackets; } return this; } @@ -199,30 +274,41 @@ public class NetworkStats implements Parcelable { } // result will have our rows, and elapsed time between snapshots + final Entry entry = new Entry(); final NetworkStats result = new NetworkStats(deltaRealtime, size); for (int i = 0; i < size; i++) { - final String iface = this.iface[i]; - final int uid = this.uid[i]; - final int tag = this.tag[i]; + entry.iface = iface[i]; + entry.uid = uid[i]; + entry.tag = tag[i]; // find remote row that matches, and subtract - final int j = value.findIndex(iface, uid, tag); + final int j = value.findIndex(entry.iface, entry.uid, entry.tag); if (j == -1) { // newly appearing row, return entire value - result.addEntry(iface, uid, tag, this.rx[i], this.tx[i]); + entry.rxBytes = rxBytes[i]; + entry.rxPackets = rxPackets[i]; + entry.txBytes = txBytes[i]; + entry.txPackets = txPackets[i]; } else { // existing row, subtract remote value - long rx = this.rx[i] - value.rx[j]; - long tx = this.tx[i] - value.tx[j]; - if (enforceMonotonic && (rx < 0 || tx < 0)) { + entry.rxBytes = rxBytes[i] - value.rxBytes[j]; + entry.rxPackets = rxPackets[i] - value.rxPackets[j]; + entry.txBytes = txBytes[i] - value.txBytes[j]; + entry.txPackets = txPackets[i] - value.txPackets[j]; + if (enforceMonotonic + && (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 + || entry.txPackets < 0)) { throw new IllegalArgumentException("found non-monotonic values"); } if (clampNegative) { - rx = Math.max(0, rx); - tx = Math.max(0, tx); + entry.rxBytes = Math.max(0, entry.rxBytes); + entry.rxPackets = Math.max(0, entry.rxPackets); + entry.txBytes = Math.max(0, entry.txBytes); + entry.txPackets = Math.max(0, entry.txPackets); } - result.addEntry(iface, uid, tag, rx, tx); } + + result.addValues(entry); } return result; @@ -235,13 +321,15 @@ public class NetworkStats implements Parcelable { public void dump(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); - for (int i = 0; i < iface.length; i++) { + for (int i = 0; i < size; i++) { pw.print(prefix); pw.print(" iface="); pw.print(iface[i]); pw.print(" uid="); pw.print(uid[i]); pw.print(" tag="); pw.print(tag[i]); - pw.print(" rx="); pw.print(rx[i]); - pw.print(" tx="); pw.println(tx[i]); + pw.print(" rxBytes="); pw.print(rxBytes[i]); + pw.print(" rxPackets="); pw.print(rxPackets[i]); + pw.print(" txBytes="); pw.print(txBytes[i]); + pw.print(" txPackets="); pw.println(txPackets[i]); } } @@ -264,8 +352,10 @@ public class NetworkStats implements Parcelable { dest.writeStringArray(iface); dest.writeIntArray(uid); dest.writeIntArray(tag); - dest.writeLongArray(rx); - dest.writeLongArray(tx); + dest.writeLongArray(rxBytes); + dest.writeLongArray(rxPackets); + dest.writeLongArray(txBytes); + dest.writeLongArray(txPackets); } public static final Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() { diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java index dd2945c..8bd1738 100644 --- a/core/java/android/net/NetworkStatsHistory.java +++ b/core/java/android/net/NetworkStatsHistory.java @@ -43,13 +43,20 @@ public class NetworkStatsHistory implements Parcelable { private static final int VERSION_INIT = 1; // TODO: teach about varint encoding to use less disk space - - public final long bucketDuration; - - public int bucketCount; - public long[] bucketStart; - public long[] rx; - public long[] tx; + // TODO: extend to record rxPackets/txPackets + + private final long bucketDuration; + private int bucketCount; + private long[] bucketStart; + private long[] rxBytes; + private long[] txBytes; + + public static class Entry { + public long bucketStart; + public long bucketDuration; + public long rxBytes; + public long txBytes; + } public NetworkStatsHistory(long bucketDuration) { this(bucketDuration, 10); @@ -58,16 +65,16 @@ public class NetworkStatsHistory implements Parcelable { public NetworkStatsHistory(long bucketDuration, int initialSize) { this.bucketDuration = bucketDuration; bucketStart = new long[initialSize]; - rx = new long[initialSize]; - tx = new long[initialSize]; + rxBytes = new long[initialSize]; + txBytes = new long[initialSize]; bucketCount = 0; } public NetworkStatsHistory(Parcel in) { bucketDuration = in.readLong(); bucketStart = readLongArray(in); - rx = in.createLongArray(); - tx = in.createLongArray(); + rxBytes = in.createLongArray(); + txBytes = in.createLongArray(); bucketCount = bucketStart.length; } @@ -75,8 +82,8 @@ public class NetworkStatsHistory implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeLong(bucketDuration); writeLongArray(out, bucketStart, bucketCount); - writeLongArray(out, rx, bucketCount); - writeLongArray(out, tx, bucketCount); + writeLongArray(out, rxBytes, bucketCount); + writeLongArray(out, txBytes, bucketCount); } public NetworkStatsHistory(DataInputStream in) throws IOException { @@ -85,8 +92,8 @@ public class NetworkStatsHistory implements Parcelable { case VERSION_INIT: { bucketDuration = in.readLong(); bucketStart = readLongArray(in); - rx = readLongArray(in); - tx = readLongArray(in); + rxBytes = readLongArray(in); + txBytes = readLongArray(in); bucketCount = bucketStart.length; break; } @@ -100,8 +107,8 @@ public class NetworkStatsHistory implements Parcelable { out.writeInt(VERSION_INIT); out.writeLong(bucketDuration); writeLongArray(out, bucketStart, bucketCount); - writeLongArray(out, rx, bucketCount); - writeLongArray(out, tx, bucketCount); + writeLongArray(out, rxBytes, bucketCount); + writeLongArray(out, txBytes, bucketCount); } /** {@inheritDoc} */ @@ -109,6 +116,42 @@ public class NetworkStatsHistory implements Parcelable { return 0; } + public int size() { + return bucketCount; + } + + public long getBucketDuration() { + return bucketDuration; + } + + public long getStart() { + if (bucketCount > 0) { + return bucketStart[0]; + } else { + return Long.MAX_VALUE; + } + } + + public long getEnd() { + if (bucketCount > 0) { + return bucketStart[bucketCount - 1] + bucketDuration; + } else { + return Long.MIN_VALUE; + } + } + + /** + * Return specific stats entry. + */ + public Entry getValues(int i, Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.bucketStart = bucketStart[i]; + entry.bucketDuration = bucketDuration; + entry.rxBytes = rxBytes[i]; + entry.txBytes = txBytes[i]; + return entry; + } + /** * Record that data traffic occurred in the given time range. Will * distribute across internal buckets, creating new buckets as needed. @@ -135,8 +178,8 @@ public class NetworkStatsHistory implements Parcelable { final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); if (overlap > 0) { - this.rx[i] += rx * overlap / duration; - this.tx[i] += tx * overlap / duration; + this.rxBytes[i] += rx * overlap / duration; + this.txBytes[i] += tx * overlap / duration; } } } @@ -149,7 +192,7 @@ public class NetworkStatsHistory implements Parcelable { for (int i = 0; i < input.bucketCount; i++) { final long start = input.bucketStart[i]; final long end = start + input.bucketDuration; - recordData(start, end, input.rx[i], input.tx[i]); + recordData(start, end, input.rxBytes[i], input.txBytes[i]); } } @@ -179,8 +222,8 @@ public class NetworkStatsHistory implements Parcelable { if (bucketCount >= bucketStart.length) { final int newLength = Math.max(bucketStart.length, 10) * 3 / 2; bucketStart = Arrays.copyOf(bucketStart, newLength); - rx = Arrays.copyOf(rx, newLength); - tx = Arrays.copyOf(tx, newLength); + rxBytes = Arrays.copyOf(rxBytes, newLength); + txBytes = Arrays.copyOf(txBytes, newLength); } // create gap when inserting bucket in middle @@ -189,13 +232,13 @@ public class NetworkStatsHistory implements Parcelable { final int length = bucketCount - index; System.arraycopy(bucketStart, index, bucketStart, dstPos, length); - System.arraycopy(rx, index, rx, dstPos, length); - System.arraycopy(tx, index, tx, dstPos, length); + System.arraycopy(rxBytes, index, rxBytes, dstPos, length); + System.arraycopy(txBytes, index, txBytes, dstPos, length); } bucketStart[index] = start; - rx[index] = 0; - tx[index] = 0; + rxBytes[index] = 0; + txBytes[index] = 0; bucketCount++; } @@ -216,8 +259,8 @@ public class NetworkStatsHistory implements Parcelable { if (i > 0) { final int length = bucketStart.length; bucketStart = Arrays.copyOfRange(bucketStart, i, length); - rx = Arrays.copyOfRange(rx, i, length); - tx = Arrays.copyOfRange(tx, i, length); + rxBytes = Arrays.copyOfRange(rxBytes, i, length); + txBytes = Arrays.copyOfRange(txBytes, i, length); bucketCount -= i; } } @@ -226,9 +269,20 @@ public class NetworkStatsHistory implements Parcelable { * Return interpolated data usage across the requested range. Interpolates * across buckets, so values may be rounded slightly. */ - public long[] getTotalData(long start, long end, long[] outTotal) { - long rx = 0; - long tx = 0; + public Entry getValues(long start, long end, Entry recycle) { + return getValues(start, end, Long.MAX_VALUE, recycle); + } + + /** + * Return interpolated data usage across the requested range. Interpolates + * across buckets, so values may be rounded slightly. + */ + public Entry getValues(long start, long end, long now, Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.bucketStart = start; + entry.bucketDuration = end - start; + entry.rxBytes = 0; + entry.txBytes = 0; for (int i = bucketCount - 1; i >= 0; i--) { final long curStart = bucketStart[i]; @@ -239,19 +293,19 @@ public class NetworkStatsHistory implements Parcelable { // bucket is newer than record; keep looking if (curStart > end) continue; + // include full value for active buckets, otherwise only fractional + final boolean activeBucket = curStart < now && curEnd > now; final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); - if (overlap > 0) { - rx += this.rx[i] * overlap / bucketDuration; - tx += this.tx[i] * overlap / bucketDuration; + if (activeBucket || overlap == bucketDuration) { + entry.rxBytes += rxBytes[i]; + entry.txBytes += txBytes[i]; + } else if (overlap > 0) { + entry.rxBytes += rxBytes[i] * overlap / bucketDuration; + entry.txBytes += txBytes[i] * overlap / bucketDuration; } } - if (outTotal == null || outTotal.length != 2) { - outTotal = new long[2]; - } - outTotal[0] = rx; - outTotal[1] = tx; - return outTotal; + return entry; } /** @@ -292,8 +346,8 @@ public class NetworkStatsHistory implements Parcelable { for (int i = start; i < bucketCount; i++) { pw.print(prefix); pw.print(" bucketStart="); pw.print(bucketStart[i]); - pw.print(" rx="); pw.print(rx[i]); - pw.print(" tx="); pw.println(tx[i]); + pw.print(" rxBytes="); pw.print(rxBytes[i]); + pw.print(" txBytes="); pw.println(txBytes[i]); } } diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 9381f1d..cd49023 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -16,9 +16,11 @@ package android.net; +import static android.net.ConnectivityManager.TYPE_ETHERNET; import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.ConnectivityManager.TYPE_WIMAX; import static android.net.ConnectivityManager.isNetworkTypeMobile; +import static android.net.NetworkIdentity.scrubSubscriberId; import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G; import static android.telephony.TelephonyManager.NETWORK_CLASS_3_G; import static android.telephony.TelephonyManager.NETWORK_CLASS_4_G; @@ -38,41 +40,69 @@ import com.android.internal.util.Objects; */ public class NetworkTemplate implements Parcelable { + /** {@hide} */ + public static final int MATCH_MOBILE_ALL = 1; + /** {@hide} */ + public static final int MATCH_MOBILE_3G_LOWER = 2; + /** {@hide} */ + public static final int MATCH_MOBILE_4G = 3; + /** {@hide} */ + public static final int MATCH_WIFI = 4; + /** {@hide} */ + public static final int MATCH_ETHERNET = 5; + /** * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style * networks together. Only uses statistics for requested IMSI. */ - public static final int MATCH_MOBILE_ALL = 1; + public static NetworkTemplate buildTemplateMobileAll(String subscriberId) { + return new NetworkTemplate(MATCH_MOBILE_ALL, subscriberId); + } /** * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style * networks together that roughly meet a "3G" definition, or lower. Only * uses statistics for requested IMSI. */ - public static final int MATCH_MOBILE_3G_LOWER = 2; + public static NetworkTemplate buildTemplateMobile3gLower(String subscriberId) { + return new NetworkTemplate(MATCH_MOBILE_3G_LOWER, subscriberId); + } /** * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style * networks together that meet a "4G" definition. Only uses statistics for * requested IMSI. */ - public static final int MATCH_MOBILE_4G = 3; + public static NetworkTemplate buildTemplateMobile4g(String subscriberId) { + return new NetworkTemplate(MATCH_MOBILE_4G, subscriberId); + } /** * Template to combine all {@link ConnectivityManager#TYPE_WIFI} style * networks together. */ - public static final int MATCH_WIFI = 4; + public static NetworkTemplate buildTemplateWifi() { + return new NetworkTemplate(MATCH_WIFI, null); + } - final int mMatchRule; - final String mSubscriberId; + /** + * Template to combine all {@link ConnectivityManager#TYPE_ETHERNET} style + * networks together. + */ + public static NetworkTemplate buildTemplateEthernet() { + return new NetworkTemplate(MATCH_ETHERNET, null); + } + + private final int mMatchRule; + private final String mSubscriberId; + /** {@hide} */ public NetworkTemplate(int matchRule, String subscriberId) { this.mMatchRule = matchRule; this.mSubscriberId = subscriberId; } - public NetworkTemplate(Parcel in) { + private NetworkTemplate(Parcel in) { mMatchRule = in.readInt(); mSubscriberId = in.readString(); } @@ -90,7 +120,7 @@ public class NetworkTemplate implements Parcelable { @Override public String toString() { - final String scrubSubscriberId = mSubscriberId != null ? "valid" : "null"; + final String scrubSubscriberId = scrubSubscriberId(mSubscriberId); return "NetworkTemplate: matchRule=" + getMatchRuleName(mMatchRule) + ", subscriberId=" + scrubSubscriberId; } @@ -110,16 +140,18 @@ public class NetworkTemplate implements Parcelable { return false; } + /** {@hide} */ public int getMatchRule() { return mMatchRule; } + /** {@hide} */ public String getSubscriberId() { return mSubscriberId; } /** - * Test if this network matches the given template and IMEI. + * Test if this network matches the given template and IMSI. */ public boolean matches(NetworkIdentity ident) { switch (mMatchRule) { @@ -131,13 +163,15 @@ public class NetworkTemplate implements Parcelable { return matchesMobile4g(ident); case MATCH_WIFI: return matchesWifi(ident); + case MATCH_ETHERNET: + return matchesEthernet(ident); default: throw new IllegalArgumentException("unknown network template"); } } /** - * Check if mobile network with matching IMEI. Also matches + * Check if mobile network with matching IMSI. Also matches * {@link #TYPE_WIMAX}. */ private boolean matchesMobile(NetworkIdentity ident) { @@ -150,7 +184,7 @@ public class NetworkTemplate implements Parcelable { } /** - * Check if mobile network classified 3G or lower with matching IMEI. + * Check if mobile network classified 3G or lower with matching IMSI. */ private boolean matchesMobile3gLower(NetworkIdentity ident) { if (isNetworkTypeMobile(ident.mType) && Objects.equal(mSubscriberId, ident.mSubscriberId)) { @@ -165,7 +199,7 @@ public class NetworkTemplate implements Parcelable { } /** - * Check if mobile network classified 4G with matching IMEI. Also matches + * Check if mobile network classified 4G with matching IMSI. Also matches * {@link #TYPE_WIMAX}. */ private boolean matchesMobile4g(NetworkIdentity ident) { @@ -190,7 +224,17 @@ public class NetworkTemplate implements Parcelable { return false; } - public static String getMatchRuleName(int matchRule) { + /** + * Check if matches Ethernet network template. + */ + private boolean matchesEthernet(NetworkIdentity ident) { + if (ident.mType == TYPE_ETHERNET) { + return true; + } + return false; + } + + private static String getMatchRuleName(int matchRule) { switch (matchRule) { case MATCH_MOBILE_3G_LOWER: return "MOBILE_3G_LOWER"; @@ -200,6 +244,8 @@ public class NetworkTemplate implements Parcelable { return "MOBILE_ALL"; case MATCH_WIFI: return "WIFI"; + case MATCH_ETHERNET: + return "ETHERNET"; default: return "UNKNOWN"; } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 8a678d6..76534ef 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -38,8 +38,22 @@ public class NetworkUtils { /** Bring the named network interface down. */ public native static int disableInterface(String interfaceName); - /** Reset any sockets that are connected via the named interface. */ - public native static int resetConnections(String interfaceName); + /** Setting bit 0 indicates reseting of IPv4 addresses required */ + public static final int RESET_IPV4_ADDRESSES = 0x01; + + /** Setting bit 1 indicates reseting of IPv4 addresses required */ + public static final int RESET_IPV6_ADDRESSES = 0x02; + + /** Reset all addresses */ + public static final int RESET_ALL_ADDRESSES = RESET_IPV4_ADDRESSES | RESET_IPV6_ADDRESSES; + + /** + * Reset IPv6 or IPv4 sockets that are connected via the named interface. + * + * @param interfaceName is the interface to reset + * @param mask {@see #RESET_IPV4_ADDRESSES} and {@see #RESET_IPV6_ADDRESSES} + */ + public native static int resetConnections(String interfaceName, int mask); /** * Start the DHCP client daemon, in order to have it request addresses diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java index 8e5ddda..275f32a 100644 --- a/core/java/android/net/RouteInfo.java +++ b/core/java/android/net/RouteInfo.java @@ -43,6 +43,7 @@ public class RouteInfo implements Parcelable { private final InetAddress mGateway; private final boolean mIsDefault; + private final boolean mIsHost; public RouteInfo(LinkAddress destination, InetAddress gateway) { if (destination == null) { @@ -68,6 +69,7 @@ public class RouteInfo implements Parcelable { destination.getNetworkPrefixLength()), destination.getNetworkPrefixLength()); mGateway = gateway; mIsDefault = isDefault(); + mIsHost = isHost(); } public RouteInfo(InetAddress gateway) { @@ -88,6 +90,10 @@ public class RouteInfo implements Parcelable { } } + private boolean isHost() { + return (mGateway.equals(Inet4Address.ANY) || mGateway.equals(Inet6Address.ANY)); + } + private boolean isDefault() { boolean val = false; if (mGateway != null) { @@ -100,6 +106,7 @@ public class RouteInfo implements Parcelable { return val; } + public LinkAddress getDestination() { return mDestination; } @@ -112,6 +119,10 @@ public class RouteInfo implements Parcelable { return mIsDefault; } + public boolean isHostRoute() { + return mIsHost; + } + public String toString() { String val = ""; if (mDestination != null) val = mDestination.toString(); diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index 641a576..c534e58 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -65,7 +65,7 @@ import android.util.Base64; import android.util.Log; /** - * Subclass of the Apache {@link DefaultHttpClient} that is configured with + * Implementation of the Apache {@link DefaultHttpClient} that is configured with * reasonable default settings and registered schemes for Android, and * also lets the user add {@link HttpRequestInterceptor} classes. * Don't create this directly, use the {@link #newInstance} factory method. diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index 0eb8cd8..b668f30 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -334,8 +334,6 @@ public final class NdefRecord implements Parcelable { /** * Creates an NDEF record of well known type URI. - * TODO: Make a public API - * @hide */ public static NdefRecord createUri(Uri uri) { return createUri(uri.toString()); @@ -343,8 +341,6 @@ public final class NdefRecord implements Parcelable { /** * Creates an NDEF record of well known type URI. - * TODO: Make a public API - * @hide */ public static NdefRecord createUri(String uriString) { byte prefix = 0x0; diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index ba69246..da2afb6 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -129,6 +129,11 @@ public final class Debug /** The shared dirty pages used by everything else. */ public int otherSharedDirty; + /** @hide */ + public static final int NUM_OTHER_STATS = 9; + + private int[] otherStats = new int[NUM_OTHER_STATS*3]; + public MemoryInfo() { } @@ -153,6 +158,38 @@ public final class Debug return dalvikSharedDirty + nativeSharedDirty + otherSharedDirty; } + /* @hide */ + public int getOtherPss(int which) { + return otherStats[which*3]; + } + + /* @hide */ + public int getOtherPrivateDirty(int which) { + return otherStats[which*3 + 1]; + } + + /* @hide */ + public int getOtherSharedDirty(int which) { + return otherStats[which*3 + 2]; + } + + + /* @hide */ + public static String getOtherLabel(int which) { + switch (which) { + case 0: return "Cursor"; + case 1: return "Ashmem"; + case 2: return "Other dev"; + case 3: return ".so mmap"; + case 4: return ".jar mmap"; + case 5: return ".apk mmap"; + case 6: return ".ttf mmap"; + case 7: return ".dex mmap"; + case 8: return "Other mmap"; + default: return "????"; + } + } + public int describeContents() { return 0; } @@ -167,6 +204,7 @@ public final class Debug dest.writeInt(otherPss); dest.writeInt(otherPrivateDirty); dest.writeInt(otherSharedDirty); + dest.writeIntArray(otherStats); } public void readFromParcel(Parcel source) { @@ -179,6 +217,7 @@ public final class Debug otherPss = source.readInt(); otherPrivateDirty = source.readInt(); otherSharedDirty = source.readInt(); + otherStats = source.createIntArray(); } public static final Creator<MemoryInfo> CREATOR = new Creator<MemoryInfo>() { diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index 165e438..cd39d5c 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -169,6 +169,21 @@ public class Handler { } /** + * Returns a string representing the name of the specified message. + * The default implementation will either return the class name of the + * message callback if any, or the hexadecimal representation of the + * message "what" field. + * + * @param message The message whose name is being queried + */ + public String getMessageName(Message message) { + if (message.callback != null) { + return message.callback.getClass().getName(); + } + return "0x" + Integer.toHexString(message.what); + } + + /** * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this). * If you don't want that facility, just call Message.obtain() instead. diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 3edd692..720e802b 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -52,7 +52,6 @@ import android.util.PrefixPrinter; */ public class Looper { private static final String TAG = "Looper"; - private static final boolean LOG_V = Log.isLoggable(TAG, Log.VERBOSE); // sThreadLocal.get() will return null unless you've called prepare(). private static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); @@ -70,7 +69,7 @@ public class Looper { * {@link #loop()} after calling this method, and end it by calling * {@link #quit()}. */ - public static final void prepare() { + public static void prepare() { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } @@ -83,12 +82,10 @@ public class Looper { * is created by the Android environment, so you should never need * to call this function yourself. See also: {@link #prepare()} */ - public static final void prepareMainLooper() { + public static void prepareMainLooper() { prepare(); setMainLooper(myLooper()); - if (Process.supportsProcesses()) { - myLooper().mQueue.mQuitAllowed = false; - } + myLooper().mQueue.mQuitAllowed = false; } private synchronized static void setMainLooper(Looper looper) { @@ -97,7 +94,7 @@ public class Looper { /** Returns the application's main looper, which lives in the main thread of the application. */ - public synchronized static final Looper getMainLooper() { + public synchronized static Looper getMainLooper() { return mMainLooper; } @@ -105,7 +102,7 @@ public class Looper { * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */ - public static final void loop() { + public static void loop() { Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); @@ -124,20 +121,36 @@ public class Looper { // No target is a magic identifier for the quit message. return; } - if (me.mLogging != null) me.mLogging.println( - ">>>>> Dispatching to " + msg.target + " " - + msg.callback + ": " + msg.what - ); + + long wallStart = 0; + long threadStart = 0; + + // This must be in a local variable, in case a UI event sets the logger + Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + wallStart = System.currentTimeMillis(); + threadStart = SystemClock.currentThreadTimeMillis(); + } + msg.target.dispatchMessage(msg); - if (me.mLogging != null) me.mLogging.println( - "<<<<< Finished to " + msg.target + " " - + msg.callback); - + + if (logging != null) { + long wallTime = System.currentTimeMillis() - wallStart; + long threadTime = SystemClock.currentThreadTimeMillis() - threadStart; + + logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); + if (logging instanceof Profiler) { + ((Profiler) logging).profile(msg, wallStart, wallTime, threadTime); + } + } + // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { - Log.wtf("Looper", "Thread identity changed from 0x" + Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " @@ -153,7 +166,7 @@ public class Looper { * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ - public static final Looper myLooper() { + public static Looper myLooper() { return sThreadLocal.get(); } @@ -175,7 +188,7 @@ public class Looper { * thread. This must be called from a thread running a Looper, or a * NullPointerException will be thrown. */ - public static final MessageQueue myQueue() { + public static MessageQueue myQueue() { return myLooper().mQueue; } @@ -227,23 +240,13 @@ public class Looper { } public String toString() { - return "Looper{" - + Integer.toHexString(System.identityHashCode(this)) - + "}"; + return "Looper{" + Integer.toHexString(System.identityHashCode(this)) + "}"; } - static class HandlerException extends Exception { - - HandlerException(Message message, Throwable cause) { - super(createMessage(cause), cause); - } - - static String createMessage(Throwable cause) { - String causeMsg = cause.getMessage(); - if (causeMsg == null) { - causeMsg = cause.toString(); - } - return causeMsg; - } + /** + * @hide + */ + public static interface Profiler { + void profile(Message message, long wallStart, long wallTime, long threadTime); } } diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 673b187..3362575 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -86,6 +86,12 @@ public class Process { public static final int WIFI_UID = 1010; /** + * Defines the UID/GID for the mediaserver process. + * @hide + */ + public static final int MEDIA_UID = 1013; + + /** * Defines the GID for the group that allows write access to the SD card. * @hide */ @@ -266,84 +272,29 @@ public class Process { * @param uid The user-id under which the process will run. * @param gid The group-id under which the process will run. * @param gids Additional group-ids associated with the process. - * @param enableDebugger True if debugging should be enabled for this process. + * @param debugFlags Additional flags. + * @param targetSdkVersion The target SDK version for the app. * @param zygoteArgs Additional arguments to supply to the zygote process. * - * @return int If > 0 the pid of the new process; if 0 the process is - * being emulated by a thread + * @return An object that describes the result of the attempt to start the process. * @throws RuntimeException on fatal start failure * * {@hide} */ - public static final int start(final String processClass, + public static final ProcessStartResult start(final String processClass, final String niceName, int uid, int gid, int[] gids, - int debugFlags, - String[] zygoteArgs) - { - if (supportsProcesses()) { - try { - return startViaZygote(processClass, niceName, uid, gid, gids, - debugFlags, zygoteArgs); - } catch (ZygoteStartFailedEx ex) { - Log.e(LOG_TAG, - "Starting VM process through Zygote failed"); - throw new RuntimeException( - "Starting VM process through Zygote failed", ex); - } - } else { - // Running in single-process mode - - Runnable runnable = new Runnable() { - public void run() { - Process.invokeStaticMain(processClass); - } - }; - - // Thread constructors must not be called with null names (see spec). - if (niceName != null) { - new Thread(runnable, niceName).start(); - } else { - new Thread(runnable).start(); - } - - return 0; - } - } - - /** - * Start a new process. Don't supply a custom nice name. - * {@hide} - */ - public static final int start(String processClass, int uid, int gid, - int[] gids, int debugFlags, String[] zygoteArgs) { - return start(processClass, "", uid, gid, gids, - debugFlags, zygoteArgs); - } - - private static void invokeStaticMain(String className) { - Class cl; - Object args[] = new Object[1]; - - args[0] = new String[0]; //this is argv - + int debugFlags, int targetSdkVersion, + String[] zygoteArgs) { try { - cl = Class.forName(className); - cl.getMethod("main", new Class[] { String[].class }) - .invoke(null, args); - } catch (Exception ex) { - // can be: ClassNotFoundException, - // NoSuchMethodException, SecurityException, - // IllegalAccessException, IllegalArgumentException - // InvocationTargetException - // or uncaught exception from main() - - Log.e(LOG_TAG, "Exception invoking static main on " - + className, ex); - - throw new RuntimeException(ex); + return startViaZygote(processClass, niceName, uid, gid, gids, + debugFlags, targetSdkVersion, zygoteArgs); + } catch (ZygoteStartFailedEx ex) { + Log.e(LOG_TAG, + "Starting VM process through Zygote failed"); + throw new RuntimeException( + "Starting VM process through Zygote failed", ex); } - } /** retry interval for opening a zygote socket */ @@ -430,14 +381,11 @@ public class Process { * and returns the child's pid. Please note: the present implementation * replaces newlines in the argument list with spaces. * @param args argument list - * @return PID of new child process + * @return An object that describes the result of the attempt to start the process. * @throws ZygoteStartFailedEx if process start failed for any reason */ - private static int zygoteSendArgsAndGetPid(ArrayList<String> args) + private static ProcessStartResult zygoteSendArgsAndGetResult(ArrayList<String> args) throws ZygoteStartFailedEx { - - int pid; - openZygoteSocketIfNeeded(); try { @@ -448,7 +396,8 @@ public class Process { * b) a number of newline-separated argument strings equal to count * * After the zygote process reads these it will write the pid of - * the child or -1 on failure. + * the child or -1 on failure, followed by boolean to + * indicate whether a wrapper process was used. */ sZygoteWriter.write(Integer.toString(args.size())); @@ -468,11 +417,13 @@ public class Process { sZygoteWriter.flush(); // Should there be a timeout on this? - pid = sZygoteInputStream.readInt(); - - if (pid < 0) { + ProcessStartResult result = new ProcessStartResult(); + result.pid = sZygoteInputStream.readInt(); + if (result.pid < 0) { throw new ZygoteStartFailedEx("fork() failed"); } + result.usingWrapper = sZygoteInputStream.readBoolean(); + return result; } catch (IOException ex) { try { if (sZygoteSocket != null) { @@ -487,8 +438,6 @@ public class Process { throw new ZygoteStartFailedEx(ex); } - - return pid; } /** @@ -500,20 +449,19 @@ public class Process { * @param gid a POSIX gid that the new process shuold setgid() to * @param gids null-ok; a list of supplementary group IDs that the * new process should setgroup() to. - * @param enableDebugger True if debugging should be enabled for this process. + * @param debugFlags Additional flags. + * @param targetSdkVersion The target SDK version for the app. * @param extraArgs Additional arguments to supply to the zygote process. - * @return PID + * @return An object that describes the result of the attempt to start the process. * @throws ZygoteStartFailedEx if process start failed for any reason */ - private static int startViaZygote(final String processClass, + private static ProcessStartResult startViaZygote(final String processClass, final String niceName, final int uid, final int gid, final int[] gids, - int debugFlags, + int debugFlags, int targetSdkVersion, String[] extraArgs) throws ZygoteStartFailedEx { - int pid; - synchronized(Process.class) { ArrayList<String> argsForZygote = new ArrayList<String>(); @@ -537,6 +485,7 @@ public class Process { if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) { argsForZygote.add("--enable-assert"); } + argsForZygote.add("--target-sdk-version=" + targetSdkVersion); //TODO optionally enable debuger //argsForZygote.add("--enable-debugger"); @@ -568,15 +517,9 @@ public class Process { argsForZygote.add(arg); } } - - pid = zygoteSendArgsAndGetPid(argsForZygote); - } - if (pid <= 0) { - throw new ZygoteStartFailedEx("zygote start failed:" + pid); + return zygoteSendArgsAndGetResult(argsForZygote); } - - return pid; } /** @@ -736,8 +679,13 @@ public class Process { * * @return Returns true if the system can run in multiple processes, else * false if everything is running in a single process. + * + * @deprecated This method always returns true. Do not use. */ - public static final native boolean supportsProcesses(); + @Deprecated + public static final boolean supportsProcesses() { + return true; + } /** * Set the out-of-memory badness adjustment for a process. @@ -855,4 +803,21 @@ public class Process { * @hide */ public static final native long getPss(int pid); + + /** + * Specifies the outcome of having started a process. + * @hide + */ + public static final class ProcessStartResult { + /** + * The PID of the newly started process. + * Always >= 0. (If the start failed, an exception will have been thrown instead.) + */ + public int pid; + + /** + * True if the process was started with a wrapper attached. + */ + public boolean usingWrapper; + } } diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index b721665..1af24f4 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -114,7 +114,7 @@ public final class ServiceManager { * @hide */ public static void initServiceCache(Map<String, IBinder> cache) { - if (sCache.size() != 0 && Process.supportsProcesses()) { + if (sCache.size() != 0) { throw new IllegalStateException("setServiceCache may only be called once"); } sCache.putAll(cache); diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 792e4c1..2c4b863 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -34,6 +34,8 @@ public class StorageVolume implements Parcelable { private final int mMtpReserveSpace; private final boolean mAllowMassStorage; private int mStorageId; + // maximum file size for the storage, or zero for no limit + private final long mMaxFileSize; // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING, // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED, @@ -41,18 +43,20 @@ public class StorageVolume implements Parcelable { public static final String EXTRA_STORAGE_VOLUME = "storage_volume"; public StorageVolume(String path, String description, boolean removable, - boolean emulated, int mtpReserveSpace, boolean allowMassStorage) { + boolean emulated, int mtpReserveSpace, boolean allowMassStorage, long maxFileSize) { mPath = path; mDescription = description; mRemovable = removable; mEmulated = emulated; mMtpReserveSpace = mtpReserveSpace; mAllowMassStorage = allowMassStorage; + mMaxFileSize = maxFileSize; } // for parcelling only private StorageVolume(String path, String description, boolean removable, - boolean emulated, int mtpReserveSpace, int storageId, boolean allowMassStorage) { + boolean emulated, int mtpReserveSpace, int storageId, + boolean allowMassStorage, long maxFileSize) { mPath = path; mDescription = description; mRemovable = removable; @@ -60,6 +64,7 @@ public class StorageVolume implements Parcelable { mMtpReserveSpace = mtpReserveSpace; mAllowMassStorage = allowMassStorage; mStorageId = storageId; + mMaxFileSize = maxFileSize; } /** @@ -142,6 +147,15 @@ public class StorageVolume implements Parcelable { return mAllowMassStorage; } + /** + * Returns maximum file size for the volume, or zero if it is unbounded. + * + * @return maximum file size + */ + public long getMaxFileSize() { + return mMaxFileSize; + } + @Override public boolean equals(Object obj) { if (obj instanceof StorageVolume && mPath != null) { @@ -158,7 +172,10 @@ public class StorageVolume implements Parcelable { @Override public String toString() { - return mPath; + return "StorageVolume [mAllowMassStorage=" + mAllowMassStorage + ", mDescription=" + + mDescription + ", mEmulated=" + mEmulated + ", mMaxFileSize=" + mMaxFileSize + + ", mMtpReserveSpace=" + mMtpReserveSpace + ", mPath=" + mPath + ", mRemovable=" + + mRemovable + ", mStorageId=" + mStorageId + "]"; } public static final Parcelable.Creator<StorageVolume> CREATOR = @@ -171,9 +188,10 @@ public class StorageVolume implements Parcelable { int storageId = in.readInt(); int mtpReserveSpace = in.readInt(); int allowMassStorage = in.readInt(); + long maxFileSize = in.readLong(); return new StorageVolume(path, description, - removable == 1, emulated == 1, - mtpReserveSpace, storageId, allowMassStorage == 1); + removable == 1, emulated == 1, mtpReserveSpace, + storageId, allowMassStorage == 1, maxFileSize); } public StorageVolume[] newArray(int size) { @@ -193,5 +211,6 @@ public class StorageVolume implements Parcelable { parcel.writeInt(mStorageId); parcel.writeInt(mMtpReserveSpace); parcel.writeInt(mAllowMassStorage ? 1 : 0); + parcel.writeLong(mMaxFileSize); } } diff --git a/core/java/android/pim/ContactsAsyncHelper.java b/core/java/android/pim/ContactsAsyncHelper.java index 7c78a81..21fc594 100644 --- a/core/java/android/pim/ContactsAsyncHelper.java +++ b/core/java/android/pim/ContactsAsyncHelper.java @@ -186,7 +186,7 @@ public class ContactsAsyncHelper extends Handler { InputStream inputStream = null; try { inputStream = Contacts.openContactPhotoInputStream( - args.context.getContentResolver(), args.uri); + args.context.getContentResolver(), args.uri, true); } catch (Exception e) { Log.e(LOG_TAG, "Error opening photo input stream", e); } diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java deleted file mode 100644 index 128b697..0000000 --- a/core/java/android/pim/EventRecurrence.java +++ /dev/null @@ -1,892 +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.pim; - -import android.text.TextUtils; -import android.text.format.Time; -import android.util.Log; -import android.util.TimeFormatException; - -import java.util.Calendar; -import java.util.HashMap; - -/** - * Event recurrence utility functions. - */ -public class EventRecurrence { - private static String TAG = "EventRecur"; - - public static final int SECONDLY = 1; - public static final int MINUTELY = 2; - public static final int HOURLY = 3; - public static final int DAILY = 4; - public static final int WEEKLY = 5; - public static final int MONTHLY = 6; - public static final int YEARLY = 7; - - public static final int SU = 0x00010000; - public static final int MO = 0x00020000; - public static final int TU = 0x00040000; - public static final int WE = 0x00080000; - public static final int TH = 0x00100000; - public static final int FR = 0x00200000; - public static final int SA = 0x00400000; - - public Time startDate; // set by setStartDate(), not parse() - - public int freq; // SECONDLY, MINUTELY, etc. - public String until; - public int count; - public int interval; - public int wkst; // SU, MO, TU, etc. - - /* lists with zero entries may be null references */ - public int[] bysecond; - public int bysecondCount; - public int[] byminute; - public int byminuteCount; - public int[] byhour; - public int byhourCount; - public int[] byday; - public int[] bydayNum; - public int bydayCount; - public int[] bymonthday; - public int bymonthdayCount; - public int[] byyearday; - public int byyeardayCount; - public int[] byweekno; - public int byweeknoCount; - public int[] bymonth; - public int bymonthCount; - public int[] bysetpos; - public int bysetposCount; - - /** maps a part string to a parser object */ - private static HashMap<String,PartParser> sParsePartMap; - static { - sParsePartMap = new HashMap<String,PartParser>(); - sParsePartMap.put("FREQ", new ParseFreq()); - sParsePartMap.put("UNTIL", new ParseUntil()); - sParsePartMap.put("COUNT", new ParseCount()); - sParsePartMap.put("INTERVAL", new ParseInterval()); - sParsePartMap.put("BYSECOND", new ParseBySecond()); - sParsePartMap.put("BYMINUTE", new ParseByMinute()); - sParsePartMap.put("BYHOUR", new ParseByHour()); - sParsePartMap.put("BYDAY", new ParseByDay()); - sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay()); - sParsePartMap.put("BYYEARDAY", new ParseByYearDay()); - sParsePartMap.put("BYWEEKNO", new ParseByWeekNo()); - sParsePartMap.put("BYMONTH", new ParseByMonth()); - sParsePartMap.put("BYSETPOS", new ParseBySetPos()); - sParsePartMap.put("WKST", new ParseWkst()); - } - - /* values for bit vector that keeps track of what we have already seen */ - private static final int PARSED_FREQ = 1 << 0; - private static final int PARSED_UNTIL = 1 << 1; - private static final int PARSED_COUNT = 1 << 2; - private static final int PARSED_INTERVAL = 1 << 3; - private static final int PARSED_BYSECOND = 1 << 4; - private static final int PARSED_BYMINUTE = 1 << 5; - private static final int PARSED_BYHOUR = 1 << 6; - private static final int PARSED_BYDAY = 1 << 7; - private static final int PARSED_BYMONTHDAY = 1 << 8; - private static final int PARSED_BYYEARDAY = 1 << 9; - private static final int PARSED_BYWEEKNO = 1 << 10; - private static final int PARSED_BYMONTH = 1 << 11; - private static final int PARSED_BYSETPOS = 1 << 12; - private static final int PARSED_WKST = 1 << 13; - - /** maps a FREQ value to an integer constant */ - private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>(); - static { - sParseFreqMap.put("SECONDLY", SECONDLY); - sParseFreqMap.put("MINUTELY", MINUTELY); - sParseFreqMap.put("HOURLY", HOURLY); - sParseFreqMap.put("DAILY", DAILY); - sParseFreqMap.put("WEEKLY", WEEKLY); - sParseFreqMap.put("MONTHLY", MONTHLY); - sParseFreqMap.put("YEARLY", YEARLY); - } - - /** maps a two-character weekday string to an integer constant */ - private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>(); - static { - sParseWeekdayMap.put("SU", SU); - sParseWeekdayMap.put("MO", MO); - sParseWeekdayMap.put("TU", TU); - sParseWeekdayMap.put("WE", WE); - sParseWeekdayMap.put("TH", TH); - sParseWeekdayMap.put("FR", FR); - sParseWeekdayMap.put("SA", SA); - } - - /** If set, allow lower-case recurrence rule strings. Minor performance impact. */ - private static final boolean ALLOW_LOWER_CASE = false; - - /** If set, validate the value of UNTIL parts. Minor performance impact. */ - private static final boolean VALIDATE_UNTIL = false; - - /** If set, require that only one of {UNTIL,COUNT} is present. Breaks compat w/ old parser. */ - private static final boolean ONLY_ONE_UNTIL_COUNT = false; - - - /** - * Thrown when a recurrence string provided can not be parsed according - * to RFC2445. - */ - public static class InvalidFormatException extends RuntimeException { - InvalidFormatException(String s) { - super(s); - } - } - - - public void setStartDate(Time date) { - startDate = date; - } - - /** - * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc. - * constants. btw, I think we should switch to those here too, to - * get rid of this function, if possible. - */ - public static int calendarDay2Day(int day) - { - switch (day) - { - case Calendar.SUNDAY: - return SU; - case Calendar.MONDAY: - return MO; - case Calendar.TUESDAY: - return TU; - case Calendar.WEDNESDAY: - return WE; - case Calendar.THURSDAY: - return TH; - case Calendar.FRIDAY: - return FR; - case Calendar.SATURDAY: - return SA; - default: - throw new RuntimeException("bad day of week: " + day); - } - } - - public static int timeDay2Day(int day) - { - switch (day) - { - case Time.SUNDAY: - return SU; - case Time.MONDAY: - return MO; - case Time.TUESDAY: - return TU; - case Time.WEDNESDAY: - return WE; - case Time.THURSDAY: - return TH; - case Time.FRIDAY: - return FR; - case Time.SATURDAY: - return SA; - default: - throw new RuntimeException("bad day of week: " + day); - } - } - public static int day2TimeDay(int day) - { - switch (day) - { - case SU: - return Time.SUNDAY; - case MO: - return Time.MONDAY; - case TU: - return Time.TUESDAY; - case WE: - return Time.WEDNESDAY; - case TH: - return Time.THURSDAY; - case FR: - return Time.FRIDAY; - case SA: - return Time.SATURDAY; - default: - throw new RuntimeException("bad day of week: " + day); - } - } - - /** - * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY - * constants. btw, I think we should switch to those here too, to - * get rid of this function, if possible. - */ - public static int day2CalendarDay(int day) - { - switch (day) - { - case SU: - return Calendar.SUNDAY; - case MO: - return Calendar.MONDAY; - case TU: - return Calendar.TUESDAY; - case WE: - return Calendar.WEDNESDAY; - case TH: - return Calendar.THURSDAY; - case FR: - return Calendar.FRIDAY; - case SA: - return Calendar.SATURDAY; - default: - throw new RuntimeException("bad day of week: " + day); - } - } - - /** - * Converts one of the internal day constants (SU, MO, etc.) to the - * two-letter string representing that constant. - * - * @param day one the internal constants SU, MO, etc. - * @return the two-letter string for the day ("SU", "MO", etc.) - * - * @throws IllegalArgumentException Thrown if the day argument is not one of - * the defined day constants. - */ - private static String day2String(int day) { - switch (day) { - case SU: - return "SU"; - case MO: - return "MO"; - case TU: - return "TU"; - case WE: - return "WE"; - case TH: - return "TH"; - case FR: - return "FR"; - case SA: - return "SA"; - default: - throw new IllegalArgumentException("bad day argument: " + day); - } - } - - private static void appendNumbers(StringBuilder s, String label, - int count, int[] values) - { - if (count > 0) { - s.append(label); - count--; - for (int i=0; i<count; i++) { - s.append(values[i]); - s.append(","); - } - s.append(values[count]); - } - } - - private void appendByDay(StringBuilder s, int i) - { - int n = this.bydayNum[i]; - if (n != 0) { - s.append(n); - } - - String str = day2String(this.byday[i]); - s.append(str); - } - - @Override - public String toString() - { - StringBuilder s = new StringBuilder(); - - s.append("FREQ="); - switch (this.freq) - { - case SECONDLY: - s.append("SECONDLY"); - break; - case MINUTELY: - s.append("MINUTELY"); - break; - case HOURLY: - s.append("HOURLY"); - break; - case DAILY: - s.append("DAILY"); - break; - case WEEKLY: - s.append("WEEKLY"); - break; - case MONTHLY: - s.append("MONTHLY"); - break; - case YEARLY: - s.append("YEARLY"); - break; - } - - if (!TextUtils.isEmpty(this.until)) { - s.append(";UNTIL="); - s.append(until); - } - - if (this.count != 0) { - s.append(";COUNT="); - s.append(this.count); - } - - if (this.interval != 0) { - s.append(";INTERVAL="); - s.append(this.interval); - } - - if (this.wkst != 0) { - s.append(";WKST="); - s.append(day2String(this.wkst)); - } - - appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond); - appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute); - appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour); - - // day - int count = this.bydayCount; - if (count > 0) { - s.append(";BYDAY="); - count--; - for (int i=0; i<count; i++) { - appendByDay(s, i); - s.append(","); - } - appendByDay(s, count); - } - - appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday); - appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday); - appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno); - appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth); - appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos); - - return s.toString(); - } - - public boolean repeatsOnEveryWeekDay() { - if (this.freq != WEEKLY) { - return false; - } - - int count = this.bydayCount; - if (count != 5) { - return false; - } - - for (int i = 0 ; i < count ; i++) { - int day = byday[i]; - if (day == SU || day == SA) { - return false; - } - } - - return true; - } - - /** - * Determines whether this rule specifies a simple monthly rule by weekday, such as - * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month). - * <p> - * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month), - * will cause "false" to be returned. - * <p> - * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every - * month) will cause "false" to be returned. (Note these are usually expressed as - * WEEKLY rules, and hence are uncommon.) - * - * @return true if this rule is of the appropriate form - */ - public boolean repeatsMonthlyOnDayCount() { - if (this.freq != MONTHLY) { - return false; - } - - if (bydayCount != 1 || bymonthdayCount != 0) { - return false; - } - - if (bydayNum[0] <= 0) { - return false; - } - - return true; - } - - /** - * Determines whether two integer arrays contain identical elements. - * <p> - * The native implementation over-allocated the arrays (and may have stuff left over from - * a previous run), so we can't just check the arrays -- the separately-maintained count - * field also matters. We assume that a null array will have a count of zero, and that the - * array can hold as many elements as the associated count indicates. - * <p> - * TODO: replace this with Arrays.equals() when the old parser goes away. - */ - private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) { - if (count1 != count2) { - return false; - } - - for (int i = 0; i < count1; i++) { - if (array1[i] != array2[i]) - return false; - } - - return true; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof EventRecurrence)) { - return false; - } - - EventRecurrence er = (EventRecurrence) obj; - return (startDate == null ? - er.startDate == null : Time.compare(startDate, er.startDate) == 0) && - freq == er.freq && - (until == null ? er.until == null : until.equals(er.until)) && - count == er.count && - interval == er.interval && - wkst == er.wkst && - arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) && - arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) && - arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) && - arraysEqual(byday, bydayCount, er.byday, er.bydayCount) && - arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) && - arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) && - arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) && - arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) && - arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) && - arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount); - } - - @Override public int hashCode() { - // We overrode equals, so we must override hashCode(). Nobody seems to need this though. - throw new UnsupportedOperationException(); - } - - /** - * Resets parser-modified fields to their initial state. Does not alter startDate. - * <p> - * The original parser always set all of the "count" fields, "wkst", and "until", - * essentially allowing the same object to be used multiple times by calling parse(). - * It's unclear whether this behavior was intentional. For now, be paranoid and - * preserve the existing behavior by resetting the fields. - * <p> - * We don't need to touch the integer arrays; they will either be ignored or - * overwritten. The "startDate" field is not set by the parser, so we ignore it here. - */ - private void resetFields() { - until = null; - freq = count = interval = bysecondCount = byminuteCount = byhourCount = - bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount = - bysetposCount = 0; - } - - /** - * Parses an rfc2445 recurrence rule string into its component pieces. Attempting to parse - * malformed input will result in an EventRecurrence.InvalidFormatException. - * - * @param recur The recurrence rule to parse (in un-folded form). - */ - public void parse(String recur) { - /* - * From RFC 2445 section 4.3.10: - * - * recur = "FREQ"=freq *( - * ; either UNTIL or COUNT may appear in a 'recur', - * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' - * - * ( ";" "UNTIL" "=" enddate ) / - * ( ";" "COUNT" "=" 1*DIGIT ) / - * - * ; the rest of these keywords are optional, - * ; but MUST NOT occur more than once - * - * ( ";" "INTERVAL" "=" 1*DIGIT ) / - * ( ";" "BYSECOND" "=" byseclist ) / - * ( ";" "BYMINUTE" "=" byminlist ) / - * ( ";" "BYHOUR" "=" byhrlist ) / - * ( ";" "BYDAY" "=" bywdaylist ) / - * ( ";" "BYMONTHDAY" "=" bymodaylist ) / - * ( ";" "BYYEARDAY" "=" byyrdaylist ) / - * ( ";" "BYWEEKNO" "=" bywknolist ) / - * ( ";" "BYMONTH" "=" bymolist ) / - * ( ";" "BYSETPOS" "=" bysplist ) / - * ( ";" "WKST" "=" weekday ) / - * ( ";" x-name "=" text ) - * ) - * - * Examples: - * FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU - * FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 - * - * Strategy: - * (1) Split the string at ';' boundaries to get an array of rule "parts". - * (2) For each part, find substrings for left/right sides of '=' (name/value). - * (3) Call a <name>-specific parsing function to parse the <value> into an - * output field. - * - * By keeping track of which names we've seen in a bit vector, we can verify the - * constraints indicated above (FREQ appears first, none of them appear more than once -- - * though x-[name] would require special treatment), and we have either UNTIL or COUNT - * but not both. - * - * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must - * be handled in a case-insensitive fashion, but case may be significant for other - * properties. We don't have any case-sensitive values in RRULE, except possibly - * for the custom "X-" properties, but we ignore those anyway. Thus, we can trivially - * convert the entire string to upper case and then use simple comparisons. - * - * Differences from previous version: - * - allows lower-case property and enumeration values [optional] - * - enforces that FREQ appears first - * - enforces that only one of UNTIL and COUNT may be specified - * - allows (but ignores) X-* parts - * - improved validation on various values (e.g. UNTIL timestamps) - * - error messages are more specific - */ - - /* TODO: replace with "if (freq != 0) throw" if nothing requires this */ - resetFields(); - - int parseFlags = 0; - String[] parts; - if (ALLOW_LOWER_CASE) { - parts = recur.toUpperCase().split(";"); - } else { - parts = recur.split(";"); - } - for (String part : parts) { - int equalIndex = part.indexOf('='); - if (equalIndex <= 0) { - /* no '=' or no LHS */ - throw new InvalidFormatException("Missing LHS in " + part); - } - - String lhs = part.substring(0, equalIndex); - String rhs = part.substring(equalIndex + 1); - if (rhs.length() == 0) { - throw new InvalidFormatException("Missing RHS in " + part); - } - - /* - * In lieu of a "switch" statement that allows string arguments, we use a - * map from strings to parsing functions. - */ - PartParser parser = sParsePartMap.get(lhs); - if (parser == null) { - if (lhs.startsWith("X-")) { - //Log.d(TAG, "Ignoring custom part " + lhs); - continue; - } - throw new InvalidFormatException("Couldn't find parser for " + lhs); - } else { - int flag = parser.parsePart(rhs, this); - if ((parseFlags & flag) != 0) { - throw new InvalidFormatException("Part " + lhs + " was specified twice"); - } - if (parseFlags == 0 && flag != PARSED_FREQ) { - throw new InvalidFormatException("FREQ must be specified first"); - } - parseFlags |= flag; - } - } - - // If not specified, week starts on Monday. - if ((parseFlags & PARSED_WKST) == 0) { - wkst = MO; - } - - // FREQ is mandatory. - if ((parseFlags & PARSED_FREQ) == 0) { - throw new InvalidFormatException("Must specify a FREQ value"); - } - - // Can't have both UNTIL and COUNT. - if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) { - if (ONLY_ONE_UNTIL_COUNT) { - throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur); - } else { - Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur); - } - } - } - - /** - * Base class for the RRULE part parsers. - */ - abstract static class PartParser { - /** - * Parses a single part. - * - * @param value The right-hand-side of the part. - * @param er The EventRecurrence into which the result is stored. - * @return A bit value indicating which part was parsed. - */ - public abstract int parsePart(String value, EventRecurrence er); - - /** - * Parses an integer, with range-checking. - * - * @param str The string to parse. - * @param minVal Minimum allowed value. - * @param maxVal Maximum allowed value. - * @param allowZero Is 0 allowed? - * @return The parsed value. - */ - public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) { - try { - if (str.charAt(0) == '+') { - // Integer.parseInt does not allow a leading '+', so skip it manually. - str = str.substring(1); - } - int val = Integer.parseInt(str); - if (val < minVal || val > maxVal || (val == 0 && !allowZero)) { - throw new InvalidFormatException("Integer value out of range: " + str); - } - return val; - } catch (NumberFormatException nfe) { - throw new InvalidFormatException("Invalid integer value: " + str); - } - } - - /** - * Parses a comma-separated list of integers, with range-checking. - * - * @param listStr The string to parse. - * @param minVal Minimum allowed value. - * @param maxVal Maximum allowed value. - * @param allowZero Is 0 allowed? - * @return A new array with values, sized to hold the exact number of elements. - */ - public static int[] parseNumberList(String listStr, int minVal, int maxVal, - boolean allowZero) { - int[] values; - - if (listStr.indexOf(",") < 0) { - // Common case: only one entry, skip split() overhead. - values = new int[1]; - values[0] = parseIntRange(listStr, minVal, maxVal, allowZero); - } else { - String[] valueStrs = listStr.split(","); - int len = valueStrs.length; - values = new int[len]; - for (int i = 0; i < len; i++) { - values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero); - } - } - return values; - } - } - - /** parses FREQ={SECONDLY,MINUTELY,...} */ - private static class ParseFreq extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - Integer freq = sParseFreqMap.get(value); - if (freq == null) { - throw new InvalidFormatException("Invalid FREQ value: " + value); - } - er.freq = freq; - return PARSED_FREQ; - } - } - /** parses UNTIL=enddate, e.g. "19970829T021400" */ - private static class ParseUntil extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - if (VALIDATE_UNTIL) { - try { - // Parse the time to validate it. The result isn't retained. - Time until = new Time(); - until.parse(value); - } catch (TimeFormatException tfe) { - throw new InvalidFormatException("Invalid UNTIL value: " + value); - } - } - er.until = value; - return PARSED_UNTIL; - } - } - /** parses COUNT=[non-negative-integer] */ - private static class ParseCount extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - er.count = parseIntRange(value, 0, Integer.MAX_VALUE, true); - return PARSED_COUNT; - } - } - /** parses INTERVAL=[non-negative-integer] */ - private static class ParseInterval extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - er.interval = parseIntRange(value, 1, Integer.MAX_VALUE, false); - return PARSED_INTERVAL; - } - } - /** parses BYSECOND=byseclist */ - private static class ParseBySecond extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] bysecond = parseNumberList(value, 0, 59, true); - er.bysecond = bysecond; - er.bysecondCount = bysecond.length; - return PARSED_BYSECOND; - } - } - /** parses BYMINUTE=byminlist */ - private static class ParseByMinute extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] byminute = parseNumberList(value, 0, 59, true); - er.byminute = byminute; - er.byminuteCount = byminute.length; - return PARSED_BYMINUTE; - } - } - /** parses BYHOUR=byhrlist */ - private static class ParseByHour extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] byhour = parseNumberList(value, 0, 23, true); - er.byhour = byhour; - er.byhourCount = byhour.length; - return PARSED_BYHOUR; - } - } - /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */ - private static class ParseByDay extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] byday; - int[] bydayNum; - int bydayCount; - - if (value.indexOf(",") < 0) { - /* only one entry, skip split() overhead */ - bydayCount = 1; - byday = new int[1]; - bydayNum = new int[1]; - parseWday(value, byday, bydayNum, 0); - } else { - String[] wdays = value.split(","); - int len = wdays.length; - bydayCount = len; - byday = new int[len]; - bydayNum = new int[len]; - for (int i = 0; i < len; i++) { - parseWday(wdays[i], byday, bydayNum, i); - } - } - er.byday = byday; - er.bydayNum = bydayNum; - er.bydayCount = bydayCount; - return PARSED_BYDAY; - } - - /** parses [int]weekday, putting the pieces into parallel array entries */ - private static void parseWday(String str, int[] byday, int[] bydayNum, int index) { - int wdayStrStart = str.length() - 2; - String wdayStr; - - if (wdayStrStart > 0) { - /* number is included; parse it out and advance to weekday */ - String numPart = str.substring(0, wdayStrStart); - int num = parseIntRange(numPart, -53, 53, false); - bydayNum[index] = num; - wdayStr = str.substring(wdayStrStart); - } else { - /* just the weekday string */ - wdayStr = str; - } - Integer wday = sParseWeekdayMap.get(wdayStr); - if (wday == null) { - throw new InvalidFormatException("Invalid BYDAY value: " + str); - } - byday[index] = wday; - } - } - /** parses BYMONTHDAY=bymodaylist */ - private static class ParseByMonthDay extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] bymonthday = parseNumberList(value, -31, 31, false); - er.bymonthday = bymonthday; - er.bymonthdayCount = bymonthday.length; - return PARSED_BYMONTHDAY; - } - } - /** parses BYYEARDAY=byyrdaylist */ - private static class ParseByYearDay extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] byyearday = parseNumberList(value, -366, 366, false); - er.byyearday = byyearday; - er.byyeardayCount = byyearday.length; - return PARSED_BYYEARDAY; - } - } - /** parses BYWEEKNO=bywknolist */ - private static class ParseByWeekNo extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] byweekno = parseNumberList(value, -53, 53, false); - er.byweekno = byweekno; - er.byweeknoCount = byweekno.length; - return PARSED_BYWEEKNO; - } - } - /** parses BYMONTH=bymolist */ - private static class ParseByMonth extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] bymonth = parseNumberList(value, 1, 12, false); - er.bymonth = bymonth; - er.bymonthCount = bymonth.length; - return PARSED_BYMONTH; - } - } - /** parses BYSETPOS=bysplist */ - private static class ParseBySetPos extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); - er.bysetpos = bysetpos; - er.bysetposCount = bysetpos.length; - return PARSED_BYSETPOS; - } - } - /** parses WKST={SU,MO,...} */ - private static class ParseWkst extends PartParser { - @Override public int parsePart(String value, EventRecurrence er) { - Integer wkst = sParseWeekdayMap.get(value); - if (wkst == null) { - throw new InvalidFormatException("Invalid WKST value: " + value); - } - er.wkst = wkst; - return PARSED_WKST; - } - } -} diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java deleted file mode 100644 index 58c5c63..0000000 --- a/core/java/android/pim/ICalendar.java +++ /dev/null @@ -1,660 +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.pim; - -import android.util.Log; - -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.ArrayList; - -/** - * Parses RFC 2445 iCalendar objects. - */ -public class ICalendar { - - private static final String TAG = "Sync"; - - // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM - // components, by type field or by subclass? subclass would allow us to - // enforce grammars. - - /** - * Exception thrown when an iCalendar object has invalid syntax. - */ - public static class FormatException extends Exception { - public FormatException() { - super(); - } - - public FormatException(String msg) { - super(msg); - } - - public FormatException(String msg, Throwable cause) { - super(msg, cause); - } - } - - /** - * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY, - * VTIMEZONE, VALARM). - */ - public static class Component { - - // components - private static final String BEGIN = "BEGIN"; - private static final String END = "END"; - private static final String NEWLINE = "\n"; - public static final String VCALENDAR = "VCALENDAR"; - public static final String VEVENT = "VEVENT"; - public static final String VTODO = "VTODO"; - public static final String VJOURNAL = "VJOURNAL"; - public static final String VFREEBUSY = "VFREEBUSY"; - public static final String VTIMEZONE = "VTIMEZONE"; - public static final String VALARM = "VALARM"; - - private final String mName; - private final Component mParent; // see if we can get rid of this - private LinkedList<Component> mChildren = null; - private final LinkedHashMap<String, ArrayList<Property>> mPropsMap = - new LinkedHashMap<String, ArrayList<Property>>(); - - /** - * Creates a new component with the provided name. - * @param name The name of the component. - */ - public Component(String name, Component parent) { - mName = name; - mParent = parent; - } - - /** - * Returns the name of the component. - * @return The name of the component. - */ - public String getName() { - return mName; - } - - /** - * Returns the parent of this component. - * @return The parent of this component. - */ - public Component getParent() { - return mParent; - } - - /** - * Helper that lazily gets/creates the list of children. - * @return The list of children. - */ - protected LinkedList<Component> getOrCreateChildren() { - if (mChildren == null) { - mChildren = new LinkedList<Component>(); - } - return mChildren; - } - - /** - * Adds a child component to this component. - * @param child The child component. - */ - public void addChild(Component child) { - getOrCreateChildren().add(child); - } - - /** - * Returns a list of the Component children of this component. May be - * null, if there are no children. - * - * @return A list of the children. - */ - public List<Component> getComponents() { - return mChildren; - } - - /** - * Adds a Property to this component. - * @param prop - */ - public void addProperty(Property prop) { - String name= prop.getName(); - ArrayList<Property> props = mPropsMap.get(name); - if (props == null) { - props = new ArrayList<Property>(); - mPropsMap.put(name, props); - } - props.add(prop); - } - - /** - * Returns a set of the property names within this component. - * @return A set of property names within this component. - */ - public Set<String> getPropertyNames() { - return mPropsMap.keySet(); - } - - /** - * Returns a list of properties with the specified name. Returns null - * if there are no such properties. - * @param name The name of the property that should be returned. - * @return A list of properties with the requested name. - */ - public List<Property> getProperties(String name) { - return mPropsMap.get(name); - } - - /** - * Returns the first property with the specified name. Returns null - * if there is no such property. - * @param name The name of the property that should be returned. - * @return The first property with the specified name. - */ - public Property getFirstProperty(String name) { - List<Property> props = mPropsMap.get(name); - if (props == null || props.size() == 0) { - return null; - } - return props.get(0); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - sb.append(NEWLINE); - return sb.toString(); - } - - /** - * Helper method that appends this component to a StringBuilder. The - * caller is responsible for appending a newline at the end of the - * component. - */ - public void toString(StringBuilder sb) { - sb.append(BEGIN); - sb.append(":"); - sb.append(mName); - sb.append(NEWLINE); - - // append the properties - for (String propertyName : getPropertyNames()) { - for (Property property : getProperties(propertyName)) { - property.toString(sb); - sb.append(NEWLINE); - } - } - - // append the sub-components - if (mChildren != null) { - for (Component component : mChildren) { - component.toString(sb); - sb.append(NEWLINE); - } - } - - sb.append(END); - sb.append(":"); - sb.append(mName); - } - } - - /** - * A property within an iCalendar component (e.g., DTSTART, DTEND, etc., - * within a VEVENT). - */ - public static class Property { - // properties - // TODO: do we want to list these here? the complete list is long. - public static final String DTSTART = "DTSTART"; - public static final String DTEND = "DTEND"; - public static final String DURATION = "DURATION"; - public static final String RRULE = "RRULE"; - public static final String RDATE = "RDATE"; - public static final String EXRULE = "EXRULE"; - public static final String EXDATE = "EXDATE"; - // ... need to add more. - - private final String mName; - private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap = - new LinkedHashMap<String, ArrayList<Parameter>>(); - private String mValue; // TODO: make this final? - - /** - * Creates a new property with the provided name. - * @param name The name of the property. - */ - public Property(String name) { - mName = name; - } - - /** - * Creates a new property with the provided name and value. - * @param name The name of the property. - * @param value The value of the property. - */ - public Property(String name, String value) { - mName = name; - mValue = value; - } - - /** - * Returns the name of the property. - * @return The name of the property. - */ - public String getName() { - return mName; - } - - /** - * Returns the value of this property. - * @return The value of this property. - */ - public String getValue() { - return mValue; - } - - /** - * Sets the value of this property. - * @param value The desired value for this property. - */ - public void setValue(String value) { - mValue = value; - } - - /** - * Adds a {@link Parameter} to this property. - * @param param The parameter that should be added. - */ - public void addParameter(Parameter param) { - ArrayList<Parameter> params = mParamsMap.get(param.name); - if (params == null) { - params = new ArrayList<Parameter>(); - mParamsMap.put(param.name, params); - } - params.add(param); - } - - /** - * Returns the set of parameter names for this property. - * @return The set of parameter names for this property. - */ - public Set<String> getParameterNames() { - return mParamsMap.keySet(); - } - - /** - * Returns the list of parameters with the specified name. May return - * null if there are no such parameters. - * @param name The name of the parameters that should be returned. - * @return The list of parameters with the specified name. - */ - public List<Parameter> getParameters(String name) { - return mParamsMap.get(name); - } - - /** - * Returns the first parameter with the specified name. May return - * nll if there is no such parameter. - * @param name The name of the parameter that should be returned. - * @return The first parameter with the specified name. - */ - public Parameter getFirstParameter(String name) { - ArrayList<Parameter> params = mParamsMap.get(name); - if (params == null || params.size() == 0) { - return null; - } - return params.get(0); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - return sb.toString(); - } - - /** - * Helper method that appends this property to a StringBuilder. The - * caller is responsible for appending a newline after this property. - */ - public void toString(StringBuilder sb) { - sb.append(mName); - Set<String> parameterNames = getParameterNames(); - for (String parameterName : parameterNames) { - for (Parameter param : getParameters(parameterName)) { - sb.append(";"); - param.toString(sb); - } - } - sb.append(":"); - sb.append(mValue); - } - } - - /** - * A parameter defined for an iCalendar property. - */ - // TODO: make this a proper class rather than a struct? - public static class Parameter { - public String name; - public String value; - - /** - * Creates a new empty parameter. - */ - public Parameter() { - } - - /** - * Creates a new parameter with the specified name and value. - * @param name The name of the parameter. - * @param value The value of the parameter. - */ - public Parameter(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - return sb.toString(); - } - - /** - * Helper method that appends this parameter to a StringBuilder. - */ - public void toString(StringBuilder sb) { - sb.append(name); - sb.append("="); - sb.append(value); - } - } - - private static final class ParserState { - // public int lineNumber = 0; - public String line; // TODO: just point to original text - public int index; - } - - // use factory method - private ICalendar() { - } - - // TODO: get rid of this -- handle all of the parsing in one pass through - // the text. - private static String normalizeText(String text) { - // it's supposed to be \r\n, but not everyone does that - text = text.replaceAll("\r\n", "\n"); - text = text.replaceAll("\r", "\n"); - - // we deal with line folding, by replacing all "\n " strings - // with nothing. The RFC specifies "\r\n " to be folded, but - // we handle "\n " and "\r " too because we can get those. - text = text.replaceAll("\n ", ""); - - return text; - } - - /** - * Parses text into an iCalendar component. Parses into the provided - * component, if not null, or parses into a new component. In the latter - * case, expects a BEGIN as the first line. Returns the provided or newly - * created top-level component. - */ - // TODO: use an index into the text, so we can make this a recursive - // function? - private static Component parseComponentImpl(Component component, - String text) - throws FormatException { - Component current = component; - ParserState state = new ParserState(); - state.index = 0; - - // split into lines - String[] lines = text.split("\n"); - - // each line is of the format: - // name *(";" param) ":" value - for (String line : lines) { - try { - current = parseLine(line, state, current); - // if the provided component was null, we will return the root - // NOTE: in this case, if the first line is not a BEGIN, a - // FormatException will get thrown. - if (component == null) { - component = current; - } - } catch (FormatException fe) { - if (false) { - Log.v(TAG, "Cannot parse " + line, fe); - } - // for now, we ignore the parse error. Google Calendar seems - // to be emitting some misformatted iCalendar objects. - } - continue; - } - return component; - } - - /** - * Parses a line into the provided component. Creates a new component if - * the line is a BEGIN, adding the newly created component to the provided - * parent. Returns whatever component is the current one (to which new - * properties will be added) in the parse. - */ - private static Component parseLine(String line, ParserState state, - Component component) - throws FormatException { - state.line = line; - int len = state.line.length(); - - // grab the name - char c = 0; - for (state.index = 0; state.index < len; ++state.index) { - c = line.charAt(state.index); - if (c == ';' || c == ':') { - break; - } - } - String name = line.substring(0, state.index); - - if (component == null) { - if (!Component.BEGIN.equals(name)) { - throw new FormatException("Expected BEGIN"); - } - } - - Property property; - if (Component.BEGIN.equals(name)) { - // start a new component - String componentName = extractValue(state); - Component child = new Component(componentName, component); - if (component != null) { - component.addChild(child); - } - return child; - } else if (Component.END.equals(name)) { - // finish the current component - String componentName = extractValue(state); - if (component == null || - !componentName.equals(component.getName())) { - throw new FormatException("Unexpected END " + componentName); - } - return component.getParent(); - } else { - property = new Property(name); - } - - if (c == ';') { - Parameter parameter = null; - while ((parameter = extractParameter(state)) != null) { - property.addParameter(parameter); - } - } - String value = extractValue(state); - property.setValue(value); - component.addProperty(property); - return component; - } - - /** - * Extracts the value ":..." on the current line. The first character must - * be a ':'. - */ - private static String extractValue(ParserState state) - throws FormatException { - String line = state.line; - if (state.index >= line.length() || line.charAt(state.index) != ':') { - throw new FormatException("Expected ':' before end of line in " - + line); - } - String value = line.substring(state.index + 1); - state.index = line.length() - 1; - return value; - } - - /** - * Extracts the next parameter from the line, if any. If there are no more - * parameters, returns null. - */ - private static Parameter extractParameter(ParserState state) - throws FormatException { - String text = state.line; - int len = text.length(); - Parameter parameter = null; - int startIndex = -1; - int equalIndex = -1; - while (state.index < len) { - char c = text.charAt(state.index); - if (c == ':') { - if (parameter != null) { - if (equalIndex == -1) { - throw new FormatException("Expected '=' within " - + "parameter in " + text); - } - parameter.value = text.substring(equalIndex + 1, - state.index); - } - return parameter; // may be null - } else if (c == ';') { - if (parameter != null) { - if (equalIndex == -1) { - throw new FormatException("Expected '=' within " - + "parameter in " + text); - } - parameter.value = text.substring(equalIndex + 1, - state.index); - return parameter; - } else { - parameter = new Parameter(); - startIndex = state.index; - } - } else if (c == '=') { - equalIndex = state.index; - if ((parameter == null) || (startIndex == -1)) { - throw new FormatException("Expected ';' before '=' in " - + text); - } - parameter.name = text.substring(startIndex + 1, equalIndex); - } else if (c == '"') { - if (parameter == null) { - throw new FormatException("Expected parameter before '\"' in " + text); - } - if (equalIndex == -1) { - throw new FormatException("Expected '=' within parameter in " + text); - } - if (state.index > equalIndex + 1) { - throw new FormatException("Parameter value cannot contain a '\"' in " + text); - } - final int endQuote = text.indexOf('"', state.index + 1); - if (endQuote < 0) { - throw new FormatException("Expected closing '\"' in " + text); - } - parameter.value = text.substring(state.index + 1, endQuote); - state.index = endQuote + 1; - return parameter; - } - ++state.index; - } - throw new FormatException("Expected ':' before end of line in " + text); - } - - /** - * Parses the provided text into an iCalendar object. The top-level - * component must be of type VCALENDAR. - * @param text The text to be parsed. - * @return The top-level VCALENDAR component. - * @throws FormatException Thrown if the text could not be parsed into an - * iCalendar VCALENDAR object. - */ - public static Component parseCalendar(String text) throws FormatException { - Component calendar = parseComponent(null, text); - if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) { - throw new FormatException("Expected " + Component.VCALENDAR); - } - return calendar; - } - - /** - * Parses the provided text into an iCalendar event. The top-level - * component must be of type VEVENT. - * @param text The text to be parsed. - * @return The top-level VEVENT component. - * @throws FormatException Thrown if the text could not be parsed into an - * iCalendar VEVENT. - */ - public static Component parseEvent(String text) throws FormatException { - Component event = parseComponent(null, text); - if (event == null || !Component.VEVENT.equals(event.getName())) { - throw new FormatException("Expected " + Component.VEVENT); - } - return event; - } - - /** - * Parses the provided text into an iCalendar component. - * @param text The text to be parsed. - * @return The top-level component. - * @throws FormatException Thrown if the text could not be parsed into an - * iCalendar component. - */ - public static Component parseComponent(String text) throws FormatException { - return parseComponent(null, text); - } - - /** - * Parses the provided text, adding to the provided component. - * @param component The component to which the parsed iCalendar data should - * be added. - * @param text The text to be parsed. - * @return The top-level component. - * @throws FormatException Thrown if the text could not be parsed as an - * iCalendar object. - */ - public static Component parseComponent(Component component, String text) - throws FormatException { - text = normalizeText(text); - return parseComponentImpl(component, text); - } -} diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java deleted file mode 100644 index b7fb320..0000000 --- a/core/java/android/pim/RecurrenceSet.java +++ /dev/null @@ -1,511 +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.pim; - -import android.content.ContentValues; -import android.database.Cursor; -import android.provider.CalendarContract; -import android.text.TextUtils; -import android.text.format.Time; -import android.util.Log; - -import java.util.List; -import java.util.regex.Pattern; - -/** - * Basic information about a recurrence, following RFC 2445 Section 4.8.5. - * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties. - */ -public class RecurrenceSet { - - private final static String TAG = "CalendarProvider"; - - private final static String RULE_SEPARATOR = "\n"; - private final static String FOLDING_SEPARATOR = "\n "; - - // TODO: make these final? - public EventRecurrence[] rrules = null; - public long[] rdates = null; - public EventRecurrence[] exrules = null; - public long[] exdates = null; - - /** - * Creates a new RecurrenceSet from information stored in the - * events table in the CalendarProvider. - * @param values The values retrieved from the Events table. - */ - public RecurrenceSet(ContentValues values) - throws EventRecurrence.InvalidFormatException { - String rruleStr = values.getAsString(CalendarContract.Events.RRULE); - String rdateStr = values.getAsString(CalendarContract.Events.RDATE); - String exruleStr = values.getAsString(CalendarContract.Events.EXRULE); - String exdateStr = values.getAsString(CalendarContract.Events.EXDATE); - init(rruleStr, rdateStr, exruleStr, exdateStr); - } - - /** - * Creates a new RecurrenceSet from information stored in a database - * {@link Cursor} pointing to the events table in the - * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE, - * and EXDATE columns. - * - * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE - * columns. - */ - public RecurrenceSet(Cursor cursor) - throws EventRecurrence.InvalidFormatException { - int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE); - int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE); - int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE); - int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE); - String rruleStr = cursor.getString(rruleColumn); - String rdateStr = cursor.getString(rdateColumn); - String exruleStr = cursor.getString(exruleColumn); - String exdateStr = cursor.getString(exdateColumn); - init(rruleStr, rdateStr, exruleStr, exdateStr); - } - - public RecurrenceSet(String rruleStr, String rdateStr, - String exruleStr, String exdateStr) - throws EventRecurrence.InvalidFormatException { - init(rruleStr, rdateStr, exruleStr, exdateStr); - } - - private void init(String rruleStr, String rdateStr, - String exruleStr, String exdateStr) - throws EventRecurrence.InvalidFormatException { - if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) { - - if (!TextUtils.isEmpty(rruleStr)) { - String[] rruleStrs = rruleStr.split(RULE_SEPARATOR); - rrules = new EventRecurrence[rruleStrs.length]; - for (int i = 0; i < rruleStrs.length; ++i) { - EventRecurrence rrule = new EventRecurrence(); - rrule.parse(rruleStrs[i]); - rrules[i] = rrule; - } - } - - if (!TextUtils.isEmpty(rdateStr)) { - rdates = parseRecurrenceDates(rdateStr); - } - - if (!TextUtils.isEmpty(exruleStr)) { - String[] exruleStrs = exruleStr.split(RULE_SEPARATOR); - exrules = new EventRecurrence[exruleStrs.length]; - for (int i = 0; i < exruleStrs.length; ++i) { - EventRecurrence exrule = new EventRecurrence(); - exrule.parse(exruleStr); - exrules[i] = exrule; - } - } - - if (!TextUtils.isEmpty(exdateStr)) { - exdates = parseRecurrenceDates(exdateStr); - } - } - } - - /** - * Returns whether or not a recurrence is defined in this RecurrenceSet. - * @return Whether or not a recurrence is defined in this RecurrenceSet. - */ - public boolean hasRecurrence() { - return (rrules != null || rdates != null); - } - - /** - * Parses the provided RDATE or EXDATE string into an array of longs - * representing each date/time in the recurrence. - * @param recurrence The recurrence to be parsed. - * @return The list of date/times. - */ - public static long[] parseRecurrenceDates(String recurrence) { - // TODO: use "local" time as the default. will need to handle times - // that end in "z" (UTC time) explicitly at that point. - String tz = Time.TIMEZONE_UTC; - int tzidx = recurrence.indexOf(";"); - if (tzidx != -1) { - tz = recurrence.substring(0, tzidx); - recurrence = recurrence.substring(tzidx + 1); - } - Time time = new Time(tz); - String[] rawDates = recurrence.split(","); - int n = rawDates.length; - long[] dates = new long[n]; - for (int i = 0; i<n; ++i) { - // The timezone is updated to UTC if the time string specified 'Z'. - time.parse(rawDates[i]); - dates[i] = time.toMillis(false /* use isDst */); - time.timezone = tz; - } - return dates; - } - - /** - * Populates the database map of values with the appropriate RRULE, RDATE, - * EXRULE, and EXDATE values extracted from the parsed iCalendar component. - * @param component The iCalendar component containing the desired - * recurrence specification. - * @param values The db values that should be updated. - * @return true if the component contained the necessary information - * to specify a recurrence. The required fields are DTSTART, - * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if - * there was an error, including if the date is out of range. - */ - public static boolean populateContentValues(ICalendar.Component component, - ContentValues values) { - ICalendar.Property dtstartProperty = - component.getFirstProperty("DTSTART"); - String dtstart = dtstartProperty.getValue(); - ICalendar.Parameter tzidParam = - dtstartProperty.getFirstParameter("TZID"); - // NOTE: the timezone may be null, if this is a floating time. - String tzid = tzidParam == null ? null : tzidParam.value; - Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid); - boolean inUtc = start.parse(dtstart); - boolean allDay = start.allDay; - - // 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; - } - - String duration = computeDuration(start, component); - String rrule = flattenProperties(component, "RRULE"); - String rdate = extractDates(component.getFirstProperty("RDATE")); - String exrule = flattenProperties(component, "EXRULE"); - String exdate = extractDates(component.getFirstProperty("EXDATE")); - - if ((TextUtils.isEmpty(dtstart))|| - (TextUtils.isEmpty(duration))|| - ((TextUtils.isEmpty(rrule))&& - (TextUtils.isEmpty(rdate)))) { - if (false) { - Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, " - + "or RRULE/RDATE: " - + component.toString()); - } - return false; - } - - if (allDay) { - start.timezone = Time.TIMEZONE_UTC; - } - long millis = start.toMillis(false /* use isDst */); - values.put(CalendarContract.Events.DTSTART, millis); - if (millis == -1) { - if (false) { - Log.d(TAG, "DTSTART is out of range: " + component.toString()); - } - return false; - } - - values.put(CalendarContract.Events.RRULE, rrule); - values.put(CalendarContract.Events.RDATE, rdate); - values.put(CalendarContract.Events.EXRULE, exrule); - values.put(CalendarContract.Events.EXDATE, exdate); - values.put(CalendarContract.Events.EVENT_TIMEZONE, tzid); - values.put(CalendarContract.Events.DURATION, duration); - values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0); - return true; - } - - // This can be removed when the old CalendarSyncAdapter is removed. - public static boolean populateComponent(Cursor cursor, - ICalendar.Component component) { - - int dtstartColumn = cursor.getColumnIndex(CalendarContract.Events.DTSTART); - int durationColumn = cursor.getColumnIndex(CalendarContract.Events.DURATION); - int tzidColumn = cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE); - int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE); - int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE); - int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE); - int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE); - int allDayColumn = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY); - - - long dtstart = -1; - if (!cursor.isNull(dtstartColumn)) { - dtstart = cursor.getLong(dtstartColumn); - } - String duration = cursor.getString(durationColumn); - String tzid = cursor.getString(tzidColumn); - String rruleStr = cursor.getString(rruleColumn); - String rdateStr = cursor.getString(rdateColumn); - String exruleStr = cursor.getString(exruleColumn); - String exdateStr = cursor.getString(exdateColumn); - boolean allDay = cursor.getInt(allDayColumn) == 1; - - if ((dtstart == -1) || - (TextUtils.isEmpty(duration))|| - ((TextUtils.isEmpty(rruleStr))&& - (TextUtils.isEmpty(rdateStr)))) { - // no recurrence. - return false; - } - - ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); - Time dtstartTime = null; - if (!TextUtils.isEmpty(tzid)) { - if (!allDay) { - dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); - } - dtstartTime = new Time(tzid); - } else { - // use the "floating" timezone - dtstartTime = new Time(Time.TIMEZONE_UTC); - } - - dtstartTime.set(dtstart); - // make sure the time is printed just as a date, if all day. - // TODO: android.pim.Time really should take care of this for us. - if (allDay) { - dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); - dtstartTime.allDay = true; - dtstartTime.hour = 0; - dtstartTime.minute = 0; - dtstartTime.second = 0; - } - - dtstartProp.setValue(dtstartTime.format2445()); - component.addProperty(dtstartProp); - ICalendar.Property durationProp = new ICalendar.Property("DURATION"); - durationProp.setValue(duration); - component.addProperty(durationProp); - - addPropertiesForRuleStr(component, "RRULE", rruleStr); - addPropertyForDateStr(component, "RDATE", rdateStr); - addPropertiesForRuleStr(component, "EXRULE", exruleStr); - addPropertyForDateStr(component, "EXDATE", exdateStr); - return true; - } - -public static boolean populateComponent(ContentValues values, - ICalendar.Component component) { - long dtstart = -1; - if (values.containsKey(CalendarContract.Events.DTSTART)) { - dtstart = values.getAsLong(CalendarContract.Events.DTSTART); - } - String duration = values.getAsString(CalendarContract.Events.DURATION); - String tzid = values.getAsString(CalendarContract.Events.EVENT_TIMEZONE); - String rruleStr = values.getAsString(CalendarContract.Events.RRULE); - String rdateStr = values.getAsString(CalendarContract.Events.RDATE); - String exruleStr = values.getAsString(CalendarContract.Events.EXRULE); - String exdateStr = values.getAsString(CalendarContract.Events.EXDATE); - Integer allDayInteger = values.getAsInteger(CalendarContract.Events.ALL_DAY); - boolean allDay = (null != allDayInteger) ? (allDayInteger == 1) : false; - - if ((dtstart == -1) || - (TextUtils.isEmpty(duration))|| - ((TextUtils.isEmpty(rruleStr))&& - (TextUtils.isEmpty(rdateStr)))) { - // no recurrence. - return false; - } - - ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); - Time dtstartTime = null; - if (!TextUtils.isEmpty(tzid)) { - if (!allDay) { - dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); - } - dtstartTime = new Time(tzid); - } else { - // use the "floating" timezone - dtstartTime = new Time(Time.TIMEZONE_UTC); - } - - dtstartTime.set(dtstart); - // make sure the time is printed just as a date, if all day. - // TODO: android.pim.Time really should take care of this for us. - if (allDay) { - dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); - dtstartTime.allDay = true; - dtstartTime.hour = 0; - dtstartTime.minute = 0; - dtstartTime.second = 0; - } - - dtstartProp.setValue(dtstartTime.format2445()); - component.addProperty(dtstartProp); - ICalendar.Property durationProp = new ICalendar.Property("DURATION"); - durationProp.setValue(duration); - component.addProperty(durationProp); - - addPropertiesForRuleStr(component, "RRULE", rruleStr); - addPropertyForDateStr(component, "RDATE", rdateStr); - addPropertiesForRuleStr(component, "EXRULE", exruleStr); - addPropertyForDateStr(component, "EXDATE", exdateStr); - return true; - } - - private static void addPropertiesForRuleStr(ICalendar.Component component, - String propertyName, - String ruleStr) { - if (TextUtils.isEmpty(ruleStr)) { - return; - } - String[] rrules = getRuleStrings(ruleStr); - for (String rrule : rrules) { - ICalendar.Property prop = new ICalendar.Property(propertyName); - prop.setValue(rrule); - component.addProperty(prop); - } - } - - private static String[] getRuleStrings(String ruleStr) { - if (null == ruleStr) { - return new String[0]; - } - String unfoldedRuleStr = unfold(ruleStr); - String[] split = unfoldedRuleStr.split(RULE_SEPARATOR); - int count = split.length; - for (int n = 0; n < count; n++) { - split[n] = fold(split[n]); - } - return split; - } - - - private static final Pattern IGNORABLE_ICAL_WHITESPACE_RE = - Pattern.compile("(?:\\r\\n?|\\n)[ \t]"); - - private static final Pattern FOLD_RE = Pattern.compile(".{75}"); - - /** - * fold and unfolds ical content lines as per RFC 2445 section 4.1. - * - * <h3>4.1 Content Lines</h3> - * - * <p>The iCalendar object is organized into individual lines of text, called - * content lines. Content lines are delimited by a line break, which is a CRLF - * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10). - * - * <p>Lines of text SHOULD NOT be longer than 75 octets, excluding the line - * break. Long content lines SHOULD be split into a multiple line - * representations using a line "folding" technique. That is, a long line can - * be split between any two characters by inserting a CRLF immediately - * followed by a single linear white space character (i.e., SPACE, US-ASCII - * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed - * immediately by a single linear white space character is ignored (i.e., - * removed) when processing the content type. - */ - public static String fold(String unfoldedIcalContent) { - return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n "); - } - - public static String unfold(String foldedIcalContent) { - return IGNORABLE_ICAL_WHITESPACE_RE.matcher( - foldedIcalContent).replaceAll(""); - } - - private static void addPropertyForDateStr(ICalendar.Component component, - String propertyName, - String dateStr) { - if (TextUtils.isEmpty(dateStr)) { - return; - } - - ICalendar.Property prop = new ICalendar.Property(propertyName); - String tz = null; - int tzidx = dateStr.indexOf(";"); - if (tzidx != -1) { - tz = dateStr.substring(0, tzidx); - dateStr = dateStr.substring(tzidx + 1); - } - if (!TextUtils.isEmpty(tz)) { - prop.addParameter(new ICalendar.Parameter("TZID", tz)); - } - prop.setValue(dateStr); - component.addProperty(prop); - } - - private static String computeDuration(Time start, - ICalendar.Component component) { - // see if a duration is defined - ICalendar.Property durationProperty = - component.getFirstProperty("DURATION"); - if (durationProperty != null) { - // just return the duration - return durationProperty.getValue(); - } - - // must compute a duration from the DTEND - ICalendar.Property dtendProperty = - component.getFirstProperty("DTEND"); - if (dtendProperty == null) { - // no DURATION, no DTEND: 0 second duration - return "+P0S"; - } - ICalendar.Parameter endTzidParameter = - dtendProperty.getFirstParameter("TZID"); - String endTzid = (endTzidParameter == null) - ? start.timezone : endTzidParameter.value; - - Time end = new Time(endTzid); - end.parse(dtendProperty.getValue()); - long durationMillis = end.toMillis(false /* use isDst */) - - start.toMillis(false /* use isDst */); - long durationSeconds = (durationMillis / 1000); - if (start.allDay && (durationSeconds % 86400) == 0) { - return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S - } else { - return "P" + durationSeconds + "S"; - } - } - - private static String flattenProperties(ICalendar.Component component, - String name) { - List<ICalendar.Property> properties = component.getProperties(name); - if (properties == null || properties.isEmpty()) { - return null; - } - - if (properties.size() == 1) { - return properties.get(0).getValue(); - } - - StringBuilder sb = new StringBuilder(); - - boolean first = true; - for (ICalendar.Property property : component.getProperties(name)) { - if (first) { - first = false; - } else { - // TODO: use commas. our RECUR parsing should handle that - // anyway. - sb.append(RULE_SEPARATOR); - } - sb.append(property.getValue()); - } - return sb.toString(); - } - - private static String extractDates(ICalendar.Property recurrence) { - if (recurrence == null) { - return null; - } - ICalendar.Parameter tzidParam = - recurrence.getFirstParameter("TZID"); - if (tzidParam != null) { - return tzidParam.value + ";" + recurrence.getValue(); - } - return recurrence.getValue(); - } -} diff --git a/core/java/android/preference/SeekBarPreference.java b/core/java/android/preference/SeekBarPreference.java index b8919c2..7133d3a 100644 --- a/core/java/android/preference/SeekBarPreference.java +++ b/core/java/android/preference/SeekBarPreference.java @@ -77,6 +77,11 @@ public class SeekBarPreference extends Preference } @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInt(index, 0); + } + + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() != KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_PLUS diff --git a/core/java/android/provider/CalendarContract.java b/core/java/android/provider/CalendarContract.java index b492615..5b29103 100644 --- a/core/java/android/provider/CalendarContract.java +++ b/core/java/android/provider/CalendarContract.java @@ -17,6 +17,8 @@ package android.provider; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentProviderClient; @@ -83,7 +85,6 @@ import android.util.Log; * adapters</li> * </ul> * - * @hide */ public final class CalendarContract { private static final String TAG = "Calendar"; @@ -92,8 +93,8 @@ public final class CalendarContract { * Broadcast Action: This is the intent that gets fired when an alarm * notification needs to be posted for a reminder. * - * @SdkConstant */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_EVENT_REMINDER = "android.intent.action.EVENT_REMINDER"; /** @@ -146,6 +147,11 @@ public final class CalendarContract { public static final String ACCOUNT_TYPE_LOCAL = "LOCAL"; /** + * This utility class cannot be instantiated + */ + private CalendarContract() {} + + /** * 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. These columns are @@ -384,7 +390,7 @@ public final class CalendarContract { * Class that represents a Calendar Entity. There is one entry per calendar. * This is a helper class to make batch operations easier. */ - public static class CalendarEntity implements BaseColumns, SyncColumns, CalendarColumns { + public static final class CalendarEntity implements BaseColumns, SyncColumns, CalendarColumns { /** * The default Uri used when creating a new calendar EntityIterator. @@ -394,6 +400,11 @@ public final class CalendarContract { "/calendar_entities"); /** + * This utility class cannot be instantiated + */ + private CalendarEntity() {} + + /** * Creates an entity iterator for the given cursor. It assumes the * cursor contains a calendars query. * @@ -566,7 +577,13 @@ public final class CalendarContract { * <li>{@link #CAL_SYNC10}</li> * </ul> */ - public static class Calendars implements BaseColumns, SyncColumns, CalendarColumns { + public static final class Calendars implements BaseColumns, SyncColumns, CalendarColumns { + + /** + * This utility class cannot be instantiated + */ + private Calendars() {} + /** * The content:// style URL for accessing Calendars */ @@ -687,7 +704,7 @@ public final class CalendarContract { /** * Fields and helpers for interacting with Attendees. Each row of this table * represents a single attendee or guest of an event. Calling - * {@link #query(ContentResolver, long)} will return a list of attendees for + * {@link #query(ContentResolver, long, String[])} will return a list of attendees for * the event with the given eventId. Both apps and sync adapters may write * to this table. There are six writable fields and all of them except * {@link #ATTENDEE_NAME} must be included when inserting a new attendee. @@ -708,12 +725,12 @@ public final class CalendarContract { */ @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/attendees"); + private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; + /** - * the projection used by the attendees query + * This utility class cannot be instantiated */ - public static final String[] PROJECTION = new String[] { - _ID, ATTENDEE_NAME, ATTENDEE_EMAIL, ATTENDEE_RELATIONSHIP, ATTENDEE_STATUS,}; - private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; + private Attendees() {} /** * Queries all attendees associated with the given event. This is a @@ -721,11 +738,12 @@ public final class CalendarContract { * * @param cr The content resolver to use for the query * @param eventId The id of the event to retrieve attendees for + * @param projection the columns to return in the cursor * @return A Cursor containing all attendees for the event */ - public static final Cursor query(ContentResolver cr, long eventId) { + public static final Cursor query(ContentResolver cr, long eventId, String[] projection) { String[] attArgs = {Long.toString(eventId)}; - return cr.query(CONTENT_URI, PROJECTION, ATTENDEES_WHERE, attArgs /* selection args */, + return cr.query(CONTENT_URI, projection, ATTENDEES_WHERE, attArgs /* selection args */, null /* sort order */); } } @@ -1068,6 +1086,11 @@ public final class CalendarContract { "/event_entities"); /** + * This utility class cannot be instantiated + */ + private EventsEntity() {} + + /** * Creates a new iterator for events * * @param cursor An event query @@ -1411,6 +1434,11 @@ public final class CalendarContract { Uri.parse("content://" + AUTHORITY + "/exception"); /** + * This utility class cannot be instantiated + */ + private Events() {} + + /** * The default sort order for this table */ private static final String DEFAULT_SORT_ORDER = ""; @@ -1484,6 +1512,11 @@ public final class CalendarContract { }; /** + * This utility class cannot be instantiated + */ + private Instances() {} + + /** * Performs a query to return all visible instances in the given range. * This is a blocking function and should not be done on the UI thread. * This will cause an expansion of recurring events to fill this time @@ -1636,7 +1669,7 @@ public final class CalendarContract { * time zone for the instances. These settings are stored using a key/value * scheme. A {@link #KEY} must be specified when updating these values. */ - public static class CalendarCache implements CalendarCacheColumns { + public static final class CalendarCache implements CalendarCacheColumns { /** * The URI to use for retrieving the properties from the Calendar db. */ @@ -1644,6 +1677,11 @@ public final class CalendarContract { Uri.parse("content://" + AUTHORITY + "/properties"); /** + * This utility class cannot be instantiated + */ + private CalendarCache() {} + + /** * They key for updating the use of auto/home time zones in Calendar. * Valid values are {@link #TIMEZONE_TYPE_AUTO} or * {@link #TIMEZONE_TYPE_HOME}. @@ -1724,6 +1762,11 @@ public final class CalendarContract { * @hide */ public static final class CalendarMetaData implements CalendarMetaDataColumns, BaseColumns { + + /** + * This utility class cannot be instantiated + */ + private CalendarMetaData() {} } protected interface EventDaysColumns { @@ -1746,14 +1789,12 @@ public final class CalendarContract { public static final class EventDays implements EventDaysColumns { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/instances/groupbyday"); + private static final String SELECTION = "selected=1"; /** - * The projection used by the EventDays query. + * This utility class cannot be instantiated */ - public static final String[] PROJECTION = { - STARTDAY, ENDDAY - }; - private static final String SELECTION = "selected=1"; + private EventDays() {} /** * Retrieves the days with events for the Julian days starting at @@ -1765,10 +1806,12 @@ public final class CalendarContract { * @param cr the ContentResolver * @param startDay the first Julian day in the range * @param numDays the number of days to load (must be at least 1) + * @param projection the columns to return in the cursor * @return a database cursor containing a list of start and end days for * events */ - public static final Cursor query(ContentResolver cr, int startDay, int numDays) { + public static final Cursor query(ContentResolver cr, int startDay, int numDays, + String[] projection) { if (numDays < 1) { return null; } @@ -1776,7 +1819,7 @@ public final class CalendarContract { Uri.Builder builder = CONTENT_URI.buildUpon(); ContentUris.appendId(builder, startDay); ContentUris.appendId(builder, endDay); - return cr.query(builder.build(), PROJECTION, SELECTION, + return cr.query(builder.build(), projection, SELECTION, null /* selection args */, STARTDAY); } } @@ -1821,7 +1864,7 @@ public final class CalendarContract { /** * Fields and helpers for accessing reminders for an event. Each row of this * table represents a single reminder for an event. Calling - * {@link #query(ContentResolver, long)} will return a list of reminders for + * {@link #query(ContentResolver, long, String[])} will return a list of reminders for * the event with the given eventId. Both apps and sync adapters may write * to this table. There are three writable fields and all of them must be * included when inserting a new reminder. They are: @@ -1833,25 +1876,26 @@ public final class CalendarContract { */ public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns { private static final String REMINDERS_WHERE = CalendarContract.Reminders.EVENT_ID + "=?"; - /** - * The projection used by the reminders query. - */ - public static final String[] PROJECTION = new String[] { - _ID, MINUTES, METHOD,}; @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/reminders"); /** + * This utility class cannot be instantiated + */ + private Reminders() {} + + /** * Queries all reminders associated with the given event. This is a * blocking call and should not be done on the UI thread. * * @param cr The content resolver to use for the query * @param eventId The id of the event to retrieve reminders for + * @param projection the columns to return in the cursor * @return A Cursor containing all reminders for the event */ - public static final Cursor query(ContentResolver cr, long eventId) { + public static final Cursor query(ContentResolver cr, long eventId, String[] projection) { String[] remArgs = {Long.toString(eventId)}; - return cr.query(CONTENT_URI, PROJECTION, REMINDERS_WHERE, remArgs /* selection args */, + return cr.query(CONTENT_URI, projection, REMINDERS_WHERE, remArgs /*selection args*/, null /* sort order */); } } @@ -1964,6 +2008,11 @@ public final class CalendarContract { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendar_alerts"); + /** + * This utility class cannot be instantiated + */ + private CalendarAlerts() {} + private static final String WHERE_ALARM_EXISTS = EVENT_ID + "=?" + " AND " + BEGIN + "=?" + " AND " + ALARM_TIME + "=?"; @@ -2134,7 +2183,7 @@ public final class CalendarContract { * given event id, begin time and alarm time. If one is found then this * alarm already exists and this method returns true. TODO Move to * provider - * + * * @param cr the ContentResolver * @param eventId the event id to match * @param begin the start time of the event in UTC millis @@ -2203,6 +2252,11 @@ public final class CalendarContract { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/extendedproperties"); + /** + * This utility class cannot be instantiated + */ + private ExtendedProperties() {} + // TODO: fill out this class when we actually start utilizing extendedproperties // in the calendar application. } @@ -2271,5 +2325,10 @@ public final class CalendarContract { * @hide */ public static final class EventsRawTimes implements BaseColumns, EventsRawTimesColumns { + + /** + * This utility class cannot be instantiated + */ + private EventsRawTimes() {} } } diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index ad71061..76f198c 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -17,6 +17,7 @@ package android.provider; import android.accounts.Account; +import android.app.Activity; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; @@ -27,6 +28,7 @@ import android.content.CursorEntityIterator; import android.content.Entity; import android.content.EntityIterator; import android.content.Intent; +import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseUtils; @@ -39,6 +41,7 @@ import android.util.Pair; import android.view.View; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -135,14 +138,6 @@ public final class ContactsContract { public static final String ALLOW_PROFILE = "allow_profile"; /** - * A query parameter key used to specify the package that is requesting a query. - * This is used for restricting data based on package name. - * - * @hide - */ - 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. @@ -271,8 +266,6 @@ public final class ContactsContract { * <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 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY - * parameter, this parameter is added.</li> * </ul> * </p> * <p> @@ -759,11 +752,25 @@ public final class ContactsContract { public static final String PHOTO_ID = "photo_id"; /** + * Photo file ID of the full-size photo. If present, this will be used to populate + * {@link #PHOTO_URI}. The ID can also be used with + * {@link ContactsContract.DisplayPhoto#CONTENT_URI} to create a URI to the photo. + * If this is present, {@link #PHOTO_ID} is also guaranteed to be populated. + * + * <P>Type: INTEGER</P> + */ + public static final String PHOTO_FILE_ID = "photo_file_id"; + + /** * A URI that can be used to retrieve the contact's full-size photo. + * If PHOTO_FILE_ID is not null, this will be populated with a URI based off + * {@link ContactsContract.DisplayPhoto#CONTENT_URI}. Otherwise, this will + * be populated with the same value as {@link #PHOTO_THUMBNAIL_URI}. * A photo can be referred to either by a URI (this field) or by ID - * (see {@link #PHOTO_ID}). If PHOTO_ID is not null, PHOTO_URI and - * PHOTO_THUMBNAIL_URI shall not be null (but not necessarily vice versa). - * Thus using PHOTO_URI is a more robust method of retrieving contact photos. + * (see {@link #PHOTO_ID}). If either PHOTO_FILE_ID or PHOTO_ID is not null, + * PHOTO_URI and PHOTO_THUMBNAIL_URI shall not be null (but not necessarily + * vice versa). Thus using PHOTO_URI is a more robust method of retrieving + * contact photos. * * <P>Type: TEXT</P> */ @@ -776,7 +783,7 @@ public final class ContactsContract { * PHOTO_THUMBNAIL_URI shall not be null (but not necessarily vice versa). * If the content provider does not differentiate between full-size photos * and thumbnail photos, PHOTO_THUMBNAIL_URI and {@link #PHOTO_URI} can contain - * the same value, but either both shell be null or both not null. + * the same value, but either both shall be null or both not null. * * <P>Type: TEXT</P> */ @@ -1536,6 +1543,23 @@ public final class ContactsContract { } /** + * A sub-directory of a single contact that contains all of the constituent raw contact + * {@link ContactsContract.StreamItems} rows. This directory can be used either + * with a {@link #CONTENT_URI} or {@link #CONTENT_LOOKUP_URI}. + */ + public static final class StreamItems implements StreamItemsColumns { + /** + * no public constructor since this is a utility class + */ + private StreamItems() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "stream_items"; + } + + /** * <p> * A <i>read-only</i> sub-directory of a single contact aggregate that * contains all aggregation suggestions (other contacts). The @@ -1683,10 +1707,15 @@ public final class ContactsContract { /** * A <i>read-only</i> sub-directory of a single contact that contains - * the contact's primary photo. + * the contact's primary photo. The photo may be stored in up to two ways - + * the default "photo" is a thumbnail-sized image stored directly in the data + * row, while the "display photo", if present, is a larger version stored as + * a file. * <p> * Usage example: - * + * <dl> + * <dt>Retrieving the thumbnail-sized photo</dt> + * <dd> * <pre> * public InputStream openPhoto(long contactId) { * Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); @@ -1709,10 +1738,29 @@ public final class ContactsContract { * return null; * } * </pre> + * </dd> + * <dt>Retrieving the larger photo version</dt> + * <dd> + * <pre> + * public InputStream openDisplayPhoto(long contactId) { + * Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); + * Uri displayPhotoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.DISPLAY_PHOTO); + * try { + * AssetFileDescriptor fd = + * getContentResolver().openAssetFile(displayPhotoUri, "r"); + * return fd.createInputStream(); + * } catch (FileNotFoundException e) { + * return null; + * } + * } + * </pre> + * </dd> + * </dl> * * </p> - * <p>You should also consider using the convenience method - * {@link ContactsContract.Contacts#openContactPhotoInputStream(ContentResolver, Uri)} + * <p>You may also consider using the convenience method + * {@link ContactsContract.Contacts#openContactPhotoInputStream(ContentResolver, Uri, boolean)} + * to retrieve the raw photo contents of either the thumbnail-sized or the full-sized photo. * </p> * <p> * This directory can be used either with a {@link #CONTENT_URI} or @@ -1731,6 +1779,19 @@ public final class ContactsContract { public static final String CONTENT_DIRECTORY = "photo"; /** + * The directory twig for retrieving the full-size display photo. + */ + public static final String DISPLAY_PHOTO = "display_photo"; + + /** + * Full-size photo file ID of the raw contact. + * See {@link ContactsContract.DisplayPhoto}. + * <p> + * Type: NUMBER + */ + public static final String PHOTO_FILE_ID = DATA14; + + /** * Thumbnail photo of the raw contact. This is the raw bytes of an image * that could be inflated using {@link android.graphics.BitmapFactory}. * <p> @@ -1740,22 +1801,37 @@ public final class ContactsContract { } /** - * Opens an InputStream for the contacts's default photo and returns the - * photo as a byte stream. If there is not photo null will be returned. - * + * Opens an InputStream for the contacts's photo and returns the + * photo as a byte stream. + * @param cr The content resolver to use for querying * @param contactUri the contact whose photo should be used. This can be used with * either a {@link #CONTENT_URI} or a {@link #CONTENT_LOOKUP_URI} URI. - * </p> - + * @param preferHighres If this is true and the contact has a higher resolution photo + * available, it is returned. If false, this function always tries to get the thumbnail * @return an InputStream of the photo, or null if no photo is present */ - public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri contactUri) { + public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri contactUri, + boolean preferHighres) { + if (preferHighres) { + final Uri displayPhotoUri = Uri.withAppendedPath(contactUri, + Contacts.Photo.DISPLAY_PHOTO); + InputStream inputStream; + try { + AssetFileDescriptor fd = cr.openAssetFileDescriptor(displayPhotoUri, "r"); + return fd.createInputStream(); + } catch (IOException e) { + // fallback to the thumbnail code + } + } + Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY); if (photoUri == null) { return null; } Cursor cursor = cr.query(photoUri, - new String[]{ContactsContract.CommonDataKinds.Photo.PHOTO}, null, null, null); + new String[] { + ContactsContract.CommonDataKinds.Photo.PHOTO + }, null, null, null); try { if (cursor == null || !cursor.moveToNext()) { return null; @@ -1771,6 +1847,20 @@ public final class ContactsContract { } } } + + /** + * Opens an InputStream for the contacts's thumbnail photo and returns the + * photo as a byte stream. + * @param cr The content resolver to use for querying + * @param contactUri the contact whose photo should be used. This can be used with + * either a {@link #CONTENT_URI} or a {@link #CONTENT_LOOKUP_URI} URI. + * @return an InputStream of the photo, or null if no photo is present + * @see #openContactPhotoInputStream(ContentResolver, Uri, boolean), if instead + * of the thumbnail the high-res picture is preferred + */ + public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri contactUri) { + return openContactPhotoInputStream(cr, contactUri, false); + } } /** @@ -1864,13 +1954,16 @@ public final class ContactsContract { public static final String CONTACT_ID = "contact_id"; /** - * Flag indicating that this {@link RawContacts} entry and its children have - * been restricted to specific platform apps. - * <P>Type: INTEGER (boolean)</P> + * The data set within the account that this row belongs to. This allows + * multiple sync adapters for the same account type to distinguish between + * each others' data. * - * @hide until finalized in future platform release + * This is empty by default, and is completely optional. It only needs to + * be populated if multiple sync adapters are entering distinct data for + * the same account type and account name. + * <P>Type: TEXT</P> */ - public static final String IS_RESTRICTED = "is_restricted"; + public static final String DATA_SET = "data_set"; /** * The aggregation mode for this contact. @@ -2194,8 +2287,8 @@ public final class ContactsContract { * <td>The name of the account instance to which this row belongs, which when paired with * {@link #ACCOUNT_TYPE} identifies a specific account. * For example, this will be the Gmail address if it is a Google account. - * It should be set at the time - * the raw contact is inserted and never changed afterwards.</td> + * It should be set at the time the raw contact is inserted and never + * changed afterwards.</td> * </tr> * <tr> * <td>String</td> @@ -2205,8 +2298,8 @@ public final class ContactsContract { * <p> * The type of account to which this row belongs, which when paired with * {@link #ACCOUNT_NAME} identifies a specific account. - * It should be set at the time - * the raw contact is inserted and never changed afterwards. + * It should be set at the time the raw contact is inserted and never + * changed afterwards. * </p> * <p> * To ensure uniqueness, new account types should be chosen according to the @@ -2216,15 +2309,38 @@ public final class ContactsContract { * </tr> * <tr> * <td>String</td> + * <td>{@link #DATA_SET}</td> + * <td>read/write-once</td> + * <td> + * <p> + * The data set within the account that this row belongs to. This allows + * multiple sync adapters for the same account type to distinguish between + * each others' data. The combination of {@link #ACCOUNT_TYPE}, + * {@link #ACCOUNT_NAME}, and {@link #DATA_SET} identifies a set of data + * that is associated with a single sync adapter. + * </p> + * <p> + * This is empty by default, and is completely optional. It only needs to + * be populated if multiple sync adapters are entering distinct data for + * the same account type and account name. + * </p> + * <p> + * It should be set at the time the raw contact is inserted and never + * changed afterwards. + * </p> + * </td> + * </tr> + * <tr> + * <td>String</td> * <td>{@link #SOURCE_ID}</td> * <td>read/write</td> * <td>String that uniquely identifies this row to its source account. * Typically it is set at the time the raw contact is inserted and never * changed afterwards. The one notable exception is a new raw contact: it - * will have an account name and type, but no source id. This - * indicates to the sync adapter that a new contact needs to be created - * server-side and its ID stored in the corresponding SOURCE_ID field on - * the phone. + * will have an account name and type (and possibly a data set), but no + * source id. This indicates to the sync adapter that a new contact needs + * to be created server-side and its ID stored in the corresponding + * SOURCE_ID field on the phone. * </td> * </tr> * <tr> @@ -2443,6 +2559,78 @@ public final class ContactsContract { } /** + * <p> + * A sub-directory of a single raw contact that contains all of its + * {@link ContactsContract.StreamItems} rows. To access this directory append + * {@link RawContacts.StreamItems#CONTENT_DIRECTORY} to the raw contact URI. See + * {@link ContactsContract.StreamItems} for a stand-alone table containing the + * same data. + * </p> + */ + public static final class StreamItems implements BaseColumns, StreamItemsColumns { + /** + * No public constructor since this is a utility class + */ + private StreamItems() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "stream_items"; + } + + /** + * <p> + * A sub-directory of a single raw contact that represents its primary + * display photo. To access this directory append + * {@link RawContacts.DisplayPhoto#CONTENT_DIRECTORY} to the raw contact URI. + * The resulting URI represents an image file, and should be interacted with + * using ContentProvider.openAssetFile. + * <p> + * <p> + * Note that this sub-directory also supports opening the photo as an asset file + * in write mode. Callers can create or replace the primary photo associated + * with this raw contact by opening the asset file and writing the full-size + * photo contents into it. When the file is closed, the image will be parsed, + * sized down if necessary for the full-size display photo and thumbnail + * dimensions, and stored. + * </p> + * <p> + * Usage example: + * <pre> + * public void writeDisplayPhoto(long rawContactId, byte[] photo) { + * Uri rawContactPhotoUri = Uri.withAppendedPath( + * ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), + * RawContacts.DisplayPhoto.CONTENT_DIRECTORY); + * try { + * AssetFileDescriptor fd = + * getContentResolver().openAssetFile(rawContactPhotoUri, "rw"); + * OutputStream os = fd.createOutputStream(); + * os.write(photo); + * os.close(); + * fd.close(); + * } catch (IOException e) { + * // Handle error cases. + * } + * } + * </pre> + * </p> + */ + public static final class DisplayPhoto { + /** + * No public constructor since this is a utility class + */ + private DisplayPhoto() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "display_photo"; + } + + /** * TODO: javadoc * @param cursor * @return @@ -2498,7 +2686,6 @@ public final class ContactsContract { DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DELETED); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, CONTACT_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, STARRED); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, IS_RESTRICTED); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, NAME_VERIFIED); android.content.Entity contact = new android.content.Entity(cv); @@ -2661,6 +2848,527 @@ public final class ContactsContract { } /** + * <p> + * Constants for the stream_items table, which contains social stream updates from + * the user's contact list. + * </p> + * <p> + * Only a certain number of stream items will ever be stored under a given raw contact. + * Users of this API can query {@link ContactsContract.StreamItems#CONTENT_LIMIT_URI} to + * determine this limit, and should restrict the number of items inserted in any given + * transaction correspondingly. Insertion of more items beyond the limit will + * automatically lead to deletion of the oldest items, by {@link StreamItems#TIMESTAMP}. + * </p> + * <h3>Operations</h3> + * <dl> + * <dt><b>Insert</b></dt> + * <dd> + * <p>Social stream updates are always associated with a raw contact. There are a couple + * of ways to insert these entries. + * <dl> + * <dt>Via the {@link RawContacts.StreamItems#CONTENT_DIRECTORY} sub-path of a raw contact:</dt> + * <dd> + * <pre> + * ContentValues values = new ContentValues(); + * values.put(StreamItems.TEXT, "Breakfasted at Tiffanys"); + * values.put(StreamItems.TIMESTAMP, timestamp); + * values.put(StreamItems.COMMENT, "3 people reshared this"); + * values.put(StreamItems.ACTION, action); + * values.put(StreamItems.ACTION_URI, actionUri); + * Uri streamItemUri = getContentResolver().insert( + * Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), + * RawContacts.StreamItems.CONTENT_DIRECTORY), values); + * long streamItemId = ContentUris.parseId(streamItemUri); + * </pre> + * </dd> + * <dt>Via the {@link StreamItems#CONTENT_URI} URI:</dt> + * <dd> + * ContentValues values = new ContentValues(); + * values.put(StreamItems.RAW_CONTACT_ID, rawContactId); + * values.put(StreamItems.TEXT, "Breakfasted at Tiffanys"); + * values.put(StreamItems.TIMESTAMP, timestamp); + * values.put(StreamItems.COMMENT, "3 people reshared this"); + * values.put(StreamItems.ACTION, action); + * values.put(StreamItems.ACTION_URI, actionUri); + * Uri streamItemUri = getContentResolver().insert(StreamItems.CONTENT_URI, values); + * long streamItemId = ContentUris.parseId(streamItemUri); + * </dd> + * </dl> + * </dd> + * </p> + * <p> + * Once a {@link StreamItems} entry has been inserted, photos associated with that + * social update can be inserted. For example, after one of the insertions above, + * photos could be added to the stream item in one of the following ways: + * <dl> + * <dt>Via a URI including the stream item ID:</dt> + * <dd> + * <pre> + * values.clear(); + * values.put(StreamItemPhotos.SORT_INDEX, 1); + * values.put(StreamItemPhotos.PICTURE, photoData); + * values.put(StreamItemPhotos.ACTION, action); + * values.put(StreamItemPhotos.ACTION_URI, actionUri); + * getContentResolver().insert(Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId), + * StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), values); + * </pre> + * </dd> + * <dt>Via {@link ContactsContract.StreamItems#CONTENT_PHOTO_URI}</dt> + * <dd> + * <pre> + * values.clear(); + * values.put(StreamItemPhotos.STREAM_ITEM_ID, streamItemId); + * values.put(StreamItemPhotos.SORT_INDEX, 1); + * values.put(StreamItemPhotos.PICTURE, photoData); + * values.put(StreamItemPhotos.ACTION, action); + * values.put(StreamItemPhotos.ACTION_URI, actionUri); + * getContentResolver().insert(StreamItems.CONTENT_PHOTO_URI, values); + * </pre> + * Note that this latter form allows the insertion of a stream item and its + * photos in a single transaction, by using {@link ContentProviderOperation} with + * back references to populate the stream item ID in the {@link ContentValues}. + * </dd> + * </dl> + * </p> + * </dd> + * <dt><b>Update</b></dt> + * <dd>Updates can be performed by appending the stream item ID to the + * {@link StreamItems#CONTENT_URI} URI. Only social stream entries that were + * created by the calling package can be updated.</dd> + * <dt><b>Delete</b></dt> + * <dd>Deletes can be performed by appending the stream item ID to the + * {@link StreamItems#CONTENT_URI} URI. Only social stream entries that were + * created by the calling package can be deleted.</dd> + * <dt><b>Query</b></dt> + * <dl> + * <dt>Finding all social stream updates for a given contact</dt> + * <dd>By Contact ID: + * <pre> + * Cursor c = getContentResolver().query(Uri.withAppendedPath( + * ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), + * Contacts.StreamItems.CONTENT_DIRECTORY), + * null, null, null, null); + * </pre> + * </dd> + * <dd>By lookup key: + * <pre> + * Cursor c = getContentResolver().query(Contacts.CONTENT_URI.buildUpon() + * .appendPath(lookupKey) + * .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(), + * null, null, null, null); + * </pre> + * </dd> + * <dt>Finding all social stream updates for a given raw contact</dt> + * <dd> + * <pre> + * Cursor c = getContentResolver().query(Uri.withAppendedPath( + * ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), + * RawContacts.StreamItems.CONTENT_DIRECTORY)), + * null, null, null, null); + * </pre> + * </dd> + * <dt>Querying for a specific stream item by ID</dt> + * <dd> + * <pre> + * Cursor c = getContentResolver().query(ContentUris.withAppendedId( + * StreamItems.CONTENT_URI, streamItemId), + * null, null, null, null); + * </pre> + * </dd> + * </dl> + */ + public static final class StreamItems implements BaseColumns, StreamItemsColumns { + /** + * This utility class cannot be instantiated + */ + private StreamItems() { + } + + /** + * The content:// style URI for this table, which handles social network stream + * updates for the user's contacts. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "stream_items"); + + /** + * <p> + * A content:// style URI for the photos stored in a sub-table underneath + * stream items. This is only used for inserts, and updates - queries and deletes + * for photos should be performed by appending + * {@link StreamItems.StreamItemPhotos#CONTENT_DIRECTORY} path to URIs for a + * specific stream item. + * </p> + * <p> + * When using this URI, the stream item ID for the photo(s) must be identified + * in the {@link ContentValues} passed in. + * </p> + */ + public static final Uri CONTENT_PHOTO_URI = Uri.withAppendedPath(CONTENT_URI, "photo"); + + /** + * This URI allows the caller to query for the maximum number of stream items + * that will be stored under any single raw contact, as well as the maximum + * photo size (in bytes) accepted in stream item photos. + */ + public static final Uri CONTENT_LIMIT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "stream_items_limit"); + + /** + * Queries to {@link ContactsContract.StreamItems#CONTENT_LIMIT_URI} will + * contain this column, with the value indicating the maximum number of + * stream items that will be stored under any single raw contact. + */ + public static final String MAX_ITEMS = "max_items"; + + /** + * Queries to {@link ContactsContract.StreamItems#CONTENT_LIMIT_URI} will + * contain this column, with the value indicating the byte limit for + * individual photos. + */ + public static final String PHOTO_MAX_BYTES = "photo_max_bytes"; + + /** + * <p> + * A sub-directory of a single stream item entry that contains all of its + * photo rows. To access this + * directory append {@link StreamItems.StreamItemPhotos#CONTENT_DIRECTORY} to + * an individual stream item URI. + * </p> + */ + public static final class StreamItemPhotos + implements BaseColumns, StreamItemPhotosColumns { + /** + * No public constructor since this is a utility class + */ + private StreamItemPhotos() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "photo"; + } + } + + /** + * Columns in the StreamItems table. + * + * @see ContactsContract.StreamItems + */ + protected interface StreamItemsColumns { + /** + * A reference to the {@link RawContacts#_ID} + * that this stream item belongs to. + */ + public static final String RAW_CONTACT_ID = "raw_contact_id"; + + /** + * The package name to use when creating {@link Resources} objects for + * this stream item. This value is only designed for use when building + * user interfaces, and should not be used to infer the owner. + * <P>Type: NUMBER</P> + */ + public static final String RES_PACKAGE = "res_package"; + + /** + * The resource ID of the icon for the source of the stream item. + * This resource should be scoped by the {@link #RES_PACKAGE}. + * <P>Type: NUMBER</P> + */ + public static final String RES_ICON = "icon"; + + /** + * The resource ID of the label describing the source of the status update, e.g. "Google + * Talk". This resource should be scoped by the {@link #RES_PACKAGE}. + * <p>Type: NUMBER</p> + */ + public static final String RES_LABEL = "label"; + + /** + * <P> + * The main textual contents of the item. Typically this is content + * that was posted by the source of this stream item, but it can also + * be a textual representation of an action (e.g. ”Checked in at Joe's”). + * This text is displayed to the user and allows formatting and embedded + * resource images via HTML (as parseable via + * {@link android.text.Html#fromHtml}). + * </P> + * <P> + * Long content may be truncated and/or ellipsized - the exact behavior + * is unspecified, but it should not break tags. + * </P> + * <P>Type: TEXT</P> + */ + public static final String TEXT = "text"; + + /** + * The absolute time (milliseconds since epoch) when this stream item was + * inserted/updated. + * <P>Type: NUMBER</P> + */ + public static final String TIMESTAMP = "timestamp"; + + /** + * <P> + * Summary information about the stream item, for example to indicate how + * many people have reshared it, how many have liked it, how many thumbs + * up and/or thumbs down it has, what the original source was, etc. + * </P> + * <P> + * This text is displayed to the user and allows simple formatting via + * HTML, in the same manner as {@link #TEXT} allows. + * </P> + * <P> + * Long content may be truncated and/or ellipsized - the exact behavior + * is unspecified, but it should not break tags. + * </P> + * <P>Type: TEXT</P> + */ + public static final String COMMENTS = "comments"; + + /** + * The activity action to execute when the item is tapped. + * <P>Type: TEXT</P> + */ + public static final String ACTION = "action"; + + /** + * The URI that is launched when the item is pressed. May be handled by + * the source app, but could also reference a website (e.g. YouTube). + * <P>Type: TEXT</P> + */ + public static final String ACTION_URI = "action_uri"; + } + + /** + * <p> + * Constants for the stream_item_photos table, which contains photos associated with + * social stream updates. + * </p> + * <h3>Operations</h3> + * <dl> + * <dt><b>Insert</b></dt> + * <dd> + * <p>Social stream photo entries are associated with a social stream item. Photos + * can be inserted into a social stream item in a couple of ways: + * <dl> + * <dt> + * Via the {@link StreamItems.StreamItemPhotos#CONTENT_DIRECTORY} sub-path of a + * stream item: + * </dt> + * <dd> + * <pre> + * ContentValues values = new ContentValues(); + * values.put(StreamItemPhotos.SORT_INDEX, 1); + * values.put(StreamItemPhotos.PICTURE, photoData); + * values.put(StreamItemPhotos.ACTION, action); + * values.put(StreamItemPhotos.ACTION_URI, actionUri); + * Uri photoUri = getContentResolver().insert(Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), values); + * long photoId = ContentUris.parseId(photoUri); + * </pre> + * </dd> + * <dt>Via the {@link ContactsContract.StreamItems#CONTENT_PHOTO_URI} URI:</dt> + * <dd> + * <pre> + * ContentValues values = new ContentValues(); + * values.put(StreamItemPhotos.STREAM_ITEM_ID, streamItemId); + * values.put(StreamItemPhotos.SORT_INDEX, 1); + * values.put(StreamItemPhotos.PICTURE, photoData); + * values.put(StreamItemPhotos.ACTION, action); + * values.put(StreamItemPhotos.ACTION_URI, actionUri); + * Uri photoUri = getContentResolver().insert(StreamItems.CONTENT_PHOTO_URI, values); + * long photoId = ContentUris.parseId(photoUri); + * </pre> + * </dd> + * </dl> + * </p> + * </dd> + * <dt><b>Update</b></dt> + * <dd> + * <p>Updates can only be made against a specific {@link StreamItemPhotos} entry, + * identified by both the stream item ID it belongs to and the stream item photo ID. + * This can be specified in two ways. + * <dl> + * <dt>Via the {@link StreamItems.StreamItemPhotos#CONTENT_DIRECTORY} sub-path of a + * stream item: + * </dt> + * <dd> + * <pre> + * ContentValues values = new ContentValues(); + * values.put(StreamItemPhotos.PICTURE, newPhotoData); + * getContentResolver().update( + * ContentUris.withAppendedId( + * Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), + * streamItemPhotoId), values, null, null); + * </pre> + * </dd> + * <dt>Via the {@link ContactsContract.StreamItems#CONTENT_PHOTO_URI} URI:</dt> + * <dd> + * <pre> + * ContentValues values = new ContentValues(); + * values.put(StreamItemPhotos.STREAM_ITEM_ID, streamItemId); + * values.put(StreamItemPhotos.PICTURE, newPhotoData); + * getContentResolver().update(StreamItems.CONTENT_PHOTO_URI, values); + * </pre> + * </dd> + * </dl> + * </p> + * </dd> + * <dt><b>Delete</b></dt> + * <dd>Deletes can be made against either a specific photo item in a stream item, or + * against all or a selected subset of photo items under a stream item. + * For example: + * <dl> + * <dt>Deleting a single photo via the + * {@link StreamItems.StreamItemPhotos#CONTENT_DIRECTORY} sub-path of a stream item: + * </dt> + * <dd> + * <pre> + * getContentResolver().delete( + * ContentUris.withAppendedId( + * Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), + * streamItemPhotoId), null, null); + * </pre> + * </dd> + * <dt>Deleting all photos under a stream item</dt> + * <dd> + * <pre> + * getContentResolver().delete( + * Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), null, null); + * </pre> + * </dd> + * </dl> + * </dd> + * <dt><b>Query</b></dt> + * <dl> + * <dt>Querying for a specific photo in a stream item</dt> + * <dd> + * <pre> + * Cursor c = getContentResolver().query( + * ContentUris.withAppendedId( + * Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), + * streamItemPhotoId), null, null, null, null); + * </pre> + * </dd> + * <dt>Querying for all photos in a stream item</dt> + * <dd> + * <pre> + * Cursor c = getContentResolver().query( + * Uri.withAppendedPath( + * ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId) + * StreamItems.StreamItemPhotos#CONTENT_DIRECTORY), + * null, null, null, StreamItemPhotos.SORT_INDEX); + * </pre> + * </dl> + * </dd> + * </dl> + */ + public static final class StreamItemPhotos implements BaseColumns, StreamItemPhotosColumns { + /** + * No public constructor since this is a utility class + */ + private StreamItemPhotos() { + } + } + + /** + * Columns in the StreamItemPhotos table. + * + * @see ContactsContract.StreamItemPhotos + */ + protected interface StreamItemPhotosColumns { + /** + * A reference to the {@link StreamItems#_ID} this photo is associated with. + * <P>Type: NUMBER</P> + */ + public static final String STREAM_ITEM_ID = "stream_item_id"; + + /** + * An integer to use for sort order for photos in the stream item. If not + * specified, the {@link StreamItemPhotos#_ID} will be used for sorting. + * <P>Type: NUMBER</P> + */ + public static final String SORT_INDEX = "sort_index"; + + /** + * The binary representation of the picture. Pictures larger than + * {@link ContactsContract.StreamItems#PHOTO_MAX_BYTES} bytes in size (as + * queryable from {@link ContactsContract.StreamItems#CONTENT_LIMIT_URI}) + * will be rejected. + * <P>Type: BLOB</P> + */ + public static final String PICTURE = "picture"; + + /** + * The activity action to execute when the photo is tapped. + * <P>Type: TEXT</P> + */ + public static final String ACTION = "action"; + + /** + * The URI that is launched when the photo is pressed. May be handled by + * the source app, but could also reference a website (e.g. YouTube). + * <P>Type: TEXT</P> + */ + public static final String ACTION_URI = "action_uri"; + } + + /** + * <p> + * Constants for the photo files table, which tracks metadata for hi-res photos + * stored in the file system. + * </p> + * + * @hide + */ + public static final class PhotoFiles implements BaseColumns, PhotoFilesColumns { + /** + * No public constructor since this is a utility class + */ + private PhotoFiles() { + } + } + + /** + * Columns in the PhotoFiles table. + * + * @see ContactsContract.PhotoFiles + * + * @hide + */ + protected interface PhotoFilesColumns { + + /** + * The height, in pixels, of the photo this entry is associated with. + * <P>Type: NUMBER</P> + */ + public static final String HEIGHT = "height"; + + /** + * The width, in pixels, of the photo this entry is associated with. + * <P>Type: NUMBER</P> + */ + public static final String WIDTH = "width"; + + /** + * The size, in bytes, of the photo stored on disk. + * <P>Type: NUMBER</P> + */ + public static final String FILESIZE = "filesize"; + } + + /** * Columns in the Data table. * * @see ContactsContract.Data @@ -3298,27 +4006,6 @@ public final class ContactsContract { /** * <p> - * If {@link #FOR_EXPORT_ONLY} is explicitly set to "1", returned Cursor toward - * Data.CONTENT_URI contains only exportable data. - * </p> - * <p> - * This flag is useful (currently) only for vCard exporter in Contacts app, which - * needs to exclude "un-exportable" data from available data to export, while - * Contacts app itself has priviledge to access all data including "un-exportable" - * ones and providers return all of them regardless of the callers' intention. - * </p> - * <p> - * Type: INTEGER - * </p> - * - * @hide Maybe available only in Eclair and not really ready for public use. - * TODO: remove, or implement this feature completely. As of now (Eclair), - * we only use this flag in queryEntities(), not query(). - */ - public static final String FOR_EXPORT_ONLY = "for_export_only"; - - /** - * <p> * Build a {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI} * style {@link Uri} for the parent {@link android.provider.ContactsContract.Contacts} * entry of the given {@link ContactsContract.Data} entry. @@ -5381,7 +6068,7 @@ public final class ContactsContract { /** * <p> - * A data kind representing an photo for the contact. + * A data kind representing a photo for the contact. * </p> * <p> * Some sync adapters will choose to download photos in a separate @@ -5401,10 +6088,17 @@ public final class ContactsContract { * <th>Alias</th><th colspan='2'>Data column</th> * </tr> * <tr> + * <td>NUMBER</td> + * <td>{@link #PHOTO_FILE_ID}</td> + * <td>{@link #DATA14}</td> + * <td>ID of the hi-res photo file.</td> + * </tr> + * <tr> * <td>BLOB</td> * <td>{@link #PHOTO}</td> * <td>{@link #DATA15}</td> - * <td>By convention, binary data is stored in DATA15.</td> + * <td>By convention, binary data is stored in DATA15. The thumbnail of the + * photo is stored in this column.</td> * </tr> * </table> */ @@ -5418,6 +6112,14 @@ public final class ContactsContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/photo"; /** + * Photo file ID for the display photo of the raw contact. + * See {@link ContactsContract.DisplayPhoto}. + * <p> + * Type: NUMBER + */ + public static final String PHOTO_FILE_ID = DATA14; + + /** * Thumbnail photo of the raw contact. This is the raw bytes of an image * that could be inflated using {@link android.graphics.BitmapFactory}. * <p> @@ -5697,6 +6399,18 @@ public final class ContactsContract { */ protected interface GroupsColumns { /** + * The data set within the account that this group belongs to. This allows + * multiple sync adapters for the same account type to distinguish between + * each others' group data. + * + * This is empty by default, and is completely optional. It only needs to + * be populated if multiple sync adapters are entering distinct group data + * for the same account type and account name. + * <P>Type: TEXT</P> + */ + public static final String DATA_SET = "data_set"; + + /** * The display title of this group. * <p> * Type: TEXT @@ -5729,6 +6443,28 @@ public final class ContactsContract { public static final String NOTES = "notes"; /** + * The Activity action to open the group in the source app (e.g. + * {@link Intent#ACTION_VIEW}). Can be NULL if the group does not have a dedicated viewer. + * This is used in conjunction with {@link #ACTION_URI}: In order to show an "Open in + * (sourceapp)"-button, both of these fields must be set + * <p> + * Type: TEXT + */ + public static final String ACTION = "action"; + + + /** + * Uri to open the group in the source app. + * Can be NULL if the group does not have a dedicated viewer. + * This is used in conjunction with {@link #ACTION}: In order to show an "Open in + * (sourceapp)"-button, both of these fields must be set + * <p> + * Type: TEXT + */ + public static final String ACTION_URI = "action_uri"; + + + /** * The ID of this group if it is a System Group, i.e. a group that has a special meaning * to the sync adapter, null otherwise. * <P>Type: TEXT</P> @@ -5822,6 +6558,29 @@ public final class ContactsContract { * In other words, it would be a really bad idea to delete and reinsert a * group. A sync adapter should always do an update instead.</td> * </tr> + # <tr> + * <td>String</td> + * <td>{@link #DATA_SET}</td> + * <td>read/write-once</td> + * <td> + * <p> + * The data set within the account that this group belongs to. This allows + * multiple sync adapters for the same account type to distinguish between + * each others' group data. The combination of {@link #ACCOUNT_TYPE}, + * {@link #ACCOUNT_NAME}, and {@link #DATA_SET} identifies a set of data + * that is associated with a single sync adapter. + * </p> + * <p> + * This is empty by default, and is completely optional. It only needs to + * be populated if multiple sync adapters are entering distinct data for + * the same account type and account name. + * </p> + * <p> + * It should be set at the time the group is inserted and never changed + * afterwards. + * </p> + * </td> + * </tr> * <tr> * <td>String</td> * <td>{@link #TITLE}</td> @@ -6502,6 +7261,66 @@ public final class ContactsContract { } /** + * Helper class for accessing full-size photos by photo file ID. + * <p> + * Usage example: + * <dl> + * <dt>Retrieving a full-size photo by photo file ID (see + * {@link ContactsContract.ContactsColumns#PHOTO_FILE_ID}) + * </dt> + * <dd> + * <pre> + * public InputStream openDisplayPhoto(long photoFileId) { + * Uri displayPhotoUri = ContentUris.withAppendedId(DisplayPhoto.CONTENT_URI, photoKey); + * try { + * AssetFileDescriptor fd = getContentResolver().openAssetFile(displayPhotoUri, "r"); + * return fd.createInputStream(); + * } catch (FileNotFoundException e) { + * return null; + * } + * } + * </pre> + * </dd> + * </dl> + * </p> + */ + public static final class DisplayPhoto { + /** + * no public constructor since this is a utility class + */ + private DisplayPhoto() {} + + /** + * The content:// style URI for this class, which allows access to full-size photos, + * given a key. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "display_photo"); + + /** + * This URI allows the caller to query for the maximum dimensions of a display photo + * or thumbnail. Requests to this URI can be performed on the UI thread because + * they are always unblocking. + */ + public static final Uri CONTENT_MAX_DIMENSIONS_URI = + Uri.withAppendedPath(AUTHORITY_URI, "photo_dimensions"); + + /** + * Queries to {@link ContactsContract.DisplayPhoto#CONTENT_MAX_DIMENSIONS_URI} will + * contain this column, populated with the maximum height and width (in pixels) + * that will be stored for a display photo. Larger photos will be down-sized to + * fit within a square of this many pixels. + */ + public static final String DISPLAY_MAX_DIM = "display_max_dim"; + + /** + * Queries to {@link ContactsContract.DisplayPhoto#CONTENT_MAX_DIMENSIONS_URI} will + * contain this column, populated with the height and width (in pixels) for photo + * thumbnails. + */ + public static final String THUMBNAIL_MAX_DIM = "thumbnail_max_dim"; + } + + /** * Contains helper classes used to create or manage {@link android.content.Intent Intents} * that involve contacts. */ diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index f799af3..f3bcedb 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -283,6 +283,13 @@ public final class MediaStore { */ public static final String IS_DRM = "is_drm"; + /** + * Used by the media scanner to suppress files from being processed as media files. + * + * <P>Type: INTEGER (boolean)</P> + * @hide + */ + public static final String NO_MEDIA = "no_media"; } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 65babc2..34699e2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2683,6 +2683,13 @@ public final class Settings { public static final String ACCESSIBILITY_ENABLED = "accessibility_enabled"; /** + * If touch exploration is requested. Touch exploration is enabled if it is + * requested by this setting, accessibility is enabled and there is at least + * one enabled accessibility serivce that provides spoken feedback. + */ + public static final String TOUCH_EXPLORATION_REQUESTED = "touch_exploration_requested"; + + /** * List of the enabled accessibility providers. */ public static final String ENABLED_ACCESSIBILITY_SERVICES = @@ -3833,6 +3840,11 @@ public final class Settings { /** {@hide} */ public static final String NETSTATS_TAG_MAX_HISTORY = "netstats_tag_max_history"; + /** Preferred NTP server. {@hide} */ + public static final String NTP_SERVER = "ntp_server"; + /** Timeout in milliseconds to wait for NTP server. {@hide} */ + public static final String NTP_TIMEOUT = "ntp_timeout"; + /** * @hide */ diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 8a6fdb4..fd277d0 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -449,6 +449,22 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); } + public synchronized boolean allowIncomingConnect(BluetoothDevice device, boolean value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + String address = device.getAddress(); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + Integer data = mBluetoothService.getAuthorizationAgentRequestData(address); + if (data == null) { + Log.w(TAG, "allowIncomingConnect(" + device + ") called but no native data available"); + return false; + } + log("allowIncomingConnect: A2DP: " + device + ":" + value); + return mBluetoothService.setAuthorizationNative(address, value, data.intValue()); + } + /** * Called by native code on a PropertyChanged signal from * org.bluez.AudioSink. diff --git a/core/java/android/server/BluetoothBondState.java b/core/java/android/server/BluetoothBondState.java index 76e7885..75f38f9 100644 --- a/core/java/android/server/BluetoothBondState.java +++ b/core/java/android/server/BluetoothBondState.java @@ -121,6 +121,8 @@ class BluetoothBondState { /** reason is ignored unless state == BOND_NOT_BONDED */ public synchronized void setBondState(String address, int state, int reason) { + if (DBG) Log.d(TAG, "setBondState " + "address" + " " + state + "reason: " + reason); + int oldState = getBondState(address); if (oldState == state) { return; @@ -136,8 +138,10 @@ class BluetoothBondState { if (state == BluetoothDevice.BOND_BONDED) { mService.addProfileState(address); - } else if (state == BluetoothDevice.BOND_NONE) { - mService.removeProfileState(address); + } else if (state == BluetoothDevice.BOND_BONDING) { + if (mA2dpProxy == null || mHeadsetProxy == null) { + getProfileProxy(); + } } setProfilePriorities(address, state); @@ -240,6 +244,8 @@ class BluetoothBondState { } public synchronized void clearPinAttempts(String address) { + if (DBG) Log.d(TAG, "clearPinAttempts: " + address); + mPinAttempt.remove(address); } @@ -265,6 +271,8 @@ class BluetoothBondState { } else { newAttempt = attempt.intValue() + 1; } + if (DBG) Log.d(TAG, "attemp newAttempt: " + newAttempt); + mPinAttempt.put(address, new Integer(newAttempt)); } diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index a220007..f345a6a 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -49,6 +49,7 @@ class BluetoothEventLoop { private boolean mInterrupted; private final HashMap<String, Integer> mPasskeyAgentRequestData; + private final HashMap<String, Integer> mAuthorizationAgentRequestData; private final BluetoothService mBluetoothService; private final BluetoothAdapter mAdapter; private BluetoothA2dp mA2dp; @@ -110,6 +111,7 @@ class BluetoothEventLoop { mBluetoothService = bluetoothService; mContext = context; mPasskeyAgentRequestData = new HashMap<String, Integer>(); + mAuthorizationAgentRequestData = new HashMap<String, Integer>(); mAdapter = adapter; //WakeLock instantiation in BluetoothEventLoop class PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); @@ -155,6 +157,10 @@ class BluetoothEventLoop { return mPasskeyAgentRequestData; } + /* package */ HashMap<String, Integer> getAuthorizationAgentRequestData() { + return mAuthorizationAgentRequestData; + } + /* package */ void start() { if (!isEventLoopRunningNative()) { @@ -654,7 +660,6 @@ class BluetoothEventLoop { case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO: - case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: if (mBluetoothService.attemptAutoPair(address)) return; } @@ -747,20 +752,22 @@ class BluetoothEventLoop { * * @param objectPath the path of the device requesting to be authorized * @param deviceUuid the UUID of the requesting device - * @return true if the authorization is allowed; false if not allowed + * @param nativeData reference for native data */ - private boolean onAgentAuthorize(String objectPath, String deviceUuid) { - if (!mBluetoothService.isEnabled()) return false; + private void onAgentAuthorize(String objectPath, String deviceUuid, int nativeData) { + if (!mBluetoothService.isEnabled()) return; String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); - return false; + return; } boolean authorized = false; ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); + BluetoothDevice device = mAdapter.getRemoteDevice(address); + mAuthorizationAgentRequestData.put(address, new Integer(nativeData)); // Bluez sends the UUID of the local service being accessed, _not_ the // remote service @@ -769,26 +776,29 @@ class BluetoothEventLoop { || BluetoothUuid.isAdvAudioDist(uuid)) && !isOtherSinkInNonDisconnectedState(address)) { authorized = mA2dp.getPriority(device) > BluetoothProfile.PRIORITY_OFF; - if (authorized) { - Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); + if (authorized && !BluetoothUuid.isAvrcpTarget(uuid)) { + Log.i(TAG, "First check pass for incoming A2DP / AVRCP connection from " + address); // Some headsets try to connect AVCTP before AVDTP - against the recommendation // If AVCTP connection fails, we get stuck in IncomingA2DP state in the state // machine. We don't handle AVCTP signals currently. We only send // intents for AVDTP state changes. We need to handle both of them in // some cases. For now, just don't move to incoming state in this case. - if (!BluetoothUuid.isAvrcpTarget(uuid)) { - mBluetoothService.notifyIncomingA2dpConnection(address); - } + mBluetoothService.notifyIncomingA2dpConnection(address); } else { - Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); + Log.i(TAG, "" + authorized + + "Incoming A2DP / AVRCP connection from " + address); + mA2dp.allowIncomingConnect(device, authorized); } } else if (mInputDevice != null && BluetoothUuid.isInputDevice(uuid)) { // We can have more than 1 input device connected. authorized = mInputDevice.getPriority(device) > BluetoothInputDevice.PRIORITY_OFF; if (authorized) { - Log.i(TAG, "Allowing incoming HID connection from " + address); + Log.i(TAG, "First check pass for incoming HID connection from " + address); + // notify profile state change + mBluetoothService.notifyIncomingHidConnection(address); } else { Log.i(TAG, "Rejecting incoming HID connection from " + address); + mBluetoothService.allowIncomingHidConnect(device, authorized); } } else if (BluetoothUuid.isBnep(uuid) && mBluetoothService.allowIncomingTethering()){ authorized = true; @@ -796,7 +806,6 @@ class BluetoothEventLoop { Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized); - return authorized; } private boolean onAgentOutOfBandDataAvailable(String objectPath) { diff --git a/core/java/android/server/BluetoothHealthProfileHandler.java b/core/java/android/server/BluetoothHealthProfileHandler.java index 7f862e0..105ff33 100644 --- a/core/java/android/server/BluetoothHealthProfileHandler.java +++ b/core/java/android/server/BluetoothHealthProfileHandler.java @@ -20,15 +20,12 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHealth; import android.bluetooth.BluetoothHealthAppConfiguration; -import android.bluetooth.BluetoothHealth; -import android.bluetooth.BluetoothInputDevice; +import android.bluetooth.IBluetoothHealthCallback; import android.content.Context; -import android.content.Intent; import android.os.Handler; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.provider.Settings; import android.util.Log; import java.util.ArrayList; @@ -36,10 +33,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map.Entry; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.IOException; - /** * This handles all the operations on the Bluetooth Health profile. * All functions are called by BluetoothService, as Bluetooth Service @@ -58,6 +51,7 @@ final class BluetoothHealthProfileHandler { private ArrayList<HealthChannel> mHealthChannels; private HashMap <BluetoothHealthAppConfiguration, String> mHealthAppConfigs; private HashMap <BluetoothDevice, Integer> mHealthDevices; + private HashMap <BluetoothHealthAppConfiguration, IBluetoothHealthCallback> mCallbacks; private static final int MESSAGE_REGISTER_APPLICATION = 0; private static final int MESSAGE_UNREGISTER_APPLICATION = 1; @@ -103,6 +97,7 @@ final class BluetoothHealthProfileHandler { } if (path == null) { + mCallbacks.remove(registerApp); callHealthApplicationStatusCallback(registerApp, BluetoothHealth.APPLICATION_REGISTRATION_FAILURE); } else { @@ -118,6 +113,7 @@ final class BluetoothHealthProfileHandler { boolean result = mBluetoothService.unregisterHealthApplicationNative( mHealthAppConfigs.get(unregisterApp)); if (result) { + mCallbacks.remove(unregisterApp); callHealthApplicationStatusCallback(unregisterApp, BluetoothHealth.APPLICATION_UNREGISTRATION_SUCCESS); } else { @@ -149,6 +145,7 @@ final class BluetoothHealthProfileHandler { mHealthAppConfigs = new HashMap<BluetoothHealthAppConfiguration, String>(); mHealthChannels = new ArrayList<HealthChannel>(); mHealthDevices = new HashMap<BluetoothDevice, Integer>(); + mCallbacks = new HashMap<BluetoothHealthAppConfiguration, IBluetoothHealthCallback>(); } static synchronized BluetoothHealthProfileHandler getInstance(Context context, @@ -157,10 +154,12 @@ final class BluetoothHealthProfileHandler { return sInstance; } - boolean registerAppConfiguration(BluetoothHealthAppConfiguration config) { + boolean registerAppConfiguration(BluetoothHealthAppConfiguration config, + IBluetoothHealthCallback callback) { Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_APPLICATION); msg.obj = config; mHandler.sendMessage(msg); + mCallbacks.put(config, callback); return true; } @@ -442,11 +441,11 @@ final class BluetoothHealthProfileHandler { debugLog("Health Device Callback: " + device + " State Change: " + prevState + "->" + state); - try { - config.getCallback().onHealthChannelStateChange(config, device, prevState, - state, fd); - } catch (RemoteException e) { - errorLog("Error while making health channel state change callback: " + e); + IBluetoothHealthCallback callback = mCallbacks.get(config); + if (callback != null) { + try { + callback.onHealthChannelStateChange(config, device, prevState, state, fd); + } catch (RemoteException e) {} } } @@ -454,10 +453,11 @@ final class BluetoothHealthProfileHandler { BluetoothHealthAppConfiguration config, int status) { debugLog("Health Device Application: " + config + " State Change: status:" + status); - try { - config.getCallback().onHealthAppConfigurationStatusChange(config, status); - } catch (RemoteException e) { - errorLog("Error while making health app registration state change callback: " + e); + IBluetoothHealthCallback callback = mCallbacks.get(config); + if (callback != null) { + try { + callback.onHealthAppConfigurationStatusChange(config, status); + } catch (RemoteException e) {} } } @@ -526,19 +526,19 @@ final class BluetoothHealthProfileHandler { List<HealthChannel> chan; switch (currDeviceState) { case BluetoothHealth.STATE_DISCONNECTED: - updateAndsendIntent(device, currDeviceState, newDeviceState); + updateAndSendIntent(device, currDeviceState, newDeviceState); break; case BluetoothHealth.STATE_CONNECTING: // Channel got connected. if (newDeviceState == BluetoothHealth.STATE_CONNECTED) { - updateAndsendIntent(device, currDeviceState, newDeviceState); + updateAndSendIntent(device, currDeviceState, newDeviceState); } else { // Channel got disconnected chan = findChannelByStates(device, new int [] { BluetoothHealth.STATE_CHANNEL_CONNECTING, BluetoothHealth.STATE_CHANNEL_DISCONNECTING}); if (chan.isEmpty()) { - updateAndsendIntent(device, currDeviceState, newDeviceState); + updateAndSendIntent(device, currDeviceState, newDeviceState); } } break; @@ -548,22 +548,23 @@ final class BluetoothHealthProfileHandler { BluetoothHealth.STATE_CHANNEL_CONNECTING, BluetoothHealth.STATE_CHANNEL_CONNECTED}); if (chan.isEmpty()) { - updateAndsendIntent(device, currDeviceState, newDeviceState); + updateAndSendIntent(device, currDeviceState, newDeviceState); } + break; case BluetoothHealth.STATE_DISCONNECTING: // Channel got disconnected. chan = findChannelByStates(device, new int [] { BluetoothHealth.STATE_CHANNEL_CONNECTING, BluetoothHealth.STATE_CHANNEL_DISCONNECTING}); if (chan.isEmpty()) { - updateAndsendIntent(device, currDeviceState, newDeviceState); + updateAndSendIntent(device, currDeviceState, newDeviceState); } break; } } } - private void updateAndsendIntent(BluetoothDevice device, int prevDeviceState, + private void updateAndSendIntent(BluetoothDevice device, int prevDeviceState, int newDeviceState) { mHealthDevices.put(device, newDeviceState); mBluetoothService.sendConnectionStateChange(device, prevDeviceState, newDeviceState); diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index b5ae298..ff16c18 100644..100755 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -24,8 +24,6 @@ package android.server; -import com.android.internal.app.IBatteryStats; - import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; @@ -40,6 +38,7 @@ import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetooth; import android.bluetooth.IBluetoothCallback; +import android.bluetooth.IBluetoothHealthCallback; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -58,14 +57,21 @@ import android.provider.Settings; import android.util.Log; import android.util.Pair; +import com.android.internal.app.IBatteryStats; + import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.DataInputStream; +import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; +import java.io.InputStreamReader; import java.io.IOException; import java.io.PrintWriter; +import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -151,6 +157,9 @@ public class BluetoothService extends IBluetooth.Stub { private BluetoothPanProfileHandler mBluetoothPanProfileHandler; private BluetoothInputProfileHandler mBluetoothInputProfileHandler; private BluetoothHealthProfileHandler mBluetoothHealthProfileHandler; + private static final String INCOMING_CONNECTION_FILE = + "/data/misc/bluetooth/incoming_connection.conf"; + private HashMap<String, Pair<Integer, String>> mIncomingConnections; private static class RemoteService { public String address; @@ -224,6 +233,7 @@ public class BluetoothService extends IBluetooth.Stub { mBluetoothInputProfileHandler = BluetoothInputProfileHandler.getInstance(mContext, this); mBluetoothPanProfileHandler = BluetoothPanProfileHandler.getInstance(mContext, this); mBluetoothHealthProfileHandler = BluetoothHealthProfileHandler.getInstance(mContext, this); + mIncomingConnections = new HashMap<String, Pair<Integer, String>>(); } public static synchronized String readDockBluetoothAddress() { @@ -2069,6 +2079,24 @@ public class BluetoothService extends IBluetooth.Stub { } } + public boolean allowIncomingHidConnect(BluetoothDevice device, boolean allow) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + String address = device.getAddress(); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + + Integer data = getAuthorizationAgentRequestData(address); + if (data == null) { + Log.w(TAG, "allowIncomingHidConnect(" + device + + ") called but no native data available"); + return false; + } + if (DBG) log("allowIncomingHidConnect: " + device + " : " + allow + " : " + data); + return setAuthorizationNative(address, allow, data.intValue()); + } + /*package*/List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { synchronized (mBluetoothInputProfileHandler) { return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states); @@ -2084,11 +2112,12 @@ public class BluetoothService extends IBluetooth.Stub { /**** Handlers for Health Device Profile ****/ // TODO: All these need to be converted to a state machine. - public boolean registerAppConfiguration(BluetoothHealthAppConfiguration config) { + public boolean registerAppConfiguration(BluetoothHealthAppConfiguration config, + IBluetoothHealthCallback callback) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); synchronized (mBluetoothHealthProfileHandler) { - return mBluetoothHealthProfileHandler.registerAppConfiguration(config); + return mBluetoothHealthProfileHandler.registerAppConfiguration(config, callback); } } @@ -2181,6 +2210,17 @@ public class BluetoothService extends IBluetooth.Stub { } } + /*package*/boolean notifyIncomingHidConnection(String address) { + BluetoothDeviceProfileState state = mDeviceProfileState.get(address); + if (state == null) { + return false; + } + Message msg = new Message(); + msg.what = BluetoothDeviceProfileState.CONNECT_HID_INCOMING; + state.sendMessage(msg); + return true; + } + public boolean connectHeadset(String address) { if (getBondState(address) != BluetoothDevice.BOND_BONDED) return false; @@ -2319,6 +2359,11 @@ public class BluetoothService extends IBluetooth.Stub { mA2dpService = a2dpService; } + /*package*/ Integer getAuthorizationAgentRequestData(String address) { + Integer data = mEventLoop.getAuthorizationAgentRequestData().remove(address); + return data; + } + public void sendProfileStateMessage(int profile, int cmd) { Message msg = new Message(); msg.what = cmd; @@ -2358,6 +2403,7 @@ public class BluetoothService extends IBluetooth.Stub { convertToAdapterState(state)); intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE, convertToAdapterState(prevState)); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); Log.d(TAG, "CONNECTION_STATE_CHANGE: " + device + ": " + prevState + " -> " + state); @@ -2422,6 +2468,120 @@ public class BluetoothService extends IBluetooth.Stub { } } + private void createIncomingConnectionStateFile() { + File f = new File(INCOMING_CONNECTION_FILE); + if (!f.exists()) { + try { + f.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "IOException: cannot create file"); + } + } + } + + /** @hide */ + public Pair<Integer, String> getIncomingState(String address) { + if (mIncomingConnections.isEmpty()) { + createIncomingConnectionStateFile(); + readIncomingConnectionState(); + } + return mIncomingConnections.get(address); + } + + private void readIncomingConnectionState() { + synchronized(mIncomingConnections) { + FileInputStream fstream = null; + try { + fstream = new FileInputStream(INCOMING_CONNECTION_FILE); + DataInputStream in = new DataInputStream(fstream); + BufferedReader file = new BufferedReader(new InputStreamReader(in)); + String line; + while((line = file.readLine()) != null) { + line = line.trim(); + if (line.length() == 0) continue; + String[] value = line.split(","); + if (value != null && value.length == 3) { + Integer val1 = Integer.parseInt(value[1]); + Pair<Integer, String> val = new Pair(val1, value[2]); + mIncomingConnections.put(value[0], val); + } + } + } catch (FileNotFoundException e) { + log("FileNotFoundException: readIncomingConnectionState" + e.toString()); + } catch (IOException e) { + log("IOException: readIncomingConnectionState" + e.toString()); + } finally { + if (fstream != null) { + try { + fstream.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + } + + private void truncateIncomingConnectionFile() { + RandomAccessFile r = null; + try { + r = new RandomAccessFile(INCOMING_CONNECTION_FILE, "rw"); + r.setLength(0); + } catch (FileNotFoundException e) { + log("FileNotFoundException: truncateIncomingConnectionState" + e.toString()); + } catch (IOException e) { + log("IOException: truncateIncomingConnectionState" + e.toString()); + } finally { + if (r != null) { + try { + r.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + /** @hide */ + public void writeIncomingConnectionState(String address, Pair<Integer, String> data) { + synchronized(mIncomingConnections) { + mIncomingConnections.put(address, data); + + truncateIncomingConnectionFile(); + BufferedWriter out = null; + StringBuilder value = new StringBuilder(); + try { + out = new BufferedWriter(new FileWriter(INCOMING_CONNECTION_FILE, true)); + for (String devAddress: mIncomingConnections.keySet()) { + Pair<Integer, String> val = mIncomingConnections.get(devAddress); + value.append(devAddress); + value.append(","); + value.append(val.first.toString()); + value.append(","); + value.append(val.second); + value.append("\n"); + } + out.write(value.toString()); + } catch (FileNotFoundException e) { + log("FileNotFoundException: writeIncomingConnectionState" + e.toString()); + } catch (IOException e) { + log("IOException: writeIncomingConnectionState" + e.toString()); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + private native static void classInitNative(); private native void initializeNativeDataNative(); private native boolean setupNativeDataNative(); @@ -2468,6 +2628,7 @@ public class BluetoothService extends IBluetooth.Stub { short channel); private native boolean removeServiceRecordNative(int handle); private native boolean setLinkTimeoutNative(String path, int num_slots); + native boolean connectInputDeviceNative(String path); native boolean disconnectInputDeviceNative(String path); @@ -2491,4 +2652,5 @@ public class BluetoothService extends IBluetooth.Stub { native String getChannelApplicationNative(String channelPath); native ParcelFileDescriptor getChannelFdNative(String channelPath); native boolean releaseChannelFdNative(String channelPath); + native boolean setAuthorizationNative(String address, boolean value, int data); } diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index c7603ee..96864c4 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -17,6 +17,7 @@ package android.speech.tts; import android.media.AudioFormat; import android.media.AudioTrack; +import android.text.TextUtils; import android.util.Log; import java.util.Iterator; @@ -25,14 +26,14 @@ import java.util.concurrent.atomic.AtomicLong; class AudioPlaybackHandler { private static final String TAG = "TTS.AudioPlaybackHandler"; + private static final boolean DBG_THREADING = false; private static final boolean DBG = false; private static final int MIN_AUDIO_BUFFER_SIZE = 8192; private static final int SYNTHESIS_START = 1; private static final int SYNTHESIS_DATA_AVAILABLE = 2; - private static final int SYNTHESIS_COMPLETE_DATA_AVAILABLE = 3; - private static final int SYNTHESIS_DONE = 4; + private static final int SYNTHESIS_DONE = 3; private static final int PLAY_AUDIO = 5; private static final int PLAY_SILENCE = 6; @@ -65,74 +66,105 @@ class AudioPlaybackHandler { * Stops all synthesis for a given {@code token}. If the current token * is currently being processed, an effort will be made to stop it but * that is not guaranteed. + * + * NOTE: This assumes that all other messages in the queue with {@code token} + * have been removed already. + * + * NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}. */ - synchronized public void stop(MessageParams token) { + private void stop(MessageParams token) { if (token == null) { return; } - removeMessages(token); + if (DBG) Log.d(TAG, "Stopping token : " + token); if (token.getType() == MessageParams.TYPE_SYNTHESIS) { AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack(); if (current != null) { // Stop the current audio track if it's still playing. - // The audio track is thread safe in this regard. + // The audio track is thread safe in this regard. The current + // handleSynthesisDataAvailable call will return soon after this + // call. current.stop(); } + // This is safe because PlaybackSynthesisCallback#stop would have + // been called before this method, and will no longer enqueue any + // audio for this token. + // + // (Even if it did, all it would result in is a warning message). mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY)); - } else { - final MessageParams current = getCurrentParams(); - - if (current != null) { - if (token.getType() == MessageParams.TYPE_AUDIO) { - ((AudioMessageParams) current).getPlayer().stop(); - } else if (token.getType() == MessageParams.TYPE_SILENCE) { - ((SilenceMessageParams) current).getConditionVariable().open(); - } - } + } else if (token.getType() == MessageParams.TYPE_AUDIO) { + ((AudioMessageParams) token).getPlayer().stop(); + // No cleanup required for audio messages. + } else if (token.getType() == MessageParams.TYPE_SILENCE) { + ((SilenceMessageParams) token).getConditionVariable().open(); + // No cleanup required for silence messages. } } + // ----------------------------------------------------- + // Methods that add and remove elements from the queue. These do not + // need to be synchronized strictly speaking, but they make the behaviour + // a lot more predictable. (though it would still be correct without + // synchronization). + // ----------------------------------------------------- + synchronized public void removePlaybackItems(String callingApp) { + if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callingApp); removeMessages(callingApp); - stop(getCurrentParams()); + + final MessageParams current = getCurrentParams(); + if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { + stop(current); + } } synchronized public void removeAllItems() { + if (DBG_THREADING) Log.d(TAG, "Removing all items"); removeAllMessages(); stop(getCurrentParams()); } /** + * @return false iff the queue is empty and no queue item is currently + * being handled, true otherwise. + */ + public boolean isSpeaking() { + return (mQueue.peek() != null) || (mCurrentParams != null); + } + + /** * Shut down the audio playback thread. */ synchronized public void quit() { + removeAllMessages(); stop(getCurrentParams()); mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY)); } - void enqueueSynthesisStart(SynthesisMessageParams token) { + synchronized void enqueueSynthesisStart(SynthesisMessageParams token) { + if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token); mQueue.add(new ListEntry(SYNTHESIS_START, token)); } - void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { + synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { + if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token); mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); } - void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) { - mQueue.add(new ListEntry(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token)); - } - - void enqueueSynthesisDone(SynthesisMessageParams token) { + synchronized void enqueueSynthesisDone(SynthesisMessageParams token) { + if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token); mQueue.add(new ListEntry(SYNTHESIS_DONE, token)); } - void enqueueAudio(AudioMessageParams token) { + synchronized void enqueueAudio(AudioMessageParams token) { + if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token); mQueue.add(new ListEntry(PLAY_AUDIO, token)); } - void enqueueSilence(SilenceMessageParams token) { + synchronized void enqueueSilence(SilenceMessageParams token) { + if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token); mQueue.add(new ListEntry(PLAY_SILENCE, token)); } @@ -177,26 +209,6 @@ class AudioPlaybackHandler { } /* - * Remove all messages from the queue that contain the supplied token. - * Note that the Iterator is thread safe, and other methods can safely - * continue adding to the queue at this point. - */ - synchronized private void removeMessages(MessageParams token) { - if (token == null) { - return; - } - - Iterator<ListEntry> it = mQueue.iterator(); - - while (it.hasNext()) { - final ListEntry current = it.next(); - if (current.mMessage == token) { - it.remove(); - } - } - } - - /* * Atomically clear the queue of all messages. */ synchronized private void removeAllMessages() { @@ -260,6 +272,13 @@ class AudioPlaybackHandler { } private void setCurrentParams(MessageParams p) { + if (DBG_THREADING) { + if (p != null) { + Log.d(TAG, "Started handling :" + p); + } else { + Log.d(TAG, "End handling : " + mCurrentParams); + } + } mCurrentParams = p; } @@ -280,8 +299,6 @@ class AudioPlaybackHandler { handleSynthesisDataAvailable(msg); } else if (entry.mWhat == SYNTHESIS_DONE) { handleSynthesisDone(msg); - } else if (entry.mWhat == SYNTHESIS_COMPLETE_DATA_AVAILABLE) { - handleSynthesisCompleteDataAvailable(msg); } else if (entry.mWhat == PLAY_AUDIO) { handleAudio(msg); } else if (entry.mWhat == PLAY_SILENCE) { @@ -352,7 +369,7 @@ class AudioPlaybackHandler { private void handleSynthesisDataAvailable(MessageParams msg) { final SynthesisMessageParams param = (SynthesisMessageParams) msg; if (param.getAudioTrack() == null) { - Log.w(TAG, "Error : null audio track in handleDataAvailable."); + Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param); return; } @@ -384,21 +401,28 @@ class AudioPlaybackHandler { } count += written; } + param.mBytesWritten += count; + param.mLogger.onPlaybackStart(); } private void handleSynthesisDone(MessageParams msg) { final SynthesisMessageParams params = (SynthesisMessageParams) msg; handleSynthesisDone(params); + // This call is delayed more than it should be, but we are + // certain at this point that we have all the data we want. + params.mLogger.onWriteData(); } - // Flush all remaining data to the audio track, stop it and release - // all it's resources. + // Wait for the audio track to stop playing, and then release it's resources. private void handleSynthesisDone(SynthesisMessageParams params) { if (DBG) Log.d(TAG, "handleSynthesisDone()"); final AudioTrack audioTrack = params.getAudioTrack(); try { if (audioTrack != null) { + if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + + audioTrack.hashCode()); + blockUntilDone(params); if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]"); // The last call to AudioTrack.write( ) will return only after // all data from the audioTrack has been sent to the mixer, so @@ -412,49 +436,25 @@ class AudioPlaybackHandler { } } - private void handleSynthesisCompleteDataAvailable(MessageParams msg) { - final SynthesisMessageParams params = (SynthesisMessageParams) msg; - if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")"); - - // Channel config and bytes per frame are checked before - // this message is sent. - int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount); - int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat); - - SynthesisMessageParams.ListEntry entry = params.getNextBuffer(); - - if (entry == null) { - Log.w(TAG, "completeDataAvailable : No buffers available to play."); + private static void blockUntilDone(SynthesisMessageParams params) { + if (params.mAudioTrack == null || params.mBytesWritten <= 0) { return; } - final AudioTrack audioTrack = new AudioTrack(params.mStreamType, params.mSampleRateInHz, - channelConfig, params.mAudioFormat, entry.mLength, AudioTrack.MODE_STATIC); - - // So that handleDone can access this correctly. - params.mAudioTrack = audioTrack; - - try { - audioTrack.write(entry.mBytes, entry.mOffset, entry.mLength); - setupVolume(audioTrack, params.mVolume, params.mPan); - audioTrack.play(); - blockUntilDone(audioTrack, bytesPerFrame, entry.mLength); - if (DBG) Log.d(TAG, "Wrote data to audio track successfully : " + entry.mLength); - } catch (IllegalStateException ex) { - Log.e(TAG, "Playback error", ex); - } finally { - handleSynthesisDone(msg); - } - } - + final AudioTrack audioTrack = params.mAudioTrack; + final int bytesPerFrame = getBytesPerFrame(params.mAudioFormat); + final int lengthInBytes = params.mBytesWritten; + final int lengthInFrames = lengthInBytes / bytesPerFrame; - private static void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) { - int lengthInFrames = length / bytesPerFrame; int currentPosition = 0; while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) { + if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { + break; + } + long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / audioTrack.getSampleRate(); - audioTrack.getPlayState(); + if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," + " Playback position : " + currentPosition); try { diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags new file mode 100644 index 0000000..1a9f5fe --- /dev/null +++ b/core/java/android/speech/tts/EventLogTags.logtags @@ -0,0 +1,6 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package android.speech.tts; + +76001 tts_speak_success (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) +76002 tts_speak_failure (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1) diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java new file mode 100644 index 0000000..63b954b --- /dev/null +++ b/core/java/android/speech/tts/EventLogger.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.os.SystemClock; +import android.text.TextUtils; + +/** + * Writes data about a given speech synthesis request to the event logs. + * The data that is logged includes the calling app, length of the utterance, + * speech rate / pitch and the latency and overall time taken. + * + * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} + * might be called from any thread, but on {@link EventLogger#onPlaybackStart()} and + * {@link EventLogger#onComplete()} must be called from a single thread + * (usually the audio playback thread} + */ +class EventLogger { + private final SynthesisRequest mRequest; + private final String mCallingApp; + private final String mServiceApp; + private final long mReceivedTime; + private long mPlaybackStartTime = -1; + private volatile long mRequestProcessingStartTime = -1; + private volatile long mEngineStartTime = -1; + private volatile long mEngineCompleteTime = -1; + + private volatile boolean mError = false; + private volatile boolean mStopped = false; + private boolean mLogWritten = false; + + EventLogger(SynthesisRequest request, String callingApp, + String serviceApp) { + mRequest = request; + mCallingApp = callingApp; + mServiceApp = serviceApp; + mReceivedTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that this request has been selected from + * the processing queue for processing. Engine latency / total time + * is measured from this baseline. + */ + public void onRequestProcessingStart() { + mRequestProcessingStartTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that a chunk of data has been received from + * the engine. Might be called multiple times. + */ + public void onEngineDataReceived() { + if (mEngineStartTime == -1) { + mEngineStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the engine has finished processing data. + * Will be called exactly once. + */ + public void onEngineComplete() { + mEngineCompleteTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that audio playback has started for some section + * of the synthesis. This is normally some amount of time after the engine + * has synthesized data and varides depending on utterances and + * other audio currently in the queue. + */ + public void onPlaybackStart() { + // For now, keep track of only the first chunk of audio + // that was played. + if (mPlaybackStartTime == -1) { + mPlaybackStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the current synthesis was stopped. + * Latency numbers are not reported for stopped syntheses. + */ + public void onStopped() { + mStopped = false; + } + + /** + * Notifies the logger that the current synthesis resulted in + * an error. This is logged using {@link EventLogTags#writeTtsSpeakFailure}. + */ + public void onError() { + mError = true; + } + + /** + * Notifies the logger that the current synthesis has completed. + * All available data is not logged. + */ + public void onWriteData() { + if (mLogWritten) { + return; + } else { + mLogWritten = true; + } + + long completionTime = SystemClock.elapsedRealtime(); + // onPlaybackStart() should normally always be called if an + // error does not occur. + if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { + EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallingApp, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch()); + return; + } + + // We don't report stopped syntheses because their overall + // total time spent will be innacurate (will not correlate with + // the length of the utterance). + if (mStopped) { + return; + } + + final long audioLatency = mPlaybackStartTime - mReceivedTime; + final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; + final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; + EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallingApp, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch(), + engineLatency, engineTotal, audioLatency); + } + + /** + * @return the length of the utterance for the given synthesis, 0 + * if the utterance was {@code null}. + */ + private int getUtteranceLength() { + final String utterance = mRequest.getText(); + return utterance == null ? 0 : utterance.length(); + } + + /** + * Returns a formatted locale string from the synthesis params of the + * form lang-country-variant. + */ + private String getLocaleString() { + StringBuilder sb = new StringBuilder(mRequest.getLanguage()); + if (!TextUtils.isEmpty(mRequest.getCountry())) { + sb.append('-'); + sb.append(mRequest.getCountry()); + + if (!TextUtils.isEmpty(mRequest.getVariant())) { + sb.append('-'); + sb.append(mRequest.getVariant()); + } + } + + return sb.toString(); + } + +} diff --git a/core/java/android/speech/tts/FileSynthesisCallback.java b/core/java/android/speech/tts/FileSynthesisCallback.java index 4f4b3fb..5808919 100644 --- a/core/java/android/speech/tts/FileSynthesisCallback.java +++ b/core/java/android/speech/tts/FileSynthesisCallback.java @@ -187,37 +187,6 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { } } - @Override - public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, - byte[] buffer, int offset, int length) { - synchronized (mStateLock) { - if (mStopped) { - if (DBG) Log.d(TAG, "Request has been aborted."); - return TextToSpeech.ERROR; - } - } - FileOutputStream out = null; - try { - out = new FileOutputStream(mFileName); - out.write(makeWavHeader(sampleRateInHz, audioFormat, channelCount, length)); - out.write(buffer, offset, length); - mDone = true; - return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); - mFileName.delete(); - return TextToSpeech.ERROR; - } finally { - try { - if (out != null) { - out.close(); - } - } catch (IOException ex) { - Log.e(TAG, "Failed to close " + mFileName + ": " + ex); - } - } - } - private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount, int dataLength) { // TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT? diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java index 4c1b6d2..e7d6da3 100644 --- a/core/java/android/speech/tts/MessageParams.java +++ b/core/java/android/speech/tts/MessageParams.java @@ -38,5 +38,10 @@ abstract class MessageParams { return mCallingApp; } + @Override + public String toString() { + return "MessageParams[" + hashCode() + "]"; + } + abstract int getType(); } diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index bdaa1b8..7dbf1ac 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -53,8 +53,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { // Handler associated with a thread that plays back audio requests. private final AudioPlaybackHandler mAudioTrackHandler; - // A request "token", which will be non null after start() or - // completeAudioAvailable() have been called. + // A request "token", which will be non null after start() has been called. private SynthesisMessageParams mToken = null; // Whether this request has been stopped. This is useful for keeping // track whether stop() has been called before start(). In all other cases, @@ -65,29 +64,40 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private final UtteranceCompletedDispatcher mDispatcher; private final String mCallingApp; + private final EventLogger mLogger; PlaybackSynthesisCallback(int streamType, float volume, float pan, AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher, - String callingApp) { + String callingApp, EventLogger logger) { mStreamType = streamType; mVolume = volume; mPan = pan; mAudioTrackHandler = audioTrackHandler; mDispatcher = dispatcher; mCallingApp = callingApp; + mLogger = logger; } @Override void stop() { if (DBG) Log.d(TAG, "stop()"); + // Note that mLogger.mError might be true too at this point. + mLogger.onStopped(); + synchronized (mStateLock) { - if (mToken == null || mStopped) { - Log.w(TAG, "stop() called twice, before start(), or after done()"); + if (mStopped) { + Log.w(TAG, "stop() called twice"); return; } - mAudioTrackHandler.stop(mToken); - mToken = null; + + // mToken will be null if the engine encounters + // an error before it called start(). + if (mToken == null) { + // In all other cases, mAudioTrackHandler.stop() will + // result in onComplete being called. + mLogger.onWriteData(); + } mStopped = true; } } @@ -124,7 +134,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } SynthesisMessageParams params = new SynthesisMessageParams( mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher, mCallingApp); + mDispatcher, mCallingApp, mLogger); mAudioTrackHandler.enqueueSynthesisStart(params); mToken = params; @@ -146,7 +156,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } synchronized (mStateLock) { - if (mToken == null) { + if (mToken == null || mStopped) { return TextToSpeech.ERROR; } @@ -157,6 +167,8 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { mAudioTrackHandler.enqueueSynthesisDataAvailable(mToken); } + mLogger.onEngineDataReceived(); + return TextToSpeech.SUCCESS; } @@ -177,6 +189,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } mAudioTrackHandler.enqueueSynthesisDone(mToken); + mLogger.onEngineComplete(); } return TextToSpeech.SUCCESS; } @@ -184,38 +197,10 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { @Override public void error() { if (DBG) Log.d(TAG, "error() [will call stop]"); + // Currently, this call will not be logged if error( ) is called + // before start. + mLogger.onError(); stop(); } - @Override - public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, - byte[] buffer, int offset, int length) { - int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); - if (channelConfig == 0) { - Log.e(TAG, "Unsupported number of channels :" + channelCount); - return TextToSpeech.ERROR; - } - - int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(audioFormat); - if (bytesPerFrame < 0) { - Log.e(TAG, "Unsupported audio format :" + audioFormat); - return TextToSpeech.ERROR; - } - - synchronized (mStateLock) { - if (mStopped) { - return TextToSpeech.ERROR; - } - SynthesisMessageParams params = new SynthesisMessageParams( - mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, - mDispatcher, mCallingApp); - params.addBuffer(buffer, offset, length); - - mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params); - mToken = params; - } - - return TextToSpeech.SUCCESS; - } - } diff --git a/core/java/android/speech/tts/SynthesisCallback.java b/core/java/android/speech/tts/SynthesisCallback.java index 1b80e40..d70c371 100644 --- a/core/java/android/speech/tts/SynthesisCallback.java +++ b/core/java/android/speech/tts/SynthesisCallback.java @@ -22,19 +22,16 @@ package android.speech.tts; * {@link #start}, then {@link #audioAvailable} until all audio has been provided, then finally * {@link #done}. * - * Alternatively, the engine can provide all the audio at once, by using - * {@link #completeAudioAvailable}. * * {@link #error} can be called at any stage in the synthesis process to - * indicate that an error has occured, but if the call is made after a call - * to {@link #done} or {@link #completeAudioAvailable} it might be discarded. + * indicate that an error has occurred, but if the call is made after a call + * to {@link #done}, it might be discarded. */ public interface SynthesisCallback { /** * @return the maximum number of bytes that the TTS engine can pass in a single call of - * {@link #audioAvailable}. This does not apply to {@link #completeAudioAvailable}. - * Calls to {@link #audioAvailable} with data lengths larger than this - * value will not succeed. + * {@link #audioAvailable}. Calls to {@link #audioAvailable} with data lengths + * larger than this value will not succeed. */ public int getMaxBufferSize(); @@ -69,23 +66,6 @@ public interface SynthesisCallback { public int audioAvailable(byte[] buffer, int offset, int length); /** - * The service can call this method instead of using {@link #start}, {@link #audioAvailable} - * and {@link #done} if all the audio data is available in a single buffer. - * - * @param sampleRateInHz Sample rate in HZ of the generated audio. - * @param audioFormat Audio format of the generated audio. Must be one of - * the ENCODING_ constants defined in {@link android.media.AudioFormat}. - * @param channelCount The number of channels. Must be {@code 1} or {@code 2}. - * @param buffer The generated audio data. This method will not hold on to {@code buffer}, - * so the caller is free to modify it after this method returns. - * @param offset The offset into {@code buffer} where the audio data starts. - * @param length The number of bytes of audio data in {@code buffer}. - * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. - */ - public int completeAudioAvailable(int sampleRateInHz, int audioFormat, - int channelCount, byte[] buffer, int offset, int length); - - /** * The service should call this method when all the synthesized audio for a request has * been passed to {@link #audioAvailable}. * diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java index 51f3d2e..ffe70e2 100644 --- a/core/java/android/speech/tts/SynthesisMessageParams.java +++ b/core/java/android/speech/tts/SynthesisMessageParams.java @@ -30,15 +30,18 @@ final class SynthesisMessageParams extends MessageParams { final int mChannelCount; final float mVolume; final float mPan; + final EventLogger mLogger; - public volatile AudioTrack mAudioTrack; + volatile AudioTrack mAudioTrack; + // Not volatile, accessed only from the synthesis thread. + int mBytesWritten; private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); SynthesisMessageParams(int streamType, int sampleRate, int audioFormat, int channelCount, float volume, float pan, UtteranceCompletedDispatcher dispatcher, - String callingApp) { + String callingApp, EventLogger logger) { super(dispatcher, callingApp); mStreamType = streamType; @@ -47,9 +50,11 @@ final class SynthesisMessageParams extends MessageParams { mChannelCount = channelCount; mVolume = volume; mPan = pan; + mLogger = logger; // initially null. mAudioTrack = null; + mBytesWritten = 0; } @Override diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 8e4725f..5126e48 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -109,6 +109,11 @@ public class TextToSpeech { /** * Broadcast Action: The TextToSpeech synthesizer has completed processing * of all the text in the speech queue. + * + * Note that this notifies callers when the <b>engine</b> has finished has + * processing text data. Audio playback might not have completed (or even started) + * at this point. If you wish to be notified when this happens, see + * {@link OnUtteranceCompletedListener}. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_TTS_QUEUE_PROCESSING_COMPLETED = @@ -181,7 +186,7 @@ public class TextToSpeech { * * @hide * @deprecated No longer in use, the default engine is determined by - * the sort order defined in {@link EngineInfoComparator}. Note that + * the sort order defined in {@link TtsEngines}. Note that * this doesn't "break" anything because there is no guarantee that * the engine specified below is installed on a given build, let * alone be the default. @@ -504,36 +509,39 @@ public class TextToSpeech { } private int initTts() { - String defaultEngine = getDefaultEngine(); - String engine = defaultEngine; - if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) { - engine = mRequestedEngine; - } - - // Try requested engine - if (connectToEngine(engine)) { - mCurrentEngine = engine; - return SUCCESS; + // Step 1: Try connecting to the engine that was requested. + if (mRequestedEngine != null && mEnginesHelper.isEngineInstalled(mRequestedEngine)) { + if (connectToEngine(mRequestedEngine)) { + mCurrentEngine = mRequestedEngine; + return SUCCESS; + } } - // Fall back to user's default engine if different from the already tested one - if (!engine.equals(defaultEngine)) { + // Step 2: Try connecting to the user's default engine. + final String defaultEngine = getDefaultEngine(); + if (defaultEngine != null && !defaultEngine.equals(mRequestedEngine)) { if (connectToEngine(defaultEngine)) { - mCurrentEngine = engine; + mCurrentEngine = defaultEngine; return SUCCESS; } } + // Step 3: Try connecting to the highest ranked engine in the + // system. final String highestRanked = mEnginesHelper.getHighestRankedEngineName(); - // Fall back to the hardcoded default if different from the two above - if (!defaultEngine.equals(highestRanked) - && !engine.equals(highestRanked)) { + if (highestRanked != null && !highestRanked.equals(mRequestedEngine) && + !highestRanked.equals(defaultEngine)) { if (connectToEngine(highestRanked)) { - mCurrentEngine = engine; + mCurrentEngine = highestRanked; return SUCCESS; } } + // NOTE: The API currently does not allow the caller to query whether + // they are actually connected to any engine. This might fail for various + // reasons like if the user disables all her TTS engines. + + mCurrentEngine = null; dispatchOnInit(ERROR); return ERROR; } @@ -573,6 +581,14 @@ public class TextToSpeech { service.setCallback(getPackageName(), null); service.stop(getPackageName()); mServiceConnection.disconnect(); + // Context#unbindService does not result in a call to + // ServiceConnection#onServiceDisconnected. As a result, the + // service ends up being destroyed (if there are no other open + // connections to it) but the process lives on and the + // ServiceConnection continues to refer to the destroyed service. + // + // This leads to tons of log spam about SynthThread being dead. + mServiceConnection = null; mCurrentEngine = null; return null; } @@ -793,7 +809,10 @@ public class TextToSpeech { } /** - * Checks whether the TTS engine is busy speaking. + * Checks whether the TTS engine is busy speaking. Note that a speech item is + * considered complete once it's audio data has been sent to the audio mixer, or + * written to a file. There might be a finite lag between this point, and when + * the audio hardware completes playback. * * @return {@code true} if the TTS engine is speaking. */ @@ -963,7 +982,7 @@ public class TextToSpeech { /** * Synthesizes the given text to a file using the specified parameters. * - * @param text Thetext that should be synthesized + * @param text The text that should be synthesized * @param params Parameters for the request. Can be null. * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. @@ -1073,7 +1092,9 @@ public class TextToSpeech { * * @deprecated This doesn't inform callers when the TTS engine has been * initialized. {@link #TextToSpeech(Context, OnInitListener, String)} - * can be used with the appropriate engine name. + * can be used with the appropriate engine name. Also, there is no + * guarantee that the engine specified will be loaded. If it isn't + * installed or disabled, the user / system wide defaults will apply. * * @param enginePackageName The package name for the synthesis engine (e.g. "com.svox.pico") * diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 7ea9373..1926c92 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -82,8 +82,7 @@ public abstract class TextToSpeechService extends Service { private AudioPlaybackHandler mAudioPlaybackHandler; private CallbackMap mCallbacks; - - private int mDefaultAvailability = TextToSpeech.LANG_NOT_SUPPORTED; + private String mPackageName; @Override public void onCreate() { @@ -99,9 +98,10 @@ public abstract class TextToSpeechService extends Service { mCallbacks = new CallbackMap(); + mPackageName = getApplicationInfo().packageName; + // Load default language - mDefaultAvailability = onLoadLanguage(getDefaultLanguage(), - getDefaultCountry(), getDefaultVariant()); + onLoadLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant()); } @Override @@ -282,6 +282,8 @@ public abstract class TextToSpeechService extends Service { if (current != null) { current.stop(); } + + // The AudioPlaybackHandler will be destroyed by the caller. } /** @@ -337,6 +339,8 @@ public abstract class TextToSpeechService extends Service { } removeCallbacksAndMessages(callingApp); + // This stops writing data to the file / or publishing + // items to the audio playback handler. SpeechItem current = setCurrentSpeechItem(null); if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { current.stop(); @@ -457,12 +461,14 @@ public abstract class TextToSpeechService extends Service { // Non null after synthesis has started, and all accesses // guarded by 'this'. private AbstractSynthesisCallback mSynthesisCallback; + private final EventLogger mEventLogger; public SynthesisSpeechItem(String callingApp, Bundle params, String text) { super(callingApp, params); mText = text; mSynthesisRequest = new SynthesisRequest(mText, mParams); setRequestParams(mSynthesisRequest); + mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName); } public String getText() { @@ -485,6 +491,7 @@ public abstract class TextToSpeechService extends Service { @Override protected int playImpl() { AbstractSynthesisCallback synthesisCallback; + mEventLogger.onRequestProcessingStart(); synchronized (this) { mSynthesisCallback = createSynthesisCallback(); synthesisCallback = mSynthesisCallback; @@ -495,7 +502,7 @@ public abstract class TextToSpeechService extends Service { protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this, getCallingApp()); + mAudioPlaybackHandler, this, getCallingApp(), mEventLogger); } private void setRequestParams(SynthesisRequest request) { @@ -625,9 +632,7 @@ public abstract class TextToSpeechService extends Service { @Override protected void stopImpl() { - if (mToken != null) { - mAudioPlaybackHandler.stop(mToken); - } + // Do nothing. } } @@ -654,9 +659,7 @@ public abstract class TextToSpeechService extends Service { @Override protected void stopImpl() { - if (mToken != null) { - mAudioPlaybackHandler.stop(mToken); - } + // Do nothing. } } @@ -716,7 +719,7 @@ public abstract class TextToSpeechService extends Service { } public boolean isSpeaking() { - return mSynthHandler.isSpeaking(); + return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking(); } public int stop(String callingApp) { @@ -764,10 +767,6 @@ public abstract class TextToSpeechService extends Service { mCallbacks.setCallback(packageName, cb); } - private boolean isDefault(String lang, String country, String variant) { - return Locale.getDefault().equals(new Locale(lang, country, variant)); - } - private String intern(String in) { // The input parameter will be non null. return in.intern(); diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 757a8c3..5a244f1 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -226,7 +226,17 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback */ public static Metrics isBoring(CharSequence text, TextPaint paint) { - return isBoring(text, paint, null); + return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); + } + + /** + * Returns null if not boring; the width, ascent, and descent if boring. + * @hide + */ + public static Metrics isBoring(CharSequence text, + TextPaint paint, + TextDirectionHeuristic textDir) { + return isBoring(text, paint, textDir, null); } /** @@ -235,6 +245,17 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * if boring. */ public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { + return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics); + } + + /** + * Returns null if not boring; the width, ascent, and descent in the + * provided Metrics object (or a new one if the provided one was null) + * if boring. + * @hide + */ + public static Metrics isBoring(CharSequence text, TextPaint paint, + TextDirectionHeuristic textDir, Metrics metrics) { char[] temp = TextUtils.obtain(500); int length = text.length(); boolean boring = true; @@ -258,6 +279,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback break outer; } } + + if (textDir.isRtl(temp, 0, n)) { + boring = false; + break outer; + } } TextUtils.recycle(temp); diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index f196b34..cb96969 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -75,12 +75,31 @@ extends Layout float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { - super((ellipsize == null) - ? display - : (display instanceof Spanned) - ? new SpannedEllipsizer(display) + this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, + spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth); + } + + /** + * Make a layout for the transformed text (password transformation + * being the primary example of a transformation) + * that will be updated as the base text is changed. + * If ellipsize is non-null, the Layout will ellipsize the text + * down to ellipsizedWidth. + * * + * *@hide + */ + public DynamicLayout(CharSequence base, CharSequence display, + TextPaint paint, + int width, Alignment align, TextDirectionHeuristic textDir, + float spacingmult, float spacingadd, + boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + super((ellipsize == null) + ? display + : (display instanceof Spanned) + ? new SpannedEllipsizer(display) : new Ellipsizer(display), - paint, width, align, spacingmult, spacingadd); + paint, width, align, textDir, spacingmult, spacingadd); mBase = base; mDisplay = display; @@ -259,7 +278,7 @@ extends Layout reflowed = new StaticLayout(true); reflowed.generate(text, where, where + after, - getPaint(), getWidth(), getAlignment(), + getPaint(), getWidth(), getAlignment(), getTextDirectionHeuristic(), getSpacingMultiplier(), getSpacingAdd(), false, true, mEllipsizedWidth, mEllipsizeAt); int n = reflowed.getLineCount(); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index aae9ccf..eabeef0 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,8 +16,6 @@ package android.text; -import com.android.internal.util.ArrayUtils; - import android.emoji.EmojiFactory; import android.graphics.Canvas; import android.graphics.Paint; @@ -32,6 +30,8 @@ import android.text.style.ParagraphStyle; import android.text.style.ReplacementSpan; import android.text.style.TabStopSpan; +import com.android.internal.util.ArrayUtils; + import java.util.Arrays; /** @@ -113,6 +113,29 @@ public abstract class Layout { protected Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd) { + this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, + spacingMult, spacingAdd); + } + + /** + * Subclasses of Layout use this constructor to set the display text, + * width, and other standard properties. + * @param text the text to render + * @param paint the default paint for the layout. Styles can override + * various attributes of the paint. + * @param width the wrapping width for the text. + * @param align whether to left, right, or center the text. Styles can + * override the alignment. + * @param spacingMult factor by which to scale the font size to get the + * default line spacing + * @param spacingAdd amount to add to the default line spacing + * + * @hide + */ + protected Layout(CharSequence text, TextPaint paint, + int width, Alignment align, TextDirectionHeuristic textDir, + float spacingMult, float spacingAdd) { + if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); @@ -133,6 +156,7 @@ public abstract class Layout { mSpacingMult = spacingMult; mSpacingAdd = spacingAdd; mSpannedText = text instanceof Spanned; + mTextDir = textDir; } /** @@ -531,6 +555,14 @@ public abstract class Layout { } /** + * Return the heuristic used to determine paragraph text direction. + * @hide + */ + public final TextDirectionHeuristic getTextDirectionHeuristic() { + return mTextDir; + } + + /** * Return the number of lines of text in this layout. */ public abstract int getLineCount(); @@ -1419,7 +1451,7 @@ public abstract class Layout { MeasuredText mt = MeasuredText.obtain(); TextLine tl = TextLine.obtain(); try { - mt.setPara(text, start, end, DIR_REQUEST_LTR); + mt.setPara(text, start, end, TextDirectionHeuristics.LTR); Directions directions; int dir; if (mt.mEasy) { @@ -1769,6 +1801,7 @@ public abstract class Layout { private float mSpacingAdd; private static final Rect sTempRect = new Rect(); private boolean mSpannedText; + private TextDirectionHeuristic mTextDir; public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index a81be09..2920ac5 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -85,7 +85,7 @@ class MeasuredText { * Analyzes text for bidirectional runs. Allocates working buffers. */ /* package */ - void setPara(CharSequence text, int start, int end, int bidiRequest) { + void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) { mText = text; mTextStart = start; @@ -115,13 +115,29 @@ class MeasuredText { } } - if (TextUtils.doesNotNeedBidi(mChars, 0, len)) { + if ((textDir == TextDirectionHeuristics.LTR || + textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR || + textDir == TextDirectionHeuristics.ANYRTL_LTR) && + 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)]; } + int bidiRequest; + if (textDir == TextDirectionHeuristics.LTR) { + bidiRequest = Layout.DIR_REQUEST_LTR; + } else if (textDir == TextDirectionHeuristics.RTL) { + bidiRequest = Layout.DIR_REQUEST_RTL; + } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { + bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR; + } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { + bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; + } else { + boolean isRtl = textDir.isRtl(mChars, 0, len); + bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; + } mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); mEasy = false; } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 9e48eff..f7b9502 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,8 +16,6 @@ package android.text; -import com.android.internal.util.ArrayUtils; - import android.graphics.Bitmap; import android.graphics.Paint; import android.text.style.LeadingMarginSpan; @@ -26,6 +24,8 @@ import android.text.style.LineHeightSpan; import android.text.style.MetricAffectingSpan; import android.text.style.TabStopSpan; +import com.android.internal.util.ArrayUtils; + /** * StaticLayout is a Layout for text that will not be edited after it * is laid out. Use {@link DynamicLayout} for text that may change. @@ -46,6 +46,17 @@ public class StaticLayout extends Layout { spacingmult, spacingadd, includepad); } + /** + * @hide + */ + public StaticLayout(CharSequence source, TextPaint paint, + int width, Alignment align, TextDirectionHeuristic textDir, + float spacingmult, float spacingadd, + boolean includepad) { + this(source, 0, source.length(), paint, width, align, textDir, + spacingmult, spacingadd, includepad); + } + public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, @@ -55,9 +66,35 @@ public class StaticLayout extends Layout { spacingmult, spacingadd, includepad, null, 0); } + /** + * @hide + */ + public StaticLayout(CharSequence source, int bufstart, int bufend, + TextPaint paint, int outerwidth, + Alignment align, TextDirectionHeuristic textDir, + float spacingmult, float spacingadd, + boolean includepad) { + this(source, bufstart, bufend, paint, outerwidth, align, textDir, + spacingmult, spacingadd, includepad, null, 0); +} + + public StaticLayout(CharSequence source, int bufstart, int bufend, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + this(source, bufstart, bufend, paint, outerwidth, align, + TextDirectionHeuristics.FIRSTSTRONG_LTR, + spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth); + } + + /** + * @hide + */ public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, - Alignment align, + Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { @@ -66,7 +103,7 @@ public class StaticLayout extends Layout { : (source instanceof Spanned) ? new SpannedEllipsizer(source) : new Ellipsizer(source), - paint, outerwidth, align, spacingmult, spacingadd); + paint, outerwidth, align, textDir, spacingmult, spacingadd); /* * This is annoying, but we can't refer to the layout until @@ -96,7 +133,7 @@ public class StaticLayout extends Layout { mMeasured = MeasuredText.obtain(); - generate(source, bufstart, bufend, paint, outerwidth, align, + generate(source, bufstart, bufend, paint, outerwidth, align, textDir, spacingmult, spacingadd, includepad, includepad, ellipsizedWidth, ellipsize); @@ -116,7 +153,7 @@ public class StaticLayout extends Layout { /* package */ void generate(CharSequence source, int bufStart, int bufEnd, TextPaint paint, int outerWidth, - Alignment align, + Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, boolean trackpad, float ellipsizedWidth, TextUtils.TruncateAt ellipsize) { @@ -157,7 +194,7 @@ public class StaticLayout extends Layout { LeadingMarginSpan lms = sp[i]; firstWidth -= sp[i].getLeadingMargin(true); restWidth -= sp[i].getLeadingMargin(false); - + // 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 @@ -195,7 +232,7 @@ public class StaticLayout extends Layout { } } - measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); + measured.setPara(source, paraStart, paraEnd, textDir); char[] chs = measured.mChars; float[] widths = measured.mWidths; byte[] chdirs = measured.mLevels; diff --git a/core/java/android/text/TextDirectionHeuristic.java b/core/java/android/text/TextDirectionHeuristic.java new file mode 100644 index 0000000..130f879 --- /dev/null +++ b/core/java/android/text/TextDirectionHeuristic.java @@ -0,0 +1,13 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.text; + +/** + * Interface for objects that guess at the paragraph direction by examining text. + * + * @hide + */ +public interface TextDirectionHeuristic { + /** @hide */ boolean isRtl(CharSequence text, int start, int end); + /** @hide */ boolean isRtl(char[] text, int start, int count); +} diff --git a/core/java/android/text/TextDirectionHeuristics.java b/core/java/android/text/TextDirectionHeuristics.java new file mode 100644 index 0000000..5f9ffc5 --- /dev/null +++ b/core/java/android/text/TextDirectionHeuristics.java @@ -0,0 +1,310 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.text; + + +/** + * Some objects that implement TextDirectionHeuristic. + * @hide + */ +public class TextDirectionHeuristics { + + /** Always decides that the direction is left to right. */ + public static final TextDirectionHeuristic LTR = + new TextDirectionHeuristicInternal(null /* no algorithm */, false); + + /** Always decides that the direction is right to left. */ + public static final TextDirectionHeuristic RTL = + new TextDirectionHeuristicInternal(null /* no algorithm */, true); + + /** + * Determines the direction based on the first strong directional character, + * including bidi format chars, falling back to left to right if it + * finds none. This is the default behavior of the Unicode Bidirectional + * Algorithm. + */ + public static final TextDirectionHeuristic FIRSTSTRONG_LTR = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false); + + /** + * Determines the direction based on the first strong directional character, + * including bidi format chars, falling back to right to left if it + * finds none. This is similar to the default behavior of the Unicode + * Bidirectional Algorithm, just with different fallback behavior. + */ + public static final TextDirectionHeuristic FIRSTSTRONG_RTL = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true); + + /** + * If the text contains any strong right to left non-format character, determines + * that the direction is right to left, falling back to left to right if it + * finds none. + */ + public static final TextDirectionHeuristic ANYRTL_LTR = + new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false); + + /** + * If the text contains any strong left to right non-format character, determines + * that the direction is left to right, falling back to right to left if it + * finds none. + */ + public static final TextDirectionHeuristic ANYLTR_RTL = + new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_LTR, true); + + /** + * Examines only the strong directional non-format characters, and if either + * left to right or right to left characters are 60% or more of this total, + * determines that the direction follows the majority of characters. Falls + * back to left to right if neither direction meets this threshold. + */ + public static final TextDirectionHeuristic CHARCOUNT_LTR = + new TextDirectionHeuristicInternal(CharCount.INSTANCE_DEFAULT, false); + + /** + * Examines only the strong directional non-format characters, and if either + * left to right or right to left characters are 60% or more of this total, + * determines that the direction follows the majority of characters. Falls + * back to right to left if neither direction meets this threshold. + */ + public static final TextDirectionHeuristic CHARCOUNT_RTL = + new TextDirectionHeuristicInternal(CharCount.INSTANCE_DEFAULT, true); + + private static enum TriState { + TRUE, FALSE, UNKNOWN; + } + + /** + * Computes the text direction based on an algorithm. Subclasses implement + * {@link #defaultIsRtl} to handle cases where the algorithm cannot determine the + * direction from the text alone. + * @hide + */ + public static abstract class TextDirectionHeuristicImpl implements TextDirectionHeuristic { + private final TextDirectionAlgorithm mAlgorithm; + + public TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm) { + mAlgorithm = algorithm; + } + + /** + * Return true if the default text direction is rtl. + */ + abstract protected boolean defaultIsRtl(); + + @Override + public boolean isRtl(CharSequence text, int start, int end) { + if (text == null || start < 0 || end < start || text.length() < end) { + throw new IllegalArgumentException(); + } + if (mAlgorithm == null) { + return defaultIsRtl(); + } + text = text.subSequence(start, end); + char[] chars = text.toString().toCharArray(); + return doCheck(chars, 0, chars.length); + } + + @Override + public boolean isRtl(char[] chars, int start, int count) { + if (chars == null || start < 0 || count < 0 || chars.length - count < start) { + throw new IllegalArgumentException(); + } + if (mAlgorithm == null) { + return defaultIsRtl(); + } + return doCheck(chars, start, count); + } + + private boolean doCheck(char[] chars, int start, int count) { + switch(mAlgorithm.checkRtl(chars, start, count)) { + case TRUE: + return true; + case FALSE: + return false; + default: + return defaultIsRtl(); + } + } + } + + private static class TextDirectionHeuristicInternal extends TextDirectionHeuristicImpl { + private final boolean mDefaultIsRtl; + + private TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm, + boolean defaultIsRtl) { + super(algorithm); + mDefaultIsRtl = defaultIsRtl; + } + + @Override + protected boolean defaultIsRtl() { + return mDefaultIsRtl; + } + } + + private static TriState isRtlText(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + return TriState.FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return TriState.TRUE; + default: + return TriState.UNKNOWN; + } + } + + private static TriState isRtlTextOrFormat(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + return TriState.FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + return TriState.TRUE; + default: + return TriState.UNKNOWN; + } + } + + /** + * Interface for an algorithm to guess the direction of a paragraph of text. + * + * @hide + */ + public static interface TextDirectionAlgorithm { + /** + * Returns whether the range of text is RTL according to the algorithm. + * + * @hide + */ + TriState checkRtl(char[] text, int start, int count); + } + + /** + * Algorithm that uses the first strong directional character to determine + * the paragraph direction. This is the standard Unicode Bidirectional + * algorithm. + * + * @hide + */ + public static class FirstStrong implements TextDirectionAlgorithm { + @Override + public TriState checkRtl(char[] text, int start, int count) { + TriState result = TriState.UNKNOWN; + for (int i = start, e = start + count; i < e && result == TriState.UNKNOWN; ++i) { + result = isRtlTextOrFormat(Character.getDirectionality(text[i])); + } + return result; + } + + private FirstStrong() { + } + + public static final FirstStrong INSTANCE = new FirstStrong(); + } + + /** + * Algorithm that uses the presence of any strong directional non-format + * character (e.g. excludes LRE, LRO, RLE, RLO) to determine the + * direction of text. + * + * @hide + */ + public static class AnyStrong implements TextDirectionAlgorithm { + private final boolean mLookForRtl; + + @Override + public TriState checkRtl(char[] text, int start, int count) { + boolean haveUnlookedFor = false; + for (int i = start, e = start + count; i < e; ++i) { + switch (isRtlText(Character.getDirectionality(text[i]))) { + case TRUE: + if (mLookForRtl) { + return TriState.TRUE; + } + haveUnlookedFor = true; + break; + case FALSE: + if (!mLookForRtl) { + return TriState.FALSE; + } + haveUnlookedFor = true; + break; + default: + break; + } + } + if (haveUnlookedFor) { + return mLookForRtl ? TriState.FALSE : TriState.TRUE; + } + return TriState.UNKNOWN; + } + + private AnyStrong(boolean lookForRtl) { + this.mLookForRtl = lookForRtl; + } + + public static final AnyStrong INSTANCE_RTL = new AnyStrong(true); + public static final AnyStrong INSTANCE_LTR = new AnyStrong(false); + } + + /** + * Algorithm that uses the relative proportion of strong directional + * characters (excluding LRE, LRO, RLE, RLO) to determine the direction + * of the paragraph, if the proportion exceeds a given threshold. + * + * @hide + */ + public static class CharCount implements TextDirectionAlgorithm { + private final float mThreshold; + + @Override + public TriState checkRtl(char[] text, int start, int count) { + int countLtr = 0; + int countRtl = 0; + for(int i = start, e = start + count; i < e; ++i) { + switch (isRtlText(Character.getDirectionality(text[i]))) { + case TRUE: + ++countLtr; + break; + case FALSE: + ++countRtl; + break; + default: + break; + } + } + int limit = (int)((countLtr + countRtl) * mThreshold); + if (limit > 0) { + if (countLtr > limit) { + return TriState.FALSE; + } + if (countRtl > limit) { + return TriState.TRUE; + } + } + return TriState.UNKNOWN; + } + + private CharCount(float threshold) { + mThreshold = threshold; + } + + public static CharCount withThreshold(float threshold) { + if (threshold < 0 || threshold > 1) { + throw new IllegalArgumentException(); + } + if (threshold == DEFAULT_THRESHOLD) { + return INSTANCE_DEFAULT; + } + return new CharCount(threshold); + } + + public static final float DEFAULT_THRESHOLD = 0.6f; + public static final CharCount INSTANCE_DEFAULT = new CharCount(DEFAULT_THRESHOLD); + } +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 6741059..29c9853 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -16,9 +16,6 @@ package android.text; -import com.android.internal.R; -import com.android.internal.util.ArrayUtils; - import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; @@ -45,6 +42,9 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; +import com.android.internal.R; +import com.android.internal.util.ArrayUtils; + import java.lang.reflect.Array; import java.util.Iterator; import java.util.regex.Pattern; @@ -1001,13 +1001,37 @@ public class TextUtils { * will be padded with zero-width spaces to preserve the original * length and offsets instead of truncating. * If <code>callback</code> is non-null, it will be called to - * report the start and end of the ellipsized range. + * report the start and end of the ellipsized range. TextDirection + * is determined by the first strong directional character. */ public static CharSequence ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback) { + return ellipsize(text, paint, avail, where, preserveLength, callback, + TextDirectionHeuristics.FIRSTSTRONG_LTR); + } + + /** + * 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 + * 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 + * length and offsets instead of truncating. + * If <code>callback</code> is non-null, it will be called to + * report the start and end of the ellipsized range. + * + * @hide + */ + public static CharSequence ellipsize(CharSequence text, + TextPaint paint, + float avail, TruncateAt where, + boolean preserveLength, + EllipsizeCallback callback, + TextDirectionHeuristic textDir) { if (sEllipsis == null) { Resources r = Resources.getSystem(); sEllipsis = r.getString(R.string.ellipsis); @@ -1017,8 +1041,7 @@ public class TextUtils { MeasuredText mt = MeasuredText.obtain(); try { - float width = setPara(mt, paint, text, 0, text.length(), - Layout.DIR_REQUEST_DEFAULT_LTR); + float width = setPara(mt, paint, text, 0, text.length(), textDir); if (width <= avail) { if (callback != null) { @@ -1108,11 +1131,20 @@ public class TextUtils { TextPaint p, float avail, String oneMore, String more) { + return commaEllipsize(text, p, avail, oneMore, more, + TextDirectionHeuristics.FIRSTSTRONG_LTR); + } + + /** + * @hide + */ + public static CharSequence commaEllipsize(CharSequence text, TextPaint p, + float avail, String oneMore, String more, TextDirectionHeuristic textDir) { MeasuredText mt = MeasuredText.obtain(); try { int len = text.length(); - float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR); + float width = setPara(mt, p, text, 0, len, textDir); if (width <= avail) { return text; } @@ -1135,9 +1167,6 @@ public class TextUtils { int count = 0; float[] widths = mt.mWidths; - int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR : - Layout.DIR_REQUEST_RTL; - MeasuredText tempMt = MeasuredText.obtain(); for (int i = 0; i < len; i++) { w += widths[i]; @@ -1155,7 +1184,7 @@ public class TextUtils { } // XXX this is probably ok, but need to look at it more - tempMt.setPara(format, 0, format.length(), request); + tempMt.setPara(format, 0, format.length(), textDir); float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); if (w + moreWid <= avail) { @@ -1175,9 +1204,9 @@ public class TextUtils { } private static float setPara(MeasuredText mt, TextPaint paint, - CharSequence text, int start, int end, int bidiRequest) { + CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - mt.setPara(text, start, end, bidiRequest); + mt.setPara(text, start, end, textDir); float width; Spanned sp = text instanceof Spanned ? (Spanned) text : null; diff --git a/core/java/android/text/method/AllCapsTransformationMethod.java b/core/java/android/text/method/AllCapsTransformationMethod.java new file mode 100644 index 0000000..f9920dd --- /dev/null +++ b/core/java/android/text/method/AllCapsTransformationMethod.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.text.method; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; + +import java.util.Locale; + +/** + * Transforms source text into an ALL CAPS string, locale-aware. + * + * @hide + */ +public class AllCapsTransformationMethod implements TransformationMethod2 { + private static final String TAG = "AllCapsTransformationMethod"; + + private boolean mEnabled; + private Locale mLocale; + + public AllCapsTransformationMethod(Context context) { + mLocale = context.getResources().getConfiguration().locale; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + if (mEnabled) { + return source != null ? source.toString().toUpperCase(mLocale) : null; + } + Log.w(TAG, "Caller did not enable length changes; not transforming text"); + return source; + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, + Rect previouslyFocusedRect) { + } + + @Override + public void setLengthChangesAllowed(boolean allowLengthChanges) { + mEnabled = allowLengthChanges; + } + +} diff --git a/core/java/android/text/method/TransformationMethod2.java b/core/java/android/text/method/TransformationMethod2.java new file mode 100644 index 0000000..ef00ecd --- /dev/null +++ b/core/java/android/text/method/TransformationMethod2.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.text.method; + +/** + * TransformationMethod2 extends the TransformationMethod interface + * and adds the ability to relax restrictions of TransformationMethod. + * + * @hide + */ +public interface TransformationMethod2 extends TransformationMethod { + /** + * Relax the contract of TransformationMethod to allow length changes, + * or revert to the length-restricted behavior. + * + * @param allowLengthChanges true to allow the transformation to change the length + * of the input string. + */ + public void setLengthChangesAllowed(boolean allowLengthChanges); +} diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index 240ad9b..555aac5 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -22,12 +22,22 @@ import android.os.Parcelable; import android.os.SystemClock; import android.text.ParcelableSpan; import android.text.TextUtils; +import android.widget.TextView; import java.util.Arrays; import java.util.Locale; /** - * Holds suggestion candidates of words under this span. + * Holds suggestion candidates for the text enclosed in this span. + * + * When such a span is edited in an EditText, double tapping on the text enclosed in this span will + * display a popup dialog listing suggestion replacement for that text. The user can then replace + * the original text by one of the suggestions. + * + * These spans should typically be created by the input method to privide correction and alternates + * for the text. + * + * @see TextView#setSuggestionsEnabled(boolean) */ public class SuggestionSpan implements ParcelableSpan { /** @@ -115,14 +125,14 @@ public class SuggestionSpan implements ParcelableSpan { } /** - * @return suggestions + * @return an array of suggestion texts for this span */ public String[] getSuggestions() { return mSuggestions; } /** - * @return locale of suggestions + * @return the locale of the suggestions */ public String getLocale() { return mLocaleString; diff --git a/core/java/android/util/JsonWriter.java b/core/java/android/util/JsonWriter.java index 47e84c5..c1e6e40 100644 --- a/core/java/android/util/JsonWriter.java +++ b/core/java/android/util/JsonWriter.java @@ -407,6 +407,11 @@ public final class JsonWriter implements Closeable { * quotation marks except for the characters that must be escaped: * quotation mark, reverse solidus, and the control characters * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets + * as newline characters. This prevents eval() from failing with a + * syntax error. + * http://code.google.com/p/google-gson/issues/detail?id=341 */ switch (c) { case '"': @@ -435,6 +440,11 @@ public final class JsonWriter implements Closeable { out.write("\\f"); break; + case '\u2028': + case '\u2029': + out.write(String.format("\\u%04x", (int) c)); + break; + default: if (c <= 0x1F) { out.write(String.format("\\u%04x", (int) c)); diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java index 5b19ecd..2179ff3 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -16,41 +16,71 @@ package android.util; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; import android.net.SntpClient; import android.os.SystemClock; +import android.provider.Settings; /** - * {@link TrustedTime} that connects with a remote NTP server as its remote - * trusted time source. + * {@link TrustedTime} that connects with a remote NTP server as its trusted + * time source. * * @hide */ public class NtpTrustedTime implements TrustedTime { - private String mNtpServer; - private long mNtpTimeout; + private static final String TAG = "NtpTrustedTime"; + private static final boolean LOGD = false; + + private static NtpTrustedTime sSingleton; + + private final String mServer; + private final long mTimeout; private boolean mHasCache; private long mCachedNtpTime; private long mCachedNtpElapsedRealtime; private long mCachedNtpCertainty; - public NtpTrustedTime() { + private NtpTrustedTime(String server, long timeout) { + if (LOGD) Log.d(TAG, "creating NtpTrustedTime using " + server); + mServer = server; + mTimeout = timeout; } - public void setNtpServer(String server, long timeout) { - mNtpServer = server; - mNtpTimeout = timeout; + public static synchronized NtpTrustedTime getInstance(Context context) { + if (sSingleton == null) { + final Resources res = context.getResources(); + final ContentResolver resolver = context.getContentResolver(); + + final String defaultServer = res.getString( + com.android.internal.R.string.config_ntpServer); + final long defaultTimeout = res.getInteger( + com.android.internal.R.integer.config_ntpTimeout); + + final String secureServer = Settings.Secure.getString( + resolver, Settings.Secure.NTP_SERVER); + final long timeout = Settings.Secure.getLong( + resolver, Settings.Secure.NTP_TIMEOUT, defaultTimeout); + + final String server = secureServer != null ? secureServer : defaultServer; + sSingleton = new NtpTrustedTime(server, timeout); + } + + return sSingleton; } /** {@inheritDoc} */ public boolean forceRefresh() { - if (mNtpServer == null) { + if (mServer == null) { // missing server, so no trusted time available return false; } + if (LOGD) Log.d(TAG, "forceRefresh() from cache miss"); final SntpClient client = new SntpClient(); - if (client.requestTime(mNtpServer, (int) mNtpTimeout)) { + if (client.requestTime(mServer, (int) mTimeout)) { mHasCache = true; mCachedNtpTime = client.getNtpTime(); mCachedNtpElapsedRealtime = client.getNtpTimeReference(); @@ -89,9 +119,19 @@ public class NtpTrustedTime implements TrustedTime { if (!mHasCache) { throw new IllegalStateException("Missing authoritative time source"); } + if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit"); // current time is age after the last ntp cache; callers who // want fresh values will hit makeAuthoritative() first. return mCachedNtpTime + getCacheAge(); } + + public long getCachedNtpTime() { + if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit"); + return mCachedNtpTime; + } + + public long getCachedNtpTimeReference() { + return mCachedNtpElapsedRealtime; + } } diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java index 7fc43b9..7cf4579 100644 --- a/core/java/android/util/SparseArray.java +++ b/core/java/android/util/SparseArray.java @@ -23,10 +23,14 @@ import com.android.internal.util.ArrayUtils; * there can be gaps in the indices. It is intended to be more efficient * than using a HashMap to map Integers to Objects. */ -public class SparseArray<E> { +public class SparseArray<E> implements Cloneable { private static final Object DELETED = new Object(); private boolean mGarbage = false; + private int[] mKeys; + private Object[] mValues; + private int mSize; + /** * Creates a new SparseArray containing no mappings. */ @@ -47,6 +51,20 @@ public class SparseArray<E> { mSize = 0; } + @Override + @SuppressWarnings("unchecked") + public SparseArray<E> clone() { + SparseArray<E> clone = null; + try { + clone = (SparseArray<E>) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + /** * Gets the Object mapped from the specified key, or <code>null</code> * if no such mapping has been made. @@ -59,6 +77,7 @@ public class SparseArray<E> { * Gets the Object mapped from the specified key, or the specified Object * if no such mapping has been made. */ + @SuppressWarnings("unchecked") public E get(int key, E valueIfKeyNotFound) { int i = binarySearch(mKeys, 0, mSize, key); @@ -209,6 +228,7 @@ public class SparseArray<E> { * the value from the <code>index</code>th key-value mapping that this * SparseArray stores. */ + @SuppressWarnings("unchecked") public E valueAt(int index) { if (mGarbage) { gc(); @@ -331,20 +351,4 @@ public class SparseArray<E> { else return ~high; } - - private void checkIntegrity() { - for (int i = 1; i < mSize; i++) { - if (mKeys[i] <= mKeys[i - 1]) { - for (int j = 0; j < mSize; j++) { - Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]); - } - - throw new RuntimeException(); - } - } - } - - private int[] mKeys; - private Object[] mValues; - private int mSize; } diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java index f7799de..76c47c6 100644 --- a/core/java/android/util/SparseBooleanArray.java +++ b/core/java/android/util/SparseBooleanArray.java @@ -24,7 +24,7 @@ import com.android.internal.util.ArrayUtils; * there can be gaps in the indices. It is intended to be more efficient * than using a HashMap to map Integers to Booleans. */ -public class SparseBooleanArray { +public class SparseBooleanArray implements Cloneable { /** * Creates a new SparseBooleanArray containing no mappings. */ @@ -45,6 +45,19 @@ public class SparseBooleanArray { mSize = 0; } + @Override + public SparseBooleanArray clone() { + SparseBooleanArray clone = null; + try { + clone = (SparseBooleanArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + /** * Gets the boolean mapped from the specified key, or <code>false</code> * if no such mapping has been made. @@ -227,18 +240,6 @@ public class SparseBooleanArray { return ~high; } - private void checkIntegrity() { - for (int i = 1; i < mSize; i++) { - if (mKeys[i] <= mKeys[i - 1]) { - for (int j = 0; j < mSize; j++) { - Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]); - } - - throw new RuntimeException(); - } - } - } - private int[] mKeys; private boolean[] mValues; private int mSize; diff --git a/core/java/android/util/SparseIntArray.java b/core/java/android/util/SparseIntArray.java index 9ab3b53..8d11177 100644 --- a/core/java/android/util/SparseIntArray.java +++ b/core/java/android/util/SparseIntArray.java @@ -23,7 +23,12 @@ import com.android.internal.util.ArrayUtils; * there can be gaps in the indices. It is intended to be more efficient * than using a HashMap to map Integers to Integers. */ -public class SparseIntArray { +public class SparseIntArray implements Cloneable { + + private int[] mKeys; + private int[] mValues; + private int mSize; + /** * Creates a new SparseIntArray containing no mappings. */ @@ -44,6 +49,19 @@ public class SparseIntArray { mSize = 0; } + @Override + public SparseIntArray clone() { + SparseIntArray clone = null; + try { + clone = (SparseIntArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + /** * Gets the int mapped from the specified key, or <code>0</code> * if no such mapping has been made. @@ -232,20 +250,4 @@ public class SparseIntArray { else return ~high; } - - private void checkIntegrity() { - for (int i = 1; i < mSize; i++) { - if (mKeys[i] <= mKeys[i - 1]) { - for (int j = 0; j < mSize; j++) { - Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]); - } - - throw new RuntimeException(); - } - } - } - - private int[] mKeys; - private int[] mValues; - private int mSize; } diff --git a/core/java/android/view/ActionProvider.java b/core/java/android/view/ActionProvider.java index 6491da0..5601dc5 100644 --- a/core/java/android/view/ActionProvider.java +++ b/core/java/android/view/ActionProvider.java @@ -28,7 +28,9 @@ import android.content.Context; * {@link android.app.ActionBar} as a substitute for the menu item when the item is * displayed as an action item. Also the provider is responsible for performing a * default action if a menu item placed on the overflow menu of the ActionBar is - * selected and none of the menu item callbacks has handled the selection. + * selected and none of the menu item callbacks has handled the selection. For this + * case the provider can also optionally provide a sub-menu for accomplishing the + * task at hand. * </p> * <p> * There are two ways for using an action provider for creating and handling of action views: @@ -76,7 +78,7 @@ public abstract class ActionProvider { * Performs an optional default action. * <p> * For the case of an action provider placed in a menu item not shown as an action this - * method is invoked if none of the callbacks for processing menu selection has handled + * method is invoked if previous callbacks for processing menu selection has handled * the event. * </p> * <p> @@ -104,11 +106,36 @@ public abstract class ActionProvider { * </ul> * </p> * <p> - * The default implementation does not perform any action. + * The default implementation does not perform any action and returns false. * </p> + */ + public boolean onPerformDefaultAction() { + return false; + } + + /** + * Determines if this ActionProvider has a submenu associated with it. + * + * <p>Associated submenus will be shown when an action view is not. This + * provider instance will receive a call to {@link #onPrepareSubMenu(SubMenu)} + * after the call to {@link #onPerformDefaultAction()} and before a submenu is + * displayed to the user. + * + * @return true if the item backed by this provider should have an associated submenu + */ + public boolean hasSubMenu() { + return false; + } + + /** + * Called to prepare an associated submenu for the menu item backed by this ActionProvider. + * + * <p>if {@link #hasSubMenu()} returns true, this method will be called when the + * menu item is selected to prepare the submenu for presentation to the user. Apps + * may use this to create or alter submenu content right before display. * - * @param actionView A view created by {@link #onCreateActionView()}. + * @param subMenu Submenu that will be displayed */ - public void onPerformDefaultAction(View actionView) { + public void onPrepareSubMenu(SubMenu subMenu) { } } diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 04f35dc..4987e2f 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -189,6 +189,19 @@ class GLES20Canvas extends HardwareCanvas { return mHeight; } + @Override + public int getMaximumBitmapWidth() { + return nGetMaximumTextureWidth(); + } + + @Override + public int getMaximumBitmapHeight() { + return nGetMaximumTextureHeight(); + } + + private static native int nGetMaximumTextureWidth(); + private static native int nGetMaximumTextureHeight(); + /////////////////////////////////////////////////////////////////////////// // Setup /////////////////////////////////////////////////////////////////////////// @@ -233,8 +246,19 @@ class GLES20Canvas extends HardwareCanvas { return nIsBackBufferPreserved(); } - private static native boolean nIsBackBufferPreserved(); - + private static native boolean nIsBackBufferPreserved(); + + /** + * Disables v-sync. For performance testing only. + * + * @hide + */ + public static void disableVsync() { + nDisableVsync(); + } + + private static native void nDisableVsync(); + @Override void onPreDraw(Rect dirty) { if (dirty != null) { @@ -252,7 +276,7 @@ class GLES20Canvas extends HardwareCanvas { void onPostDraw() { nFinish(mRenderer); } - + private static native void nFinish(int renderer); @Override diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index bbfb4c1..011e44c 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -57,6 +57,16 @@ public abstract class HardwareRenderer { * "false", to disable partial invalidates */ static final String RENDER_DIRTY_REGIONS_PROPERTY = "hwui.render_dirty_regions"; + + /** + * System property used to enable or disable vsync. + * The default value of this property is assumed to be false. + * + * Possible values: + * "true", to disable vsync + * "false", to enable vsync + */ + static final String DISABLE_VSYNC_PROPERTY = "hwui.disable_vsync"; /** * Turn on to draw dirty regions every other frame. @@ -118,8 +128,23 @@ public abstract class HardwareRenderer { abstract void updateSurface(SurfaceHolder holder) throws Surface.OutOfResourcesException; /** - * Setup the hardware renderer for drawing. This is called for every - * frame to draw. + * This method should be invoked whenever the current hardware renderer + * context should be reset. + */ + abstract void invalidate(); + + /** + * This method should be invoked to ensure the hardware renderer is in + * valid state (for instance, to ensure the correct EGL context is bound + * to the current thread.) + * + * @return true if the renderer is now valid, false otherwise + */ + abstract boolean validate(); + + /** + * Setup the hardware renderer for drawing. This is called whenever the + * size of the target surface changes or when the surface is first created. * * @param width Width of the drawing surface. * @param height Height of the drawing surface. @@ -279,9 +304,9 @@ public abstract class HardwareRenderer { static abstract class GlRenderer extends HardwareRenderer { // These values are not exposed in our EGL APIs static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - static final int EGL_SURFACE_TYPE = 0x3033; - static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; static final int EGL_OPENGL_ES2_BIT = 4; + static final int EGL_SURFACE_TYPE = 0x3033; + static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; private static final int SURFACE_STATE_ERROR = 0; private static final int SURFACE_STATE_SUCCESS = 1; @@ -301,8 +326,17 @@ public abstract class HardwareRenderer { int mFrameCount; Paint mDebugPaint; - boolean mDirtyRegions; - final boolean mDirtyRegionsRequested; + static boolean sDirtyRegions; + static final boolean sDirtyRegionsRequested; + static { + String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); + //noinspection PointlessBooleanExpression,ConstantConditions + sDirtyRegions = RENDER_DIRTY_REGIONS && "true".equalsIgnoreCase(dirtyProperty); + sDirtyRegionsRequested = sDirtyRegions; + } + + boolean mDirtyRegionsEnabled; + final boolean mVsyncDisabled; final int mGlVersion; final boolean mTranslucent; @@ -314,17 +348,19 @@ public abstract class HardwareRenderer { GlRenderer(int glVersion, boolean translucent) { mGlVersion = glVersion; mTranslucent = translucent; - final String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); - //noinspection PointlessBooleanExpression,ConstantConditions - mDirtyRegions = RENDER_DIRTY_REGIONS && "true".equalsIgnoreCase(dirtyProperty); - mDirtyRegionsRequested = mDirtyRegions; + + final String vsyncProperty = SystemProperties.get(DISABLE_VSYNC_PROPERTY, "false"); + mVsyncDisabled = "true".equalsIgnoreCase(vsyncProperty); + if (mVsyncDisabled) { + Log.d(LOG_TAG, "Disabling v-sync"); + } } /** * Indicates whether this renderer instance can track and update dirty regions. */ boolean hasDirtyRegions() { - return mDirtyRegions; + return mDirtyRegionsEnabled; } /** @@ -461,8 +497,8 @@ public abstract class HardwareRenderer { sEglConfig = chooseEglConfig(); if (sEglConfig == null) { // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without - if (mDirtyRegions) { - mDirtyRegions = false; + if (sDirtyRegions) { + sDirtyRegions = false; sEglConfig = chooseEglConfig(); if (sEglConfig == null) { throw new RuntimeException("eglConfig not initialized"); @@ -482,7 +518,7 @@ public abstract class HardwareRenderer { private EGLConfig chooseEglConfig() { int[] configsCount = new int[1]; EGLConfig[] configs = new EGLConfig[1]; - int[] configSpec = getConfig(mDirtyRegions); + int[] configSpec = getConfig(sDirtyRegions); if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { throw new IllegalArgumentException("eglChooseConfig failed " + getEGLErrorString(sEgl.eglGetError())); @@ -548,18 +584,18 @@ public abstract class HardwareRenderer { // If mDirtyRegions is set, this means we have an EGL configuration // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set - if (mDirtyRegions) { - if (!GLES20Canvas.preserveBackBuffer()) { + if (sDirtyRegions) { + if (!(mDirtyRegionsEnabled = GLES20Canvas.preserveBackBuffer())) { Log.w(LOG_TAG, "Backbuffer cannot be preserved"); } - } else if (mDirtyRegionsRequested) { + } else if (sDirtyRegionsRequested) { // If mDirtyRegions is not set, our EGL configuration does not // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default // swap behavior might be EGL_BUFFER_PRESERVED, which means we // want to set mDirtyRegions. We try to do this only if dirty // regions were initially requested as part of the device // configuration (see RENDER_DIRTY_REGIONS) - mDirtyRegions = GLES20Canvas.isBackBufferPreserved(); + mDirtyRegionsEnabled = GLES20Canvas.isBackBufferPreserved(); } return sEglContext.getGL(); @@ -602,6 +638,19 @@ public abstract class HardwareRenderer { } @Override + void invalidate() { + // Cancels any existing buffer to ensure we'll get a buffer + // of the right size before we call eglSwapBuffers + sEgl.eglMakeCurrent(sEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + } + + @Override + boolean validate() { + return checkCurrent() != SURFACE_STATE_ERROR; + } + + @Override void setup(int width, int height) { mCanvas.setViewport(width, height); } @@ -627,7 +676,7 @@ public abstract class HardwareRenderer { attachInfo.mDrawingTime = SystemClock.uptimeMillis(); view.mPrivateFlags |= View.DRAWN; - + final int surfaceState = checkCurrent(); if (surfaceState != SURFACE_STATE_ERROR) { // We had to change the current surface and/or context, redraw everything @@ -688,10 +737,21 @@ public abstract class HardwareRenderer { } } } - + + /** + * Ensures the currnet EGL context is the one we expect. + * + * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, + * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or + * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one + */ private int checkCurrent() { - // TODO: Don't check the current context when we have one per UI thread - // TODO: Use a threadlocal flag to know whether the surface has changed + if (sEglThread != Thread.currentThread()) { + throw new IllegalStateException("Hardware acceleration can only be used with a " + + "single UI thread.\nOriginal thread: " + sEglThread + "\n" + + "Current thread: " + Thread.currentThread()); + } + if (!sEglContext.equals(sEgl.eglGetCurrentContext()) || !mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL10.EGL_DRAW))) { if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, sEglContext)) { @@ -765,6 +825,14 @@ public abstract class HardwareRenderer { } @Override + void setup(int width, int height) { + super.setup(width, height); + if (mVsyncDisabled) { + GLES20Canvas.disableVsync(); + } + } + + @Override DisplayList createDisplayList(View v) { return new GLES20DisplayList(v); } diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java index a7f0cba..c46e43a 100644 --- a/core/java/android/view/MenuInflater.java +++ b/core/java/android/view/MenuInflater.java @@ -166,7 +166,12 @@ public class MenuInflater { // Add the item if it hasn't been added (if the item was // a submenu, it would have been added already) if (!menuState.hasAddedItem()) { - menuState.addItem(); + if (menuState.itemActionProvider != null && + menuState.itemActionProvider.hasSubMenu()) { + menuState.addSubMenuItem(); + } else { + menuState.addItem(); + } } } else if (tagName.equals(XML_MENU)) { reachedEndOfMenu = true; @@ -270,6 +275,8 @@ public class MenuInflater { private String itemListenerMethodName; + private ActionProvider itemActionProvider; + private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; @@ -347,6 +354,19 @@ public class MenuInflater { itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass); + final boolean hasActionProvider = itemActionProviderClassName != null; + if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { + itemActionProvider = newInstance(itemActionProviderClassName, + ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, + mActionProviderConstructorArguments); + } else { + if (hasActionProvider) { + Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'." + + " Action view already specified."); + } + itemActionProvider = null; + } + a.recycle(); itemAdded = false; @@ -406,16 +426,8 @@ public class MenuInflater { + " Action view already specified."); } } - if (itemActionProviderClassName != null) { - if (!actionViewSpecified) { - ActionProvider actionProvider = newInstance(itemActionProviderClassName, - ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, - mActionProviderConstructorArguments); - item.setActionProvider(actionProvider); - } else { - Log.w(LOG_TAG, "Ignoring attribute 'itemActionProviderClass'." - + " Action view already specified."); - } + if (itemActionProvider != null) { + item.setActionProvider(itemActionProvider); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1245898..4385c2f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,13 +16,6 @@ package android.view; -import android.util.FloatProperty; -import android.util.LocaleUtil; -import android.util.Property; -import com.android.internal.R; -import com.android.internal.util.Predicate; -import com.android.internal.view.menu.MenuBuilder; - import android.content.ClipData; import android.content.Context; import android.content.res.Configuration; @@ -53,11 +46,14 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.LocaleUtil; import android.util.Log; import android.util.Pool; import android.util.Poolable; import android.util.PoolableManager; import android.util.Pools; +import android.util.Property; import android.util.SparseArray; import android.util.TypedValue; import android.view.ContextMenu.ContextMenuInfo; @@ -72,6 +68,10 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.ScrollBarDrawable; +import com.android.internal.R; +import com.android.internal.util.Predicate; +import com.android.internal.view.menu.MenuBuilder; + import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -2167,21 +2167,27 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit /** * Cache the paddingRight set by the user to append to the scrollbar's size. + * + * @hide */ @ViewDebug.ExportedProperty(category = "padding") - int mUserPaddingRight; + protected int mUserPaddingRight; /** * Cache the paddingBottom set by the user to append to the scrollbar's size. + * + * @hide */ @ViewDebug.ExportedProperty(category = "padding") - int mUserPaddingBottom; + protected int mUserPaddingBottom; /** * Cache the paddingLeft set by the user to append to the scrollbar's size. + * + * @hide */ @ViewDebug.ExportedProperty(category = "padding") - int mUserPaddingLeft; + protected int mUserPaddingLeft; /** * Cache if the user padding is relative. @@ -2493,12 +2499,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit private boolean mSendingHoverAccessibilityEvents; /** - * Undefined text direction (used by resolution algorithm). - * @hide - */ - public static final int TEXT_DIRECTION_UNDEFINED = -1; - - /** * Text direction is inherited thru {@link ViewGroup} * @hide */ @@ -2507,7 +2507,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit /** * Text direction is using "first strong algorithm". The first strong directional character * determines the paragraph direction. If there is no strong directional character, the - * paragraph direction is the view’s resolved ayout direction. + * paragraph direction is the view's resolved ayout direction. * * @hide */ @@ -2516,7 +2516,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit /** * Text direction is using "any-RTL" algorithm. The paragraph direction is RTL if it contains * any strong RTL character, otherwise it is LTR if it contains any strong LTR characters. - * If there are neither, the paragraph direction is the view’s resolved layout direction. + * If there are neither, the paragraph direction is the view's resolved layout direction. * * @hide */ @@ -2560,7 +2560,6 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * {@hide} */ @ViewDebug.ExportedProperty(category = "text", mapping = { - @ViewDebug.IntToString(from = TEXT_DIRECTION_UNDEFINED, to = "UNDEFINED"), @ViewDebug.IntToString(from = TEXT_DIRECTION_INHERIT, to = "INHERIT"), @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"), @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"), @@ -2568,21 +2567,25 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"), @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL") }) - protected int mTextDirection = DEFAULT_TEXT_DIRECTION; + private int mTextDirection = DEFAULT_TEXT_DIRECTION; /** - * The resolved text direction. If resolution has not yet been done or has been reset, it will - * be equal to {@link #TEXT_DIRECTION_UNDEFINED}. Otherwise it will be either {@link #TEXT_DIRECTION_LTR} - * or {@link #TEXT_DIRECTION_RTL}. + * The resolved text direction. This needs resolution if the value is + * TEXT_DIRECTION_INHERIT. The resolution matches mTextDirection if that is + * not TEXT_DIRECTION_INHERIT, otherwise resolution proceeds up the parent + * chain of the view. * * {@hide} */ @ViewDebug.ExportedProperty(category = "text", mapping = { - @ViewDebug.IntToString(from = TEXT_DIRECTION_UNDEFINED, to = "UNDEFINED"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_INHERIT, to = "INHERIT"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"), + @ViewDebug.IntToString(from = TEXT_DIRECTION_CHAR_COUNT, to = "CHAR_COUNT"), @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"), @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL") }) - protected int mResolvedTextDirection = TEXT_DIRECTION_UNDEFINED; + private int mResolvedTextDirection = TEXT_DIRECTION_INHERIT; /** * Consistency verifier for debugging purposes. @@ -4474,7 +4477,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit @RemotableViewMethod public void setLayoutDirection(int layoutDirection) { if (getLayoutDirection() != layoutDirection) { - resetLayoutDirectionResolution(); + resetResolvedLayoutDirection(); // Setting the flag will also request a layout. setFlags(layoutDirection, LAYOUT_DIRECTION_MASK); } @@ -9043,10 +9046,8 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit mPrivateFlags &= ~AWAKEN_SCROLL_BARS_ON_ATTACH; } jumpDrawablesToCurrentState(); - resetLayoutDirectionResolution(); resolveLayoutDirectionIfNeeded(); resolvePadding(); - resetResolvedTextDirection(); resolveTextDirection(); if (isFocused()) { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -9068,11 +9069,20 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit // Set resolved depending on layout direction switch (getLayoutDirection()) { case LAYOUT_DIRECTION_INHERIT: + // We cannot do the resolution if there is no parent + if (mParent == null) return; + // If this is root view, no need to look at parent's layout dir. - if (mParent != null && - mParent instanceof ViewGroup && - ((ViewGroup) mParent).getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) { - mPrivateFlags2 |= LAYOUT_DIRECTION_RESOLVED_RTL; + if (mParent instanceof ViewGroup) { + ViewGroup viewGroup = ((ViewGroup) mParent); + + // Check if the parent view group can resolve + if (! viewGroup.canResolveLayoutDirection()) { + return; + } + if (viewGroup.getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) { + mPrivateFlags2 |= LAYOUT_DIRECTION_RESOLVED_RTL; + } } break; case LAYOUT_DIRECTION_RTL: @@ -9117,12 +9127,12 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit // Start user padding override Left user padding. Otherwise, if Left user // padding is not defined, use the default left padding. If Left user padding // is defined, just use it. - if (mUserPaddingStart > 0) { + if (mUserPaddingStart >= 0) { mUserPaddingLeft = mUserPaddingStart; } else if (mUserPaddingLeft < 0) { mUserPaddingLeft = mPaddingLeft; } - if (mUserPaddingEnd > 0) { + if (mUserPaddingEnd >= 0) { mUserPaddingRight = mUserPaddingEnd; } else if (mUserPaddingRight < 0) { mUserPaddingRight = mPaddingRight; @@ -9134,6 +9144,15 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit recomputePadding(); } + protected boolean canResolveLayoutDirection() { + switch (getLayoutDirection()) { + case LAYOUT_DIRECTION_INHERIT: + return (mParent != null); + default: + return true; + } + } + /** * Reset the resolved layout direction. * @@ -9143,7 +9162,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * * @hide */ - protected void resetLayoutDirectionResolution() { + protected void resetResolvedLayoutDirection() { // Reset the current View resolution mPrivateFlags2 &= ~LAYOUT_DIRECTION_RESOLVED; } @@ -9190,6 +9209,9 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit } mCurrentAnimation = null; + + resetResolvedLayoutDirection(); + resetResolvedTextDirection(); } /** @@ -11100,6 +11122,10 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * background */ public void setBackgroundDrawable(Drawable d) { + if (d == mBGDrawable) { + return; + } + boolean requestLayout = false; mBackgroundResource = 0; @@ -13047,43 +13073,41 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * * @return the resolved text direction. Return one of: * + * {@link #TEXT_DIRECTION_FIRST_STRONG} + * {@link #TEXT_DIRECTION_ANY_RTL}, + * {@link #TEXT_DIRECTION_CHAR_COUNT}, * {@link #TEXT_DIRECTION_LTR}, * {@link #TEXT_DIRECTION_RTL}, * * @hide */ public int getResolvedTextDirection() { - if (!isResolvedTextDirection()) { + if (mResolvedTextDirection == TEXT_DIRECTION_INHERIT) { resolveTextDirection(); } return mResolvedTextDirection; } /** - * Resolve the text direction. Classes that extend View and want to do a specific text direction - * resolution will need to implement this method and set the mResolvedTextDirection to - * either TEXT_DIRECTION_LTR if direction is LTR or TEXT_DIRECTION_RTL if - * direction is RTL. + * Resolve the text direction. */ protected void resolveTextDirection() { + if (mTextDirection != TEXT_DIRECTION_INHERIT) { + mResolvedTextDirection = mTextDirection; + return; + } + if (mParent != null && mParent instanceof ViewGroup) { + mResolvedTextDirection = ((ViewGroup) mParent).getResolvedTextDirection(); + return; + } + mResolvedTextDirection = TEXT_DIRECTION_FIRST_STRONG; } /** - * Return if the text direction has been resolved or not. - * - * @return true, if resolved and false if not resolved - * - * @hide - */ - public boolean isResolvedTextDirection() { - return (mResolvedTextDirection != TEXT_DIRECTION_UNDEFINED); - } - - /** - * Reset resolved text direction. Will be resolved during a call to getResolvedLayoutDirection(). + * Reset resolved text direction. Will be resolved during a call to getResolvedTextDirection(). */ protected void resetResolvedTextDirection() { - mResolvedTextDirection = TEXT_DIRECTION_UNDEFINED; + mResolvedTextDirection = TEXT_DIRECTION_INHERIT; } // diff --git a/core/java/android/view/ViewAncestor.java b/core/java/android/view/ViewAncestor.java index d70c798..ac73611 100644 --- a/core/java/android/view/ViewAncestor.java +++ b/core/java/android/view/ViewAncestor.java @@ -901,6 +901,7 @@ public final class ViewAncestor extends Handler implements ViewParent, !mAttachInfo.mTurnOffWindowResizeAnim && mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled() && + mAttachInfo.mHardwareRenderer.validate() && lp != null && !PixelFormat.formatHasAlpha(lp.format)) { disposeResizeBuffer(); @@ -1315,6 +1316,9 @@ public final class ViewAncestor extends Handler implements ViewParent, mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled())) { mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight); + if (!hwInitialized) { + mAttachInfo.mHardwareRenderer.invalidate(); + } } if (!mStopped) { @@ -2205,6 +2209,62 @@ public final class ViewAncestor extends Handler implements ViewParent, public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1023; @Override + public String getMessageName(Message message) { + switch (message.what) { + case DO_TRAVERSAL: + return "DO_TRAVERSAL"; + case DIE: + return "DIE"; + case RESIZED: + return "RESIZED"; + case RESIZED_REPORT: + return "RESIZED_REPORT"; + case WINDOW_FOCUS_CHANGED: + return "WINDOW_FOCUS_CHANGED"; + case DISPATCH_KEY: + return "DISPATCH_KEY"; + case DISPATCH_POINTER: + return "DISPATCH_POINTER"; + case DISPATCH_TRACKBALL: + return "DISPATCH_TRACKBALL"; + case DISPATCH_APP_VISIBILITY: + return "DISPATCH_APP_VISIBILITY"; + case DISPATCH_GET_NEW_SURFACE: + return "DISPATCH_GET_NEW_SURFACE"; + case FINISHED_EVENT: + return "FINISHED_EVENT"; + case DISPATCH_KEY_FROM_IME: + return "DISPATCH_KEY_FROM_IME"; + case FINISH_INPUT_CONNECTION: + return "FINISH_INPUT_CONNECTION"; + case CHECK_FOCUS: + return "CHECK_FOCUS"; + case CLOSE_SYSTEM_DIALOGS: + return "CLOSE_SYSTEM_DIALOGS"; + case DISPATCH_DRAG_EVENT: + return "DISPATCH_DRAG_EVENT"; + case DISPATCH_DRAG_LOCATION_EVENT: + return "DISPATCH_DRAG_LOCATION_EVENT"; + case DISPATCH_SYSTEM_UI_VISIBILITY: + return "DISPATCH_SYSTEM_UI_VISIBILITY"; + case DISPATCH_GENERIC_MOTION: + return "DISPATCH_GENERIC_MOTION"; + case UPDATE_CONFIGURATION: + return "UPDATE_CONFIGURATION"; + case DO_PERFORM_ACCESSIBILITY_ACTION: + return "DO_PERFORM_ACCESSIBILITY_ACTION"; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: + return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: + return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: + return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT"; + + } + return super.getMessageName(message); + } + + @Override public void handleMessage(Message msg) { switch (msg.what) { case View.AttachInfo.INVALIDATE_MSG: @@ -2630,8 +2690,9 @@ public final class ViewAncestor extends Handler implements ViewParent, mInputEventDeliverTimeNanos = System.nanoTime(); } + final boolean isTouchEvent = event.isTouchEvent(); if (mInputEventConsistencyVerifier != null) { - if (event.isTouchEvent()) { + if (isTouchEvent) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } else { mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); @@ -2649,9 +2710,9 @@ public final class ViewAncestor extends Handler implements ViewParent, mTranslator.translateEventInScreenToAppWindow(event); } - // Enter touch mode on the down. - boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN; - if (isDown) { + // Enter touch mode on down or scroll. + final int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) { ensureTouchMode(true); } @@ -2664,8 +2725,10 @@ public final class ViewAncestor extends Handler implements ViewParent, } // Remember the touch position for possible drag-initiation. - mLastTouchPoint.x = event.getRawX(); - mLastTouchPoint.y = event.getRawY(); + if (isTouchEvent) { + mLastTouchPoint.x = event.getRawX(); + mLastTouchPoint.y = event.getRawY(); + } // Dispatch touch to view hierarchy. boolean handled = mView.dispatchPointerEvent(event); @@ -2677,51 +2740,6 @@ public final class ViewAncestor extends Handler implements ViewParent, return; } - // Apply edge slop and try again, if appropriate. - final int edgeFlags = event.getEdgeFlags(); - if (edgeFlags != 0 && mView instanceof ViewGroup) { - final int edgeSlop = mViewConfiguration.getScaledEdgeSlop(); - int direction = View.FOCUS_UP; - int x = (int)event.getX(); - int y = (int)event.getY(); - final int[] deltas = new int[2]; - - if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) { - direction = View.FOCUS_DOWN; - if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { - deltas[0] = edgeSlop; - x += edgeSlop; - } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { - deltas[0] = -edgeSlop; - x -= edgeSlop; - } - } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) { - direction = View.FOCUS_UP; - if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { - deltas[0] = edgeSlop; - x += edgeSlop; - } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { - deltas[0] = -edgeSlop; - x -= edgeSlop; - } - } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) { - direction = View.FOCUS_RIGHT; - } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) { - direction = View.FOCUS_LEFT; - } - - View nearest = FocusFinder.getInstance().findNearestTouchable( - ((ViewGroup) mView), x, y, direction, deltas); - if (nearest != null) { - event.offsetLocation(deltas[0], deltas[1]); - event.setEdgeFlags(0); - if (mView.dispatchPointerEvent(event)) { - finishMotionEvent(event, sendDone, true); - return; - } - } - } - // Pointer event was unhandled. finishMotionEvent(event, sendDone, false); } @@ -3845,10 +3863,6 @@ public final class ViewAncestor extends Handler implements ViewParent, } private static int checkCallingPermission(String permission) { - if (!Process.supportsProcesses()) { - return PackageManager.PERMISSION_GRANTED; - } - try { return ActivityManagerNative.getDefault().checkPermission( permission, Binder.getCallingPid(), Binder.getCallingUid()); diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index f014070..f7f5a21 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -16,41 +16,45 @@ package android.view; -import android.util.Log; -import android.util.DisplayMetrics; -import android.content.res.Resources; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; -import android.os.Environment; import android.os.Debug; +import android.os.Environment; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Printer; +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.File; -import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; -import java.io.FileOutputStream; -import java.io.DataOutputStream; -import java.io.OutputStreamWriter; -import java.io.BufferedOutputStream; import java.io.OutputStream; -import java.util.List; -import java.util.LinkedList; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.lang.annotation.Target; +import java.io.OutputStreamWriter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. @@ -106,13 +110,6 @@ public class ViewDebug { public static final boolean DEBUG_PROFILE_LAYOUT = false; /** - * Profiles real fps (times between draws) and displays the result. - * - * @hide - */ - public static final boolean DEBUG_SHOW_FPS = false; - - /** * Enables detailed logging of drag/drop operations. * @hide */ @@ -396,6 +393,9 @@ public class ViewDebug { private static List<RecyclerTrace> sRecyclerTraces; private static String sRecyclerTracePrefix; + private static final ThreadLocal<LooperProfiler> sLooperProfilerStorage = + new ThreadLocal<LooperProfiler>(); + /** * Returns the number of instanciated Views. * @@ -419,6 +419,124 @@ public class ViewDebug { } /** + * Starts profiling the looper associated with the current thread. + * You must call {@link #stopLooperProfiling} to end profiling + * and obtain the traces. Both methods must be invoked on the + * same thread. + * + * @param traceFile The path where to write the looper traces + * + * @see #stopLooperProfiling() + */ + public static void startLooperProfiling(File traceFile) { + if (sLooperProfilerStorage.get() == null) { + LooperProfiler profiler = new LooperProfiler(traceFile); + sLooperProfilerStorage.set(profiler); + Looper.myLooper().setMessageLogging(profiler); + } + } + + /** + * Stops profiling the looper associated with the current thread. + * + * @see #startLooperProfiling(java.io.File) + */ + public static void stopLooperProfiling() { + LooperProfiler profiler = sLooperProfilerStorage.get(); + if (profiler != null) { + sLooperProfilerStorage.remove(); + Looper.myLooper().setMessageLogging(null); + profiler.save(); + } + } + + private static class LooperProfiler implements Looper.Profiler, Printer { + private static final int LOOPER_PROFILER_VERSION = 1; + + private static final String LOG_TAG = "LooperProfiler"; + + private final ArrayList<Entry> mTraces = new ArrayList<Entry>(512); + private final File mTraceFile; + + public LooperProfiler(File traceFile) { + mTraceFile = traceFile; + } + + @Override + public void println(String x) { + // Ignore messages + } + + @Override + public void profile(Message message, long wallStart, long wallTime, long threadTime) { + Entry entry = new Entry(); + entry.messageId = message.what; + entry.name = message.getTarget().getMessageName(message); + entry.wallStart = wallStart; + entry.wallTime = wallTime; + entry.threadTime = threadTime; + + mTraces.add(entry); + } + + void save() { + // Don't block the UI thread + new Thread(new Runnable() { + @Override + public void run() { + saveTraces(); + } + }, "LooperProfiler[" + mTraceFile + "]").start(); + } + + private void saveTraces() { + FileOutputStream fos; + try { + fos = new FileOutputStream(mTraceFile); + } catch (FileNotFoundException e) { + Log.e(LOG_TAG, "Could not open trace file: " + mTraceFile); + return; + } + + DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos)); + + try { + out.writeInt(LOOPER_PROFILER_VERSION); + out.writeInt(mTraces.size()); + for (Entry entry : mTraces) { + saveTrace(entry, out); + } + + Log.d(LOG_TAG, "Looper traces ready: " + mTraceFile); + } catch (IOException e) { + Log.e(LOG_TAG, "Could not write trace file: ", e); + } finally { + try { + out.close(); + } catch (IOException e) { + // Ignore + } + } + } + + private void saveTrace(Entry entry, DataOutputStream out) throws IOException { + out.writeInt(entry.messageId); + out.writeUTF(entry.name); + out.writeLong(entry.wallStart); + out.writeLong(entry.wallTime); + out.writeLong(entry.threadTime); + } + + static class Entry { + int messageId; + String name; + long wallStart; + long wallTime; + long threadTime; + } + } + + /** * Outputs a trace to the currently opened recycler traces. The trace records the type of * recycler action performed on the supplied view as well as a number of parameters. * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 41412de..cb3e9c6 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -41,12 +41,12 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.LayoutAnimationController; import android.view.animation.Transformation; + import com.android.internal.R; import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.HashSet; -import java.util.Locale; /** * <p> @@ -5000,50 +5000,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - protected void resetLayoutDirectionResolution() { - super.resetLayoutDirectionResolution(); + protected void resetResolvedLayoutDirection() { + super.resetResolvedLayoutDirection(); // Take care of resetting the children resolution too final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getLayoutDirection() == LAYOUT_DIRECTION_INHERIT) { - child.resetLayoutDirectionResolution(); + child.resetResolvedLayoutDirection(); } } } - /** - * This method will be called during text direction resolution (text direction resolution - * inheritance) - */ - @Override - protected void resolveTextDirection() { - int resolvedTextDirection = TEXT_DIRECTION_UNDEFINED; - switch(mTextDirection) { - default: - case TEXT_DIRECTION_INHERIT: - // Try to the text direction from the parent layout - if (mParent != null && mParent instanceof ViewGroup) { - resolvedTextDirection = ((ViewGroup) mParent).getResolvedTextDirection(); - } else { - // We reached the top of the View hierarchy, so set the text direction - // heuristic to "first strong" - resolvedTextDirection = TEXT_DIRECTION_FIRST_STRONG; - } - break; - // Pass down the hierarchy the following text direction values - case TEXT_DIRECTION_FIRST_STRONG: - case TEXT_DIRECTION_ANY_RTL: - case TEXT_DIRECTION_CHAR_COUNT: - case TEXT_DIRECTION_LTR: - case TEXT_DIRECTION_RTL: - resolvedTextDirection = mTextDirection; - break; - } - mResolvedTextDirection = resolvedTextDirection; - } - @Override protected void resetResolvedTextDirection() { super.resetResolvedTextDirection(); diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index ac86769..9be2a67 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -552,7 +552,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par /** * Returns a cached instance if such is available or a new one is - * initialized with from the given <code>event</code>. + * created. The returned instance is initialized from the given + * <code>event</code>. * * @param event The other event. * @return An instance. diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 314b7ca..83c73cb 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -71,7 +71,9 @@ public final class AccessibilityManager { private static AccessibilityManager sInstance; - private static final int DO_SET_ENABLED = 10; + private static final int DO_SET_ACCESSIBILITY_ENABLED = 10; + + private static final int DO_SET_TOUCH_EXPLORATION_ENABLED = 20; final IAccessibilityManager mService; @@ -79,6 +81,8 @@ public final class AccessibilityManager { boolean mIsEnabled; + boolean mIsTouchExplorationEnabled; + final CopyOnWriteArrayList<AccessibilityStateChangeListener> mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<AccessibilityStateChangeListener>(); @@ -97,7 +101,12 @@ public final class AccessibilityManager { final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { public void setEnabled(boolean enabled) { - mHandler.obtainMessage(DO_SET_ENABLED, enabled ? 1 : 0, 0).sendToTarget(); + mHandler.obtainMessage(DO_SET_ACCESSIBILITY_ENABLED, enabled ? 1 : 0, 0).sendToTarget(); + } + + public void setTouchExplorationEnabled(boolean enabled) { + mHandler.obtainMessage(DO_SET_TOUCH_EXPLORATION_ENABLED, + enabled ? 1 : 0, 0).sendToTarget(); } }; @@ -110,9 +119,14 @@ public final class AccessibilityManager { @Override public void handleMessage(Message message) { switch (message.what) { - case DO_SET_ENABLED : - final boolean isEnabled = (message.arg1 == 1); - setAccessibilityState(isEnabled); + case DO_SET_ACCESSIBILITY_ENABLED : + final boolean isAccessibilityEnabled = (message.arg1 == 1); + setAccessibilityState(isAccessibilityEnabled); + return; + case DO_SET_TOUCH_EXPLORATION_ENABLED : + synchronized (mHandler) { + mIsTouchExplorationEnabled = (message.arg1 == 1); + } return; default : Log.w(LOG_TAG, "Unknown message type: " + message.what); @@ -168,6 +182,17 @@ public final class AccessibilityManager { } /** + * Returns if the touch exploration in the system is enabled. + * + * @return True if touch exploration is enabled, false otherwise. + */ + public boolean isTouchExplorationEnabled() { + synchronized (mHandler) { + return mIsTouchExplorationEnabled; + } + } + + /** * Returns the client interface this instance registers in * the centralized accessibility manager service. * diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 031c6ae..0e04471 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -120,7 +120,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mText; private CharSequence mContentDescription; - private final SparseIntArray mChildAccessibilityIds = new SparseIntArray(); + private SparseIntArray mChildAccessibilityIds = new SparseIntArray(); private int mActions; private IAccessibilityServiceConnection mConnection; @@ -873,6 +873,20 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Returns a cached instance if such is available or a new one is + * create. The returned instance is initialized from the given + * <code>info</code>. + * + * @param info The other info. + * @return An instance. + */ + public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) { + AccessibilityNodeInfo infoClone = AccessibilityNodeInfo.obtain(); + infoClone.init(info); + return infoClone; + } + + /** * Return an instance back to be reused. * <p> * <strong>Note:</strong> You must not touch the object after calling this function. @@ -945,6 +959,28 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Initializes this instance from another one. + * + * @param other The other instance. + */ + private void init(AccessibilityNodeInfo other) { + mSealed = other.mSealed; + mConnection = other.mConnection; + mAccessibilityViewId = other.mAccessibilityViewId; + mParentAccessibilityViewId = other.mParentAccessibilityViewId; + mAccessibilityWindowId = other.mAccessibilityWindowId; + mBoundsInParent.set(other.mBoundsInParent); + mBoundsInScreen.set(other.mBoundsInScreen); + mPackageName = other.mPackageName; + mClassName = other.mClassName; + mText = other.mText; + mContentDescription = other.mContentDescription; + mActions= other.mActions; + mBooleanProperties = other.mBooleanProperties; + mChildAccessibilityIds = other.mChildAccessibilityIds.clone(); + } + + /** * Creates a new instance from a {@link Parcel}. * * @param parcel A parcel containing the state of a {@link AccessibilityNodeInfo}. @@ -994,6 +1030,7 @@ public class AccessibilityNodeInfo implements Parcelable { mConnection = null; mAccessibilityViewId = View.NO_ID; mParentAccessibilityViewId = View.NO_ID; + mAccessibilityWindowId = View.NO_ID; mChildAccessibilityIds.clear(); mBoundsInParent.set(0, 0, 0, 0); mBoundsInScreen.set(0, 0, 0, 0); diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index f4d5e89..210106f 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -50,7 +50,7 @@ import java.util.List; */ public class AccessibilityRecord { - private static final int INVALID_POSITION = -1; + private static final int UNDEFINED = -1; private static final int PROPERTY_CHECKED = 0x00000001; private static final int PROPERTY_ENABLED = 0x00000002; @@ -68,15 +68,15 @@ public class AccessibilityRecord { boolean mSealed; int mBooleanProperties; - int mCurrentItemIndex; - int mItemCount; - int mFromIndex; - int mToIndex; - int mScrollX; - int mScrollY; - - int mAddedCount; - int mRemovedCount; + int mCurrentItemIndex = UNDEFINED; + int mItemCount = UNDEFINED; + int mFromIndex = UNDEFINED; + int mToIndex = UNDEFINED; + int mScrollX = UNDEFINED; + int mScrollY = UNDEFINED; + + int mAddedCount= UNDEFINED; + int mRemovedCount = UNDEFINED; int mSourceViewId = View.NO_ID; int mSourceWindowId = View.NO_ID; @@ -681,14 +681,14 @@ public class AccessibilityRecord { void clear() { mSealed = false; mBooleanProperties = 0; - mCurrentItemIndex = INVALID_POSITION; - mItemCount = 0; - mFromIndex = 0; - mToIndex = 0; - mScrollX = 0; - mScrollY = 0; - mAddedCount = 0; - mRemovedCount = 0; + mCurrentItemIndex = UNDEFINED; + mItemCount = UNDEFINED; + mFromIndex = UNDEFINED; + mToIndex = UNDEFINED; + mScrollX = UNDEFINED; + mScrollY = UNDEFINED; + mAddedCount = UNDEFINED; + mRemovedCount = UNDEFINED; mClassName = null; mContentDescription = null; mBeforeText = null; diff --git a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl index 1eb60fc..4e69692 100644 --- a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl @@ -26,4 +26,5 @@ oneway interface IAccessibilityManagerClient { void setEnabled(boolean enabled); + void setTouchExplorationEnabled(boolean enabled); } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 0294e3f..88583df 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -120,6 +120,7 @@ class CallbackProxy extends Handler { private static final int AUTO_LOGIN = 140; private static final int CLIENT_CERT_REQUEST = 141; private static final int SEARCHBOX_IS_SUPPORTED_CALLBACK = 142; + private static final int SEARCHBOX_DISPATCH_COMPLETE_CALLBACK= 143; // Message triggered by the client to resume execution private static final int NOTIFY = 200; @@ -821,6 +822,13 @@ class CallbackProxy extends Handler { searchBox.handleIsSupportedCallback(supported); break; } + case SEARCHBOX_DISPATCH_COMPLETE_CALLBACK: { + SearchBoxImpl searchBox = (SearchBoxImpl) mWebView.getSearchBox(); + Boolean success = (Boolean) msg.obj; + searchBox.handleDispatchCompleteCallback(msg.getData().getString("function"), + msg.getData().getInt("id"), success); + break; + } } } @@ -1641,4 +1649,13 @@ class CallbackProxy extends Handler { msg.obj = new Boolean(isSupported); sendMessage(msg); } + + void onSearchboxDispatchCompleteCallback(String function, int id, boolean success) { + Message msg = obtainMessage(SEARCHBOX_DISPATCH_COMPLETE_CALLBACK); + msg.obj = Boolean.valueOf(success); + msg.getData().putString("function", function); + msg.getData().putInt("id", id); + + sendMessage(msg); + } } diff --git a/core/java/android/webkit/L10nUtils.java b/core/java/android/webkit/L10nUtils.java index 4c42cde..a1c6a53 100644 --- a/core/java/android/webkit/L10nUtils.java +++ b/core/java/android/webkit/L10nUtils.java @@ -74,7 +74,19 @@ public class L10nUtils { com.android.internal.R.string.autofill_country_code_re, // IDS_AUTOFILL_COUNTRY_CODE_RE com.android.internal.R.string.autofill_area_code_notext_re, // IDS_AUTOFILL_AREA_CODE_NOTEXT_RE com.android.internal.R.string.autofill_phone_prefix_separator_re, // IDS_AUTOFILL_PHONE_PREFIX_SEPARATOR_RE - com.android.internal.R.string.autofill_phone_suffix_separator_re // IDS_AUTOFILL_PHONE_SUFFIX_SEPARATOR_RE + com.android.internal.R.string.autofill_phone_suffix_separator_re, // IDS_AUTOFILL_PHONE_SUFFIX_SEPARATOR_RE + com.android.internal.R.string.autofill_province, // IDS_AUTOFILL_DIALOG_PROVINCE + com.android.internal.R.string.autofill_postal_code, // IDS_AUTOFILL_DIALOG_POSTAL_CODE + com.android.internal.R.string.autofill_state, // IDS_AUTOFILL_DIALOG_STATE + com.android.internal.R.string.autofill_zip_code, // IDS_AUTOFILL_DIALOG_ZIP_CODE + com.android.internal.R.string.autofill_county, // IDS_AUTOFILL_DIALOG_COUNTY + com.android.internal.R.string.autofill_island, // IDS_AUTOFILL_DIALOG_ISLAND + com.android.internal.R.string.autofill_district, // IDS_AUTOFILL_DIALOG_DISTRICT + com.android.internal.R.string.autofill_department, // IDS_AUTOFILL_DIALOG_DEPARTMENT + com.android.internal.R.string.autofill_prefecture, // IDS_AUTOFILL_DIALOG_PREFECTURE + com.android.internal.R.string.autofill_parish, // IDS_AUTOFILL_DIALOG_PARISH + com.android.internal.R.string.autofill_area, // IDS_AUTOFILL_DIALOG_AREA + com.android.internal.R.string.autofill_emirate // IDS_AUTOFILL_DIALOG_EMIRATE }; private static Context mApplicationContext; diff --git a/core/java/android/webkit/SearchBox.java b/core/java/android/webkit/SearchBox.java index 5075302..6512c4b 100644 --- a/core/java/android/webkit/SearchBox.java +++ b/core/java/android/webkit/SearchBox.java @@ -68,11 +68,15 @@ public interface SearchBox { * Notify the search page of any changes to the searchbox. Such as * a change in the typed query (onchange), the user commiting a given query * (onsubmit), or a change in size of a suggestions dropdown (onresize). + * + * @param listener an optional listener to notify of the success of the operation, + * indicating if the javascript function existed and could be called or not. + * It will be called on the UI thread. */ - void onchange(); - void onsubmit(); - void onresize(); - void oncancel(); + void onchange(SearchBoxListener listener); + void onsubmit(SearchBoxListener listener); + void onresize(SearchBoxListener listener); + void oncancel(SearchBoxListener listener); /** * Add and remove listeners to the given Searchbox. Listeners are notified @@ -91,8 +95,12 @@ public interface SearchBox { * Listeners (if any) will be called on the thread that created the * webview. */ - interface SearchBoxListener { - void onSuggestionsReceived(String query, List<String> suggestions); + public abstract class SearchBoxListener { + public void onSuggestionsReceived(String query, List<String> suggestions) {} + public void onChangeComplete(boolean called) {} + public void onSubmitComplete(boolean called) {} + public void onResizeComplete(boolean called) {} + public void onCancelComplete(boolean called) {} } interface IsSupportedCallback { diff --git a/core/java/android/webkit/SearchBoxImpl.java b/core/java/android/webkit/SearchBoxImpl.java index 61fb2ce..9942d25 100644 --- a/core/java/android/webkit/SearchBoxImpl.java +++ b/core/java/android/webkit/SearchBoxImpl.java @@ -16,10 +16,12 @@ package android.webkit; +import android.text.TextUtils; import android.util.Log; import android.webkit.WebViewCore.EventHub; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import org.json.JSONArray; @@ -69,7 +71,7 @@ final class SearchBoxImpl implements SearchBox { private static final String SET_VERBATIM_SCRIPT = "if (window.chrome && window.chrome.searchBox) {" - + " window.chrome.searchBox.verbatim = %s;" + + " window.chrome.searchBox.verbatim = %1$s;" + "}"; private static final String SET_SELECTION_SCRIPT @@ -89,13 +91,21 @@ final class SearchBoxImpl implements SearchBox { + "}"; private static final String DISPATCH_EVENT_SCRIPT - = "if (window.chrome && window.chrome.searchBox &&" - + " window.chrome.searchBox.on%1$s) { window.chrome.searchBox.on%1$s(); }"; + = "if (window.chrome && window.chrome.searchBox && window.chrome.searchBox.on%1$s) {" + + " window.chrome.searchBox.on%1$s();" + + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, true);" + + "} else {" + + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, false);" + + "}"; + + private static final String EVENT_CHANGE = "change"; + private static final String EVENT_SUBMIT = "submit"; + private static final String EVENT_RESIZE = "resize"; + private static final String EVENT_CANCEL = "cancel"; private static final String IS_SUPPORTED_SCRIPT = "if (window.searchBoxJavaBridge_) {" - + " if (window.chrome && window.chrome.searchBox && " - + " window.chrome.searchBox.onsubmit) {" + + " if (window.chrome && window.chrome.sv) {" + " window.searchBoxJavaBridge_.isSupportedCallback(true);" + " } else {" + " window.searchBoxJavaBridge_.isSupportedCallback(false);" @@ -105,11 +115,14 @@ final class SearchBoxImpl implements SearchBox { private final WebViewCore mWebViewCore; private final CallbackProxy mCallbackProxy; private IsSupportedCallback mSupportedCallback; + private int mNextEventId = 1; + private final HashMap<Integer, SearchBoxListener> mEventCallbacks; SearchBoxImpl(WebViewCore webViewCore, CallbackProxy callbackProxy) { mListeners = new ArrayList<SearchBoxListener>(); mWebViewCore = webViewCore; mCallbackProxy = callbackProxy; + mEventCallbacks = new HashMap<Integer, SearchBoxListener>(); } @Override @@ -141,27 +154,36 @@ final class SearchBoxImpl implements SearchBox { } @Override - public void onchange() { - dispatchEvent("change"); + public void onchange(SearchBoxListener callback) { + dispatchEvent(EVENT_CHANGE, callback); } @Override - public void onsubmit() { - dispatchEvent("submit"); + public void onsubmit(SearchBoxListener callback) { + dispatchEvent(EVENT_SUBMIT, callback); } @Override - public void onresize() { - dispatchEvent("resize"); + public void onresize(SearchBoxListener callback) { + dispatchEvent(EVENT_RESIZE, callback); } @Override - public void oncancel() { - dispatchEvent("cancel"); + public void oncancel(SearchBoxListener callback) { + dispatchEvent(EVENT_CANCEL, callback); } - private void dispatchEvent(String eventName) { - final String js = String.format(DISPATCH_EVENT_SCRIPT, eventName); + private void dispatchEvent(String eventName, SearchBoxListener callback) { + int eventId; + if (callback != null) { + synchronized(this) { + eventId = mNextEventId++; + mEventCallbacks.put(eventId, callback); + } + } else { + eventId = 0; + } + final String js = String.format(DISPATCH_EVENT_SCRIPT, eventName, eventId); dispatchJs(js); } @@ -202,9 +224,35 @@ final class SearchBoxImpl implements SearchBox { } } + // Called by Javascript through the Java bridge. + public void dispatchCompleteCallback(String function, int id, boolean successful) { + mCallbackProxy.onSearchboxDispatchCompleteCallback(function, id, successful); + } + + public void handleDispatchCompleteCallback(String function, int id, boolean successful) { + if (id != 0) { + SearchBoxListener listener; + synchronized(this) { + listener = mEventCallbacks.get(id); + mEventCallbacks.remove(id); + } + if (listener != null) { + if (TextUtils.equals(EVENT_CHANGE, function)) { + listener.onChangeComplete(successful); + } else if (TextUtils.equals(EVENT_SUBMIT, function)) { + listener.onSubmitComplete(successful); + } else if (TextUtils.equals(EVENT_RESIZE, function)) { + listener.onResizeComplete(successful); + } else if (TextUtils.equals(EVENT_CANCEL, function)) { + listener.onCancelComplete(successful); + } + } + } + } + // This is used as a hackish alternative to javascript escaping. // There appears to be no such functionality in the core framework. - private String jsonSerialize(String query) { + private static String jsonSerialize(String query) { JSONStringer stringer = new JSONStringer(); try { stringer.array().value(query).endArray(); diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 761007f..d584acd 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1783,6 +1783,20 @@ public class WebSettings { } /** + * @hide + */ + public void setProperty(String key, String value) { + mWebView.nativeSetProperty(key, value); + } + + /** + * @hide + */ + public String getProperty(String key) { + return mWebView.nativeGetProperty(key); + } + + /** * Transfer messages from the queue to the new WebCoreThread. Called from * WebCore thread. */ diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 7ba86a5..b22c57b 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -9089,6 +9089,52 @@ public class WebView extends AbsoluteLayout } } + /** + * Begin collecting per-tile profiling data + * + * @hide only used by profiling tests + */ + public void tileProfilingStart() { + nativeTileProfilingStart(); + } + /** + * Return per-tile profiling data + * + * @hide only used by profiling tests + */ + public float tileProfilingStop() { + return nativeTileProfilingStop(); + } + + /** @hide only used by profiling tests */ + public void tileProfilingClear() { + nativeTileProfilingClear(); + } + /** @hide only used by profiling tests */ + public int tileProfilingNumFrames() { + return nativeTileProfilingNumFrames(); + } + /** @hide only used by profiling tests */ + public int tileProfilingNumTilesInFrame(int frame) { + return nativeTileProfilingNumTilesInFrame(frame); + } + /** @hide only used by profiling tests */ + public int tileProfilingGetX(int frame, int tile) { + return nativeTileProfilingGetX(frame, tile); + } + /** @hide only used by profiling tests */ + public int tileProfilingGetY(int frame, int tile) { + return nativeTileProfilingGetY(frame, tile); + } + /** @hide only used by profiling tests */ + public boolean tileProfilingGetReady(int frame, int tile) { + return nativeTileProfilingGetReady(frame, tile); + } + /** @hide only used by profiling tests */ + public int tileProfilingGetLevel(int frame, int tile) { + return nativeTileProfilingGetLevel(frame, tile); + } + private native int nativeCacheHitFramePointer(); private native boolean nativeCacheHitIsPlugin(); private native Rect nativeCacheHitNodeBounds(); @@ -9211,6 +9257,15 @@ public class WebView extends AbsoluteLayout private native void nativeStopGL(); private native Rect nativeSubtractLayers(Rect content); private native int nativeTextGeneration(); + private native void nativeTileProfilingStart(); + private native float nativeTileProfilingStop(); + private native void nativeTileProfilingClear(); + private native int nativeTileProfilingNumFrames(); + private native int nativeTileProfilingNumTilesInFrame(int frame); + private native int nativeTileProfilingGetX(int frame, int tile); + private native int nativeTileProfilingGetY(int frame, int tile); + private native boolean nativeTileProfilingGetReady(int frame, int tile); + private native int nativeTileProfilingGetLevel(int frame, int tile); // Never call this version except by updateCachedTextfield(String) - // we always want to pass in our generation number. private native void nativeUpdateCachedTextfield(String updatedText, @@ -9234,4 +9289,6 @@ public class WebView extends AbsoluteLayout */ private native boolean nativeScrollLayer(int layer, int newX, int newY); private native int nativeGetBackgroundColor(); + native void nativeSetProperty(String key, String value); + native String nativeGetProperty(String key); } diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 4f97066..5414b79 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -248,7 +248,7 @@ public final class WebViewCore { /* Get the BrowserFrame component. This is used for subwindow creation and * is called only from BrowserFrame in the WebCore thread. */ - /* package */ BrowserFrame getBrowserFrame() { + /* package */ synchronized BrowserFrame getBrowserFrame() { return mBrowserFrame; } @@ -2235,16 +2235,15 @@ public final class WebViewCore { // called by JNI private void updateViewport() { - // if updateViewport is called before first layout, wait until first - // layout to update the viewport. In the rare case, this is called after - // first layout, force an update as we have just parsed the viewport - // meta tag. - if (mBrowserFrame.firstLayoutDone()) { - setupViewport(true); - } + // Update viewport asap to make sure we get correct one. + setupViewport(true); } private void setupViewport(boolean updateViewState) { + if (mWebView == null || mSettings == null) { + // We've been destroyed or are being destroyed, return early + return; + } // set the viewport settings from WebKit setViewportSettingsFromNative(); @@ -2375,8 +2374,12 @@ public final class WebViewCore { (float) webViewWidth / mViewportWidth; } else { mInitialViewState.mTextWrapScale = adjust; - // 0 will trigger WebView to turn on zoom overview mode - mInitialViewState.mViewScale = 0; + if (mSettings.getUseWideViewPort()) { + // 0 will trigger WebView to turn on zoom overview mode + mInitialViewState.mViewScale = 0; + } else { + mInitialViewState.mViewScale = adjust; + } } } @@ -2407,7 +2410,7 @@ public final class WebViewCore { mEventHub.removeMessages(EventHub.VIEW_SIZE_CHANGED); mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, EventHub.VIEW_SIZE_CHANGED, data)); - } else if (mSettings.getUseWideViewPort()) { + } else { if (viewportWidth == 0) { // Trick to ensure VIEW_SIZE_CHANGED will be sent from WebView // to WebViewCore diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java index 252fc8f..49ea944 100644 --- a/core/java/android/webkit/ZoomManager.java +++ b/core/java/android/webkit/ZoomManager.java @@ -300,7 +300,7 @@ class ZoomManager { } public final float getDefaultScale() { - return mDefaultScale; + return mInitialScale > 0 ? mInitialScale : mDefaultScale; } public final float getReadingLevelScale() { @@ -344,6 +344,8 @@ class ZoomManager { public final void setInitialScaleInPercent(int scaleInPercent) { mInitialScale = scaleInPercent * 0.01f; + mActualScale = mInitialScale > 0 ? mInitialScale : mDefaultScale; + mInvActualScale = 1 / mActualScale; } public final float computeScaleWithLimits(float scale) { @@ -1087,6 +1089,7 @@ class ZoomManager { float scale; if (mInitialScale > 0) { scale = mInitialScale; + mTextWrapScale = scale; } else if (viewState.mViewScale > 0) { mTextWrapScale = viewState.mTextWrapScale; scale = viewState.mViewScale; @@ -1105,7 +1108,7 @@ class ZoomManager { } boolean reflowText = false; if (!viewState.mIsRestored) { - if (settings.getUseFixedViewport()) { + if (settings.getUseFixedViewport() && mInitialScale == 0) { // Override the scale only in case of fixed viewport. scale = Math.max(scale, overviewScale); mTextWrapScale = Math.max(mTextWrapScale, overviewScale); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 1449b18..8f8c1d0 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -271,12 +271,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te Drawable mSelector; /** - * Set to true if we would like to have the selector showing itself. - * We still need to draw and position it even if this is false. - */ - boolean mSelectorShowing; - - /** * The current position of the selector in the list. */ int mSelectorPosition = INVALID_POSITION; @@ -1669,7 +1663,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); mSelectedTop = 0; - mSelectorShowing = false; mSelectorPosition = INVALID_POSITION; mSelectorRect.setEmpty(); invalidate(); @@ -2025,7 +2018,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final boolean isChildViewEnabled = mIsChildViewEnabled; if (sel.isEnabled() != isChildViewEnabled) { mIsChildViewEnabled = !isChildViewEnabled; - if (mSelectorShowing) { + if (getSelectedItemPosition() != INVALID_POSITION) { refreshDrawableState(); } } @@ -2769,6 +2762,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // touch mode). Force an initial layout to get rid of the selection. layoutChildren(); } + updateSelectorState(); } else { int touchMode = mTouchMode; if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { @@ -2847,14 +2841,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { - if (ev.getEdgeFlags() != 0 && motionPosition < 0) { - // If we couldn't find a view to click on, but the down event - // was touching the edge, we will bail out and try again. - // This allows the edge correcting code in ViewAncestor to try to - // find a nearby view to select - return false; - } - if (mTouchMode == TOUCH_MODE_FLING) { // Stopped a fling. It is a scroll. createScrollingCache(); @@ -2888,7 +2874,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } case MotionEvent.ACTION_MOVE: { - final int pointerIndex = ev.findPointerIndex(mActivePointerId); + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + pointerIndex = 0; + mActivePointerId = ev.getPointerId(pointerIndex); + } final int y = (int) ev.getY(pointerIndex); deltaY = y - mMotionY; switch (mTouchMode) { @@ -3464,7 +3454,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te case MotionEvent.ACTION_MOVE: { switch (mTouchMode) { case TOUCH_MODE_DOWN: - final int pointerIndex = ev.findPointerIndex(mActivePointerId); + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + pointerIndex = 0; + mActivePointerId = ev.getPointerId(pointerIndex); + } final int y = (int) ev.getY(pointerIndex); if (startScrollIfNeeded(y - mMotionY)) { return true; @@ -4521,7 +4515,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); mSelectedTop = 0; - mSelectorShowing = false; } } @@ -4645,6 +4638,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te childrenTop += getVerticalFadingEdgeLength(); } } + // Don't ever focus a disabled item. + if (!mAdapter.isEnabled(i)) continue; + if (top >= childrenTop) { // Found a view whose top is fully visisble selectedPos = firstPosition + i; diff --git a/core/java/android/widget/ActivityChooserModel.java b/core/java/android/widget/ActivityChooserModel.java index 83f80ff..d7429b3 100644 --- a/core/java/android/widget/ActivityChooserModel.java +++ b/core/java/android/widget/ActivityChooserModel.java @@ -126,7 +126,7 @@ public class ActivityChooserModel extends DataSetObservable { */ // This cannot be done by a simple comparator since an Activity weight // is computed from history. Note that Activity implements Comparable. - public void sort(Intent intent, List<Activity> activities, + public void sort(Intent intent, List<ActivityResolveInfo> activities, List<HistoricalRecord> historicalRecords); } @@ -215,7 +215,7 @@ public class ActivityChooserModel extends DataSetObservable { /** * List of activities that can handle the current intent. */ - private final List<Activity> mActivitys = new ArrayList<Activity>(); + private final List<ActivityResolveInfo> mActivites = new ArrayList<ActivityResolveInfo>(); /** * List with historical choice records. @@ -311,9 +311,6 @@ public class ActivityChooserModel extends DataSetObservable { * @return The model. */ public static ActivityChooserModel get(Context context, String historyFileName) { - if (historyFileName == null) { - return new ActivityChooserModel(context, historyFileName); - } synchronized (sRegistryLock) { ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName); if (dataModel == null) { @@ -380,7 +377,7 @@ public class ActivityChooserModel extends DataSetObservable { */ public int getActivityCount() { synchronized (mInstanceLock) { - return mActivitys.size(); + return mActivites.size(); } } @@ -389,12 +386,12 @@ public class ActivityChooserModel extends DataSetObservable { * * @return The activity. * - * @see Activity + * @see ActivityResolveInfo * @see #setIntent(Intent) */ public ResolveInfo getActivity(int index) { synchronized (mInstanceLock) { - return mActivitys.get(index).resolveInfo; + return mActivites.get(index).resolveInfo; } } @@ -406,10 +403,10 @@ public class ActivityChooserModel extends DataSetObservable { * @return The index if found, -1 otherwise. */ public int getActivityIndex(ResolveInfo activity) { - List<Activity> activities = mActivitys; + List<ActivityResolveInfo> activities = mActivites; final int activityCount = activities.size(); for (int i = 0; i < activityCount; i++) { - Activity currentActivity = activities.get(i); + ActivityResolveInfo currentActivity = activities.get(i); if (currentActivity.resolveInfo == activity) { return i; } @@ -433,8 +430,7 @@ public class ActivityChooserModel extends DataSetObservable { * @see HistoricalRecord */ public Intent chooseActivity(int index) { - Activity chosenActivity = mActivitys.get(index); - Activity defaultActivity = mActivitys.get(0); + ActivityResolveInfo chosenActivity = mActivites.get(index); ComponentName chosenName = new ComponentName( chosenActivity.resolveInfo.activityInfo.packageName, @@ -460,8 +456,8 @@ public class ActivityChooserModel extends DataSetObservable { */ public ResolveInfo getDefaultActivity() { synchronized (mInstanceLock) { - if (!mActivitys.isEmpty()) { - return mActivitys.get(0).resolveInfo; + if (!mActivites.isEmpty()) { + return mActivites.get(0).resolveInfo; } } return null; @@ -478,8 +474,8 @@ public class ActivityChooserModel extends DataSetObservable { * @param index The index of the activity to set as default. */ public void setDefaultActivity(int index) { - Activity newDefaultActivity = mActivitys.get(index); - Activity oldDefaultActivity = mActivitys.get(0); + ActivityResolveInfo newDefaultActivity = mActivites.get(index); + ActivityResolveInfo oldDefaultActivity = mActivites.get(0); final float weight; if (oldDefaultActivity != null) { @@ -572,8 +568,8 @@ public class ActivityChooserModel extends DataSetObservable { */ private void sortActivities() { synchronized (mInstanceLock) { - if (mActivitySorter != null && !mActivitys.isEmpty()) { - mActivitySorter.sort(mIntent, mActivitys, + if (mActivitySorter != null && !mActivites.isEmpty()) { + mActivitySorter.sort(mIntent, mActivites, Collections.unmodifiableList(mHistoricalRecords)); notifyChanged(); } @@ -661,14 +657,14 @@ public class ActivityChooserModel extends DataSetObservable { * Loads the activities. */ private void loadActivitiesLocked() { - mActivitys.clear(); + mActivites.clear(); if (mIntent != null) { List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentActivities(mIntent, 0); final int resolveInfoCount = resolveInfos.size(); for (int i = 0; i < resolveInfoCount; i++) { ResolveInfo resolveInfo = resolveInfos.get(i); - mActivitys.add(new Activity(resolveInfo)); + mActivites.add(new ActivityResolveInfo(resolveInfo)); } sortActivities(); } else { @@ -797,7 +793,7 @@ public class ActivityChooserModel extends DataSetObservable { /** * Represents an activity. */ - public final class Activity implements Comparable<Activity> { + public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> { /** * The {@link ResolveInfo} of the activity. @@ -814,7 +810,7 @@ public class ActivityChooserModel extends DataSetObservable { * * @param resolveInfo activity {@link ResolveInfo}. */ - public Activity(ResolveInfo resolveInfo) { + public ActivityResolveInfo(ResolveInfo resolveInfo) { this.resolveInfo = resolveInfo; } @@ -834,14 +830,14 @@ public class ActivityChooserModel extends DataSetObservable { if (getClass() != obj.getClass()) { return false; } - Activity other = (Activity) obj; + ActivityResolveInfo other = (ActivityResolveInfo) obj; if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { return false; } return true; } - public int compareTo(Activity another) { + public int compareTo(ActivityResolveInfo another) { return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); } @@ -862,18 +858,18 @@ public class ActivityChooserModel extends DataSetObservable { private final class DefaultSorter implements ActivitySorter { private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; - private final Map<String, Activity> mPackageNameToActivityMap = - new HashMap<String, Activity>(); + private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap = + new HashMap<String, ActivityResolveInfo>(); - public void sort(Intent intent, List<Activity> activities, + public void sort(Intent intent, List<ActivityResolveInfo> activities, List<HistoricalRecord> historicalRecords) { - Map<String, Activity> packageNameToActivityMap = + Map<String, ActivityResolveInfo> packageNameToActivityMap = mPackageNameToActivityMap; packageNameToActivityMap.clear(); final int activityCount = activities.size(); for (int i = 0; i < activityCount; i++) { - Activity activity = activities.get(i); + ActivityResolveInfo activity = activities.get(i); activity.weight = 0.0f; String packageName = activity.resolveInfo.activityInfo.packageName; packageNameToActivityMap.put(packageName, activity); @@ -884,9 +880,11 @@ public class ActivityChooserModel extends DataSetObservable { for (int i = lastShareIndex; i >= 0; i--) { HistoricalRecord historicalRecord = historicalRecords.get(i); String packageName = historicalRecord.activity.getPackageName(); - Activity activity = packageNameToActivityMap.get(packageName); - activity.weight += historicalRecord.weight * nextRecordWeight; - nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; + ActivityResolveInfo activity = packageNameToActivityMap.get(packageName); + if (activity != null) { + activity.weight += historicalRecord.weight * nextRecordWeight; + nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; + } } Collections.sort(activities); diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index 2fe8162..5b69aa8 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -16,23 +16,29 @@ package android.widget; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.drawable.Drawable; -import android.os.Debug; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ActivityChooserModel; import android.widget.ActivityChooserModel.ActivityChooserModelClient; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; +import android.widget.TextView; import com.android.internal.R; @@ -56,11 +62,6 @@ import com.android.internal.R; * </li> * </ul> * </p> - * </p> - * This view is backed by a {@link ActivityChooserModel}. Calling {@link #showPopup()} - * while this view is attached to the view hierarchy will show a popup with - * activities while if the view is not attached it will show a dialog. - * </p> * * @hide */ @@ -92,29 +93,26 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod private final ImageButton mDefaultActionButton; /** - * The header for handlers list. + * The maximal width of the list popup. */ - private final View mListHeaderView; + private final int mListPopupMaxWidth; /** - * The footer for handlers list. + * Observer for the model data. */ - private final View mListFooterView; + private final DataSetObserver mModelDataSetOberver = new DataSetObserver() { - /** - * The title of the header view. - */ - private TextView mListHeaderViewTitle; - - /** - * The title for expanding the activities list. - */ - private final String mListHeaderViewTitleSelectDefault; - - /** - * The title if no activity exist. - */ - private final String mListHeaderViewTitleNoActivities; + @Override + public void onChanged() { + super.onChanged(); + mAdapter.notifyDataSetChanged(); + } + @Override + public void onInvalidated() { + super.onInvalidated(); + mAdapter.notifyDataSetInvalidated(); + } + }; /** * Popup window for showing the activity overflow list. @@ -122,11 +120,6 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod private ListPopupWindow mListPopupWindow; /** - * Alert dialog for showing the activity overflow list. - */ - private AlertDialog mAlertDialog; - - /** * Listener for the dismissal of the popup/alert. */ private PopupWindow.OnDismissListener mOnDismissListener; @@ -147,16 +140,6 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod private boolean mIsAttachedToWindow; /** - * Flag whether this view is showing an alert dialog. - */ - private boolean mIsShowingAlertDialog; - - /** - * Flag whether this view is showing a popup window. - */ - private boolean mIsShowingPopuWindow; - - /** * Create a new instance. * * @param context The application environment. @@ -195,8 +178,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable( R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable); - LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); + LayoutInflater inflater = LayoutInflater.from(mContext); inflater.inflate(R.layout.activity_chooser_view, this, true); mCallbacks = new Callbacks(); @@ -209,16 +191,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod mExpandActivityOverflowButton = (ImageButton) findViewById(R.id.expand_activities_button); mExpandActivityOverflowButton.setOnClickListener(mCallbacks); - mExpandActivityOverflowButton.setBackgroundDrawable(expandActivityOverflowButtonDrawable); - - mListHeaderView = inflater.inflate(R.layout.activity_chooser_list_header, null); - mListFooterView = inflater.inflate(R.layout.activity_chooser_list_footer, null); - - mListHeaderViewTitle = (TextView) mListHeaderView.findViewById(R.id.title); - mListHeaderViewTitleSelectDefault = context.getString( - R.string.activity_chooser_view_select_default); - mListHeaderViewTitleNoActivities = context.getString( - R.string.activity_chooser_view_no_activities); + mExpandActivityOverflowButton.setImageDrawable(expandActivityOverflowButtonDrawable); mAdapter = new ActivityChooserViewAdapter(); mAdapter.registerDataSetObserver(new DataSetObserver() { @@ -228,6 +201,10 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod updateButtons(); } }); + + Resources resources = context.getResources(); + mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2, + resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); } /** @@ -253,7 +230,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * @param drawable The drawable. */ public void setExpandActivityOverflowButtonDrawable(Drawable drawable) { - mExpandActivityOverflowButton.setBackgroundDrawable(drawable); + mExpandActivityOverflowButton.setImageDrawable(drawable); } /** @@ -262,7 +239,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * @return True if the popup was shown, false if already showing. */ public boolean showPopup() { - if (isShowingPopup()) { + if (isShowingPopup() || !mIsAttachedToWindow) { return false; } mIsSelectingDefaultActivity = false; @@ -276,38 +253,30 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * @param maxActivityCount The max number of activities to display. */ private void showPopupUnchecked(int maxActivityCount) { - mAdapter.setMaxActivityCount(maxActivityCount); - if (mIsSelectingDefaultActivity) { - if (mAdapter.getActivityCount() > 0) { - mListHeaderViewTitle.setText(mListHeaderViewTitleSelectDefault); - } else { - mListHeaderViewTitle.setText(mListHeaderViewTitleNoActivities); - } - mAdapter.setHeaderView(mListHeaderView); - } else { - mAdapter.setHeaderView(null); + if (mAdapter.getDataModel() == null) { + throw new IllegalStateException("No data model. Did you call #setDataModel?"); } - if (mAdapter.getActivityCount() > maxActivityCount + 1) { - mAdapter.setFooterView(mListFooterView); + mAdapter.setMaxActivityCount(maxActivityCount); + + final int activityCount = mAdapter.getActivityCount(); + if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED + && activityCount > maxActivityCount + 1) { + mAdapter.setShowFooterView(true); } else { - mAdapter.setFooterView(null); + mAdapter.setShowFooterView(false); } - if (!mIsAttachedToWindow || mIsShowingAlertDialog) { - AlertDialog alertDialog = getAlertDilalog(); - if (!alertDialog.isShowing()) { - alertDialog.setCustomTitle(this); - alertDialog.show(); - mIsShowingAlertDialog = true; - } - } else { - ListPopupWindow popupWindow = getListPopupWindow(); - if (!popupWindow.isShowing()) { - popupWindow.setContentWidth(mAdapter.measureContentWidth()); - popupWindow.show(); - mIsShowingPopuWindow = true; + ListPopupWindow popupWindow = getListPopupWindow(); + if (!popupWindow.isShowing()) { + if (mIsSelectingDefaultActivity) { + mAdapter.setShowDefaultActivity(true); + } else { + mAdapter.setShowDefaultActivity(false); } + final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth); + popupWindow.setContentWidth(contentWidth); + popupWindow.show(); } } @@ -317,12 +286,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * @return True if dismissed, false if already dismissed. */ public boolean dismissPopup() { - if (!isShowingPopup()) { - return false; - } - if (mIsShowingAlertDialog) { - getAlertDilalog().dismiss(); - } else if (mIsShowingPopuWindow) { + if (isShowingPopup()) { getListPopupWindow().dismiss(); } return true; @@ -334,12 +298,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * @return True if the popup is shown. */ public boolean isShowingPopup() { - if (mIsShowingAlertDialog) { - return getAlertDilalog().isShowing(); - } else if (mIsShowingPopuWindow) { - return getListPopupWindow().isShowing(); - } - return false; + return getListPopupWindow().isShowing(); } @Override @@ -347,6 +306,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod super.onAttachedToWindow(); ActivityChooserModel dataModel = mAdapter.getDataModel(); if (dataModel != null) { + dataModel.registerObserver(mModelDataSetOberver); dataModel.readHistoricalData(); } mIsAttachedToWindow = true; @@ -357,6 +317,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod super.onDetachedFromWindow(); ActivityChooserModel dataModel = mAdapter.getDataModel(); if (dataModel != null) { + dataModel.unregisterObserver(mModelDataSetOberver); dataModel.persistHistoricalData(); } mIsAttachedToWindow = false; @@ -371,13 +332,11 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - mActivityChooserContent.layout(left, top, right, bottom); - if (mIsShowingPopuWindow) { - if (isShown()) { - showPopupUnchecked(mAdapter.getMaxActivityCount()); - } else { - dismissPopup(); - } + mActivityChooserContent.layout(0, 0, right - left, bottom - top); + if (getListPopupWindow().isShowing()) { + showPopupUnchecked(mAdapter.getMaxActivityCount()); + } else { + dismissPopup(); } } @@ -429,22 +388,6 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } /** - * Gets the alert dialog which is lazily initialized. - * - * @return The popup. - */ - private AlertDialog getAlertDilalog() { - if (mAlertDialog == null) { - Builder builder = new Builder(getContext()); - builder.setAdapter(mAdapter, null); - mAlertDialog = builder.create(); - mAlertDialog.getListView().setOnItemClickListener(mCallbacks); - mAlertDialog.setOnDismissListener(mCallbacks); - } - return mAlertDialog; - } - - /** * Updates the buttons state. */ private void updateButtons() { @@ -458,7 +401,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } ResolveInfo activity = mAdapter.getDefaultActivity(); PackageManager packageManager = mContext.getPackageManager(); - mDefaultActionButton.setBackgroundDrawable(activity.loadIcon(packageManager)); + mDefaultActionButton.setImageDrawable(activity.loadIcon(packageManager)); } else { mDefaultActionButton.setVisibility(View.INVISIBLE); mExpandActivityOverflowButton.setEnabled(false); @@ -469,24 +412,23 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * Interface implementation to avoid publishing them in the APIs. */ private class Callbacks implements AdapterView.OnItemClickListener, - View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener, - DialogInterface.OnDismissListener { + View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener { // AdapterView#OnItemClickListener public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter(); final int itemViewType = adapter.getItemViewType(position); switch (itemViewType) { - case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_HEADER: { - /* do nothing */ - } break; case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: { showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED); } break; case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: { dismissPopup(); if (mIsSelectingDefaultActivity) { - mAdapter.getDataModel().setDefaultActivity(position); + // The item at position zero is the default already. + if (position > 0) { + mAdapter.getDataModel().setDefaultActivity(position); + } } else { // The first item in the model is default action => adjust index Intent launchIntent = mAdapter.getDataModel().chooseActivity(position + 1); @@ -530,16 +472,6 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod // PopUpWindow.OnDismissListener#onDismiss public void onDismiss() { - mIsShowingPopuWindow = false; - notifyOnDismissListener(); - } - - // DialogInterface.OnDismissListener#onDismiss - @Override - public void onDismiss(DialogInterface dialog) { - mIsShowingAlertDialog = false; - AlertDialog alertDialog = (AlertDialog) dialog; - alertDialog.setCustomTitle(null); notifyOnDismissListener(); } @@ -559,59 +491,35 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4; - private static final int ITEM_VIEW_TYPE_HEADER = 0; - - private static final int ITEM_VIEW_TYPE_ACTIVITY = 1; + private static final int ITEM_VIEW_TYPE_ACTIVITY = 0; - private static final int ITEM_VIEW_TYPE_FOOTER = 2; + private static final int ITEM_VIEW_TYPE_FOOTER = 1; private static final int ITEM_VIEW_TYPE_COUNT = 3; - private final DataSetObserver mDataSetOberver = new DataSetObserver() { - - @Override - public void onChanged() { - super.onChanged(); - notifyDataSetChanged(); - } - @Override - public void onInvalidated() { - super.onInvalidated(); - notifyDataSetInvalidated(); - } - }; - private ActivityChooserModel mDataModel; private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT; - private ResolveInfo mDefaultActivity; + private boolean mShowDefaultActivity; - private View mHeaderView; - - private View mFooterView; + private boolean mShowFooterView; public void setDataModel(ActivityChooserModel dataModel) { + ActivityChooserModel oldDataModel = mAdapter.getDataModel(); + if (oldDataModel != null && isShown()) { + oldDataModel.unregisterObserver(mModelDataSetOberver); + } mDataModel = dataModel; - mDataModel.registerObserver(mDataSetOberver); - notifyDataSetChanged(); - } - - @Override - public void notifyDataSetChanged() { - if (mDataModel.getActivityCount() > 0) { - mDefaultActivity = mDataModel.getDefaultActivity(); - } else { - mDefaultActivity = null; + if (dataModel != null && isShown()) { + dataModel.registerObserver(mModelDataSetOberver); } - super.notifyDataSetChanged(); + notifyDataSetChanged(); } @Override public int getItemViewType(int position) { - if (mHeaderView != null && position == 0) { - return ITEM_VIEW_TYPE_HEADER; - } else if (mFooterView != null && position == getCount() - 1) { + if (mShowFooterView && position == getCount() - 1) { return ITEM_VIEW_TYPE_FOOTER; } else { return ITEM_VIEW_TYPE_ACTIVITY; @@ -626,14 +534,11 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod public int getCount() { int count = 0; int activityCount = mDataModel.getActivityCount(); - if (activityCount > 0) { + if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) { activityCount--; } count = Math.min(activityCount, mMaxActivityCount); - if (mHeaderView != null) { - count++; - } - if (mFooterView != null) { + if (mShowFooterView) { count++; } return count; @@ -642,16 +547,13 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod public Object getItem(int position) { final int itemViewType = getItemViewType(position); switch (itemViewType) { - case ITEM_VIEW_TYPE_HEADER: - return mHeaderView; case ITEM_VIEW_TYPE_FOOTER: - return mFooterView; + return null; case ITEM_VIEW_TYPE_ACTIVITY: - int targetIndex = (mHeaderView == null) ? position : position - 1; - if (mDefaultActivity != null) { - targetIndex++; + if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) { + position++; } - return mDataModel.getActivity(targetIndex); + return mDataModel.getActivity(position); default: throw new IllegalArgumentException(); } @@ -661,27 +563,19 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod return position; } - @Override - public boolean isEnabled(int position) { - final int itemViewType = getItemViewType(position); - switch (itemViewType) { - case ITEM_VIEW_TYPE_HEADER: - return false; - case ITEM_VIEW_TYPE_FOOTER: - case ITEM_VIEW_TYPE_ACTIVITY: - return true; - default: - throw new IllegalArgumentException(); - } - } - public View getView(int position, View convertView, ViewGroup parent) { final int itemViewType = getItemViewType(position); switch (itemViewType) { - case ITEM_VIEW_TYPE_HEADER: - return mHeaderView; case ITEM_VIEW_TYPE_FOOTER: - return mFooterView; + if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) { + convertView = LayoutInflater.from(getContext()).inflate( + R.layout.activity_chooser_view_list_item, parent, false); + convertView.setId(ITEM_VIEW_TYPE_FOOTER); + TextView titleView = (TextView) convertView.findViewById(R.id.title); + titleView.setText(mContext.getString( + R.string.activity_chooser_view_see_all)); + } + return convertView; case ITEM_VIEW_TYPE_ACTIVITY: if (convertView == null || convertView.getId() != R.id.list_item) { convertView = LayoutInflater.from(getContext()).inflate( @@ -691,10 +585,16 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod // Set the icon ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); ResolveInfo activity = (ResolveInfo) getItem(position); - iconView.setBackgroundDrawable(activity.loadIcon(packageManager)); + iconView.setImageDrawable(activity.loadIcon(packageManager)); // Set the title. TextView titleView = (TextView) convertView.findViewById(R.id.title); titleView.setText(activity.loadLabel(packageManager)); + // Highlight the default. + if (mShowDefaultActivity && position == 0) { + convertView.setActivated(true); + } else { + convertView.setActivated(false); + } return convertView; default: throw new IllegalArgumentException(); @@ -702,7 +602,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } public int measureContentWidth() { - // The user may have specified some of the target not to be show but we + // The user may have specified some of the target not to be shown but we // want to measure all of them since after expansion they should fit. final int oldMaxActivityCount = mMaxActivityCount; mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED; @@ -733,19 +633,12 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } public ResolveInfo getDefaultActivity() { - return mDefaultActivity; - } - - public void setHeaderView(View headerView) { - if (mHeaderView != headerView) { - mHeaderView = headerView; - notifyDataSetChanged(); - } + return mDataModel.getDefaultActivity(); } - public void setFooterView(View footerView) { - if (mFooterView != footerView) { - mFooterView = footerView; + public void setShowFooterView(boolean showFooterView) { + if (mShowFooterView != showFooterView) { + mShowFooterView = showFooterView; notifyDataSetChanged(); } } @@ -761,5 +654,12 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod public ActivityChooserModel getDataModel() { return mDataModel; } + + public void setShowDefaultActivity(boolean showDefaultActivity) { + if (mShowDefaultActivity != showDefaultActivity) { + mShowDefaultActivity = showDefaultActivity; + notifyDataSetChanged(); + } + } } } diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index f8c76f2..1b713c3 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -21,6 +21,7 @@ import com.android.internal.R; import android.annotation.Widget; import android.app.Service; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; @@ -239,21 +240,11 @@ public class CalendarView extends FrameLayout { private String[] mDayLabels; /** - * Temporary instance to avoid multiple instantiations. - */ - private Calendar mTempDate = Calendar.getInstance(); - - /** * The first day of the week. */ private int mFirstDayOfWeek; /** - * The first day of the focused month. - */ - private Calendar mFirstDayOfMonth = Calendar.getInstance(); - - /** * Which month should be displayed/highlighted [0-11]. */ private int mCurrentMonthDisplayed; @@ -289,14 +280,24 @@ public class CalendarView extends FrameLayout { private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); /** + * Temporary instance to avoid multiple instantiations. + */ + private Calendar mTempDate; + + /** + * The first day of the focused month. + */ + private Calendar mFirstDayOfMonth; + + /** * The start date of the range supported by this picker. */ - private Calendar mMinDate = Calendar.getInstance(); + private Calendar mMinDate; /** * The end date of the range supported by this picker. */ - private Calendar mMaxDate = Calendar.getInstance(); + private Calendar mMaxDate; /** * Date format for parsing dates. @@ -304,6 +305,11 @@ public class CalendarView extends FrameLayout { private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); /** + * The current locale. + */ + private Locale mCurrentLocale; + + /** * The callback used to indicate the user changes the date. */ public interface OnDateChangeListener { @@ -330,6 +336,9 @@ public class CalendarView extends FrameLayout { public CalendarView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, 0); + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + TypedValue calendarViewStyle = new TypedValue(); context.getTheme().resolveAttribute(R.attr.calendarViewStyle, calendarViewStyle, true); TypedArray attributesArray = context.obtainStyledAttributes(calendarViewStyle.resourceId, @@ -366,6 +375,7 @@ public class CalendarView extends FrameLayout { com.android.internal.R.styleable.TextAppearance); mDateTextSize = dateTextAppearance.getDimensionPixelSize( R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); + dateTextAppearance.recycle(); int weekDayTextAppearanceResId = attributesArray.getResourceId( R.styleable.CalendarView_weekDayTextAppearance, @@ -413,6 +423,12 @@ public class CalendarView extends FrameLayout { return mListView.isEnabled(); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + setCurrentLocale(newConfig.locale); + } + /** * Gets the minimal date supported by this {@link CalendarView} in milliseconds * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time @@ -624,6 +640,41 @@ public class CalendarView extends FrameLayout { } /** + * Sets the current locale. + * + * @param locale The current locale. + */ + private void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; + } + + mCurrentLocale = locale; + + mTempDate = getCalendarForLocale(mTempDate, locale); + mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + } + + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } + + /** * @return True if the <code>firstDate</code> is the same as the <code> * secondDate</code>. */ diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 8d4aaea..49616cc 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -130,10 +130,10 @@ public class CheckedTextView extends TextView implements Checkable { setMinHeight(d.getIntrinsicHeight()); mCheckMarkWidth = d.getIntrinsicWidth(); - mPaddingRight = mCheckMarkWidth + mBasePaddingRight; + mUserPaddingRight = mCheckMarkWidth + mBasePaddingRight; d.setState(getDrawableState()); } else { - mPaddingRight = mBasePaddingRight; + mUserPaddingRight = mBasePaddingRight; } mCheckMarkDrawable = d; requestLayout(); @@ -142,7 +142,7 @@ public class CheckedTextView extends TextView implements Checkable { @Override public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); - mBasePaddingRight = mPaddingRight; + mBasePaddingRight = mUserPaddingRight; } @Override @@ -167,9 +167,9 @@ public class CheckedTextView extends TextView implements Checkable { int right = getWidth(); checkMarkDrawable.setBounds( - right - mCheckMarkWidth - mBasePaddingRight, + right - mUserPaddingRight, y, - right - mBasePaddingRight, + right - mUserPaddingRight + mCheckMarkWidth, y + height); checkMarkDrawable.draw(canvas); } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 2410eb2..d3cdad8 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -217,6 +217,7 @@ public abstract class CompoundButton extends Button implements Checkable { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + info.setCheckable(true); info.setChecked(mChecked); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 30fb927..4812283 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -16,10 +16,9 @@ package android.widget; -import com.android.internal.R; - import android.annotation.Widget; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; @@ -33,6 +32,8 @@ import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.widget.NumberPicker.OnValueChangeListener; +import com.android.internal.R; + import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; @@ -89,23 +90,23 @@ public class DatePicker extends FrameLayout { private final CalendarView mCalendarView; - private OnDateChangedListener mOnDateChangedListener; + private Locale mCurrentLocale; - private Locale mMonthLocale; + private OnDateChangedListener mOnDateChangedListener; - private final Calendar mTempDate = Calendar.getInstance(); + private String[] mShortMonths; - private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; + private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); - private final String[] mShortMonths = new String[mNumberOfMonths]; + private int mNumberOfMonths; - private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); + private Calendar mTempDate; - private final Calendar mMinDate = Calendar.getInstance(); + private Calendar mMinDate; - private final Calendar mMaxDate = Calendar.getInstance(); + private Calendar mMaxDate; - private final Calendar mCurrentDate = Calendar.getInstance(); + private Calendar mCurrentDate; private boolean mIsEnabled = DEFAULT_ENABLED_STATE; @@ -137,6 +138,9 @@ public class DatePicker extends FrameLayout { public DatePicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, defStyle, 0); boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, @@ -213,7 +217,7 @@ public class DatePicker extends FrameLayout { mMonthSpinner = (NumberPicker) findViewById(R.id.month); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(mNumberOfMonths - 1); - mMonthSpinner.setDisplayedValues(getShortMonths()); + mMonthSpinner.setDisplayedValues(mShortMonths); mMonthSpinner.setOnLongPressUpdateInterval(200); mMonthSpinner.setOnValueChangedListener(onChangeListener); @@ -363,6 +367,12 @@ public class DatePicker extends FrameLayout { event.getText().add(selectedDateUtterance); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + setCurrentLocale(newConfig.locale); + } + /** * Gets whether the {@link CalendarView} is shown. * @@ -411,6 +421,48 @@ public class DatePicker extends FrameLayout { } /** + * Sets the current locale. + * + * @param locale The current locale. + */ + private void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; + } + + mCurrentLocale = locale; + + mTempDate = getCalendarForLocale(mTempDate, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + + mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; + mShortMonths = new String[mNumberOfMonths]; + for (int i = 0; i < mNumberOfMonths; i++) { + mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i, + DateUtils.LENGTH_MEDIUM); + } + } + + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } + + /** * Reorders the spinners according to the date format that is * explicitly set by the user and if no such is set fall back * to the current locale's default format. @@ -507,23 +559,6 @@ public class DatePicker extends FrameLayout { } } - /** - * @return The short month abbreviations. - */ - private String[] getShortMonths() { - final Locale currentLocale = Locale.getDefault(); - if (currentLocale.equals(mMonthLocale)) { - return mShortMonths; - } else { - for (int i = 0; i < mNumberOfMonths; i++) { - mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i, - DateUtils.LENGTH_MEDIUM); - } - mMonthLocale = currentLocale; - return mShortMonths; - } - } - private boolean isNewDate(int year, int month, int dayOfMonth) { return (mCurrentDate.get(Calendar.YEAR) != year || mCurrentDate.get(Calendar.MONTH) != dayOfMonth @@ -569,7 +604,7 @@ public class DatePicker extends FrameLayout { // make sure the month names are a zero based array // with the months in the month spinner - String[] displayedValues = Arrays.copyOfRange(getShortMonths(), + String[] displayedValues = Arrays.copyOfRange(mShortMonths, mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); mMonthSpinner.setDisplayedValues(displayedValues); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 7c0470e..b9eb5ff 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -53,12 +53,12 @@ import static java.lang.Math.min; * container and grid index {@code N} is fixed to its trailing edge * (after padding is taken into account). * - * <h4>Row and Column Groups</h4> + * <h4>Row and Column Specs</h4> * * Children occupy one or more contiguous cells, as defined - * by their {@link GridLayout.LayoutParams#rowGroup rowGroup} and - * {@link GridLayout.LayoutParams#columnGroup columnGroup} layout parameters. - * Each group specifies the set of rows or columns that are to be + * by their {@link GridLayout.LayoutParams#rowSpec rowSpec} and + * {@link GridLayout.LayoutParams#columnSpec columnSpec} layout parameters. + * Each spec defines the set of rows or columns that are to be * occupied; and how children should be aligned within the resulting group of cells. * Although cells do not normally overlap in a GridLayout, GridLayout does * not prevent children being defined to occupy the same cell or group of cells. @@ -92,10 +92,8 @@ import static java.lang.Math.min; * * <h4>Excess Space Distribution</h4> * - * Like {@link LinearLayout}, a child's ability to stretch is controlled - * using <em>weights</em>, which are specified using the - * {@link GridLayout.LayoutParams#widthSpec widthSpec} and - * {@link GridLayout.LayoutParams#heightSpec heightSpec} layout parameters. + * A child's ability to stretch is controlled using the flexibility + * properties of its row and column groups. * <p> * <p> * See {@link GridLayout.LayoutParams} for a full description of the @@ -169,8 +167,7 @@ public class GridLayout extends ViewGroup { // Misc constants private static final String TAG = GridLayout.class.getName(); - private static final boolean DEBUG = false; - private static final double GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; + static final boolean DEBUG = false; private static final int PRF = 1; // Defaults @@ -180,8 +177,9 @@ public class GridLayout extends ViewGroup { private static final boolean DEFAULT_USE_DEFAULT_MARGINS = false; private static final boolean DEFAULT_ORDER_PRESERVED = false; private static final int DEFAULT_ALIGNMENT_MODE = ALIGN_MARGINS; - // todo remove this - private static final int DEFAULT_CONTAINER_MARGIN = 20; + private static final int DEFAULT_CONTAINER_MARGIN = 0; + private static final int DEFAULT_MARGIN = 8; + private static final int DEFAULT_CONTAINER_PADDING = 16; private static final int MAX_SIZE = 100000; // TypedArray indices @@ -280,12 +278,12 @@ public class GridLayout extends ViewGroup { /** * Returns the current number of rows. This is either the last value that was set * with {@link #setRowCount(int)} or, if no such value was set, the maximum - * value of each the upper bounds defined in {@link LayoutParams#rowGroup}. + * value of each the upper bounds defined in {@link LayoutParams#rowSpec}. * * @return the current number of rows * * @see #setRowCount(int) - * @see LayoutParams#rowGroup + * @see LayoutParams#rowSpec * * @attr ref android.R.styleable#GridLayout_rowCount */ @@ -301,7 +299,7 @@ public class GridLayout extends ViewGroup { * @param rowCount the number of rows * * @see #getRowCount() - * @see LayoutParams#rowGroup + * @see LayoutParams#rowSpec * * @attr ref android.R.styleable#GridLayout_rowCount */ @@ -312,12 +310,12 @@ public class GridLayout extends ViewGroup { /** * Returns the current number of columns. This is either the last value that was set * with {@link #setColumnCount(int)} or, if no such value was set, the maximum - * value of each the upper bounds defined in {@link LayoutParams#columnGroup}. + * value of each the upper bounds defined in {@link LayoutParams#columnSpec}. * * @return the current number of columns * * @see #setColumnCount(int) - * @see LayoutParams#columnGroup + * @see LayoutParams#columnSpec * * @attr ref android.R.styleable#GridLayout_columnCount */ @@ -333,7 +331,7 @@ public class GridLayout extends ViewGroup { * @param columnCount the number of columns. * * @see #getColumnCount() - * @see LayoutParams#columnGroup + * @see LayoutParams#columnSpec * * @attr ref android.R.styleable#GridLayout_columnCount */ @@ -383,6 +381,10 @@ public class GridLayout extends ViewGroup { */ public void setUseDefaultMargins(boolean useDefaultMargins) { mUseDefaultMargins = useDefaultMargins; + if (useDefaultMargins) { + int padding = DEFAULT_CONTAINER_PADDING; + setPadding(padding, padding, padding, padding); + } requestLayout(); } @@ -520,6 +522,14 @@ public class GridLayout extends ViewGroup { return result; } + private static int sum(int[] a) { + int result = 0; + for (int i = 0, N = a.length; i < N; i++) { + result += a[i]; + } + return result; + } + private static <T> T[] append(T[] a, T[] b) { T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length); System.arraycopy(a, 0, result, 0, a.length); @@ -528,16 +538,10 @@ public class GridLayout extends ViewGroup { } private int getDefaultMargin(View c, boolean horizontal, boolean leading) { - // In the absence of any other information, calculate a default gap such - // that, in a grid of identical components, the heights and the vertical - // gaps are in the proportion of the golden ratio. - // To effect this with equal margins at each edge, set each of the - // four margin values to half this amount. - return (int) (c.getMeasuredHeight() / GOLDEN_RATIO / 2); + return DEFAULT_MARGIN; } private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) { - // todo remove DEFAULT_CONTAINER_MARGIN. Use padding? Seek advice on Themes/Styles, etc. return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, horizontal, leading); } @@ -545,9 +549,9 @@ public class GridLayout extends ViewGroup { if (!mUseDefaultMargins) { return 0; } - Group group = horizontal ? p.columnGroup : p.rowGroup; + Spec spec = horizontal ? p.columnSpec : p.rowSpec; Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; - Interval span = group.span; + Interval span = spec.span; boolean isAtEdge = leading ? (span.min == 0) : (span.max == axis.getCount()); return getDefaultMargin(c, isAtEdge, horizontal, leading); @@ -595,12 +599,12 @@ public class GridLayout extends ViewGroup { if (isGone(c)) continue; LayoutParams lp = getLayoutParams1(c); - final Group colGroup = lp.columnGroup; - final Interval cols = colGroup.span; + final Spec colSpec = lp.columnSpec; + final Interval cols = colSpec.span; final int colSpan = cols.size(); - final Group rowGroup = lp.rowGroup; - final Interval rows = rowGroup.span; + final Spec rowSpec = lp.rowSpec; + final Interval rows = rowSpec.span; final int rowSpan = rows.size(); if (horizontal) { @@ -625,8 +629,8 @@ public class GridLayout extends ViewGroup { maxSize = max(maxSize, colSpan); } - lp.setColumnGroupSpan(new Interval(col, col + colSpan)); - lp.setRowGroupSpan(new Interval(row, row + rowSpan)); + lp.setColumnSpecSpan(new Interval(col, col + colSpan)); + lp.setRowSpecSpan(new Interval(row, row + rowSpan)); if (horizontal) { col = col + colSpan; @@ -739,7 +743,7 @@ public class GridLayout extends ViewGroup { } // Draw margins - paint.setColor(Color.YELLOW); + paint.setColor(Color.MAGENTA); for (int i = 0; i < getChildCount(); i++) { View c = getChildAt(i); drawRectangle(canvas, @@ -874,11 +878,11 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); if (isGone(c)) continue; LayoutParams lp = getLayoutParams(c); - Group columnGroup = lp.columnGroup; - Group rowGroup = lp.rowGroup; + Spec columnSpec = lp.columnSpec; + Spec rowSpec = lp.rowSpec; - Interval colSpan = columnGroup.span; - Interval rowSpan = rowGroup.span; + Interval colSpan = columnSpec.span; + Interval rowSpan = rowSpec.span; int x1 = mHorizontalAxis.getLocationIncludingMargin(true, colSpan.min); int y1 = mVerticalAxis.getLocationIncludingMargin(true, rowSpan.min); @@ -892,8 +896,8 @@ public class GridLayout extends ViewGroup { int pWidth = getMeasurement(c, true); int pHeight = getMeasurement(c, false); - Alignment hAlign = columnGroup.alignment; - Alignment vAlign = rowGroup.alignment; + Alignment hAlign = columnSpec.alignment; + Alignment vAlign = rowSpec.alignment; int dx, dy; @@ -963,7 +967,7 @@ public class GridLayout extends ViewGroup { public boolean countValid = false; public boolean countWasExplicitySet = false; - PackedMap<Group, Bounds> groupBounds; + PackedMap<Spec, Bounds> groupBounds; public boolean groupBoundsValid = false; PackedMap<Interval, MutableInt> forwardLinks; @@ -1000,9 +1004,9 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); if (isGone(c)) continue; LayoutParams params = getLayoutParams(c); - Group g = horizontal ? params.columnGroup : params.rowGroup; - count = max(count, g.span.min); - count = max(count, g.span.max); + Spec spec = horizontal ? params.columnSpec : params.rowSpec; + count = max(count, spec.span.min); + count = max(count, spec.span.max); } return count == -1 ? UNDEFINED : count; } @@ -1029,17 +1033,17 @@ public class GridLayout extends ViewGroup { invalidateStructure(); } - private PackedMap<Group, Bounds> createGroupBounds() { - Assoc<Group, Bounds> assoc = Assoc.of(Group.class, Bounds.class); + private PackedMap<Spec, Bounds> createGroupBounds() { + Assoc<Spec, Bounds> assoc = Assoc.of(Spec.class, Bounds.class); for (int i = 0, N = getChildCount(); i < N; i++) { View c = getChildAt(i); if (isGone(c)) { - assoc.put(Group.GONE, Bounds.GONE); + assoc.put(Spec.GONE, Bounds.GONE); } else { LayoutParams lp = getLayoutParams(c); - Group group = horizontal ? lp.columnGroup : lp.rowGroup; - Bounds bounds = group.alignment.getBounds(); - assoc.put(group, bounds); + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + Bounds bounds = spec.alignment.getBounds(); + assoc.put(spec, bounds); } } return assoc.pack(); @@ -1054,12 +1058,12 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); if (isGone(c)) continue; LayoutParams lp = getLayoutParams(c); - Group g = horizontal ? lp.columnGroup : lp.rowGroup; - groupBounds.getValue(i).include(c, g, GridLayout.this, this, lp); + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + groupBounds.getValue(i).include(c, spec, GridLayout.this, this); } } - private PackedMap<Group, Bounds> getGroupBounds() { + private PackedMap<Spec, Bounds> getGroupBounds() { if (groupBounds == null) { groupBounds = createGroupBounds(); } @@ -1073,7 +1077,7 @@ public class GridLayout extends ViewGroup { // Add values computed by alignment - taking the max of all alignments in each span private PackedMap<Interval, MutableInt> createLinks(boolean min) { Assoc<Interval, MutableInt> result = Assoc.of(Interval.class, MutableInt.class); - Group[] keys = getGroupBounds().keys; + Spec[] keys = getGroupBounds().keys; for (int i = 0, N = keys.length; i < N; i++) { Interval span = min ? keys[i].span : keys[i].span.inverse(); result.put(span, new MutableInt()); @@ -1087,13 +1091,16 @@ public class GridLayout extends ViewGroup { spans[i].reset(); } - // use getter to trigger a re-evaluation + // Use getter to trigger a re-evaluation Bounds[] bounds = getGroupBounds().values; for (int i = 0; i < bounds.length; i++) { int size = bounds[i].size(min); - int value = min ? size : -size; MutableInt valueHolder = links.getValue(i); - valueHolder.value = max(valueHolder.value, value); + if (min) { + valueHolder.value = max(valueHolder.value, size); + } else { + valueHolder.value = -max(-valueHolder.value, size); + } } } @@ -1155,7 +1162,7 @@ public class GridLayout extends ViewGroup { int[] sizes = new int[N]; for (Arc arc : arcs) { sizes[arc.span.min]++; - } + } for (int i = 0; i < sizes.length; i++) { result[i] = new Arc[sizes[i]]; } @@ -1234,8 +1241,8 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); if (isGone(c)) continue; LayoutParams lp = getLayoutParams(c); - Group g = horizontal ? lp.columnGroup : lp.rowGroup; - Interval span = g.span; + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + Interval span = spec.span; leadingEdgeCount[span.min]++; trailingEdgeCount[span.max]++; } @@ -1434,8 +1441,8 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); if (isGone(c)) continue; LayoutParams lp = getLayoutParams(c); - Group g = horizontal ? lp.columnGroup : lp.rowGroup; - Interval span = g.span; + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + Interval span = spec.span; int index = leading ? span.min : span.max; margins[index] = max(margins[index], getMargin(c, horizontal, leading)); } @@ -1512,6 +1519,12 @@ public class GridLayout extends ViewGroup { } private void setParentConstraints(int min, int max) { + if (mAlignmentMode != ALIGN_MARGINS) { + int margins = sum(getLeadingMargins()) + sum(getTrailingMargins()); + min -= margins; + max -= margins; + } + parentMin.value = min; parentMax.value = -max; locationsValid = false; @@ -1527,7 +1540,7 @@ public class GridLayout extends ViewGroup { int size = MeasureSpec.getSize(measureSpec); switch (mode) { case MeasureSpec.UNSPECIFIED: { - return getMeasure(0, MAX_SIZE); + return getMeasure(0, MAX_SIZE); } case MeasureSpec.EXACTLY: { return getMeasure(size, size); @@ -1582,14 +1595,14 @@ public class GridLayout extends ViewGroup { * GridLayout supports both row and column spanning and arbitrary forms of alignment within * each cell group. The fundamental parameters associated with each cell group are * gathered into their vertical and horizontal components and stored - * in the {@link #rowGroup} and {@link #columnGroup} layout parameters. - * {@link Group Groups} are immutable structures and may be shared between the layout + * in the {@link #rowSpec} and {@link #columnSpec} layout parameters. + * {@link android.widget.GridLayout.Spec Specs} are immutable structures and may be shared between the layout * parameters of different children. * <p> - * The row and column groups contain the leading and trailing indices along each axis + * The row and column specs contain the leading and trailing indices along each axis * and together specify the four grid indices that delimit the cells of this cell group. * <p> - * The {@link Group#alignment alignment} fields of the row and column groups together specify + * The alignment properties of the row and column specs together specify * both aspects of alignment within the cell group. It is also possible to specify a child's * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)} * method. @@ -1618,20 +1631,18 @@ public class GridLayout extends ViewGroup { * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is * {@code false}; otherwise {@link #UNDEFINED}, to * indicate that a default value should be computed on demand. </li> - * <li>{@link #rowGroup}{@code .span} = {@code [0, 1]} </li> - * <li>{@link #rowGroup}{@code .alignment} = {@link #BASELINE} </li> - * <li>{@link #columnGroup}{@code .span} = {@code [0, 1]} </li> - * <li>{@link #columnGroup}{@code .alignment} = {@link #LEFT} </li> - * <li>{@link #widthSpec} = {@link #FIXED} </li> - * <li>{@link #heightSpec} = {@link #FIXED} </li> + * <li>{@link #rowSpec}{@code .span} = {@code [0, 1]} </li> + * <li>{@link #rowSpec}{@code .alignment} = {@link #BASELINE} </li> + * <li>{@link #columnSpec}{@code .span} = {@code [0, 1]} </li> + * <li>{@link #columnSpec}{@code .alignment} = {@link #LEFT} </li> * </ul> * * @attr ref android.R.styleable#GridLayout_Layout_layout_row * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan - * @attr ref android.R.styleable#GridLayout_Layout_layout_heightSpec + * @attr ref android.R.styleable#GridLayout_Layout_layout_rowFlexibility * @attr ref android.R.styleable#GridLayout_Layout_layout_column * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan - * @attr ref android.R.styleable#GridLayout_Layout_layout_widthSpec + * @attr ref android.R.styleable#GridLayout_Layout_layout_columnFlexibility * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity */ public static class LayoutParams extends MarginLayoutParams { @@ -1647,19 +1658,12 @@ public class GridLayout extends ViewGroup { private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size(); private static final Alignment DEFAULT_COLUMN_ALIGNMENT = LEFT; private static final Alignment DEFAULT_ROW_ALIGNMENT = BASELINE; - private static final Group DEFAULT_COLUMN_GROUP = - new Group(DEFAULT_SPAN, DEFAULT_COLUMN_ALIGNMENT); - private static final Group DEFAULT_ROW_GROUP = - new Group(DEFAULT_SPAN, DEFAULT_ROW_ALIGNMENT); - private static final Spec DEFAULT_SPEC = FIXED; - private static final int DEFAULT_SPEC_INDEX = 0; // Misc private static final Rect CONTAINER_BOUNDS = new Rect(0, 0, 2, 2); private static final Alignment[] COLUMN_ALIGNMENTS = { LEFT, CENTER, RIGHT }; private static final Alignment[] ROW_ALIGNMENTS = { TOP, CENTER, BOTTOM }; - private static final Spec[] SPECS = { FIXED, CAN_SHRINK, CAN_STRETCH }; // TypedArray indices @@ -1672,69 +1676,61 @@ public class GridLayout extends ViewGroup { private static final int COLUMN = styleable.GridLayout_Layout_layout_column; private static final int COLUMN_SPAN = styleable.GridLayout_Layout_layout_columnSpan; - private static final int WIDTH_SPEC = styleable.GridLayout_Layout_layout_widthSpec; + private static final int COLUMN_FLEXIBILITY = + styleable.GridLayout_Layout_layout_columnFlexibility; + private static final int ROW = styleable.GridLayout_Layout_layout_row; private static final int ROW_SPAN = styleable.GridLayout_Layout_layout_rowSpan; - private static final int HEIGHT_SPEC = styleable.GridLayout_Layout_layout_heightSpec; + private static final int ROW_FLEXIBILITY = + styleable.GridLayout_Layout_layout_rowFlexibility; + private static final int GRAVITY = styleable.GridLayout_Layout_layout_gravity; // Instance variables /** - * The group that specifies the vertical characteristics of the cell group + * The spec that specifies the vertical characteristics of the cell group * described by these layout parameters. */ - public Group rowGroup; + public Spec rowSpec; /** - * The group that specifies the horizontal characteristics of the cell group + * The spec that specifies the horizontal characteristics of the cell group * described by these layout parameters. */ - public Group columnGroup; - /** - * The proportional space that should be taken by the associated column group - * during excess space distribution. - */ - public Spec widthSpec; - /** - * The proportional space that should be taken by the associated row group - * during excess space distribution. - */ - public Spec heightSpec; + public Spec columnSpec; // Constructors private LayoutParams( int width, int height, int left, int top, int right, int bottom, - Group rowGroup, Group columnGroup, - Spec widthSpec, Spec heightSpec) { + Spec rowSpec, Spec columnSpec) { super(width, height); setMargins(left, top, right, bottom); - this.rowGroup = rowGroup; - this.columnGroup = columnGroup; - this.heightSpec = heightSpec; - this.widthSpec = widthSpec; + this.rowSpec = rowSpec; + this.columnSpec = columnSpec; } /** - * Constructs a new LayoutParams instance for this <code>rowGroup</code> - * and <code>columnGroup</code>. All other fields are initialized with + * Constructs a new LayoutParams instance for this <code>rowSpec</code> + * and <code>columnSpec</code>. All other fields are initialized with * default values as defined in {@link LayoutParams}. * - * @param rowGroup the rowGroup - * @param columnGroup the columnGroup + * @param rowSpec the rowSpec + * @param columnSpec the columnSpec */ - public LayoutParams(Group rowGroup, Group columnGroup) { + public LayoutParams(Spec rowSpec, Spec columnSpec) { this(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, - rowGroup, columnGroup, DEFAULT_SPEC, DEFAULT_SPEC); + rowSpec, columnSpec); } /** * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. */ public LayoutParams() { - this(DEFAULT_ROW_GROUP, DEFAULT_COLUMN_GROUP); + this(new Spec(DEFAULT_SPAN, DEFAULT_ROW_ALIGNMENT, Spec.DEFAULT_FLEXIBILITY), + new Spec(DEFAULT_SPAN, DEFAULT_COLUMN_ALIGNMENT, Spec.DEFAULT_FLEXIBILITY)); } // Copying constructors @@ -1758,10 +1754,8 @@ public class GridLayout extends ViewGroup { */ public LayoutParams(LayoutParams that) { super(that); - this.columnGroup = that.columnGroup; - this.rowGroup = that.rowGroup; - this.widthSpec = that.widthSpec; - this.heightSpec = that.heightSpec; + this.columnSpec = new Spec(that.columnSpec); + this.rowSpec = new Spec(that.rowSpec); } // AttributeSet constructors @@ -1825,7 +1819,7 @@ public class GridLayout extends ViewGroup { // Gravity. For conversion from the static the integers defined in the Gravity class, // use Gravity.apply() to apply gravity to a view of zero size and see where it ends up. - private static Alignment getColumnAlignment(int gravity, int width) { + private static Alignment getColAlignment(int gravity, int width) { Rect r = new Rect(0, 0, 0, 0); Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); @@ -1853,14 +1847,14 @@ public class GridLayout extends ViewGroup { int column = a.getInt(COLUMN, DEFAULT_COLUMN); int columnSpan = a.getInt(COLUMN_SPAN, DEFAULT_SPAN_SIZE); Interval hSpan = new Interval(column, column + columnSpan); - this.columnGroup = new Group(hSpan, getColumnAlignment(gravity, width)); - this.widthSpec = SPECS[a.getInt(WIDTH_SPEC, DEFAULT_SPEC_INDEX)]; + int hFlexibility = a.getInt(COLUMN_FLEXIBILITY, Spec.DEFAULT_FLEXIBILITY); + this.columnSpec = new Spec(hSpan, getColAlignment(gravity, width), hFlexibility); int row = a.getInt(ROW, DEFAULT_ROW); int rowSpan = a.getInt(ROW_SPAN, DEFAULT_SPAN_SIZE); Interval vSpan = new Interval(row, row + rowSpan); - this.rowGroup = new Group(vSpan, getRowAlignment(gravity, height)); - this.heightSpec = SPECS[a.getInt(HEIGHT_SPEC, DEFAULT_SPEC_INDEX)]; + int vFlexibility = a.getInt(ROW_FLEXIBILITY, Spec.DEFAULT_FLEXIBILITY); + this.rowSpec = new Spec(vSpan, getRowAlignment(gravity, height), vFlexibility); } finally { a.recycle(); } @@ -1875,8 +1869,8 @@ public class GridLayout extends ViewGroup { * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity */ public void setGravity(int gravity) { - columnGroup = columnGroup.copyWriteAlignment(getColumnAlignment(gravity, width)); - rowGroup = rowGroup.copyWriteAlignment(getRowAlignment(gravity, height)); + columnSpec = columnSpec.copyWriteAlignment(getColAlignment(gravity, width)); + rowSpec = rowSpec.copyWriteAlignment(getRowAlignment(gravity, height)); } @Override @@ -1885,12 +1879,12 @@ public class GridLayout extends ViewGroup { this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT); } - private void setRowGroupSpan(Interval span) { - rowGroup = rowGroup.copyWriteSpan(span); + private void setRowSpecSpan(Interval span) { + rowSpec = rowSpec.copyWriteSpan(span); } - private void setColumnGroupSpan(Interval span) { - columnGroup = columnGroup.copyWriteSpan(span); + private void setColumnSpecSpan(Interval span) { + columnSpec = columnSpec.copyWriteSpan(span); } } @@ -2036,14 +2030,14 @@ public class GridLayout extends ViewGroup { } /* - For each Group (with a given alignment) we need to store the amount of space required + For each group (with a given alignment) we need to store the amount of space required before the alignment point and the amount of space required after it. One side of this calculation is always 0 for LEADING and TRAILING alignments but we don't make use of this. For CENTER and BASELINE alignments both sides are needed and in the BASELINE case no simple optimisations are possible. The general algorithm therefore is to create a Map (actually a PackedMap) from - Group to Bounds and to loop through all Views in the group taking the maximum + group to Bounds and to loop through all Views in the group taking the maximum of the values for each View. */ private static class Bounds { @@ -2051,7 +2045,7 @@ public class GridLayout extends ViewGroup { public int before; public int after; - public boolean canStretch; + public int flexibility; private Bounds() { reset(); @@ -2060,7 +2054,7 @@ public class GridLayout extends ViewGroup { protected void reset() { before = Integer.MIN_VALUE; after = Integer.MIN_VALUE; - canStretch = false; + flexibility = UNDEFINED_FLEXIBILITY; } protected void include(int before, int after) { @@ -2069,8 +2063,13 @@ public class GridLayout extends ViewGroup { } protected int size(boolean min) { - if (!min && canStretch) { - return MAX_SIZE; + if (!min) { + // Note in the usual case, components don't define anything + // leaving their flexibility is undefined and their stretchability + // defined as if the CAN_STRETCH flag was false. + if (canStretch(flexibility) && !isUndefined(flexibility)) { + return MAX_SIZE; + } } return before + after; } @@ -2079,14 +2078,11 @@ public class GridLayout extends ViewGroup { return before - alignment.getAlignmentValue(c, size); } - protected void include(View c, Group g, GridLayout gridLayout, Axis axis, LayoutParams lp) { - Spec spec = axis.horizontal ? lp.widthSpec : lp.heightSpec; - if (spec == CAN_STRETCH) { - canStretch = true; - } + protected void include(View c, Spec spec, GridLayout gridLayout, Axis axis) { + this.flexibility &= spec.flexibility; int size = gridLayout.getMeasurementIncludingMargin(c, axis.horizontal); // todo test this works correctly when the returned value is UNDEFINED - int before = g.alignment.getAlignmentValue(c, size); + int before = spec.alignment.getAlignmentValue(c, size); include(before, size - before); } @@ -2191,14 +2187,13 @@ public class GridLayout extends ViewGroup { } /** - * A group specifies either the horizontal or vertical characteristics of a group of + * A spec defines either the horizontal or vertical characteristics of a group of * cells. - * <p> - * Groups are immutable and so may be shared between views with the same - * {@code span} and {@code alignment}. */ - public static class Group { - private static final Group GONE = new Group(Interval.GONE, Alignment.GONE); + public static class Spec { + private static final int DEFAULT_FLEXIBILITY = UNDEFINED_FLEXIBILITY; + + private static final Spec GONE = new Spec(Interval.GONE, Alignment.GONE); /** * The grid indices of the leading and trailing edges of this cell group for the @@ -2213,69 +2208,60 @@ public class GridLayout extends ViewGroup { * For row groups, this specifies the vertical alignment. * For column groups, this specifies the horizontal alignment. */ - public final Alignment alignment; + final Alignment alignment; /** - * Construct a new Group, {@code group}, where: - * <ul> - * <li> {@code group.span = span} </li> - * <li> {@code group.alignment = alignment} </li> - * </ul> + * The flexibility field tells GridLayout how to derive minimum and maximum size + * values for a component. Specifications are made with respect to a child's + * 'measured size'. A child's measured size is, in turn, controlled by its + * height and width layout parameters which either specify a size or, in + * the case of {@link LayoutParams#WRAP_CONTENT WRAP_CONTENT}, defer to + * the computed size of the component. * - * @param span the span - * @param alignment the alignment + * @see GridLayout#CAN_STRETCH */ - Group(Interval span, Alignment alignment) { + final int flexibility; + + private Spec(Interval span, Alignment alignment, int flexibility) { this.span = span; this.alignment = alignment; + this.flexibility = flexibility; } - /** - * Construct a new Group, {@code group}, where: - * <ul> - * <li> {@code group.span = [start, start + size]} </li> - * <li> {@code group.alignment = alignment} </li> - * </ul> - * - * @param start the start - * @param size the size - * @param alignment the alignment - */ - public Group(int start, int size, Alignment alignment) { - this(new Interval(start, start + size), alignment); + private Spec(Interval span, Alignment alignment) { + this(span, alignment, DEFAULT_FLEXIBILITY); } - /** - * Construct a new Group, {@code group}, where: - * <ul> - * <li> {@code group.span = [start, start + 1]} </li> - * <li> {@code group.alignment = alignment} </li> - * </ul> - * - * @param start the start index - * @param alignment the alignment - */ - public Group(int start, Alignment alignment) { - this(start, 1, alignment); + /* Copying constructor */ + private Spec(Spec that) { + this(that.span, that.alignment, that.flexibility); } - private Group copyWriteSpan(Interval span) { - return new Group(span, alignment); + Spec(int start, int size, Alignment alignment, int flexibility) { + this(new Interval(start, start + size), alignment, flexibility); } - private Group copyWriteAlignment(Alignment alignment) { - return new Group(span, alignment); + private Spec copyWriteSpan(Interval span) { + return new Spec(span, alignment, flexibility); + } + + private Spec copyWriteAlignment(Alignment alignment) { + return new Spec(span, alignment, flexibility); + } + + private Spec copyWriteFlexibility(int flexibility) { + return new Spec(span, alignment, flexibility); } /** - * Returns {@code true} if the {@link #getClass class}, {@link #alignment} and {@code span} - * properties of this Group and the supplied parameter are pairwise equal, + * Returns {@code true} if the {@code class}, {@code alignment} and {@code span} + * properties of this Spec and the supplied parameter are pairwise equal, * {@code false} otherwise. * - * @param that the object to compare this group with + * @param that the object to compare this spec with * * @return {@code true} if the specified object is equal to this - * {@code Group}; {@code false} otherwise + * {@code Spec}; {@code false} otherwise */ @Override public boolean equals(Object that) { @@ -2286,12 +2272,12 @@ public class GridLayout extends ViewGroup { return false; } - Group group = (Group) that; + Spec spec = (Spec) that; - if (!alignment.equals(group.alignment)) { + if (!alignment.equals(spec.alignment)) { return false; } - if (!span.equals(group.span)) { + if (!span.equals(spec.span)) { return false; } @@ -2307,12 +2293,93 @@ public class GridLayout extends ViewGroup { } /** + * Temporary backward compatibility class for Launcher - to avoid + * dependent multi-project commit. This class will be deleted after + * AppsCustomizePagedView is updated to new API. + * + * @hide + */ + @Deprecated + public static class Group extends Spec { + /** + * @deprecated Please replace with {@link #spec(int, int, Alignment)} + * @hide + */ + @Deprecated + public Group(int start, int size, Alignment alignment) { + super(start, size, alignment, UNDEFINED_FLEXIBILITY); + } + } + + /** + * Return a Spec, {@code spec}, where: + * <ul> + * <li> {@code spec.span = [start, start + size]} </li> + * <li> {@code spec.alignment = alignment} </li> + * <li> {@code spec.flexibility = flexibility} </li> + * </ul> + * + * @param start the start + * @param size the size + * @param alignment the alignment + * @param flexibility the flexibility + */ + public static Spec spec(int start, int size, Alignment alignment, int flexibility) { + return new Spec(start, size, alignment, flexibility); + } + + /** + * Return a Spec, {@code spec}, where: + * <ul> + * <li> {@code spec.span = [start, start + 1]} </li> + * <li> {@code spec.alignment = alignment} </li> + * <li> {@code spec.flexibility = flexibility} </li> + * </ul> + * + * @param start the start + * @param alignment the alignment + * @param flexibility the flexibility + */ + public static Spec spec(int start, Alignment alignment, int flexibility) { + return spec(start, 1, alignment, flexibility); + } + + /** + * Return a Spec, {@code spec}, where: + * <ul> + * <li> {@code spec.span = [start, start + size]} </li> + * <li> {@code spec.alignment = alignment} </li> + * </ul> + * + * @param start the start + * @param size the size + * @param alignment the alignment + */ + public static Spec spec(int start, int size, Alignment alignment) { + return spec(start, size, alignment, Spec.DEFAULT_FLEXIBILITY); + } + + /** + * Return a Spec, {@code spec}, where: + * <ul> + * <li> {@code spec.span = [start, start + 1]} </li> + * <li> {@code spec.alignment = alignment} </li> + * </ul> + * + * @param start the start index + * @param alignment the alignment + */ + public static Spec spec(int start, Alignment alignment) { + return spec(start, 1, alignment); + } + + /** * Alignments specify where a view should be placed within a cell group and * what size it should be. * <p> - * The {@link LayoutParams} class contains a {@link LayoutParams#rowGroup rowGroup} - * and a {@link LayoutParams#columnGroup columnGroup} each of which contains an - * {@link Group#alignment alignment}. Overall placement of the view in the cell + * The {@link LayoutParams} class contains a {@link LayoutParams#rowSpec rowSpec} + * and a {@link LayoutParams#columnSpec columnSpec} each of which contains an + * {@code alignment}. Overall placement of the view in the cell * group is specified by the two alignments which act along each axis independently. * <p> * The GridLayout class defines the most common alignments used in general layout: @@ -2413,8 +2480,8 @@ public class GridLayout extends ViewGroup { /** * Indicates that a view should be <em>centered</em> with the other views in its cell group. - * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and {@link - * LayoutParams#columnGroup columnGroups}. + * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and {@link + * LayoutParams#columnSpec columnSpecs}. */ public static final Alignment CENTER = new Alignment() { public int getAlignmentValue(View view, int viewSize) { @@ -2425,7 +2492,7 @@ public class GridLayout extends ViewGroup { /** * Indicates that a view should be aligned with the <em>baselines</em> * of the other views in its cell group. - * This constant may only be used as an alignment in {@link LayoutParams#rowGroup rowGroups}. + * This constant may only be used as an alignment in {@link LayoutParams#rowSpec rowSpecs}. * * @see View#getBaseline() */ @@ -2476,8 +2543,8 @@ public class GridLayout extends ViewGroup { /** * Indicates that a view should expanded to fit the boundaries of its cell group. - * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and - * {@link LayoutParams#columnGroup columnGroups}. + * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and + * {@link LayoutParams#columnSpec columnSpecs}. */ public static final Alignment FILL = new Alignment() { public int getAlignmentValue(View view, int viewSize) { @@ -2490,40 +2557,42 @@ public class GridLayout extends ViewGroup { } }; - /** - * Spec's tell GridLayout how to derive minimum and maximum size values for a - * component. Specifications are made with respect to a child's 'measured size'. - * A child's measured size is, in turn, controlled by its height and width - * layout parameters which either specify a size or, in the case of - * WRAP_CONTENT, defer to the computed size of the component. - */ - public static abstract class Spec { + private static boolean canStretch(int flexibility) { + return (flexibility & CAN_STRETCH) != 0; + } + + private static boolean isUndefined(int flexibility) { + return (flexibility & UNDEFINED) != 0; } /** * Indicates that a view requests precisely the size specified by its layout parameters. * - * @see Spec + * @see Spec#flexibility */ - public static final Spec FIXED = new Spec() { - }; + private static final int NONE = 0; /** * Indicates that a view's size should lie between its minimum and the size specified by * its layout parameters. * - * @see Spec + * @see Spec#flexibility */ - public static final Spec CAN_SHRINK = new Spec() { - }; + private static final int CAN_SHRINK = 1; /** * Indicates that a view's size should be greater than or equal to the size specified by * its layout parameters. * - * @see Spec + * @see Spec#flexibility */ - public static final Spec CAN_STRETCH = new Spec() { - }; + public static final int CAN_STRETCH = 2; + + /** + * A default value for flexibility. + * + * @see Spec#flexibility + */ + private static final int UNDEFINED_FLEXIBILITY = UNDEFINED | CAN_SHRINK | CAN_STRETCH; } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 7c9be1e..b428301 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -498,13 +498,6 @@ public class HorizontalScrollView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { - - if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { - // Don't handle edge touches immediately -- they may actually belong to one of our - // descendants. - return false; - } - if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 5642877..f057d07 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -62,6 +62,7 @@ public class ListPopupWindow { private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; private int mDropDownHorizontalOffset; private int mDropDownVerticalOffset; + private boolean mDropDownVerticalOffsetSet; private boolean mDropDownAlwaysVisible = false; private boolean mForceIgnoreOutsideTouch = false; @@ -404,6 +405,9 @@ public class ListPopupWindow { * @return The vertical offset of the popup from its anchor in pixels. */ public int getVerticalOffset() { + if (!mDropDownVerticalOffsetSet) { + return 0; + } return mDropDownVerticalOffset; } @@ -414,6 +418,7 @@ public class ListPopupWindow { */ public void setVerticalOffset(int offset) { mDropDownVerticalOffset = offset; + mDropDownVerticalOffsetSet = true; } /** @@ -1061,21 +1066,27 @@ public class ListPopupWindow { } } - // 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, + // 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 we don't have an explicit vertical offset, determine one from the window + // background so that content will line up. + if (!mDropDownVerticalOffsetSet) { + mDropDownVerticalOffset = -mTempRect.top; + } } + // 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); + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { return maxHeight + padding; } diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index e7a9e41..1f29b16 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -3588,17 +3588,6 @@ public class ListView extends AbsListView { return null; } - @Override - public boolean onTouchEvent(MotionEvent ev) { - //noinspection SimplifiableIfStatement - if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { - // Don't handle edge touches immediately -- they may actually belong to one of our - // descendants. - return false; - } - return super.onTouchEvent(ev); - } - /** * Returns the set of checked items ids. The result is only valid if the * choice mode has not been set to {@link #CHOICE_MODE_NONE}. diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 4c47d37..867ebb4 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -53,6 +53,10 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // This ensures that we don't stay continually bound to the service and that it can be destroyed // if we need the memory elsewhere in the system. private static final int sUnbindServiceDelay = 5000; + + // Default height for the default loading view, in case we cannot get inflate the first view + private static final int sDefaultLoadingViewHeight = 50; + // Type defs for controlling different messages across the main and worker message queues private static final int sDefaultMessageType = 0; private static final int sUnbindServiceMessageType = 1; @@ -386,21 +390,39 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // Create a new loading view synchronized (mCache) { + boolean customLoadingViewAvailable = false; + if (mUserLoadingView != null) { - // A user-specified loading view - View loadingView = mUserLoadingView.apply(parent.getContext(), parent); - loadingView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(0)); - layout.addView(loadingView); - } else { + // Try to inflate user-specified loading view + try { + View loadingView = mUserLoadingView.apply(parent.getContext(), parent); + loadingView.setTagInternal(com.android.internal.R.id.rowTypeId, + new Integer(0)); + layout.addView(loadingView); + customLoadingViewAvailable = true; + } catch (Exception e) { + Log.w(TAG, "Error inflating custom loading view, using default loading" + + "view instead", e); + } + } + if (!customLoadingViewAvailable) { // A default loading view // Use the size of the first row as a guide for the size of the loading view if (mFirstViewHeight < 0) { - View firstView = mFirstView.apply(parent.getContext(), parent); - firstView.measure( - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mFirstViewHeight = firstView.getMeasuredHeight(); - mFirstView = null; + try { + View firstView = mFirstView.apply(parent.getContext(), parent); + firstView.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mFirstViewHeight = firstView.getMeasuredHeight(); + mFirstView = null; + } catch (Exception e) { + float density = mContext.getResources().getDisplayMetrics().density; + mFirstViewHeight = (int) + Math.round(sDefaultLoadingViewHeight * density); + mFirstView = null; + Log.w(TAG, "Error inflating first RemoteViews" + e); + } } // Compose the loading view text @@ -937,24 +959,40 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback indexMetaData.isRequested = true; int typeId = indexMetaData.typeId; - // Reuse the convert view where possible - if (layout != null) { - if (convertViewTypeId == typeId) { - rv.reapply(context, convertViewChild); - return layout; + try { + // Reuse the convert view where possible + if (layout != null) { + if (convertViewTypeId == typeId) { + rv.reapply(context, convertViewChild); + return layout; + } + layout.removeAllViews(); + } else { + layout = new RemoteViewsFrameLayout(context); } - layout.removeAllViews(); - } else { - layout = new RemoteViewsFrameLayout(context); - } - // Otherwise, create a new view to be returned - View newView = rv.apply(context, parent); - newView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(typeId)); - layout.addView(newView); - if (hasNewItems) loadNextIndexInBackground(); - - return layout; + // Otherwise, create a new view to be returned + View newView = rv.apply(context, parent); + newView.setTagInternal(com.android.internal.R.id.rowTypeId, + new Integer(typeId)); + layout.addView(newView); + return layout; + + } catch (Exception e){ + // We have to make sure that we successfully inflated the RemoteViews, if not + // we return the loading view instead. + Log.w(TAG, "Error inflating RemoteViews at position: " + position + ", using" + + "loading view instead" + e); + + RemoteViewsFrameLayout loadingView = null; + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + loadingView = metaData.createLoadingView(position, convertView, parent); + } + return loadingView; + } finally { + if (hasNewItems) loadNextIndexInBackground(); + } } else { // If the cache does not have the RemoteViews at this position, then create a // loading view and queue the actual position to be loaded in the background diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 12775a4..e59f731 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -65,7 +65,6 @@ public class ScrollView extends FrameLayout { static final float MAX_SCROLL_FACTOR = 0.5f; - private long mLastScroll; private final Rect mTempRect = new Rect(); @@ -506,13 +505,6 @@ public class ScrollView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { - - if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { - // Don't handle edge touches immediately -- they may actually belong to one of our - // descendants. - return false; - } - if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } @@ -1438,17 +1430,6 @@ public class ScrollView extends FrameLayout { final boolean movingDown = velocityY > 0; - View currentFocused = findFocus(); - View newFocused = - findFocusableViewInMyBounds(movingDown, mScroller.getFinalY(), currentFocused); - if (newFocused == null) { - newFocused = this; - } - - if (newFocused != currentFocused) { - newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP); - } - if (mFlingStrictSpan == null) { mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling"); } diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index d6e426f..665109a 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -18,18 +18,23 @@ package android.widget; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.ActionProvider; +import android.view.Menu; import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.SubMenu; import android.view.View; import com.android.internal.R; /** * This is a provider for a share action. It is responsible for creating views - * that enable data sharing and also to perform a default action for showing - * a share dialog. + * that enable data sharing and also to show a sub menu with sharing activities + * if the hosting item is placed on the overflow menu. * <p> * Here is how to use the action provider with custom backing file in a {@link MenuItem}: * </p> @@ -47,15 +52,13 @@ import com.android.internal.R; * // {@link ActionProvider#onCreateActionView()} which uses the backing file name. Omit this * // line if using the default share history file is desired. * mShareActionProvider.setShareHistoryFileName("custom_share_history.xml"); - * // Get the action view and hold onto it to set the share intent. - * mActionView = menuItem.getActionView(); * . . . * } * * // Somewhere in the application. * public void doShare(Intent shareIntent) { * // When you want to share set the share intent. - * mShareActionProvider.setShareIntent(mActionView, shareIntent); + * mShareActionProvider.setShareIntent(shareIntent); * } * </pre> * </code> @@ -70,11 +73,34 @@ import com.android.internal.R; public class ShareActionProvider extends ActionProvider { /** + * The default for the maximal number of activities shown in the sub-menu. + */ + private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4; + + /** + * The the maximum number activities shown in the sub-menu. + */ + private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT; + + /** + * Listener for handling menu item clicks. + */ + private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener = + new ShareMenuItemOnMenuItemClickListener(); + + /** * The default name for storing share history. */ public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml"; + /** + * Context for accessing resources. + */ private final Context mContext; + + /** + * The name of the file with share history data. + */ private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME; /** @@ -92,13 +118,17 @@ public class ShareActionProvider extends ActionProvider { */ @Override public View onCreateActionView() { + // Create the view and set its data model. ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); ActivityChooserView activityChooserView = new ActivityChooserView(mContext); activityChooserView.setActivityChooserModel(dataModel); + + // Lookup and set the expand action icon. TypedValue outTypedValue = new TypedValue(); mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true); Drawable drawable = mContext.getResources().getDrawable(outTypedValue.resourceId); activityChooserView.setExpandActivityOverflowButtonDrawable(drawable); + return activityChooserView; } @@ -106,12 +136,43 @@ public class ShareActionProvider extends ActionProvider { * {@inheritDoc} */ @Override - public void onPerformDefaultAction(View actionView) { - if (actionView instanceof ActivityChooserView) { - ActivityChooserView activityChooserView = (ActivityChooserView) actionView; - activityChooserView.showPopup(); - } else { - throw new IllegalArgumentException("actionView not instance of ActivityChooserView"); + public boolean hasSubMenu() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void onPrepareSubMenu(SubMenu subMenu) { + // Clear since the order of items may change. + subMenu.clear(); + + ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName); + PackageManager packageManager = mContext.getPackageManager(); + + final int expandedActivityCount = dataModel.getActivityCount(); + final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount); + + // Populate the sub-menu with a sub set of the activities. + for (int i = 0; i < collapsedActivityCount; i++) { + ResolveInfo activity = dataModel.getActivity(i); + subMenu.add(0, i, i, activity.loadLabel(packageManager)) + .setIcon(activity.loadIcon(packageManager)) + .setOnMenuItemClickListener(mOnMenuItemClickListener); + } + + if (collapsedActivityCount < expandedActivityCount) { + // Add a sub-menu for showing all activities as a list item. + SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount, + collapsedActivityCount, + mContext.getString(R.string.activity_chooser_view_see_all)); + for (int i = 0; i < expandedActivityCount; i++) { + ResolveInfo activity = dataModel.getActivity(i); + expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager)) + .setIcon(activity.loadIcon(packageManager)) + .setOnMenuItemClickListener(mOnMenuItemClickListener); + } } } @@ -147,18 +208,29 @@ public class ShareActionProvider extends ActionProvider { * </code> * </p> * - * @param actionView An action view created by {@link #onCreateActionView()}. * @param shareIntent The share intent. * * @see Intent#ACTION_SEND * @see Intent#ACTION_SEND_MULTIPLE */ - public void setShareIntent(View actionView, Intent shareIntent) { - if (actionView instanceof ActivityChooserView) { - ActivityChooserView activityChooserView = (ActivityChooserView) actionView; - activityChooserView.getDataModel().setIntent(shareIntent); - } else { - throw new IllegalArgumentException("actionView not instance of ActivityChooserView"); + public void setShareIntent(Intent shareIntent) { + ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, + mShareHistoryFileName); + dataModel.setIntent(shareIntent); + } + + /** + * Reusable listener for handling share item clicks. + */ + private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener { + @Override + public boolean onMenuItemClick(MenuItem item) { + ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, + mShareHistoryFileName); + final int itemId = item.getItemId(); + Intent launchIntent = dataModel.chooseActivity(itemId); + mContext.startActivity(launchIntent); + return true; } } } diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index b23a855..2fba18b 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -165,10 +165,17 @@ public class Spinner extends AbsSpinner implements OnClickListener { ViewGroup.LayoutParams.WRAP_CONTENT); 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)); + final int verticalOffset = a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0); + if (verticalOffset != 0) { + popup.setVerticalOffset(verticalOffset); + } + + final int horizontalOffset = a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0); + if (horizontalOffset != 0) { + popup.setHorizontalOffset(horizontalOffset); + } mPopup = popup; break; @@ -231,7 +238,8 @@ public class Spinner extends AbsSpinner implements OnClickListener { } if (child != null) { - return child.getTop() + child.getBaseline(); + final int childBaseline = child.getBaseline(); + return childBaseline >= 0 ? child.getTop() + childBaseline : -1; } else { return -1; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 772e8e9..769f5e3 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -16,6 +16,11 @@ package android.widget; +import com.android.internal.util.FastMath; +import com.android.internal.widget.EditableInputConnection; + +import org.xmlpull.v1.XmlPullParserException; + import android.R; import android.content.ClipData; import android.content.ClipData.Item; @@ -59,9 +64,17 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; +import android.text.TextDirectionHeuristic; +import android.text.TextDirectionHeuristics; +import android.text.TextDirectionHeuristics.AnyStrong; +import android.text.TextDirectionHeuristics.CharCount; +import android.text.TextDirectionHeuristics.FirstStrong; +import android.text.TextDirectionHeuristics.TextDirectionAlgorithm; +import android.text.TextDirectionHeuristics.TextDirectionHeuristicImpl; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.method.AllCapsTransformationMethod; import android.text.method.ArrowKeyMovementMethod; import android.text.method.DateKeyListener; import android.text.method.DateTimeKeyListener; @@ -76,6 +89,7 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; +import android.text.method.TransformationMethod2; import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; @@ -125,11 +139,6 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.RemoteViews.RemoteView; -import com.android.internal.util.FastMath; -import com.android.internal.widget.EditableInputConnection; - -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.lang.ref.WeakReference; import java.text.BreakIterator; @@ -258,9 +267,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener class Drawables { final Rect mCompoundRect = new Rect(); - Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; - int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; - int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; + Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight, + mDrawableStart, mDrawableEnd; + int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight, + mDrawableSizeStart, mDrawableSizeEnd; + int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight, + mDrawableHeightStart, mDrawableHeightEnd; int mDrawablePadding; } private Drawables mDrawables; @@ -350,6 +362,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END; } + private boolean bResolvedDrawables = false; + /* * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). @@ -424,6 +438,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int textSize = 15; int typefaceIndex = -1; int styleIndex = -1; + boolean allCaps = false; /* * Look the appearance up without checking first if it exists because @@ -471,6 +486,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextAppearance_textStyle: styleIndex = appearance.getInt(attr, -1); break; + + case com.android.internal.R.styleable.TextAppearance_textAllCaps: + allCaps = appearance.getBoolean(attr, false); + break; } } @@ -487,7 +506,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int buffertype = 0; boolean selectallonfocus = false; Drawable drawableLeft = null, drawableTop = null, drawableRight = null, - drawableBottom = null; + drawableBottom = null, drawableStart = null, drawableEnd = null; int drawablePadding = 0; int ellipsize = -1; boolean singleLine = false; @@ -564,6 +583,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener drawableBottom = a.getDrawable(attr); break; + case com.android.internal.R.styleable.TextView_drawableStart: + drawableStart = a.getDrawable(attr); + break; + + case com.android.internal.R.styleable.TextView_drawableEnd: + drawableEnd = a.getDrawable(attr); + break; + case com.android.internal.R.styleable.TextView_drawablePadding: drawablePadding = a.getDimensionPixelSize(attr, drawablePadding); break; @@ -822,6 +849,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_suggestionsEnabled: mSuggestionsEnabled = a.getBoolean(attr, true); break; + + case com.android.internal.R.styleable.TextView_textAllCaps: + allCaps = a.getBoolean(attr, false); + break; } } a.recycle(); @@ -969,6 +1000,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setCompoundDrawablesWithIntrinsicBounds( drawableLeft, drawableTop, drawableRight, drawableBottom); + setRelativeDrawablesIfNeeded(drawableStart, drawableEnd); setCompoundDrawablePadding(drawablePadding); // Same as setSingleLine(), but make sure the transformation method and the maximum number @@ -1004,6 +1036,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } setRawTextSize(textSize); + if (allCaps) { + setTransformationMethod(new AllCapsTransformationMethod(getContext())); + } + if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) { setTransformationMethod(PasswordTransformationMethod.getInstance()); typefaceIndex = MONOSPACE; @@ -1090,6 +1126,42 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTypeface(tf, styleIndex); } + private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) { + boolean hasRelativeDrawables = (start != null) || (end != null); + if (hasRelativeDrawables) { + Drawables dr = mDrawables; + if (dr == null) { + mDrawables = dr = new Drawables(); + } + final Rect compoundRect = dr.mCompoundRect; + int[] state = getDrawableState(); + if (start != null) { + start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); + start.setState(state); + start.copyBounds(compoundRect); + start.setCallback(this); + + dr.mDrawableStart = start; + dr.mDrawableSizeStart = compoundRect.width(); + dr.mDrawableHeightStart = compoundRect.height(); + } else { + dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; + } + if (end != null) { + end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); + end.setState(state); + end.copyBounds(compoundRect); + end.setCallback(this); + + dr.mDrawableEnd = end; + dr.mDrawableSizeEnd = compoundRect.width(); + dr.mDrawableHeightEnd = compoundRect.height(); + } else { + dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; + } + } + } + @Override public void setEnabled(boolean enabled) { if (enabled == isEnabled()) { @@ -1104,6 +1176,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } super.setEnabled(enabled); + prepareCursorControllers(); } /** @@ -1331,6 +1404,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTransformation = method; + if (method instanceof TransformationMethod2) { + TransformationMethod2 method2 = (TransformationMethod2) method; + mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable); + method2.setLengthChangesAllowed(mAllowTransformationLengthChange); + } else { + mAllowTransformationLengthChange = false; + } + setText(mText); } @@ -1387,6 +1468,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns the start padding of the view, plus space for the start + * Drawable if any. + * + * @hide + */ + public int getCompoundPaddingStart() { + resolveDrawables(); + switch(getResolvedLayoutDirection()) { + default: + case LAYOUT_DIRECTION_LTR: + return getCompoundPaddingLeft(); + case LAYOUT_DIRECTION_RTL: + return getCompoundPaddingRight(); + } + } + + /** + * Returns the end padding of the view, plus space for the end + * Drawable if any. + * + * @hide + */ + public int getCompoundPaddingEnd() { + resolveDrawables(); + switch(getResolvedLayoutDirection()) { + default: + case LAYOUT_DIRECTION_LTR: + return getCompoundPaddingRight(); + case LAYOUT_DIRECTION_RTL: + return getCompoundPaddingLeft(); + } + } + + /** * Returns the extended top padding of the view, including both the * top Drawable if any and any extra space to keep more than maxLines * of text from showing. It is only valid to call this after measuring. @@ -1469,6 +1584,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns the total start padding of the view, including the start + * Drawable if any. + * + * @hide + */ + public int getTotalPaddingStart() { + return getCompoundPaddingStart(); + } + + /** + * Returns the total end padding of the view, including the end + * Drawable if any. + * + * @hide + */ + public int getTotalPaddingEnd() { + return getCompoundPaddingEnd(); + } + + /** * Returns the total top padding of the view, including the top * Drawable if any, the extra space to keep more than maxLines * from showing, and the vertical offset for gravity, if any. @@ -1655,6 +1790,185 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets the Drawables (if any) to appear to the start of, above, + * to the end of, and below the text. Use null if you do not + * want a Drawable there. The Drawables must already have had + * {@link Drawable#setBounds} called. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + * + * @hide + */ + public void setCompoundDrawablesRelative(Drawable start, Drawable top, + Drawable end, Drawable bottom) { + Drawables dr = mDrawables; + + final boolean drawables = start != null || top != null + || end != null || bottom != null; + + if (!drawables) { + // Clearing drawables... can we free the data structure? + if (dr != null) { + if (dr.mDrawablePadding == 0) { + mDrawables = null; + } else { + // We need to retain the last set padding, so just clear + // out all of the fields in the existing structure. + if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null); + dr.mDrawableStart = null; + if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); + dr.mDrawableTop = null; + if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null); + dr.mDrawableEnd = null; + if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); + dr.mDrawableBottom = null; + dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; + dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; + dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; + dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; + } + } + } else { + if (dr == null) { + mDrawables = dr = new Drawables(); + } + + if (dr.mDrawableStart != start && dr.mDrawableStart != null) { + dr.mDrawableStart.setCallback(null); + } + dr.mDrawableStart = start; + + if (dr.mDrawableTop != top && dr.mDrawableTop != null) { + dr.mDrawableTop.setCallback(null); + } + dr.mDrawableTop = top; + + if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) { + dr.mDrawableEnd.setCallback(null); + } + dr.mDrawableEnd = end; + + if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { + dr.mDrawableBottom.setCallback(null); + } + dr.mDrawableBottom = bottom; + + final Rect compoundRect = dr.mCompoundRect; + int[] state; + + state = getDrawableState(); + + if (start != null) { + start.setState(state); + start.copyBounds(compoundRect); + start.setCallback(this); + dr.mDrawableSizeStart = compoundRect.width(); + dr.mDrawableHeightStart = compoundRect.height(); + } else { + dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; + } + + if (end != null) { + end.setState(state); + end.copyBounds(compoundRect); + end.setCallback(this); + dr.mDrawableSizeEnd = compoundRect.width(); + dr.mDrawableHeightEnd = compoundRect.height(); + } else { + dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; + } + + if (top != null) { + top.setState(state); + top.copyBounds(compoundRect); + top.setCallback(this); + dr.mDrawableSizeTop = compoundRect.height(); + dr.mDrawableWidthTop = compoundRect.width(); + } else { + dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; + } + + if (bottom != null) { + bottom.setState(state); + bottom.copyBounds(compoundRect); + bottom.setCallback(this); + dr.mDrawableSizeBottom = compoundRect.height(); + dr.mDrawableWidthBottom = compoundRect.width(); + } else { + dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; + } + } + + resolveDrawables(); + invalidate(); + requestLayout(); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, + * to the end of, and below the text. Use 0 if you do not + * want a Drawable there. The Drawables' bounds will be set to + * their intrinsic bounds. + * + * @param start Resource identifier of the start Drawable. + * @param top Resource identifier of the top Drawable. + * @param end Resource identifier of the end Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + * + * @hide + */ + public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, + int bottom) { + resetResolvedDrawables(); + final Resources resources = getContext().getResources(); + setCompoundDrawablesRelativeWithIntrinsicBounds( + start != 0 ? resources.getDrawable(start) : null, + top != 0 ? resources.getDrawable(top) : null, + end != 0 ? resources.getDrawable(end) : null, + bottom != 0 ? resources.getDrawable(bottom) : null); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, + * to the end of, and below the text. Use null if you do not + * want a Drawable there. The Drawables' bounds will be set to + * their intrinsic bounds. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + * + * @hide + */ + public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top, + Drawable end, Drawable bottom) { + + resetResolvedDrawables(); + if (start != null) { + start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); + } + if (end != null) { + end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); + } + if (top != null) { + top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); + } + if (bottom != null) { + bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); + } + setCompoundDrawablesRelative(start, top, end, bottom); + } + + /** * Returns drawables for the left, top, right, and bottom borders. */ public Drawable[] getCompoundDrawables() { @@ -1669,6 +1983,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns drawables for the start, top, end, and bottom borders. + * + * @hide + */ + public Drawable[] getCompoundDrawablesRelative() { + final Drawables dr = mDrawables; + if (dr != null) { + return new Drawable[] { + dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom + }; + } else { + return new Drawable[] { null, null, null, null }; + } + } + + /** * Sets the size of the padding between the compound drawables and * the text. * @@ -1775,6 +2105,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTypefaceByIndex(typefaceIndex, styleIndex); + if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps, + false)) { + setTransformationMethod(new AllCapsTransformationMethod(getContext())); + } + appearance.recycle(); } @@ -2466,6 +2801,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { dr.mDrawableRight.setState(state); } + if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) { + dr.mDrawableStart.setState(state); + } + if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) { + dr.mDrawableEnd.setState(state); + } } } @@ -2813,8 +3154,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Do not change the movement method for text that support text selection as it // would prevent an arbitrary cursor displacement. - final boolean hasTextSelection = this instanceof EditText || mTextIsSelectable; - if (mLinksClickable && !hasTextSelection) { + if (mLinksClickable && !textCanBeSelected()) { setMovementMethod(LinkMovementMethod.getInstance()); } } @@ -2823,14 +3163,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mBufferType = type; mText = text; - if (mTransformation == null) + if (mTransformation == null) { mTransformed = text; - else + } else { mTransformed = mTransformation.getTransformation(text, this); + } final int textLength = text.length(); - if (text instanceof Spannable) { + if (text instanceof Spannable && !mAllowTransformationLengthChange) { Spannable sp = (Spannable) text; // Remove any ChangeWatchers that might have come @@ -2852,7 +3193,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mTransformation != null) { sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } if (mMovement != null) { @@ -3518,7 +3858,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mErrorWasChanged = true; final Drawables dr = mDrawables; if (dr != null) { - setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, dr.mDrawableBottom); + switch (getResolvedLayoutDirection()) { + default: + case LAYOUT_DIRECTION_LTR: + setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, + dr.mDrawableBottom); + break; + case LAYOUT_DIRECTION_RTL: + setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight, + dr.mDrawableBottom); + break; + } } else { setCompoundDrawables(null, null, icon, null); } @@ -4020,6 +4370,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mSelectionModifierCursorController != null) { observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); } + + // Resolve drawables as the layout direction has been resolved + resolveDrawables(); } @Override @@ -4049,6 +4402,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } hideControllers(); + + resetResolvedDrawables(); } @Override @@ -4083,7 +4438,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final boolean verified = super.verifyDrawable(who); if (!verified && mDrawables != null) { return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop || - who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom; + who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom || + who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd; } return verified; } @@ -4104,6 +4460,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mDrawables.mDrawableBottom != null) { mDrawables.mDrawableBottom.jumpToCurrentState(); } + if (mDrawables.mDrawableStart != null) { + mDrawables.mDrawableStart.jumpToCurrentState(); + } + if (mDrawables.mDrawableEnd != null) { + mDrawables.mDrawableEnd.jumpToCurrentState(); + } } } @@ -4164,7 +4526,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mDrawables != null) { final Drawables drawables = mDrawables; if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight || - who == drawables.mDrawableTop || who == drawables.mDrawableBottom) { + who == drawables.mDrawableTop || who == drawables.mDrawableBottom || + who == drawables.mDrawableStart || who == drawables.mDrawableEnd) { return getResolvedLayoutDirection(); } } @@ -4183,6 +4546,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha); if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha); if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha); + if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha); + if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha); } return true; } @@ -4200,7 +4565,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView * selectable (text is not selectable by default). * - * Note that the content of an EditText is always selectable. + * Note that this method simply returns the state of this flag. Although this flag has to be set + * in order to select text in non-editable TextView, the content of an {@link EditText} can + * always be selected, independently of the value of this flag. * * @return True if the text displayed in this TextView can be selected by the user. * @@ -4437,12 +4804,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selStart = getSelectionStart(); selEnd = getSelectionEnd(); - if ((isCursorVisible() || mTextIsSelectable) && selStart >= 0 && isEnabled()) { + if (selStart >= 0) { if (mHighlightPath == null) mHighlightPath = new Path(); if (selStart == selEnd) { - if (!mTextIsSelectable && + if (isCursorVisible() && (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) { if (mHighlightPathBogus) { mHighlightPath.reset(); @@ -4461,7 +4828,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener highlight = mHighlightPath; drawCursor = mCursorCount > 0; } - } else { + } else if (textCanBeSelected()) { if (mHighlightPathBogus) { mHighlightPath.reset(); mLayout.getSelectionPath(selStart, selEnd, mHighlightPath); @@ -5472,6 +5839,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (curs >= 0) { mHighlightPathBogus = true; makeBlink(); + bringPointIntoView(curs); } checkForResize(); @@ -5543,8 +5911,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - protected void resetLayoutDirectionResolution() { - super.resetLayoutDirectionResolution(); + protected void resetResolvedLayoutDirection() { + super.resetResolvedLayoutDirection(); if (mLayoutAlignment != null && (mTextAlign == TextAlign.VIEW_START || @@ -5632,14 +6000,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Layout.Alignment alignment = getLayoutAlignment(); boolean shouldEllipsize = mEllipsize != null && mInput == null; + if (mTextDir == null) { + resolveTextDirection(); + } if (mText instanceof Spannable) { mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w, - alignment, mSpacingMult, + alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null, ellipsisWidth); } else { if (boring == UNKNOWN_BORING) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring); + boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } @@ -5676,23 +6047,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (shouldEllipsize) { mLayout = new StaticLayout(mTransformed, 0, mTransformed.length(), - mTextPaint, w, alignment, mSpacingMult, + mTextPaint, w, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mEllipsize, ellipsisWidth); } else { mLayout = new StaticLayout(mTransformed, mTextPaint, - w, alignment, mSpacingMult, mSpacingAdd, + w, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad); } } else if (shouldEllipsize) { mLayout = new StaticLayout(mTransformed, 0, mTransformed.length(), - mTextPaint, w, alignment, mSpacingMult, + mTextPaint, w, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mEllipsize, ellipsisWidth); } else { mLayout = new StaticLayout(mTransformed, mTextPaint, - w, alignment, mSpacingMult, mSpacingAdd, + w, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad); } } @@ -5704,7 +6075,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (shouldEllipsize) hintWidth = w; if (hintBoring == UNKNOWN_BORING) { - hintBoring = BoringLayout.isBoring(mHint, mTextPaint, + hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; @@ -5742,23 +6113,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (shouldEllipsize) { mHintLayout = new StaticLayout(mHint, 0, mHint.length(), - mTextPaint, hintWidth, alignment, mSpacingMult, + mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mEllipsize, ellipsisWidth); } else { mHintLayout = new StaticLayout(mHint, mTextPaint, - hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad); } } else if (shouldEllipsize) { mHintLayout = new StaticLayout(mHint, 0, mHint.length(), - mTextPaint, hintWidth, alignment, mSpacingMult, + mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mEllipsize, ellipsisWidth); } else { mHintLayout = new StaticLayout(mHint, mTextPaint, - hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad); } } @@ -5859,6 +6230,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener BoringLayout.Metrics boring = UNKNOWN_BORING; BoringLayout.Metrics hintBoring = UNKNOWN_BORING; + if (mTextDir == null) { + resolveTextDirection(); + } + int des = -1; boolean fromexisting = false; @@ -5871,7 +6246,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (des < 0) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring); + boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } @@ -6184,6 +6559,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int scrollx, scrolly; + // Convert to left, center, or right alignment. + if (a == Layout.Alignment.ALIGN_NORMAL) { + a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT : + Layout.Alignment.ALIGN_RIGHT; + } else if (a == Layout.Alignment.ALIGN_OPPOSITE){ + a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT : + Layout.Alignment.ALIGN_LEFT; + } + if (a == Layout.Alignment.ALIGN_CENTER) { /* * Keep centered if possible, or, if it is too wide to fit, @@ -6202,28 +6586,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollx = left; } } - } else if (a == Layout.Alignment.ALIGN_NORMAL) { - /* - * Keep leading edge in view. - */ - - if (dir < 0) { - int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); - scrollx = right - hspace; - } else { - scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); - } - } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ { - /* - * Keep trailing edge in view. - */ - - if (dir < 0) { - scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); - } else { - int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); - scrollx = right - hspace; - } + } else if (a == Layout.Alignment.ALIGN_RIGHT) { + int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); + scrollx = right - hspace; + } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default) + scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); } if (ht < vspace) { @@ -6251,6 +6618,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean bringPointIntoView(int offset) { boolean changed = false; + if (mLayout == null) return changed; + int line = mLayout.getLineForOffset(offset); // FIXME: Is it okay to truncate this, or should we round? @@ -6265,20 +6634,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int grav; switch (mLayout.getParagraphAlignment(line)) { - case ALIGN_NORMAL: + case ALIGN_LEFT: grav = 1; break; - - case ALIGN_OPPOSITE: + case ALIGN_RIGHT: grav = -1; break; - + case ALIGN_NORMAL: + grav = mLayout.getParagraphDirection(line); + break; + case ALIGN_OPPOSITE: + grav = -mLayout.getParagraphDirection(line); + break; + case ALIGN_CENTER: default: grav = 0; + break; } - grav *= mLayout.getParagraphDirection(line); - int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); @@ -6570,6 +6943,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets the properties of this field to transform input to ALL CAPS + * display. This may use a "small caps" formatting if available. + * This setting will be ignored if this field is editable or selectable. + * + * This call replaces the current transformation method. Disabling this + * will not necessarily restore the previous behavior from before this + * was enabled. + * + * @see #setTransformationMethod(TransformationMethod) + * @attr ref android.R.styleable#TextView_textAllCaps + */ + public void setAllCaps(boolean allCaps) { + if (allCaps) { + setTransformationMethod(new AllCapsTransformationMethod(getContext())); + } else { + setTransformationMethod(null); + } + } + + /** * If true, sets the properties of this field (number of lines, horizontally scrolling, * transformation method) to be for a single-line input; if false, restores these to the default * conditions. @@ -7406,9 +7799,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); - if (hasInsertionController()) { - getInsertionController().onTouchEvent(event); - } if (hasSelectionController()) { getSelectionController().onTouchEvent(event); } @@ -7836,7 +8226,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // prepareCursorController() relies on this method. // If you change this condition, make sure prepareCursorController is called anywhere // the value of this condition might be changed. - return mText instanceof Spannable && mMovement != null && mMovement.canSelectArbitrarily(); + if (mMovement == null || !mMovement.canSelectArbitrarily()) return false; + return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled()); } private boolean canCut() { @@ -7924,6 +8315,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int minOffset = extractRangeStartFromLong(lastTouchOffsets); final int maxOffset = extractRangeEndFromLong(lastTouchOffsets); + // Safety check in case standard touch event handling has been bypassed + if (minOffset < 0 || minOffset >= mText.length()) return false; + if (maxOffset < 0 || maxOffset >= mText.length()) return false; + int selectionStart, selectionEnd; // If a URLSpan (web address, email, phone...) is found at that position, select it. @@ -9063,7 +9458,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void onClick(View v) { if (canPaste()) { - paste(getSelectionStart(), getSelectionEnd()); + onTextContextMenuItem(ID_PASTE); } hide(); } @@ -9679,13 +10074,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void hide(); /** - * 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 boolean onTouchEvent(MotionEvent event); - - /** * Called when the view is detached from window. Perform house keeping task, such as * stopping Runnable thread that would otherwise keep a reference on the context, thus * preventing the activity from being recycled. @@ -9712,10 +10100,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - public boolean onTouchEvent(MotionEvent ev) { - return false; - } - public void onTouchModeChanged(boolean isInTouchMode) { if (!isInTouchMode) { hide(); @@ -9774,52 +10158,49 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEndHandle != null) mEndHandle.hide(); } - public boolean onTouchEvent(MotionEvent event) { + public void onTouchEvent(MotionEvent event) { // This is done even when the View does not have focus, so that long presses can start // selection and tap can move cursor from this tap position. - if (isTextEditable() || mTextIsSelectable) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - final float x = event.getX(); - final float y = event.getY(); - - // Remember finger down position, to be able to start selection from there - mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); - - // Double tap detection - long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; - if (duration <= ViewConfiguration.getDoubleTapTimeout() && - isPositionOnText(x, y)) { - final float deltaX = x - mPreviousTapPositionX; - final float deltaY = y - mPreviousTapPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { - showSuggestions(); - mDiscardNextActionUp = true; - } + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + final float x = event.getX(); + final float y = event.getY(); + + // Remember finger down position, to be able to start selection from there + mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); + + // Double tap detection + long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; + if (duration <= ViewConfiguration.getDoubleTapTimeout() && + isPositionOnText(x, y)) { + final float deltaX = x - mPreviousTapPositionX; + final float deltaY = y - mPreviousTapPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + if (distanceSquared < mSquaredTouchSlopDistance) { + showSuggestions(); + mDiscardNextActionUp = true; } + } - mPreviousTapPositionX = x; - mPreviousTapPositionY = y; + mPreviousTapPositionX = x; + mPreviousTapPositionY = y; - break; + break; - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - // Handle multi-point gestures. Keep min and max offset positions. - // Only activated for devices that correctly handle multi-touch. - if (mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { - updateMinAndMaxOffsets(event); - } - break; + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + // Handle multi-point gestures. Keep min and max offset positions. + // Only activated for devices that correctly handle multi-touch. + if (mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { + updateMinAndMaxOffsets(event); + } + break; - case MotionEvent.ACTION_UP: - mPreviousTapUpTime = SystemClock.uptimeMillis(); - break; - } + case MotionEvent.ACTION_UP: + mPreviousTapUpTime = SystemClock.uptimeMillis(); + break; } - return false; } /** @@ -10075,11 +10456,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mInBatchEditControllers; } + private class TextViewDirectionHeuristic extends TextDirectionHeuristicImpl { + private TextViewDirectionHeuristic(TextDirectionAlgorithm algorithm) { + super(algorithm); + } + @Override + protected boolean defaultIsRtl() { + return getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + } + /** * Resolve the text direction. * * Text direction of paragraphs in a TextView is determined using a heuristic. If the correct - * text direction cannot be determined by the heuristic, the view’s resolved layout direction + * text direction cannot be determined by the heuristic, the view's resolved layout direction * determines the direction. * * This heuristic and result is applied individually to each paragraph in a TextView, based on @@ -10088,157 +10479,89 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @Override protected void resolveTextDirection() { - int resolvedTextDirection = TEXT_DIRECTION_UNDEFINED; - switch(mTextDirection) { + super.resolveTextDirection(); + + int textDir = getResolvedTextDirection(); + switch (textDir) { default: - case TEXT_DIRECTION_INHERIT: - // Try to the text direction from the parent layout. If not possible, then we will - // use the default layout direction to decide later - if (mParent != null && mParent instanceof ViewGroup) { - resolvedTextDirection = ((ViewGroup) mParent).getResolvedTextDirection(); - } - break; case TEXT_DIRECTION_FIRST_STRONG: - resolvedTextDirection = getTextDirectionFromFirstStrong(mText); + mTextDir = new TextViewDirectionHeuristic(FirstStrong.INSTANCE); break; case TEXT_DIRECTION_ANY_RTL: - resolvedTextDirection = getTextDirectionFromAnyRtl(mText); + mTextDir = new TextViewDirectionHeuristic(AnyStrong.INSTANCE_RTL); break; case TEXT_DIRECTION_CHAR_COUNT: - resolvedTextDirection = getTextDirectionFromCharCount(mText); + mTextDir = new TextViewDirectionHeuristic(CharCount.INSTANCE_DEFAULT); break; case TEXT_DIRECTION_LTR: - resolvedTextDirection = TEXT_DIRECTION_LTR; + mTextDir = TextDirectionHeuristics.LTR; break; case TEXT_DIRECTION_RTL: - resolvedTextDirection = TEXT_DIRECTION_RTL; + mTextDir = TextDirectionHeuristics.RTL; break; } - // if we have been so far unable to get the text direction from the heuristics, then we are - // falling back using the layout direction - if (resolvedTextDirection == TEXT_DIRECTION_UNDEFINED) { - switch(getResolvedLayoutDirection()) { - default: - case LAYOUT_DIRECTION_LTR: - resolvedTextDirection = TEXT_DIRECTION_LTR; - break; - case LAYOUT_DIRECTION_RTL: - resolvedTextDirection = TEXT_DIRECTION_RTL; - break; - } - } - mResolvedTextDirection = resolvedTextDirection; } /** - * Get text direction following the "first strong" heuristic. + * Subclasses will need to override this method to implement their own way of resolving + * drawables depending on the layout direction. * - * @param cs the CharSequence used to get the text direction. + * A call to the super method will be required from the subclasses implementation. * - * @return {@link #TEXT_DIRECTION_RTL} if direction it RTL, {@link #TEXT_DIRECTION_LTR} if - * direction it LTR or {@link #TEXT_DIRECTION_UNDEFINED} if direction cannot be found. */ - private static int getTextDirectionFromFirstStrong(final CharSequence cs) { - final int length = cs.length(); - if (length == 0) { - return TEXT_DIRECTION_UNDEFINED; + protected void resolveDrawables() { + // No need to resolve twice + if (bResolvedDrawables) { + return; } - for(int i = 0; i < length; i++) { - final char c = cs.charAt(i); - final byte dir = Character.getDirectionality(c); - if (isStrongLtrChar(dir)) { - return TEXT_DIRECTION_LTR; - } else if (isStrongRtlChar(dir)) { - return TEXT_DIRECTION_RTL; - } + // No drawable to resolve + if (mDrawables == null) { + return; } - return TEXT_DIRECTION_UNDEFINED; - } - - /** - * Get text direction following the "any RTL" heuristic. - * - * @param cs the CharSequence used to get the text direction. - * - * @return {@link #TEXT_DIRECTION_RTL} if direction it RTL, {@link #TEXT_DIRECTION_LTR} if - * direction it LTR or {@link #TEXT_DIRECTION_UNDEFINED} if direction cannot be found. - */ - private static int getTextDirectionFromAnyRtl(final CharSequence cs) { - final int length = cs.length(); - if (length == 0) { - return TEXT_DIRECTION_UNDEFINED; + // No relative drawable to resolve + if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) { + bResolvedDrawables = true; + return; } - boolean foundStrongLtr = false; - boolean foundStrongRtl = false; - for(int i = 0; i < length; i++) { - final char c = cs.charAt(i); - final byte dir = Character.getDirectionality(c); - if (isStrongLtrChar(dir)) { - foundStrongLtr = true; - } else if (isStrongRtlChar(dir)) { - foundStrongRtl = true; + + Drawables dr = mDrawables; + switch(getResolvedLayoutDirection()) { + case LAYOUT_DIRECTION_RTL: + if (dr.mDrawableStart != null) { + dr.mDrawableRight = dr.mDrawableStart; + + dr.mDrawableSizeRight = dr.mDrawableSizeStart; + dr.mDrawableHeightRight = dr.mDrawableHeightStart; + } + if (dr.mDrawableEnd != null) { + dr.mDrawableLeft = dr.mDrawableEnd; + + dr.mDrawableSizeLeft = dr.mDrawableSizeEnd; + dr.mDrawableHeightLeft = dr.mDrawableHeightEnd; + } break; - } - } - if (foundStrongRtl) { - return TEXT_DIRECTION_RTL; - } - if (foundStrongLtr) { - return TEXT_DIRECTION_LTR; - } - return TEXT_DIRECTION_UNDEFINED; - } - /** - * Get text direction following the "char count" heuristic. - * - * @param cs the CharSequence used to get the text direction. - * - * @return {@link #TEXT_DIRECTION_RTL} if direction it RTL, {@link #TEXT_DIRECTION_LTR} if - * direction it LTR or {@link #TEXT_DIRECTION_UNDEFINED} if direction cannot be found. - */ - private int getTextDirectionFromCharCount(CharSequence cs) { - final int length = cs.length(); - if (length == 0) { - return TEXT_DIRECTION_UNDEFINED; - } - int countLtr = 0; - int countRtl = 0; - for(int i = 0; i < length; i++) { - final char c = cs.charAt(i); - final byte dir = Character.getDirectionality(c); - if (isStrongLtrChar(dir)) { - countLtr++; - } else if (isStrongRtlChar(dir)) { - countRtl++; - } - } - final float percentLtr = ((float) countLtr) / (countLtr + countRtl); - if (percentLtr > DEFAULT_TEXT_DIRECTION_CHAR_COUNT_THRESHOLD) { - return TEXT_DIRECTION_LTR; - } - final float percentRtl = ((float) countRtl) / (countLtr + countRtl); - if (percentRtl > DEFAULT_TEXT_DIRECTION_CHAR_COUNT_THRESHOLD) { - return TEXT_DIRECTION_RTL; - } - return TEXT_DIRECTION_UNDEFINED; - } + case LAYOUT_DIRECTION_LTR: + default: + if (dr.mDrawableStart != null) { + dr.mDrawableLeft = dr.mDrawableStart; - /** - * Return true if the char direction is corresponding to a "strong RTL char" following the - * Unicode Bidirectional Algorithm (UBA). - */ - private static boolean isStrongRtlChar(final byte dir) { - return (dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || - dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC); + dr.mDrawableSizeLeft = dr.mDrawableSizeStart; + dr.mDrawableHeightLeft = dr.mDrawableHeightStart; + } + if (dr.mDrawableEnd != null) { + dr.mDrawableRight = dr.mDrawableEnd; + + dr.mDrawableSizeRight = dr.mDrawableSizeEnd; + dr.mDrawableHeightRight = dr.mDrawableHeightEnd; + } + break; + } + bResolvedDrawables = true; } - /** - * Return true if the char direction is corresponding to a "strong LTR char" following the - * Unicode Bidirectional Algorithm (UBA). - */ - private static boolean isStrongLtrChar(final byte dir) { - return (dir == Character.DIRECTIONALITY_LEFT_TO_RIGHT); + protected void resetResolvedDrawables() { + bResolvedDrawables = false; } @ViewDebug.ExportedProperty(category = "text") @@ -10254,6 +10577,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private MovementMethod mMovement; private TransformationMethod mTransformation; + private boolean mAllowTransformationLengthChange; private ChangeWatcher mChangeWatcher; private ArrayList<TextWatcher> mListeners = null; @@ -10341,6 +10665,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private BoringLayout mSavedLayout, mSavedHintLayout; + private TextDirectionHeuristic mTextDir = null; + private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private InputFilter[] mFilters = NO_FILTERS; private static final Spanned EMPTY_SPANNED = new SpannedString(""); diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 423e735..0547438 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -20,6 +20,7 @@ import com.android.internal.R; import android.annotation.Widget; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; @@ -32,6 +33,7 @@ import android.widget.NumberPicker.OnValueChangeListener; import java.text.DateFormatSymbols; import java.util.Calendar; +import java.util.Locale; /** * A view for selecting the time of day, in either 24 hour or AM/PM mode. The @@ -92,6 +94,8 @@ public class TimePicker extends FrameLayout { private Calendar mTempCalendar; + private Locale mCurrentLocale; + /** * The callback interface used to indicate the time has been adjusted. */ @@ -116,6 +120,9 @@ public class TimePicker extends FrameLayout { public TimePicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + // process style attributes TypedArray attributesArray = context.obtainStyledAttributes( attrs, R.styleable.TimePicker, defStyle, 0); @@ -211,8 +218,6 @@ public class TimePicker extends FrameLayout { updateHourControl(); updateAmPmControl(); - // initialize to current time - mTempCalendar = Calendar.getInstance(); setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); // set to current time @@ -248,6 +253,25 @@ public class TimePicker extends FrameLayout { return mIsEnabled; } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + setCurrentLocale(newConfig.locale); + } + + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + private void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; + } + mCurrentLocale = locale; + mTempCalendar = Calendar.getInstance(locale); + } + /** * Used to save / restore state of time picker */ |