diff options
Diffstat (limited to 'core/java')
74 files changed, 2299 insertions, 1213 deletions
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 4f3405b..5fee4de 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -62,6 +62,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -275,7 +276,7 @@ public class AccountManagerService accountNames.add(accountName); } } - for (HashMap.Entry<String, ArrayList<String>> cur + for (Map.Entry<String, ArrayList<String>> cur : accountNamesByType.entrySet()) { final String accountType = cur.getKey(); final ArrayList<String> accountNames = cur.getValue(); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 4fe9cef..9661b9e 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1442,9 +1442,10 @@ public class ActivityManager { public int getLauncherLargeIconDensity() { final Resources res = mContext.getResources(); final int density = res.getDisplayMetrics().densityDpi; + final int sw = res.getConfiguration().smallestScreenWidthDp; - if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) - != Configuration.SCREENLAYOUT_SIZE_XLARGE) { + if (sw < 600) { + // Smaller than approx 7" tablets, use the regular icon size. return density; } @@ -1456,9 +1457,13 @@ public class ActivityManager { case DisplayMetrics.DENSITY_HIGH: return DisplayMetrics.DENSITY_XHIGH; case DisplayMetrics.DENSITY_XHIGH: - return DisplayMetrics.DENSITY_MEDIUM * 2; + return DisplayMetrics.DENSITY_XXHIGH; + case DisplayMetrics.DENSITY_XXHIGH: + return DisplayMetrics.DENSITY_XHIGH * 2; default: - return density; + // The density is some abnormal value. Return some other + // abnormal value that is a reasonable scaling of it. + return (int)(density*1.5f); } } @@ -1471,9 +1476,10 @@ public class ActivityManager { public int getLauncherLargeIconSize() { final Resources res = mContext.getResources(); final int size = res.getDimensionPixelSize(android.R.dimen.app_icon_size); + final int sw = res.getConfiguration().smallestScreenWidthDp; - if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) - != Configuration.SCREENLAYOUT_SIZE_XLARGE) { + if (sw < 600) { + // Smaller than approx 7" tablets, use the regular icon size. return size; } @@ -1487,9 +1493,13 @@ public class ActivityManager { case DisplayMetrics.DENSITY_HIGH: return (size * DisplayMetrics.DENSITY_XHIGH) / DisplayMetrics.DENSITY_HIGH; case DisplayMetrics.DENSITY_XHIGH: - return (size * DisplayMetrics.DENSITY_MEDIUM * 2) / DisplayMetrics.DENSITY_XHIGH; + return (size * DisplayMetrics.DENSITY_XXHIGH) / DisplayMetrics.DENSITY_XHIGH; + case DisplayMetrics.DENSITY_XXHIGH: + return (size * DisplayMetrics.DENSITY_XHIGH*2) / DisplayMetrics.DENSITY_XXHIGH; default: - return size; + // The density is some abnormal value. Return some other + // abnormal value that is a reasonable scaling of it. + return (int)(size*1.5f); } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index bac3c6c..9807b89 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -486,7 +486,6 @@ public final class ActivityThread { private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%21s %8d"; private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d"; - private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d"; private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s"; // Formatting for checkin service - update version if row format changes @@ -867,7 +866,6 @@ public final class ActivityThread { int binderProxyObjectCount = Debug.getBinderProxyObjectCount(); int binderDeathObjectCount = Debug.getBinderDeathObjectCount(); long openSslSocketCount = Debug.countInstancesOfClass(OpenSSLSocketImpl.class); - long sqliteAllocated = SQLiteDebug.getHeapAllocatedSize() / 1024; SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo(); // For checkin, we print one long comma-separated list of values @@ -935,9 +933,9 @@ public final class ActivityThread { pw.print(openSslSocketCount); pw.print(','); // SQL - pw.print(sqliteAllocated); pw.print(','); pw.print(stats.memoryUsed / 1024); pw.print(','); - pw.print(stats.pageCacheOverflo / 1024); pw.print(','); + pw.print(stats.memoryUsed / 1024); pw.print(','); + pw.print(stats.pageCacheOverflow / 1024); pw.print(','); pw.print(stats.largestMemAlloc / 1024); for (int i = 0; i < stats.dbStats.size(); i++) { DbStats dbStats = stats.dbStats.get(i); @@ -1003,10 +1001,9 @@ public final class ActivityThread { // SQLite mem info pw.println(" "); pw.println(" SQL"); - printRow(pw, TWO_COUNT_COLUMNS_DB, "heap:", sqliteAllocated, "MEMORY_USED:", - stats.memoryUsed / 1024); - printRow(pw, TWO_COUNT_COLUMNS_DB, "PAGECACHE_OVERFLOW:", - stats.pageCacheOverflo / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024); + printRow(pw, ONE_COUNT_COLUMN, "MEMORY_USED:", stats.memoryUsed / 1024); + printRow(pw, TWO_COUNT_COLUMNS, "PAGECACHE_OVERFLOW:", + stats.pageCacheOverflow / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024); pw.println(" "); int N = stats.dbStats.size(); if (N > 0) { @@ -3744,7 +3741,6 @@ public final class ActivityThread { } final void handleTrimMemory(int level) { - WindowManagerImpl.getDefault().trimMemory(level); ArrayList<ComponentCallbacks2> callbacks; synchronized (mPackages) { @@ -3755,6 +3751,7 @@ public final class ActivityThread { for (int i=0; i<N; i++) { callbacks.get(i).onTrimMemory(level); } + WindowManagerImpl.getDefault().trimMemory(level); } private void setupGraphicsSupport(LoadedApk info) { @@ -3807,7 +3804,7 @@ public final class ActivityThread { // implementation to use the pool executor. Normally, we use the // serialized executor as the default. This has to happen in the // main thread so the main looper is set right. - if (data.appInfo.targetSdkVersion <= 12) { + if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index cc3219b..0debb84 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -1034,8 +1034,11 @@ public abstract class ContentResolver { * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. * By default, CursorAdapter objects will get this notification. * - * @param uri - * @param observer The observer that originated the change, may be <code>null</null> + * @param uri The uri of the content that was changed. + * @param observer The observer that originated the change, may be <code>null</null>. + * The observer that originated the change will only receive the notification if it + * has requested to receive self-change notifications by implementing + * {@link ContentObserver#deliverSelfNotifications()} to return true. */ public void notifyChange(Uri uri, ContentObserver observer) { notifyChange(uri, observer, true /* sync to network */); @@ -1046,8 +1049,11 @@ public abstract class ContentResolver { * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. * By default, CursorAdapter objects will get this notification. * - * @param uri - * @param observer The observer that originated the change, may be <code>null</null> + * @param uri The uri of the content that was changed. + * @param observer The observer that originated the change, may be <code>null</null>. + * The observer that originated the change will only receive the notification if it + * has requested to receive self-change notifications by implementing + * {@link ContentObserver#deliverSelfNotifications()} to return true. * @param syncToNetwork If true, attempt to sync the change to the network. */ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 0e83dc0..fc4c262 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -176,7 +176,7 @@ public final class ContentService extends IContentService.Stub { for (int i=0; i<numCalls; i++) { ObserverCall oc = calls.get(i); try { - oc.mObserver.onChange(oc.mSelfNotify); + oc.mObserver.onChange(oc.mSelfChange, uri); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri); } @@ -218,13 +218,12 @@ public final class ContentService extends IContentService.Stub { public static final class ObserverCall { final ObserverNode mNode; final IContentObserver mObserver; - final boolean mSelfNotify; + final boolean mSelfChange; - ObserverCall(ObserverNode node, IContentObserver observer, - boolean selfNotify) { + ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange) { mNode = node; mObserver = observer; - mSelfNotify = selfNotify; + mSelfChange = selfChange; } } @@ -668,7 +667,7 @@ public final class ContentService extends IContentService.Stub { } private void collectMyObserversLocked(boolean leaf, IContentObserver observer, - boolean selfNotify, ArrayList<ObserverCall> calls) { + boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) { int N = mObservers.size(); IBinder observerBinder = observer == null ? null : observer.asBinder(); for (int i = 0; i < N; i++) { @@ -676,28 +675,29 @@ public final class ContentService extends IContentService.Stub { // Don't notify the observer if it sent the notification and isn't interesed // in self notifications - if (entry.observer.asBinder() == observerBinder && !selfNotify) { + boolean selfChange = (entry.observer.asBinder() == observerBinder); + if (selfChange && !observerWantsSelfNotifications) { continue; } // Make sure the observer is interested in the notification if (leaf || (!leaf && entry.notifyForDescendents)) { - calls.add(new ObserverCall(this, entry.observer, selfNotify)); + calls.add(new ObserverCall(this, entry.observer, selfChange)); } } } public void collectObserversLocked(Uri uri, int index, IContentObserver observer, - boolean selfNotify, ArrayList<ObserverCall> calls) { + boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) { String segment = null; int segmentCount = countUriSegments(uri); if (index >= segmentCount) { // This is the leaf node, notify all observers - collectMyObserversLocked(true, observer, selfNotify, calls); + collectMyObserversLocked(true, observer, observerWantsSelfNotifications, calls); } else if (index < segmentCount){ segment = getUriSegment(uri, index); // Notify any observers at this level who are interested in descendents - collectMyObserversLocked(false, observer, selfNotify, calls); + collectMyObserversLocked(false, observer, observerWantsSelfNotifications, calls); } int N = mChildren.size(); @@ -705,7 +705,8 @@ public final class ContentService extends IContentService.Stub { ObserverNode node = mChildren.get(i); if (segment == null || node.mName.equals(segment)) { // We found the child, - node.collectObserversLocked(uri, index + 1, observer, selfNotify, calls); + node.collectObserversLocked(uri, index + 1, + observer, observerWantsSelfNotifications, calls); if (segment != null) { break; } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e3b1f54..fbc1b2b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -43,6 +43,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; import java.util.Set; /** @@ -4420,22 +4421,24 @@ public class Intent implements Parcelable, Cloneable { /** * Set the data this intent is operating on. This method automatically - * clears any type that was previously set by {@link #setType}. + * clears any type that was previously set by {@link #setType} or + * {@link #setTypeAndNormalize}. * - * <p><em>Note: scheme and host name matching in the Android framework is - * case-sensitive, unlike the formal RFC. As a result, - * you should always ensure that you write your Uri with these elements - * using lower case letters, and normalize any Uris you receive from - * outside of Android to ensure the scheme and host is lower case.</em></p> + * <p><em>Note: scheme matching in the Android framework is + * case-sensitive, unlike the formal RFC. As a result, + * you should always write your Uri with a lower case scheme, + * or use {@link Uri#normalize} or + * {@link #setDataAndNormalize} + * to ensure that the scheme is converted to lower case.</em> * - * @param data The URI of the data this intent is now targeting. + * @param data The Uri of the data this intent is now targeting. * * @return Returns the same Intent object, for chaining multiple calls * into a single statement. * * @see #getData - * @see #setType - * @see #setDataAndType + * @see #setDataAndNormalize + * @see android.net.Intent#normalize */ public Intent setData(Uri data) { mData = data; @@ -4444,16 +4447,45 @@ public class Intent implements Parcelable, Cloneable { } /** - * Set an explicit MIME data type. This is used to create intents that - * only specify a type and not data, for example to indicate the type of - * data to return. This method automatically clears any data that was - * previously set by {@link #setData}. + * Normalize and set the data this intent is operating on. + * + * <p>This method automatically clears any type that was + * previously set (for example, by {@link #setType}). + * + * <p>The data Uri is normalized using + * {@link android.net.Uri#normalize} before it is set, + * so really this is just a convenience method for + * <pre> + * setData(data.normalize()) + * </pre> + * + * @param data The Uri of the data this intent is now targeting. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getData + * @see #setType + * @see android.net.Uri#normalize + */ + public Intent setDataAndNormalize(Uri data) { + return setData(data.normalize()); + } + + /** + * Set an explicit MIME data type. + * + * <p>This is used to create intents that only specify a type and not data, + * for example to indicate the type of data to return. + * + * <p>This method automatically clears any data that was + * previously set (for example by {@link #setData}). * * <p><em>Note: MIME type matching in the Android framework is * case-sensitive, unlike formal RFC MIME types. As a result, * you should always write your MIME types with lower case letters, - * and any MIME types you receive from outside of Android should be - * converted to lower case before supplying them here.</em></p> + * or use {@link #normalizeMimeType} or {@link #setTypeAndNormalize} + * to ensure that it is converted to lower case.</em> * * @param type The MIME type of the data being handled by this intent. * @@ -4461,8 +4493,9 @@ public class Intent implements Parcelable, Cloneable { * into a single statement. * * @see #getType - * @see #setData + * @see #setTypeAndNormalize * @see #setDataAndType + * @see #normalizeMimeType */ public Intent setType(String type) { mData = null; @@ -4471,26 +4504,58 @@ public class Intent implements Parcelable, Cloneable { } /** + * Normalize and set an explicit MIME data type. + * + * <p>This is used to create intents that only specify a type and not data, + * for example to indicate the type of data to return. + * + * <p>This method automatically clears any data that was + * previously set (for example by {@link #setData}). + * + * <p>The MIME type is normalized using + * {@link #normalizeMimeType} before it is set, + * so really this is just a convenience method for + * <pre> + * setType(Intent.normalizeMimeType(type)) + * </pre> + * + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getType + * @see #setData + * @see #normalizeMimeType + */ + public Intent setTypeAndNormalize(String type) { + return setType(normalizeMimeType(type)); + } + + /** * (Usually optional) Set the data for the intent along with an explicit * MIME data type. This method should very rarely be used -- it allows you * to override the MIME type that would ordinarily be inferred from the * data with your own type given here. * - * <p><em>Note: MIME type, Uri scheme, and host name matching in the + * <p><em>Note: MIME type and Uri scheme matching in the * Android framework is case-sensitive, unlike the formal RFC definitions. * As a result, you should always write these elements with lower case letters, - * and normalize any MIME types or Uris you receive from - * outside of Android to ensure these elements are lower case before - * supplying them here.</em></p> + * or use {@link #normalizeMimeType} or {@link android.net.Uri#normalize} or + * {@link #setDataAndTypeAndNormalize} + * to ensure that they are converted to lower case.</em> * - * @param data The URI of the data this intent is now targeting. + * @param data The Uri of the data this intent is now targeting. * @param type The MIME type of the data being handled by this intent. * * @return Returns the same Intent object, for chaining multiple calls * into a single statement. * - * @see #setData * @see #setType + * @see #setData + * @see #normalizeMimeType + * @see android.net.Uri#normalize + * @see #setDataAndTypeAndNormalize */ public Intent setDataAndType(Uri data, String type) { mData = data; @@ -4499,6 +4564,35 @@ public class Intent implements Parcelable, Cloneable { } /** + * (Usually optional) Normalize and set both the data Uri and an explicit + * MIME data type. This method should very rarely be used -- it allows you + * to override the MIME type that would ordinarily be inferred from the + * data with your own type given here. + * + * <p>The data Uri and the MIME type are normalize using + * {@link android.net.Uri#normalize} and {@link #normalizeMimeType} + * before they are set, so really this is just a convenience method for + * <pre> + * setDataAndType(data.normalize(), Intent.normalizeMimeType(type)) + * </pre> + * + * @param data The Uri of the data this intent is now targeting. + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setType + * @see #setData + * @see #setDataAndType + * @see #normalizeMimeType + * @see android.net.Uri#normalize + */ + public Intent setDataAndTypeAndNormalize(Uri data, String type) { + return setDataAndType(data.normalize(), normalizeMimeType(type)); + } + + /** * Add a new category to the intent. Categories provide additional detail * about the action the intent is perform. When resolving an intent, only * activities that provide <em>all</em> of the requested categories will be @@ -5566,7 +5660,7 @@ public class Intent implements Parcelable, Cloneable { * * <ul> * <li> action, as set by {@link #setAction}. - * <li> data URI and MIME type, as set by {@link #setData(Uri)}, + * <li> data Uri and MIME type, as set by {@link #setData(Uri)}, * {@link #setType(String)}, or {@link #setDataAndType(Uri, String)}. * <li> categories, as set by {@link #addCategory}. * <li> package, as set by {@link #setPackage}. @@ -6229,4 +6323,38 @@ public class Intent implements Parcelable, Cloneable { return intent; } + + /** + * Normalize a MIME data type. + * + * <p>A normalized MIME type has white-space trimmed, + * content-type parameters removed, and is lower-case. + * This aligns the type with Android best practices for + * intent filtering. + * + * <p>For example, "text/plain; charset=utf-8" becomes "text/plain". + * "text/x-vCard" becomes "text/x-vcard". + * + * <p>All MIME types received from outside Android (such as user input, + * or external sources like Bluetooth, NFC, or the Internet) should + * be normalized before they are used to create an Intent. + * + * @param type MIME data type to normalize + * @return normalized MIME data type, or null if the input was null + * @see {@link #setType} + * @see {@link #setTypeAndNormalize} + */ + public static String normalizeMimeType(String type) { + if (type == null) { + return null; + } + + type = type.trim().toLowerCase(Locale.US); + + final int semicolonIndex = type.indexOf(';'); + if (semicolonIndex != -1) { + type = type.substring(0, semicolonIndex); + } + return type; + } } diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 74fef29..b28ed8d 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -284,23 +284,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } - /** - * This is hidden until the data set change model has been re-evaluated. - * @hide - */ - protected void notifyDataSetChange() { - mDataSetObservable.notifyChanged(); - } - - /** - * This is hidden until the data set change model has been re-evaluated. - * @hide - */ - protected DataSetObservable getDataSetObservable() { - return mDataSetObservable; - - } - public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); } @@ -317,7 +300,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { */ protected void onChange(boolean selfChange) { synchronized (mSelfObserverLock) { - mContentObservable.dispatchChange(selfChange); + mContentObservable.dispatchChange(selfChange, null); if (mNotifyUri != null && selfChange) { mContentResolver.notifyChange(mNotifyUri, mSelfObserver); } diff --git a/core/java/android/database/ContentObservable.java b/core/java/android/database/ContentObservable.java index 8d7b7c5..7692bb3 100644 --- a/core/java/android/database/ContentObservable.java +++ b/core/java/android/database/ContentObservable.java @@ -16,40 +16,75 @@ package android.database; +import android.net.Uri; + /** - * A specialization of Observable for ContentObserver that provides methods for - * invoking the various callback methods of ContentObserver. + * A specialization of {@link Observable} for {@link ContentObserver} + * that provides methods for sending notifications to a list of + * {@link ContentObserver} objects. */ public class ContentObservable extends Observable<ContentObserver> { - + // Even though the generic method defined in Observable would be perfectly + // fine on its own, we can't delete this overridden method because it would + // potentially break binary compatibility with existing applications. @Override public void registerObserver(ContentObserver observer) { super.registerObserver(observer); } /** - * invokes dispatchUpdate on each observer, unless the observer doesn't want - * self-notifications and the update is from a self-notification - * @param selfChange + * Invokes {@link ContentObserver#dispatchChange(boolean)} on each observer. + * <p> + * If <code>selfChange</code> is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + * </p> + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead. */ + @Deprecated public void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + + /** + * Invokes {@link ContentObserver#dispatchChange(boolean, Uri)} on each observer. + * Includes the changed content Uri when available. + * <p> + * If <code>selfChange</code> is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public void dispatchChange(boolean selfChange, Uri uri) { synchronized(mObservers) { for (ContentObserver observer : mObservers) { if (!selfChange || observer.deliverSelfNotifications()) { - observer.dispatchChange(selfChange); + observer.dispatchChange(selfChange, uri); } } } } /** - * invokes onChange on each observer - * @param selfChange + * Invokes {@link ContentObserver#onChange} on each observer. + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange} instead. */ + @Deprecated public void notifyChange(boolean selfChange) { synchronized(mObservers) { for (ContentObserver observer : mObservers) { - observer.onChange(selfChange); + observer.onChange(selfChange, null); } } } diff --git a/core/java/android/database/ContentObserver.java b/core/java/android/database/ContentObserver.java index 3b829a3..e4fbc28 100644 --- a/core/java/android/database/ContentObserver.java +++ b/core/java/android/database/ContentObserver.java @@ -16,65 +16,23 @@ package android.database; +import android.net.Uri; import android.os.Handler; /** - * Receives call backs for changes to content. Must be implemented by objects which are added - * to a {@link ContentObservable}. + * Receives call backs for changes to content. + * Must be implemented by objects which are added to a {@link ContentObservable}. */ public abstract class ContentObserver { + private final Object mLock = new Object(); + private Transport mTransport; // guarded by mLock - private Transport mTransport; - - // Protects mTransport - private Object lock = new Object(); - - /* package */ Handler mHandler; - - private final class NotificationRunnable implements Runnable { - - private boolean mSelf; - - public NotificationRunnable(boolean self) { - mSelf = self; - } - - public void run() { - ContentObserver.this.onChange(mSelf); - } - } - - private static final class Transport extends IContentObserver.Stub { - ContentObserver mContentObserver; - - public Transport(ContentObserver contentObserver) { - mContentObserver = contentObserver; - } - - public boolean deliverSelfNotifications() { - ContentObserver contentObserver = mContentObserver; - if (contentObserver != null) { - return contentObserver.deliverSelfNotifications(); - } - return false; - } - - public void onChange(boolean selfChange) { - ContentObserver contentObserver = mContentObserver; - if (contentObserver != null) { - contentObserver.dispatchChange(selfChange); - } - } - - public void releaseContentObserver() { - mContentObserver = null; - } - } + Handler mHandler; /** - * onChange() will happen on the provider Handler. + * Creates a content observer. * - * @param handler The handler to run {@link #onChange} on. + * @param handler The handler to run {@link #onChange} on, or null if none. */ public ContentObserver(Handler handler) { mHandler = handler; @@ -86,7 +44,7 @@ public abstract class ContentObserver { * {@hide} */ public IContentObserver getContentObserver() { - synchronized(lock) { + synchronized (mLock) { if (mTransport == null) { mTransport = new Transport(this); } @@ -101,8 +59,8 @@ public abstract class ContentObserver { * {@hide} */ public IContentObserver releaseContentObserver() { - synchronized(lock) { - Transport oldTransport = mTransport; + synchronized (mLock) { + final Transport oldTransport = mTransport; if (oldTransport != null) { oldTransport.releaseContentObserver(); mTransport = null; @@ -112,27 +70,134 @@ public abstract class ContentObserver { } /** - * Returns true if this observer is interested in notifications for changes - * made through the cursor the observer is registered with. + * Returns true if this observer is interested receiving self-change notifications. + * + * Subclasses should override this method to indicate whether the observer + * is interested in receiving notifications for changes that it made to the + * content itself. + * + * @return True if self-change notifications should be delivered to the observer. */ public boolean deliverSelfNotifications() { return false; } /** - * This method is called when a change occurs to the cursor that - * is being observed. - * - * @param selfChange true if the update was caused by a call to <code>commit</code> on the - * cursor that is being observed. + * This method is called when a content change occurs. + * <p> + * Subclasses should override this method to handle content changes. + * </p> + * + * @param selfChange True if this is a self-change notification. */ - public void onChange(boolean selfChange) {} + public void onChange(boolean selfChange) { + // Do nothing. Subclass should override. + } + /** + * This method is called when a content change occurs. + * Includes the changed content Uri when available. + * <p> + * Subclasses should override this method to handle content changes. + * To ensure correct operation on older versions of the framework that + * did not provide a Uri argument, applications should also implement + * the {@link #onChange(boolean)} overload of this method whenever they + * implement the {@link #onChange(boolean, Uri)} overload. + * </p><p> + * Example implementation: + * <pre><code> + * // Implement the onChange(boolean) method to delegate the change notification to + * // the onChange(boolean, Uri) method to ensure correct operation on older versions + * // of the framework that did not have the onChange(boolean, Uri) method. + * {@literal @Override} + * public void onChange(boolean selfChange) { + * onChange(selfChange, null); + * } + * + * // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument. + * {@literal @Override} + * public void onChange(boolean selfChange, Uri uri) { + * // Handle change. + * } + * </code></pre> + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public void onChange(boolean selfChange, Uri uri) { + onChange(selfChange); + } + + /** + * Dispatches a change notification to the observer. + * <p> + * If a {@link Handler} was supplied to the {@link ContentObserver} constructor, + * then a call to the {@link #onChange} method is posted to the handler's message queue. + * Otherwise, the {@link #onChange} method is invoked immediately on this thread. + * </p> + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead. + */ + @Deprecated public final void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + + /** + * Dispatches a change notification to the observer. + * Includes the changed content Uri when available. + * <p> + * If a {@link Handler} was supplied to the {@link ContentObserver} constructor, + * then a call to the {@link #onChange} method is posted to the handler's message queue. + * Otherwise, the {@link #onChange} method is invoked immediately on this thread. + * </p> + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public final void dispatchChange(boolean selfChange, Uri uri) { if (mHandler == null) { - onChange(selfChange); + onChange(selfChange, uri); } else { - mHandler.post(new NotificationRunnable(selfChange)); + mHandler.post(new NotificationRunnable(selfChange, uri)); + } + } + + private final class NotificationRunnable implements Runnable { + private final boolean mSelfChange; + private final Uri mUri; + + public NotificationRunnable(boolean selfChange, Uri uri) { + mSelfChange = selfChange; + mUri = uri; + } + + @Override + public void run() { + ContentObserver.this.onChange(mSelfChange, mUri); + } + } + + private static final class Transport extends IContentObserver.Stub { + private ContentObserver mContentObserver; + + public Transport(ContentObserver contentObserver) { + mContentObserver = contentObserver; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + ContentObserver contentObserver = mContentObserver; + if (contentObserver != null) { + contentObserver.dispatchChange(selfChange, uri); + } + } + + public void releaseContentObserver() { + mContentObserver = null; } } } diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index a9a71cf..59ec89d 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -341,6 +341,7 @@ public interface Cursor { * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. * Inactive Cursors use fewer resources than active Cursors. * Calling {@link #requery} will make the cursor active again. + * @deprecated Since {@link #requery()} is deprecated, so too is this. */ void deactivate(); diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index aa0f61e..167278a 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -16,6 +16,7 @@ package android.database; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -78,9 +79,9 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } @Override - public void onChange(boolean selfChange) { + public void onChange(boolean selfChange, Uri uri) { try { - mRemote.onChange(selfChange); + mRemote.onChange(selfChange, uri); } catch (RemoteException ex) { // Do nothing, the far side is dead } diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java index 51c72c1..ca77a13 100644 --- a/core/java/android/database/DataSetObservable.java +++ b/core/java/android/database/DataSetObservable.java @@ -17,13 +17,15 @@ package android.database; /** - * A specialization of Observable for DataSetObserver that provides methods for - * invoking the various callback methods of DataSetObserver. + * A specialization of {@link Observable} for {@link DataSetObserver} + * that provides methods for sending notifications to a list of + * {@link DataSetObserver} objects. */ public class DataSetObservable extends Observable<DataSetObserver> { /** - * Invokes onChanged on each observer. Called when the data set being observed has - * changed, and which when read contains the new state of the data. + * Invokes {@link DataSetObserver#onChanged} on each observer. + * Called when the contents of the data set have changed. The recipient + * will obtain the new contents the next time it queries the data set. */ public void notifyChanged() { synchronized(mObservers) { @@ -38,8 +40,9 @@ public class DataSetObservable extends Observable<DataSetObserver> { } /** - * Invokes onInvalidated on each observer. Called when the data set being monitored - * has changed such that it is no longer valid. + * Invokes {@link DataSetObserver#onInvalidated} on each observer. + * Called when the data set is no longer valid and cannot be queried again, + * such as when the data set has been closed. */ public void notifyInvalidated() { synchronized (mObservers) { diff --git a/core/java/android/database/IContentObserver.aidl b/core/java/android/database/IContentObserver.aidl index ac2f975..13aff05 100755 --- a/core/java/android/database/IContentObserver.aidl +++ b/core/java/android/database/IContentObserver.aidl @@ -17,6 +17,8 @@ package android.database; +import android.net.Uri; + /** * @hide */ @@ -27,5 +29,5 @@ interface IContentObserver * observed. selfUpdate is true if the update was caused by a call to * commit on the cursor that is being observed. */ - oneway void onChange(boolean selfUpdate); + oneway void onChange(boolean selfUpdate, in Uri uri); } diff --git a/core/java/android/database/Observable.java b/core/java/android/database/Observable.java index b6fecab..aff32db 100644 --- a/core/java/android/database/Observable.java +++ b/core/java/android/database/Observable.java @@ -19,7 +19,12 @@ package android.database; import java.util.ArrayList; /** - * Provides methods for (un)registering arbitrary observers in an ArrayList. + * Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}. + * + * This abstract class is intended to be subclassed and specialized to maintain + * a registry of observers of specific types and dispatch notifications to them. + * + * @param T The observer type. */ public abstract class Observable<T> { /** @@ -66,13 +71,13 @@ public abstract class Observable<T> { mObservers.remove(index); } } - + /** - * Remove all registered observer + * Remove all registered observers. */ public void unregisterAll() { synchronized(mObservers) { mObservers.clear(); - } + } } } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index e45d66d..72f62fd 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -74,10 +74,17 @@ import java.util.regex.Pattern; * queues. * </p> * + * <h2>Reentrance</h2> + * <p> + * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + * </p> + * * @hide */ public final class SQLiteConnection { private static final String TAG = "SQLiteConnection"; + private static final boolean DEBUG = false; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; @@ -187,8 +194,6 @@ public final class SQLiteConnection { } private void open() { - SQLiteGlobal.initializeOnce(); - mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, mConfiguration.label, SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); @@ -205,13 +210,13 @@ public final class SQLiteConnection { } if (mConnectionPtr != 0) { - mRecentOperations.beginOperation("close", null, null); + final int cookie = mRecentOperations.beginOperation("close", null, null); try { mPreparedStatementCache.evictAll(); nativeClose(mConnectionPtr); mConnectionPtr = 0; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } } @@ -304,9 +309,9 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("prepare", sql, null); + final int cookie = mRecentOperations.beginOperation("prepare", sql, null); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { if (outStatementInfo != null) { outStatementInfo.numParameters = statement.mNumParameters; @@ -328,10 +333,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -349,9 +354,9 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("execute", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -361,10 +366,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -384,9 +389,9 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("executeForLong", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -396,10 +401,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -419,9 +424,9 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("executeForString", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -431,10 +436,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -456,9 +461,10 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("executeForBlobFileDescriptor", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor", + sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -470,10 +476,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -493,9 +499,10 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("executeForChangedRowCount", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount", + sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -506,10 +513,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -529,9 +536,10 @@ public final class SQLiteConnection { throw new IllegalArgumentException("sql must not be null."); } - mRecentOperations.beginOperation("executeForLastInsertedRowId", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId", + sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -542,10 +550,10 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - mRecentOperations.endOperation(); + mRecentOperations.endOperation(cookie); } } @@ -581,9 +589,10 @@ public final class SQLiteConnection { int actualPos = -1; int countedRows = -1; int filledRows = -1; - mRecentOperations.beginOperation("executeForCursorWindow", sql, bindArgs); + final int cookie = mRecentOperations.beginOperation("executeForCursorWindow", + sql, bindArgs); try { - PreparedStatement statement = acquirePreparedStatement(sql); + final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); @@ -600,11 +609,11 @@ public final class SQLiteConnection { releasePreparedStatement(statement); } } catch (RuntimeException ex) { - mRecentOperations.failOperation(ex); + mRecentOperations.failOperation(cookie, ex); throw ex; } finally { - if (mRecentOperations.endOperationDeferLog()) { - mRecentOperations.logOperation("window='" + window + if (mRecentOperations.endOperationDeferLog(cookie)) { + mRecentOperations.logOperation(cookie, "window='" + window + "', startPos=" + startPos + ", actualPos=" + actualPos + ", filledRows=" + filledRows @@ -615,8 +624,15 @@ public final class SQLiteConnection { private PreparedStatement acquirePreparedStatement(String sql) { PreparedStatement statement = mPreparedStatementCache.get(sql); + boolean skipCache = false; if (statement != null) { - return statement; + if (!statement.mInUse) { + return statement; + } + // The statement is already in the cache but is in use (this statement appears + // to be not only re-entrant but recursive!). So prepare a new copy of the + // statement but do not cache it. + skipCache = true; } final int statementPtr = nativePrepareStatement(mConnectionPtr, sql); @@ -625,7 +641,7 @@ public final class SQLiteConnection { final int type = DatabaseUtils.getSqlStatementType(sql); final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly); - if (isCacheable(type)) { + if (!skipCache && isCacheable(type)) { mPreparedStatementCache.put(sql, statement); statement.mInCache = true; } @@ -637,31 +653,38 @@ public final class SQLiteConnection { } throw ex; } + statement.mInUse = true; return statement; } private void releasePreparedStatement(PreparedStatement statement) { + statement.mInUse = false; if (statement.mInCache) { try { nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr); } catch (SQLiteException ex) { - // The statement could not be reset due to an error. - // The entryRemoved() callback for the cache will recursively call - // releasePreparedStatement() again, but this time mInCache will be false - // so the statement will be finalized and recycled. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "Could not reset prepared statement due to an exception. " + // The statement could not be reset due to an error. Remove it from the cache. + // When remove() is called, the cache will invoke its entryRemoved() callback, + // which will in turn call finalizePreparedStatement() to finalize and + // recycle the statement. + if (DEBUG) { + Log.d(TAG, "Could not reset prepared statement due to an exception. " + "Removing it from the cache. SQL: " + trimSqlForDisplay(statement.mSql), ex); } + mPreparedStatementCache.remove(statement.mSql); } } else { - nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); - recyclePreparedStatement(statement); + finalizePreparedStatement(statement); } } + private void finalizePreparedStatement(PreparedStatement statement) { + nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); + recyclePreparedStatement(statement); + } + private void bindArguments(PreparedStatement statement, Object[] bindArgs) { final int count = bindArgs != null ? bindArgs.length : 0; if (count != statement.mNumParameters) { @@ -735,9 +758,10 @@ public final class SQLiteConnection { * Dumps debugging information about this connection. * * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. */ - public void dump(Printer printer) { - dumpUnsafe(printer); + public void dump(Printer printer, boolean verbose) { + dumpUnsafe(printer, verbose); } /** @@ -752,15 +776,21 @@ public final class SQLiteConnection { * it should not crash. This is ok as it is only used for diagnostic purposes. * * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. */ - void dumpUnsafe(Printer printer) { + void dumpUnsafe(Printer printer, boolean verbose) { printer.println("Connection #" + mConnectionId + ":"); + if (verbose) { + printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr)); + } printer.println(" isPrimaryConnection: " + mIsPrimaryConnection); - printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr)); printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations); mRecentOperations.dump(printer); - mPreparedStatementCache.dump(printer); + + if (verbose) { + mPreparedStatementCache.dump(printer); + } } /** @@ -917,6 +947,12 @@ public final class SQLiteConnection { // True if the statement is in the cache. public boolean mInCache; + + // True if the statement is in use (currently executing). + // We need this flag because due to the use of custom functions in triggers, it's + // possible for SQLite calls to be re-entrant. Consequently we need to prevent + // in use statements from being finalized until they are no longer in use. + public boolean mInUse; } private final class PreparedStatementCache @@ -929,7 +965,9 @@ public final class SQLiteConnection { protected void entryRemoved(boolean evicted, String key, PreparedStatement oldValue, PreparedStatement newValue) { oldValue.mInCache = false; - releasePreparedStatement(oldValue); + if (!oldValue.mInUse) { + finalizePreparedStatement(oldValue); + } } public void dump(Printer printer) { @@ -957,12 +995,15 @@ public final class SQLiteConnection { } private static final class OperationLog { - private static final int MAX_RECENT_OPERATIONS = 10; + private static final int MAX_RECENT_OPERATIONS = 20; + private static final int COOKIE_GENERATION_SHIFT = 8; + private static final int COOKIE_INDEX_MASK = 0xff; private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; private int mIndex; + private int mGeneration; - public void beginOperation(String kind, String sql, Object[] bindArgs) { + public int beginOperation(String kind, String sql, Object[] bindArgs) { synchronized (mOperations) { final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS; Operation operation = mOperations[index]; @@ -995,47 +1036,54 @@ public final class SQLiteConnection { } } } + operation.mCookie = newOperationCookieLocked(index); mIndex = index; + return operation.mCookie; } } - public void failOperation(Exception ex) { + public void failOperation(int cookie, Exception ex) { synchronized (mOperations) { - final Operation operation = mOperations[mIndex]; - operation.mException = ex; + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mException = ex; + } } } - public boolean endOperationDeferLog() { + public void endOperation(int cookie) { synchronized (mOperations) { - return endOperationDeferLogLocked(); + if (endOperationDeferLogLocked(cookie)) { + logOperationLocked(cookie, null); + } } } - private boolean endOperationDeferLogLocked() { - final Operation operation = mOperations[mIndex]; - operation.mEndTime = System.currentTimeMillis(); - operation.mFinished = true; - return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( - operation.mEndTime - operation.mStartTime); + public boolean endOperationDeferLog(int cookie) { + synchronized (mOperations) { + return endOperationDeferLogLocked(cookie); + } } - public void endOperation() { + public void logOperation(int cookie, String detail) { synchronized (mOperations) { - if (endOperationDeferLogLocked()) { - logOperationLocked(null); - } + logOperationLocked(cookie, detail); } } - public void logOperation(String detail) { - synchronized (mOperations) { - logOperationLocked(detail); + private boolean endOperationDeferLogLocked(int cookie) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mEndTime = System.currentTimeMillis(); + operation.mFinished = true; + return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( + operation.mEndTime - operation.mStartTime); } + return false; } - private void logOperationLocked(String detail) { - final Operation operation = mOperations[mIndex]; + private void logOperationLocked(int cookie, String detail) { + final Operation operation = getOperationLocked(cookie); StringBuilder msg = new StringBuilder(); operation.describe(msg); if (detail != null) { @@ -1044,6 +1092,17 @@ public final class SQLiteConnection { Log.d(TAG, msg.toString()); } + private int newOperationCookieLocked(int index) { + final int generation = mGeneration++; + return generation << COOKIE_GENERATION_SHIFT | index; + } + + private Operation getOperationLocked(int cookie) { + final int index = cookie & COOKIE_INDEX_MASK; + final Operation operation = mOperations[index]; + return operation.mCookie == cookie ? operation : null; + } + public String describeCurrentOperation() { synchronized (mOperations) { final Operation operation = mOperations[mIndex]; @@ -1097,6 +1156,7 @@ public final class SQLiteConnection { public ArrayList<Object> mBindArgs; public boolean mFinished; public Exception mException; + public int mCookie; public void describe(StringBuilder msg) { msg.append(mKind); diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index b88bfee..5469213 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -833,8 +833,9 @@ public final class SQLiteConnectionPool implements Closeable { * Dumps debugging information about this connection pool. * * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. */ - public void dump(Printer printer) { + public void dump(Printer printer, boolean verbose) { Printer indentedPrinter = PrefixPrinter.create(printer, " "); synchronized (mLock) { printer.println("Connection pool for " + mConfiguration.path + ":"); @@ -843,7 +844,7 @@ public final class SQLiteConnectionPool implements Closeable { printer.println(" Available primary connection:"); if (mAvailablePrimaryConnection != null) { - mAvailablePrimaryConnection.dump(indentedPrinter); + mAvailablePrimaryConnection.dump(indentedPrinter, verbose); } else { indentedPrinter.println("<none>"); } @@ -852,7 +853,7 @@ public final class SQLiteConnectionPool implements Closeable { if (!mAvailableNonPrimaryConnections.isEmpty()) { final int count = mAvailableNonPrimaryConnections.size(); for (int i = 0; i < count; i++) { - mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter); + mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose); } } else { indentedPrinter.println("<none>"); @@ -863,7 +864,7 @@ public final class SQLiteConnectionPool implements Closeable { for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) { final SQLiteConnection connection = entry.getKey(); - connection.dumpUnsafe(indentedPrinter); + connection.dumpUnsafe(indentedPrinter, verbose); indentedPrinter.println(" Pending reconfiguration: " + entry.getValue()); } } else { diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 9dcb498..82bb23e 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -65,8 +65,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from - * {@code FROM} onward would be in the params argument. This constructor - * has package scope. + * {@code FROM} onward would be in the params argument. * * @param db a reference to a Database object that is already constructed * and opened. This param is not used any longer @@ -86,8 +85,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from - * {@code FROM} onward would be in the params argument. This constructor - * has package scope. + * {@code FROM} onward would be in the params argument. * * @param editTable the name of the table used for this query * @param query the {@link SQLiteQuery} object associated with this cursor object. @@ -269,7 +267,6 @@ public class SQLiteCursor extends AbstractWindowedCursor { mStackTrace); } close(); - SQLiteDebug.notifyActiveCursorFinalized(); } } finally { super.finalize(); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 377a680..9cb6480 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -1665,17 +1665,17 @@ public class SQLiteDatabase extends SQLiteClosable { * Dump detailed information about all open databases in the current process. * Used by bug report. */ - static void dumpAll(Printer printer) { + static void dumpAll(Printer printer, boolean verbose) { for (SQLiteDatabase db : getActiveDatabases()) { - db.dump(printer); + db.dump(printer, verbose); } } - private void dump(Printer printer) { + private void dump(Printer printer, boolean verbose) { synchronized (mLock) { if (mConnectionPoolLocked != null) { printer.println(""); - mConnectionPoolLocked.dump(printer); + mConnectionPoolLocked.dump(printer, verbose); } } } diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index d87c3e4..204483d 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -29,6 +29,8 @@ import android.util.Printer; * {@hide} */ public final class SQLiteDebug { + private static native void nativeGetPagerStats(PagerStats stats); + /** * Controls the printing of informational SQL log messages. */ @@ -49,31 +51,6 @@ public final class SQLiteDebug { Log.isLoggable("SQLiteTime", Log.VERBOSE); /** - * Controls the printing of compiled-sql-statement cache stats. - */ - public static final boolean DEBUG_SQL_CACHE = - Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE); - - /** - * Controls the stack trace reporting of active cursors being - * finalized. - */ - public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = - Log.isLoggable("SQLiteCursorClosing", Log.VERBOSE); - - /** - * Controls the tracking of time spent holding the database lock. - */ - public static final boolean DEBUG_LOCK_TIME_TRACKING = - Log.isLoggable("SQLiteLockTime", Log.VERBOSE); - - /** - * Controls the printing of stack traces when tracking the time spent holding the database lock. - */ - public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE = - Log.isLoggable("SQLiteLockStackTrace", Log.VERBOSE); - - /** * True to enable database performance testing instrumentation. * @hide */ @@ -98,30 +75,9 @@ public final class SQLiteDebug { /** * Contains statistics about the active pagers in the current process. * - * @see #getPagerStats(PagerStats) + * @see #nativeGetPagerStats(PagerStats) */ public static class PagerStats { - /** The total number of bytes in all pagers in the current process - * @deprecated not used any longer - */ - @Deprecated - public long totalBytes; - /** The number of bytes in referenced pages in all pagers in the current process - * @deprecated not used any longer - * */ - @Deprecated - public long referencedBytes; - /** The number of bytes in all database files opened in the current process - * @deprecated not used any longer - */ - @Deprecated - public long databaseBytes; - /** The number of pagers opened in the current process - * @deprecated not used any longer - */ - @Deprecated - public int numPagers; - /** the current amount of memory checked out by sqlite using sqlite3_malloc(). * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html */ @@ -134,7 +90,7 @@ public final class SQLiteDebug { * that overflowed because no space was left in the page cache. * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html */ - public int pageCacheOverflo; + public int pageCacheOverflow; /** records the largest memory allocation request handed to sqlite3. * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html @@ -182,7 +138,7 @@ public final class SQLiteDebug { */ public static PagerStats getDatabaseInfo() { PagerStats stats = new PagerStats(); - getPagerStats(stats); + nativeGetPagerStats(stats); stats.dbStats = SQLiteDatabase.getDbStats(); return stats; } @@ -190,52 +146,16 @@ public final class SQLiteDebug { /** * Dumps detailed information about all databases used by the process. * @param printer The printer for dumping database state. + * @param args Command-line arguments supplied to dumpsys dbinfo */ public static void dump(Printer printer, String[] args) { - SQLiteDatabase.dumpAll(printer); - } - - /** - * Gathers statistics about all pagers in the current process. - */ - public static native void getPagerStats(PagerStats stats); - - /** - * Returns the size of the SQLite heap. - * @return The size of the SQLite heap in bytes. - */ - public static native long getHeapSize(); - - /** - * Returns the amount of allocated memory in the SQLite heap. - * @return The allocated size in bytes. - */ - public static native long getHeapAllocatedSize(); - - /** - * Returns the amount of free memory in the SQLite heap. - * @return The freed size in bytes. - */ - public static native long getHeapFreeSize(); - - /** - * Determines the number of dirty belonging to the SQLite - * heap segments of this process. pages[0] returns the number of - * shared pages, pages[1] returns the number of private pages - */ - public static native void getHeapDirtyPages(int[] pages); - - private static int sNumActiveCursorsFinalized = 0; - - /** - * Returns the number of active cursors that have been finalized. This depends on the GC having - * run but is still useful for tests. - */ - public static int getNumActiveCursorsFinalized() { - return sNumActiveCursorsFinalized; - } + boolean verbose = false; + for (String arg : args) { + if (arg.equals("-v")) { + verbose = true; + } + } - static synchronized void notifyActiveCursorFinalized() { - sNumActiveCursorsFinalized++; + SQLiteDatabase.dumpAll(printer, verbose); } } diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java index 5e129be..dbefd63 100644 --- a/core/java/android/database/sqlite/SQLiteGlobal.java +++ b/core/java/android/database/sqlite/SQLiteGlobal.java @@ -22,57 +22,35 @@ import android.os.StatFs; * Provides access to SQLite functions that affect all database connection, * such as memory management. * + * The native code associated with SQLiteGlobal is also sets global configuration options + * using sqlite3_config() then calls sqlite3_initialize() to ensure that the SQLite + * library is properly initialized exactly once before any other framework or application + * code has a chance to run. + * + * Verbose SQLite logging is enabled if the "log.tag.SQLiteLog" property is set to "V". + * (per {@link SQLiteDebug#DEBUG_SQL_LOG}). + * * @hide */ public final class SQLiteGlobal { private static final String TAG = "SQLiteGlobal"; private static final Object sLock = new Object(); - private static boolean sInitialized; - private static int sSoftHeapLimit; private static int sDefaultPageSize; - private static native void nativeConfig(boolean verboseLog, int softHeapLimit); - private static native int nativeReleaseMemory(int bytesToFree); + private static native int nativeReleaseMemory(); private SQLiteGlobal() { } /** - * Initializes global SQLite settings the first time it is called. - * Should be called before opening the first (or any) database. - * Does nothing on repeated subsequent calls. - */ - public static void initializeOnce() { - synchronized (sLock) { - if (!sInitialized) { - sInitialized = true; - - // Limit to 8MB for now. This is 4 times the maximum cursor window - // size, as has been used by the original code in SQLiteDatabase for - // a long time. - // TODO: We really do need to test whether this helps or hurts us. - sSoftHeapLimit = 8 * 1024 * 1024; - - // Configure SQLite. - nativeConfig(SQLiteDebug.DEBUG_SQL_LOG, sSoftHeapLimit); - } - } - } - - /** * Attempts to release memory by pruning the SQLite page cache and other * internal data structures. * * @return The number of bytes that were freed. */ public static int releaseMemory() { - synchronized (sLock) { - if (!sInitialized) { - return 0; - } - return nativeReleaseMemory(sSoftHeapLimit); - } + return nativeReleaseMemory(); } /** diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 31da7e4..46d9369 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -81,7 +81,8 @@ public abstract class SQLiteOpenHelper { * @param name of the database file, or null for an in-memory database * @param factory to use for creating cursor objects, or null for the default * @param version number of the database (starting at 1); if the database is older, - * {@link #onUpgrade} will be used to upgrade the database + * {@link #onUpgrade} will be used to upgrade the database; if the database is + * newer, {@link #onDowngrade} will be used to downgrade the database * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database * corruption. */ @@ -100,7 +101,7 @@ public abstract class SQLiteOpenHelper { } /** - * Return the name of the SQLite database being opened, as given tp + * Return the name of the SQLite database being opened, as given to * the constructor. */ public String getDatabaseName() { @@ -297,7 +298,7 @@ public abstract class SQLiteOpenHelper { public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); /** - * Called when the database needs to be downgraded. This is stricly similar to + * Called when the database needs to be downgraded. This is strictly similar to * onUpgrade() method, but is called whenever current version is newer than requested one. * However, this method is not abstract, so it is not mandatory for a customer to * implement it. If not overridden, default implementation will reject downgrade and diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java index 61fe45a..a933051 100644 --- a/core/java/android/database/sqlite/SQLiteSession.java +++ b/core/java/android/database/sqlite/SQLiteSession.java @@ -150,6 +150,12 @@ import android.os.ParcelFileDescriptor; * A query that works well on 100 rows may struggle with 10,000.</li> * </ul> * + * <h2>Reentrance</h2> + * <p> + * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + * </p> + * * TODO: Support timeouts on all possibly blocking operations. * * @hide @@ -159,6 +165,7 @@ public final class SQLiteSession { private SQLiteConnection mConnection; private int mConnectionFlags; + private int mConnectionUseCount; private Transaction mTransactionPool; private Transaction mTransactionStack; @@ -289,7 +296,9 @@ public final class SQLiteSession { private void beginTransactionUnchecked(int transactionMode, SQLiteTransactionListener transactionListener, int connectionFlags) { - acquireConnectionIfNoTransaction(null, connectionFlags); // might throw + if (mTransactionStack == null) { + acquireConnection(null, connectionFlags); // might throw + } try { // Set up the transaction such that we can back out safely // in case we fail part way. @@ -325,7 +334,9 @@ public final class SQLiteSession { transaction.mParent = mTransactionStack; mTransactionStack = transaction; } finally { - releaseConnectionIfNoTransaction(); // might throw + if (mTransactionStack == null) { + releaseConnection(); // might throw + } } } @@ -408,7 +419,7 @@ public final class SQLiteSession { mConnection.execute("ROLLBACK;", null); // might throw } } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -534,11 +545,11 @@ public final class SQLiteSession { throw new IllegalArgumentException("sql must not be null."); } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { mConnection.prepare(sql, outStatementInfo); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -562,11 +573,11 @@ public final class SQLiteSession { return; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { mConnection.execute(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -592,11 +603,11 @@ public final class SQLiteSession { return 0; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForLong(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -622,11 +633,11 @@ public final class SQLiteSession { return null; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForString(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -655,11 +666,11 @@ public final class SQLiteSession { return null; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForBlobFileDescriptor(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -685,11 +696,11 @@ public final class SQLiteSession { return 0; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForChangedRowCount(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -715,11 +726,11 @@ public final class SQLiteSession { return 0; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForLastInsertedRowId(sql, bindArgs); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -760,12 +771,12 @@ public final class SQLiteSession { return 0; } - acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags); // might throw try { return mConnection.executeForCursorWindow(sql, bindArgs, window, startPos, requiredPos, countAllRows); // might throw } finally { - releaseConnectionIfNoTransaction(); // might throw + releaseConnection(); // might throw } } @@ -807,16 +818,19 @@ public final class SQLiteSession { return false; } - private void acquireConnectionIfNoTransaction(String sql, int connectionFlags) { - if (mTransactionStack == null) { - assert mConnection == null; + private void acquireConnection(String sql, int connectionFlags) { + if (mConnection == null) { + assert mConnectionUseCount == 0; mConnection = mConnectionPool.acquireConnection(sql, connectionFlags); // might throw mConnectionFlags = connectionFlags; } + mConnectionUseCount += 1; } - private void releaseConnectionIfNoTransaction() { - if (mTransactionStack == null && mConnection != null) { + private void releaseConnection() { + assert mConnection != null; + assert mConnectionUseCount > 0; + if (--mConnectionUseCount == 0) { try { mConnectionPool.releaseConnection(mConnection); // might throw } finally { diff --git a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java b/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java deleted file mode 100644 index bcf95e2..0000000 --- a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java +++ /dev/null @@ -1,31 +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.database.sqlite; - -/** - * Thrown if the database can't be closed because of some un-closed - * Cursor or SQLiteStatement objects. Could happen when a thread is trying to close - * the database while another thread still hasn't closed a Cursor on that database. - * @hide - */ -public class SQLiteUnfinalizedObjectsException extends SQLiteException { - public SQLiteUnfinalizedObjectsException() {} - - public SQLiteUnfinalizedObjectsException(String error) { - super(error); - } -} diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 10c1195..23ae21b 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -100,6 +100,9 @@ public class ExtractEditText extends EditText { @Override public boolean onTextContextMenuItem(int id) { if (mIME != null && mIME.onExtractTextContextMenuItem(id)) { + // Mode was started on Extracted, needs to be stopped here. + // Cut and paste will change the text, which stops selection mode. + if (id == android.R.id.copy) stopSelectionActionMode(); return true; } return super.onTextContextMenuItem(id); diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index e8f60b4..7a1ef66 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -102,6 +102,15 @@ public class NetworkStats implements Parcelable { this.operations = operations; } + public boolean isNegative() { + return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0; + } + + public boolean isEmpty() { + return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0 + && operations == 0; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -343,6 +352,7 @@ public class NetworkStats implements Parcelable { * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface}, * since operation counts are at data layer. */ + @Deprecated public void spliceOperationsFrom(NetworkStats stats) { for (int i = 0; i < size; i++) { final int j = stats.findIndex(IFACE_ALL, uid[i], set[i], tag[i]); @@ -397,7 +407,7 @@ public class NetworkStats implements Parcelable { * Return total of all fields represented by this snapshot object. */ public Entry getTotal(Entry recycle) { - return getTotal(recycle, null, UID_ALL); + return getTotal(recycle, null, UID_ALL, false); } /** @@ -405,7 +415,7 @@ public class NetworkStats implements Parcelable { * the requested {@link #uid}. */ public Entry getTotal(Entry recycle, int limitUid) { - return getTotal(recycle, null, limitUid); + return getTotal(recycle, null, limitUid, false); } /** @@ -413,7 +423,11 @@ public class NetworkStats implements Parcelable { * the requested {@link #iface}. */ public Entry getTotal(Entry recycle, HashSet<String> limitIface) { - return getTotal(recycle, limitIface, UID_ALL); + return getTotal(recycle, limitIface, UID_ALL, false); + } + + public Entry getTotalIncludingTags(Entry recycle) { + return getTotal(recycle, null, UID_ALL, true); } /** @@ -423,7 +437,8 @@ public class NetworkStats implements Parcelable { * @param limitIface Set of {@link #iface} to include in total; or {@code * null} to include all ifaces. */ - private Entry getTotal(Entry recycle, HashSet<String> limitIface, int limitUid) { + private Entry getTotal( + Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) { final Entry entry = recycle != null ? recycle : new Entry(); entry.iface = IFACE_ALL; @@ -442,7 +457,7 @@ public class NetworkStats implements Parcelable { if (matchesUid && matchesIface) { // skip specific tags, since already counted in TAG_NONE - if (tag[i] != TAG_NONE) continue; + if (tag[i] != TAG_NONE && !includeTags) continue; entry.rxBytes += rxBytes[i]; entry.rxPackets += rxPackets[i]; @@ -460,7 +475,7 @@ public class NetworkStats implements Parcelable { * time, and that none of them have disappeared. */ public NetworkStats subtract(NetworkStats right) { - return subtract(this, right, null); + return subtract(this, right, null, null); } /** @@ -471,12 +486,12 @@ public class NetworkStats implements Parcelable { * If counters have rolled backwards, they are clamped to {@code 0} and * reported to the given {@link NonMonotonicObserver}. */ - public static NetworkStats subtract( - NetworkStats left, NetworkStats right, NonMonotonicObserver observer) { + public static <C> NetworkStats subtract( + NetworkStats left, NetworkStats right, NonMonotonicObserver<C> observer, C cookie) { long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime; if (deltaRealtime < 0) { if (observer != null) { - observer.foundNonMonotonic(left, -1, right, -1); + observer.foundNonMonotonic(left, -1, right, -1, cookie); } deltaRealtime = 0; } @@ -510,7 +525,7 @@ public class NetworkStats implements Parcelable { if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0 || entry.operations < 0) { if (observer != null) { - observer.foundNonMonotonic(left, i, right, j); + observer.foundNonMonotonic(left, i, right, j, cookie); } entry.rxBytes = Math.max(entry.rxBytes, 0); entry.rxPackets = Math.max(entry.rxPackets, 0); @@ -663,8 +678,8 @@ public class NetworkStats implements Parcelable { } }; - public interface NonMonotonicObserver { + public interface NonMonotonicObserver<C> { public void foundNonMonotonic( - NetworkStats left, int leftIndex, NetworkStats right, int rightIndex); + NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie); } } diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java index 8c01331..faf8a3f 100644 --- a/core/java/android/net/NetworkStatsHistory.java +++ b/core/java/android/net/NetworkStatsHistory.java @@ -26,16 +26,18 @@ import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray; import static android.net.NetworkStatsHistory.Entry.UNKNOWN; import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray; import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray; +import static com.android.internal.util.ArrayUtils.total; import android.os.Parcel; import android.os.Parcelable; import android.util.MathUtils; +import com.android.internal.util.IndentingPrintWriter; + import java.io.CharArrayWriter; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.io.PrintWriter; import java.net.ProtocolException; import java.util.Arrays; import java.util.Random; @@ -74,6 +76,7 @@ public class NetworkStatsHistory implements Parcelable { private long[] txBytes; private long[] txPackets; private long[] operations; + private long totalBytes; public static class Entry { public static final long UNKNOWN = -1; @@ -106,6 +109,12 @@ public class NetworkStatsHistory implements Parcelable { if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize]; if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize]; bucketCount = 0; + totalBytes = 0; + } + + public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) { + this(bucketDuration, existing.estimateResizeBuckets(bucketDuration)); + recordEntireHistory(existing); } public NetworkStatsHistory(Parcel in) { @@ -118,6 +127,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = readLongArray(in); operations = readLongArray(in); bucketCount = bucketStart.length; + totalBytes = in.readLong(); } /** {@inheritDoc} */ @@ -130,6 +140,7 @@ public class NetworkStatsHistory implements Parcelable { writeLongArray(out, txBytes, bucketCount); writeLongArray(out, txPackets, bucketCount); writeLongArray(out, operations, bucketCount); + out.writeLong(totalBytes); } public NetworkStatsHistory(DataInputStream in) throws IOException { @@ -144,6 +155,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = new long[bucketStart.length]; operations = new long[bucketStart.length]; bucketCount = bucketStart.length; + totalBytes = total(rxBytes) + total(txBytes); break; } case VERSION_ADD_PACKETS: @@ -158,6 +170,7 @@ public class NetworkStatsHistory implements Parcelable { txPackets = readVarLongArray(in); operations = readVarLongArray(in); bucketCount = bucketStart.length; + totalBytes = total(rxBytes) + total(txBytes); break; } default: { @@ -208,6 +221,13 @@ public class NetworkStatsHistory implements Parcelable { } /** + * Return total bytes represented by this history. + */ + public long getTotalBytes() { + return totalBytes; + } + + /** * Return index of bucket that contains or is immediately before the * requested time. */ @@ -266,13 +286,16 @@ public class NetworkStatsHistory implements Parcelable { * distribute across internal buckets, creating new buckets as needed. */ public void recordData(long start, long end, NetworkStats.Entry entry) { - if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0 - || entry.operations < 0) { + long rxBytes = entry.rxBytes; + long rxPackets = entry.rxPackets; + long txBytes = entry.txBytes; + long txPackets = entry.txPackets; + long operations = entry.operations; + + if (entry.isNegative()) { throw new IllegalArgumentException("tried recording negative data"); } - if (entry.rxBytes == 0 && entry.rxPackets == 0 && entry.txBytes == 0 && entry.txPackets == 0 - && entry.operations == 0) { - // nothing to record; skip + if (entry.isEmpty()) { return; } @@ -295,21 +318,23 @@ public class NetworkStatsHistory implements Parcelable { if (overlap <= 0) continue; // integer math each time is faster than floating point - final long fracRxBytes = entry.rxBytes * overlap / duration; - final long fracRxPackets = entry.rxPackets * overlap / duration; - final long fracTxBytes = entry.txBytes * overlap / duration; - final long fracTxPackets = entry.txPackets * overlap / duration; - final long fracOperations = entry.operations * overlap / duration; + final long fracRxBytes = rxBytes * overlap / duration; + final long fracRxPackets = rxPackets * overlap / duration; + final long fracTxBytes = txBytes * overlap / duration; + final long fracTxPackets = txPackets * overlap / duration; + final long fracOperations = operations * overlap / duration; addLong(activeTime, i, overlap); - addLong(rxBytes, i, fracRxBytes); entry.rxBytes -= fracRxBytes; - addLong(rxPackets, i, fracRxPackets); entry.rxPackets -= fracRxPackets; - addLong(txBytes, i, fracTxBytes); entry.txBytes -= fracTxBytes; - addLong(txPackets, i, fracTxPackets); entry.txPackets -= fracTxPackets; - addLong(operations, i, fracOperations); entry.operations -= fracOperations; + addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes; + addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets; + addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes; + addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets; + addLong(this.operations, i, fracOperations); operations -= fracOperations; duration -= overlap; } + + totalBytes += entry.rxBytes + entry.txBytes; } /** @@ -394,6 +419,7 @@ public class NetworkStatsHistory implements Parcelable { /** * Remove buckets older than requested cutoff. */ + @Deprecated public void removeBucketsBefore(long cutoff) { int i; for (i = 0; i < bucketCount; i++) { @@ -415,6 +441,8 @@ public class NetworkStatsHistory implements Parcelable { if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length); if (operations != null) operations = Arrays.copyOfRange(operations, i, length); bucketCount -= i; + + // TODO: subtract removed values from totalBytes } } @@ -527,19 +555,17 @@ public class NetworkStatsHistory implements Parcelable { return (long) (start + (r.nextFloat() * (end - start))); } - public void dump(String prefix, PrintWriter pw, boolean fullHistory) { - pw.print(prefix); + public void dump(IndentingPrintWriter pw, boolean fullHistory) { pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration); + pw.increaseIndent(); final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32); if (start > 0) { - pw.print(prefix); - pw.print(" (omitting "); pw.print(start); pw.println(" buckets)"); + pw.print("(omitting "); pw.print(start); pw.println(" buckets)"); } for (int i = start; i < bucketCount; i++) { - pw.print(prefix); - pw.print(" bucketStart="); pw.print(bucketStart[i]); + pw.print("bucketStart="); pw.print(bucketStart[i]); if (activeTime != null) { pw.print(" activeTime="); pw.print(activeTime[i]); } if (rxBytes != null) { pw.print(" rxBytes="); pw.print(rxBytes[i]); } if (rxPackets != null) { pw.print(" rxPackets="); pw.print(rxPackets[i]); } @@ -548,12 +574,14 @@ public class NetworkStatsHistory implements Parcelable { if (operations != null) { pw.print(" operations="); pw.print(operations[i]); } pw.println(); } + + pw.decreaseIndent(); } @Override public String toString() { final CharArrayWriter writer = new CharArrayWriter(); - dump("", new PrintWriter(writer), false); + dump(new IndentingPrintWriter(writer, " "), false); return writer.toString(); } @@ -579,6 +607,10 @@ public class NetworkStatsHistory implements Parcelable { if (array != null) array[i] += value; } + public int estimateResizeBuckets(long newBucketDuration) { + return (int) (size() * getBucketDuration() / newBucketDuration); + } + /** * Utility methods for interacting with {@link DataInputStream} and * {@link DataOutputStream}, mostly dealing with writing partial arrays. diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 418b82f..8ebfd8d 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -18,6 +18,7 @@ package android.net; import static android.net.ConnectivityManager.TYPE_ETHERNET; import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; import static android.net.ConnectivityManager.TYPE_WIMAX; import static android.net.NetworkIdentity.scrubSubscriberId; import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G; @@ -231,10 +232,13 @@ public class NetworkTemplate implements Parcelable { * Check if matches Wi-Fi network template. */ private boolean matchesWifi(NetworkIdentity ident) { - if (ident.mType == TYPE_WIFI) { - return true; + switch (ident.mType) { + case TYPE_WIFI: + case TYPE_WIFI_P2P: + return true; + default: + return false; } - return false; } /** diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index 8bdb669..dfdea38 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -195,7 +195,7 @@ public class TrafficStats { // subtract starting values and return delta final NetworkStats profilingStop = getDataLayerSnapshotForUid(context); final NetworkStats profilingDelta = NetworkStats.subtract( - profilingStop, sActiveProfilingStart, null); + profilingStop, sActiveProfilingStart, null, null); sActiveProfilingStart = null; return profilingDelta; } diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 0fb49bc..defe7aa 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.RandomAccess; import java.util.Set; import libcore.net.UriCodec; @@ -1716,6 +1717,38 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { return (!"false".equals(flag) && !"0".equals(flag)); } + /** + * Return a normalized representation of this Uri. + * + * <p>A normalized Uri has a lowercase scheme component. + * This aligns the Uri with Android best practices for + * intent filtering. + * + * <p>For example, "HTTP://www.android.com" becomes + * "http://www.android.com" + * + * <p>All URIs received from outside Android (such as user input, + * or external sources like Bluetooth, NFC, or the Internet) should + * be normalized before they are used to create an Intent. + * + * <p class="note">This method does <em>not</em> validate bad URI's, + * or 'fix' poorly formatted URI's - so do not use it for input validation. + * A Uri will always be returned, even if the Uri is badly formatted to + * begin with and a scheme component cannot be found. + * + * @return normalized Uri (never null) + * @see {@link android.content.Intent#setData} + * @see {@link #setNormalizedData} + */ + public Uri normalize() { + String scheme = getScheme(); + if (scheme == null) return this; // give up + String lowerScheme = scheme.toLowerCase(Locale.US); + if (scheme.equals(lowerScheme)) return this; // no change + + return buildUpon().scheme(lowerScheme).build(); + } + /** Identifies a null parcelled Uri. */ private static final int NULL_TYPE_ID = 0; diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 0b93ad0..d2afbb9 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -17,7 +17,6 @@ package android.nfc; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.IntentFilter; import android.nfc.NdefMessage; import android.nfc.Tag; @@ -44,4 +43,6 @@ interface INfcAdapter void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); void setForegroundNdefPush(in NdefMessage msg, in INdefPushCallback callback); + + void dispatch(in Tag tag, in NdefMessage message); } diff --git a/core/java/android/nfc/LlcpPacket.aidl b/core/java/android/nfc/LlcpPacket.aidl deleted file mode 100644 index 80f424d..0000000 --- a/core/java/android/nfc/LlcpPacket.aidl +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.nfc; - -/** - * @hide - */ -parcelable LlcpPacket;
\ No newline at end of file diff --git a/core/java/android/nfc/LlcpPacket.java b/core/java/android/nfc/LlcpPacket.java deleted file mode 100644 index 9919dc4..0000000 --- a/core/java/android/nfc/LlcpPacket.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.nfc; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Represents a LLCP packet received in a LLCP Connectionless communication; - * @hide - */ -public class LlcpPacket implements Parcelable { - - private final int mRemoteSap; - - private final byte[] mDataBuffer; - - /** - * Creates a LlcpPacket to be sent to a remote Service Access Point number - * (SAP) - * - * @param sap Remote Service Access Point number - * @param data Data buffer - */ - public LlcpPacket(int sap, byte[] data) { - mRemoteSap = sap; - mDataBuffer = data; - } - - /** - * Returns the remote Service Access Point number - */ - public int getRemoteSap() { - return mRemoteSap; - } - - /** - * Returns the data buffer - */ - public byte[] getDataBuffer() { - return mDataBuffer; - } - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mRemoteSap); - dest.writeInt(mDataBuffer.length); - dest.writeByteArray(mDataBuffer); - } - - public static final Parcelable.Creator<LlcpPacket> CREATOR = new Parcelable.Creator<LlcpPacket>() { - public LlcpPacket createFromParcel(Parcel in) { - // Remote SAP - short sap = (short)in.readInt(); - - // Data Buffer - int dataLength = in.readInt(); - byte[] data = new byte[dataLength]; - in.readByteArray(data); - - return new LlcpPacket(sap, data); - } - - public LlcpPacket[] newArray(int size) { - return new LlcpPacket[size]; - } - }; -}
\ No newline at end of file diff --git a/core/java/android/nfc/NdefMessage.java b/core/java/android/nfc/NdefMessage.java index 38bc16d..c83144f 100644 --- a/core/java/android/nfc/NdefMessage.java +++ b/core/java/android/nfc/NdefMessage.java @@ -92,9 +92,7 @@ public final class NdefMessage implements Parcelable { * @throws FormatException if the data cannot be parsed */ public NdefMessage(byte[] data) throws FormatException { - if (data == null) { - throw new NullPointerException("null data"); - } + if (data == null) throw new NullPointerException("data is null"); ByteBuffer buffer = ByteBuffer.wrap(data); mRecords = NdefRecord.parse(buffer, false); @@ -112,9 +110,8 @@ public final class NdefMessage implements Parcelable { */ public NdefMessage(NdefRecord record, NdefRecord ... records) { // validate - if (record == null) { - throw new NullPointerException("record cannot be null"); - } + if (record == null) throw new NullPointerException("record cannot be null"); + for (NdefRecord r : records) { if (r == null) { throw new NullPointerException("record cannot be null"); @@ -147,7 +144,12 @@ public final class NdefMessage implements Parcelable { /** * Get the NDEF Records inside this NDEF Message.<p> - * An NDEF Message always has one or more NDEF Records. + * An {@link NdefMessage} always has one or more NDEF Records: so the + * following code to retrieve the first record is always safe + * (no need to check for null or array length >= 1): + * <pre> + * NdefRecord firstRecord = ndefMessage.getRecords()[0]; + * </pre> * * @return array of one or more NDEF records. */ diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index b4c488b..0e9e8f4 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -16,6 +16,7 @@ package android.nfc; +import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; @@ -25,6 +26,7 @@ import java.nio.charset.Charsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** * Represents an immutable NDEF Record. @@ -305,9 +307,9 @@ public final class NdefRecord implements Parcelable { * @return Android application NDEF record */ public static NdefRecord createApplicationRecord(String packageName) { - if (packageName.length() == 0) { - throw new IllegalArgumentException("empty package name"); - } + if (packageName == null) throw new NullPointerException("packageName is null"); + if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty"); + return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, packageName.getBytes(Charsets.UTF_8)); } @@ -318,32 +320,27 @@ public final class NdefRecord implements Parcelable { * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} * and {@link #RTD_URI}. This is the most efficient encoding * of a URI into NDEF.<p> + * The uri parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uri + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * * Reference specification: NFCForum-TS-RTD_URI_1.0 * * @param uri URI to encode. * @return an NDEF Record containing the URI - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if the uri is empty or invalid */ public static NdefRecord createUri(Uri uri) { - return createUri(uri.toString()); - } + if (uri == null) throw new NullPointerException("uri is null"); - /** - * Create a new NDEF Record containing a URI.<p> - * Use this method to encode a URI (or URL) into an NDEF Record.<p> - * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} - * and {@link #RTD_URI}. This is the most efficient encoding - * of a URI into NDEF.<p> - * Reference specification: NFCForum-TS-RTD_URI_1.0 - * - * @param uriString string URI to encode. - * @return an NDEF Record containing the URI - * @throws IllegalArugmentException if a valid record cannot be created - */ - public static NdefRecord createUri(String uriString) { - if (uriString.length() == 0) { - throw new IllegalArgumentException("empty uriString"); - } + uri = uri.normalize(); + String uriString = uri.toString(); + if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty"); byte prefix = 0; for (int i = 1; i < URI_PREFIX_MAP.length; i++) { @@ -361,28 +358,72 @@ public final class NdefRecord implements Parcelable { } /** + * Create a new NDEF Record containing a URI.<p> + * Use this method to encode a URI (or URL) into an NDEF Record.<p> + * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.<p> + * The uriString parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uriString + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uriString string URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uriString is empty or invalid + */ + public static NdefRecord createUri(String uriString) { + return createUri(Uri.parse(uriString)); + } + + /** * Create a new NDEF Record containing MIME data.<p> * Use this method to encode MIME-typed data into an NDEF Record, * such as "text/plain", or "image/jpeg".<p> - * Expects US-ASCII characters in mimeType. The encoding of the - * mimeData depends on the mimeType.<p> + * The mimeType parameter will be normalized with + * {@link Intent#normalizeMimeType} to follow Android best + * practices for intent filtering, for example to force lower-case. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown + * if the mimeType parameter has serious problems, + * for example if it is empty, so always catch this + * exception if you are passing user-generated data into this method. + * <p> * For efficiency, This method might not make an internal copy of the * mimeData byte array, so take care not - * to re-use the mimeData byte array while still using the returned + * to modify the mimeData byte array while still using the returned * NdefRecord. * - * @param mimeType MIME type, expects US-ASCII characters only + * @param mimeType a valid MIME type * @param mimeData MIME data as bytes * @return an NDEF Record containing the MIME-typed data - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if the mimeType is empty or invalid + * */ public static NdefRecord createMime(String mimeType, byte[] mimeData) { - if (mimeType.length() == 0) { - throw new IllegalArgumentException("empty mimeType"); + if (mimeType == null) throw new NullPointerException("mimeType is null"); + + // We only do basic MIME type validation: trying to follow the + // RFCs strictly only ends in tears, since there are lots of MIME + // types in common use that are not strictly valid as per RFC rules + mimeType = Intent.normalizeMimeType(mimeType); + if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); + int slashIndex = mimeType.indexOf('/'); + if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); + if (slashIndex == mimeType.length() - 1) { + throw new IllegalArgumentException("mimeType must have minor type"); } + // missing '/' is allowed - return new NdefRecord(TNF_MIME_MEDIA, mimeType.getBytes(Charsets.US_ASCII), null, - mimeData); + // MIME RFCs suggest ASCII encoding for content-type + byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII); + return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData); } /** @@ -391,32 +432,38 @@ public final class NdefRecord implements Parcelable { * The data is typed by a domain name (usually your Android package name) and * a domain-specific type. This data is packaged into a "NFC Forum External * Type" NDEF Record.<p> - * Both the domain and type used to construct an external record are case - * insensitive, and this implementation will encode all characters to lower - * case. Only a subset of ASCII characters are allowed for the domain - * and type. There are no restrictions on the payload data.<p> + * NFC Forum requires that the domain and type used in an external record + * are treated as case insensitive, however Android intent filtering is + * always case sensitive. So this method will force the domain and type to + * lower-case before creating the NDEF Record.<p> + * The unchecked exception {@link IllegalArgumentException} will be thrown + * if the domain and type have serious problems, for example if either field + * is empty, so always catch this + * exception if you are passing user-generated data into this method.<p> + * There are no such restrictions on the payload data.<p> * For efficiency, This method might not make an internal copy of the * data byte array, so take care not - * to re-use the data byte array while still using the returned + * to modify the data byte array while still using the returned * NdefRecord. * * Reference specification: NFCForum-TS-RTD_1.0 * @param domain domain-name of issuing organization * @param type domain-specific type of data * @param data payload as bytes - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if either domain or type are empty or invalid */ public static NdefRecord createExternal(String domain, String type, byte[] data) { - if (domain.length() == 0 || type.length() == 0) { - throw new IllegalArgumentException("empty domain or type"); - } - byte[] byteDomain = domain.getBytes(Charsets.US_ASCII); - ensureValidDomain(byteDomain); - toLowerCase(byteDomain); - byte[] byteType = type.getBytes(Charsets.US_ASCII); - ensureValidWkt(byteType); - toLowerCase(byteType); + if (domain == null) throw new NullPointerException("domain is null"); + if (type == null) throw new NullPointerException("type is null"); + + domain = domain.trim().toLowerCase(Locale.US); + type = type.trim().toLowerCase(Locale.US); + + if (domain.length() == 0) throw new IllegalArgumentException("domain is empty"); + if (type.length() == 0) throw new IllegalArgumentException("type is empty"); + byte[] byteDomain = domain.getBytes(Charsets.UTF_8); + byte[] byteType = type.getBytes(Charsets.UTF_8); byte[] b = new byte[byteDomain.length + 1 + byteType.length]; System.arraycopy(byteDomain, 0, b, 0, byteDomain.length); b[byteDomain.length] = ':'; @@ -574,51 +621,113 @@ public final class NdefRecord implements Parcelable { } /** - * Helper to return the NdefRecord as a URI. - * TODO: Consider making a member method instead of static - * TODO: Consider more validation that this is a URI record - * TODO: Make a public API - * @hide + * Map this record to a MIME type, or return null if it cannot be mapped.<p> + * Currently this method considers all {@link #TNF_MIME_MEDIA} records to + * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as + * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string + * is returned, otherwise null is returned.<p> + * This method does not perform validation that the MIME type is + * actually valid. It always attempts to + * return a string containing the type if this is a MIME record.<p> + * The returned MIME type will by normalized to lower-case using + * {@link Intent#normalizeMimeType}.<p> + * The MIME payload can be obtained using {@link #getPayload}. + * + * @return MIME type as a string, or null if this is not a MIME record */ - public static Uri parseWellKnownUriRecord(NdefRecord record) throws FormatException { - byte[] payload = record.getPayload(); - if (payload.length < 2) { - throw new FormatException("Payload is not a valid URI (missing prefix)"); + public String toMimeType() { + switch (mTnf) { + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) { + return "text/plain"; + } + break; + case NdefRecord.TNF_MIME_MEDIA: + String mimeType = new String(mType, Charsets.US_ASCII); + return Intent.normalizeMimeType(mimeType); } + return null; + } - /* - * payload[0] contains the URI Identifier Code, per the - * NFC Forum "URI Record Type Definition" section 3.2.2. - * - * payload[1]...payload[payload.length - 1] contains the rest of - * the URI. - */ - int prefixIndex = (payload[0] & 0xff); - if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { - throw new FormatException("Payload is not a valid URI (invalid prefix)"); + /** + * Map this record to a URI, or return null if it cannot be mapped.<p> + * Currently this method considers the following to be URI records: + * <ul> + * <li>{@link #TNF_ABSOLUTE_URI} records.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER} + * and containing a URI record in the NDEF message nested in the payload. + * </li> + * <li>{@link #TNF_EXTERNAL_TYPE} records.</li> + * </ul> + * If this is not a URI record by the above rules, then null is returned.<p> + * This method does not perform validation that the URI is + * actually valid: it always attempts to create and return a URI if + * this record appears to be a URI record by the above rules.<p> + * The returned URI will be normalized to have a lower case scheme + * using {@link Uri#normalize}.<p> + * + * @return URI, or null if this is not a URI record + */ + public Uri toUri() { + return toUri(false); + } + + private Uri toUri(boolean inSmartPoster) { + switch (mTnf) { + case TNF_WELL_KNOWN: + if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) { + try { + // check payload for a nested NDEF Message containing a URI + NdefMessage nestedMessage = new NdefMessage(mPayload); + for (NdefRecord nestedRecord : nestedMessage.getRecords()) { + Uri uri = nestedRecord.toUri(true); + if (uri != null) { + return uri; + } + } + } catch (FormatException e) { } + } else if (Arrays.equals(mType, RTD_URI)) { + return parseWktUri().normalize(); + } + break; + + case TNF_ABSOLUTE_URI: + Uri uri = Uri.parse(new String(mType, Charsets.UTF_8)); + return uri.normalize(); + + case TNF_EXTERNAL_TYPE: + if (inSmartPoster) { + break; + } + return Uri.parse("vnd.android.nfc://ext/" + new String(mType, Charsets.US_ASCII)); } - String prefix = URI_PREFIX_MAP[prefixIndex]; - byte[] fullUri = concat(prefix.getBytes(Charsets.UTF_8), - Arrays.copyOfRange(payload, 1, payload.length)); - return Uri.parse(new String(fullUri, Charsets.UTF_8)); + return null; } - private static byte[] concat(byte[]... arrays) { - int length = 0; - for (byte[] array : arrays) { - length += array.length; + /** + * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records. + * @return complete URI, or null if invalid + */ + private Uri parseWktUri() { + if (mPayload.length < 2) { + return null; } - byte[] result = new byte[length]; - int pos = 0; - for (byte[] array : arrays) { - System.arraycopy(array, 0, result, pos, array.length); - pos += array.length; + + // payload[0] contains the URI Identifier Code, as per + // NFC Forum "URI Record Type Definition" section 3.2.2. + int prefixIndex = (mPayload[0] & (byte)0xFF); + if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { + return null; } - return result; + String prefix = URI_PREFIX_MAP[prefixIndex]; + String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length), + Charsets.UTF_8); + return Uri.parse(prefix + suffix); } /** - * Main parsing method.<p> + * Main record parsing method.<p> * Expects NdefMessage to begin immediately, allows trailing data.<p> * Currently has strict validation of all fields as per NDEF 1.0 * specification section 2.5. We will attempt to keep this as strict as @@ -902,42 +1011,4 @@ public final class NdefRecord implements Parcelable { } return s; } - - /** Ensure valid 'DNS-char' as per RFC2234 */ - private static void ensureValidDomain(byte[] bs) { - for (int i = 0; i < bs.length; i++) { - byte b = bs[i]; - if ((b >= 'A' && b <= 'Z') || - (b >= 'a' && b <= 'z') || - (b >= '0' && b <= '9') || - b == '.' || b == '-') { - continue; - } - throw new IllegalArgumentException("invalid character in domain"); - } - } - - /** Ensure valid 'WKT-char' as per RFC2234 */ - private static void ensureValidWkt(byte[] bs) { - for (int i = 0; i < bs.length; i++) { - byte b = bs[i]; - if ((b >= 'A' && b <= 'Z') || - (b >= 'a' && b <= 'z') || - (b >= '0' && b <= '9') || - b == '(' || b == ')' || b == '+' || b == ',' || b == '-' || - b == ':' || b == '=' || b == '@' || b == ';' || b == '$' || - b == '_' || b == '!' || b == '*' || b == '\'' || b == '.') { - continue; - } - throw new IllegalArgumentException("invalid character in type"); - } - } - - private static void toLowerCase(byte[] b) { - for (int i = 0; i < b.length; i++) { - if (b[i] >= 'A' && b[i] <= 'Z') { - b[i] += 0x20; - } - } - } } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 53a0341..224a8bc 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -66,6 +66,9 @@ public final class NfcAdapter { * <p>If the tag has an NDEF payload this intent is started before * {@link #ACTION_TECH_DISCOVERED}. If any activities respond to this intent neither * {@link #ACTION_TECH_DISCOVERED} or {@link #ACTION_TAG_DISCOVERED} will be started. + * + * <p>The MIME type or data URI of this intent are normalized before dispatch - + * so that MIME, URI scheme and URI host are always lower-case. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; @@ -151,9 +154,13 @@ public final class NfcAdapter { public static final String EXTRA_TAG = "android.nfc.extra.TAG"; /** - * Optional extra containing an array of {@link NdefMessage} present on the discovered tag for - * the {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and - * {@link #ACTION_TAG_DISCOVERED} intents. + * Extra containing an array of {@link NdefMessage} present on the discovered tag.<p> + * This extra is mandatory for {@link #ACTION_NDEF_DISCOVERED} intents, + * and optional for {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents.<p> + * When this extra is present there will always be at least one + * {@link NdefMessage} element. Most NDEF tags have only one NDEF message, + * but we use an array for future compatibility. */ public static final String EXTRA_NDEF_MESSAGES = "android.nfc.extra.NDEF_MESSAGES"; @@ -386,10 +393,10 @@ public final class NfcAdapter { */ @Deprecated public static NfcAdapter getDefaultAdapter() { - // introduce in API version 9 (GB 2.3) + // introduced in API version 9 (GB 2.3) // deprecated in API version 10 (GB 2.3.3) // removed from public API in version 16 (ICS MR2) - // will need to maintain this as a hidden API for a while longer... + // should maintain as a hidden API for binary compatibility for a little longer Log.w(TAG, "WARNING: NfcAdapter.getDefaultAdapter() is deprecated, use " + "NfcAdapter.getDefaultAdapter(Context) instead", new Exception()); @@ -803,6 +810,7 @@ public final class NfcAdapter { * @throws IllegalStateException if the Activity has already been paused * @deprecated use {@link #setNdefPushMessage} instead */ + @Deprecated public void disableForegroundNdefPush(Activity activity) { if (activity == null) { throw new NullPointerException(); @@ -875,6 +883,24 @@ public final class NfcAdapter { } /** + * Inject a mock NFC tag.<p> + * Used for testing purposes. + * <p class="note">Requires the + * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission. + * @hide + */ + public void dispatch(Tag tag, NdefMessage message) { + if (tag == null) { + throw new NullPointerException("tag cannot be null"); + } + try { + sService.dispatch(tag, message); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** * @hide */ public INfcAdapterExtras getNfcAdapterExtrasInterface() { diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 88fea91..c106092 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -167,6 +167,8 @@ public class Build { * medium density normal size screens unless otherwise indicated). * They can still explicitly specify screen support either way with the * supports-screens manifest tag. + * <li> {@link android.widget.TabHost} will use the new dark tab + * background design. * </ul> */ public static final int DONUT = 4; @@ -208,6 +210,13 @@ public class Build { /** * November 2010: Android 2.3 + * + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li> The application's notification icons will be shown on the new + * dark status bar background, so must be visible in this situation. + * </ul> */ public static final int GINGERBREAD = 9; @@ -224,14 +233,34 @@ public class Build { * <ul> * <li> The default theme for applications is now dark holographic: * {@link android.R.style#Theme_Holo}. + * <li> On large screen devices that do not have a physical menu + * button, the soft (compatibility) menu is disabled. * <li> The activity lifecycle has changed slightly as per * {@link android.app.Activity}. + * <li> An application will crash if it does not call through + * to the super implementation of its + * {@link android.app.Activity#onPause Activity.onPause()} method. * <li> When an application requires a permission to access one of * its components (activity, receiver, service, provider), this * permission is no longer enforced when the application wants to * access its own component. This means it can require a permission * on a component that it does not itself hold and still access that * component. + * <li> {@link android.content.Context#getSharedPreferences + * Context.getSharedPreferences()} will not automatically reload + * the preferences if they have changed on storage, unless + * {@link android.content.Context#MODE_MULTI_PROCESS} is used. + * <li> {@link android.view.ViewGroup#setMotionEventSplittingEnabled} + * will default to true. + * <li> {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} + * is enabled by default on windows. + * <li> {@link android.widget.PopupWindow#isSplitTouchEnabled() + * PopupWindow.isSplitTouchEnabled()} will return true by default. + * <li> {@link android.widget.GridView} and {@link android.widget.ListView} + * will use {@link android.view.View#setActivated View.setActivated} + * for selected items if they do not implement {@link android.widget.Checkable}. + * <li> {@link android.widget.Scroller} will be constructed with + * "flywheel" behavior enabled by default. * </ul> */ public static final int HONEYCOMB = 11; @@ -266,13 +295,26 @@ public class Build { * preferred over the older screen size buckets and for older devices * the appropriate buckets will be inferred from them.</p> * - * <p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT} + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li><p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT} * and {@link android.content.pm.PackageManager#FEATURE_SCREEN_LANDSCAPE} - * features are introduced in this release. Applications that target + * features were introduced in this release. Applications that target * previous platform versions are assumed to require both portrait and * landscape support in the device; when targeting Honeycomb MR1 or * greater the application is responsible for specifying any specific * orientation it requires.</p> + * <li><p>{@link android.os.AsyncTask} will use the serial executor + * by default when calling {@link android.os.AsyncTask#execute}.</p> + * <li><p>{@link android.content.pm.ActivityInfo#configChanges + * ActivityInfo.configChanges} will have the + * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE} and + * {@link android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE} + * bits set; these need to be cleared for older applications because + * some developers have done absolute comparisons against this value + * instead of correctly masking the bits they are interested in. + * </ul> */ public static final int HONEYCOMB_MR2 = 13; @@ -306,14 +348,31 @@ public class Build { * <li> The fadingEdge attribute on views will be ignored (fading edges is no * longer a standard part of the UI). A new requiresFadingEdge attribute allows * applications to still force fading edges on for special cases. + * <li> {@link android.content.Context#bindService Context.bindService()} + * will not automatically add in {@link android.content.Context#BIND_WAIVE_PRIORITY}. + * <li> App Widgets will have standard padding automatically added around + * them, rather than relying on the padding being baked into the widget itself. + * <li> An exception will be thrown if you try to change the type of a + * window after it has been added to the window manager. Previously this + * would result in random incorrect behavior. + * <li> {@link android.view.animation.AnimationSet} will parse out + * the duration, fillBefore, fillAfter, repeatMode, and startOffset + * XML attributes that are defined. + * <li> {@link android.app.ActionBar#setHomeButtonEnabled + * ActionBar.setHomeButtonEnabled()} is false by default. * </ul> */ public static final int ICE_CREAM_SANDWICH = 14; /** - * Android 4.0.3. + * December 2011: Android 4.0.3. */ public static final int ICE_CREAM_SANDWICH_MR1 = 15; + + /** + * Next up on Android! + */ + public static final int JELLY_BEAN = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 9a53d76..270e9be 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -45,4 +45,5 @@ interface IPowerManager // sets the brightness of the backlights (screen, keyboard, button) 0-255 void setBacklightBrightness(int brightness); void setAttentionLight(boolean on, int color); + void setAutoBrightnessAdjustment(float adj); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0202c47..ef8cb16 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1401,6 +1401,12 @@ public final class Settings { public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode"; /** + * Adjustment to auto-brightness to make it generally more (>0.0 <1.0) + * or less (<0.0 >-1.0) bright. + */ + public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj"; + + /** * SCREEN_BRIGHTNESS_MODE value for manual mode. */ public static final int SCREEN_BRIGHTNESS_MODE_MANUAL = 0; @@ -1927,6 +1933,7 @@ public final class Settings { SCREEN_OFF_TIMEOUT, SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_MODE, + SCREEN_AUTO_BRIGHTNESS_ADJ, VIBRATE_ON, MODE_RINGER, MODE_RINGER_STREAMS_AFFECTED, @@ -4104,17 +4111,38 @@ public final class Settings { /** {@hide} */ public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval"; /** {@hide} */ - public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold"; + public static final String NETSTATS_TIME_CACHE_MAX_AGE = "netstats_time_cache_max_age"; + /** {@hide} */ + public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes"; /** {@hide} */ - public static final String NETSTATS_NETWORK_BUCKET_DURATION = "netstats_network_bucket_duration"; + public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled"; + + /** {@hide} */ + public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration"; /** {@hide} */ - public static final String NETSTATS_NETWORK_MAX_HISTORY = "netstats_network_max_history"; + public static final String NETSTATS_DEV_PERSIST_BYTES = "netstats_dev_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_DEV_ROTATE_AGE = "netstats_dev_rotate_age"; + /** {@hide} */ + public static final String NETSTATS_DEV_DELETE_AGE = "netstats_dev_delete_age"; + /** {@hide} */ public static final String NETSTATS_UID_BUCKET_DURATION = "netstats_uid_bucket_duration"; /** {@hide} */ - public static final String NETSTATS_UID_MAX_HISTORY = "netstats_uid_max_history"; + public static final String NETSTATS_UID_PERSIST_BYTES = "netstats_uid_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_UID_ROTATE_AGE = "netstats_uid_rotate_age"; + /** {@hide} */ + public static final String NETSTATS_UID_DELETE_AGE = "netstats_uid_delete_age"; + + /** {@hide} */ + public static final String NETSTATS_UID_TAG_BUCKET_DURATION = "netstats_uid_tag_bucket_duration"; + /** {@hide} */ + public static final String NETSTATS_UID_TAG_PERSIST_BYTES = "netstats_uid_tag_persist_bytes"; + /** {@hide} */ + public static final String NETSTATS_UID_TAG_ROTATE_AGE = "netstats_uid_tag_rotate_age"; /** {@hide} */ - public static final String NETSTATS_TAG_MAX_HISTORY = "netstats_tag_max_history"; + public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age"; /** Preferred NTP server. {@hide} */ public static final String NTP_SERVER = "ntp_server"; diff --git a/core/java/android/provider/UserDictionary.java b/core/java/android/provider/UserDictionary.java index 5a7ef85..a9b106a 100644 --- a/core/java/android/provider/UserDictionary.java +++ b/core/java/android/provider/UserDictionary.java @@ -40,6 +40,9 @@ public class UserDictionary { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); + private static final int FREQUENCY_MIN = 0; + private static final int FREQUENCY_MAX = 255; + /** * Contains the user defined words. */ @@ -87,12 +90,24 @@ public class UserDictionary { */ public static final String APP_ID = "appid"; - /** The locale type to specify that the word is common to all locales. */ + /** + * An optional shortcut for this word. When the shortcut is typed, supporting IMEs should + * suggest the word in this row as an alternate spelling too. + */ + public static final String SHORTCUT = "shortcut"; + + /** + * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}. + */ + @Deprecated public static final int LOCALE_TYPE_ALL = 0; - - /** The locale type to specify that the word is for the current locale. */ + + /** + * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}. + */ + @Deprecated public static final int LOCALE_TYPE_CURRENT = 1; - + /** * Sort by descending order of frequency. */ @@ -100,35 +115,65 @@ public class UserDictionary { /** Adds a word to the dictionary, with the given frequency and the specified * specified locale type. + * + * @deprecated Please use + * {@link #addWord(Context, String, int, String, Locale)} instead. + * * @param context the current application context * @param word the word to add to the dictionary. This should not be null or * empty. * @param localeType the locale type for this word. It should be one of * {@link #LOCALE_TYPE_ALL} or {@link #LOCALE_TYPE_CURRENT}. */ - public static void addWord(Context context, String word, + @Deprecated + public static void addWord(Context context, String word, int frequency, int localeType) { - final ContentResolver resolver = context.getContentResolver(); - if (TextUtils.isEmpty(word) || localeType < 0 || localeType > 1) { + if (localeType != LOCALE_TYPE_ALL && localeType != LOCALE_TYPE_CURRENT) { return; } - - if (frequency < 0) frequency = 0; - if (frequency > 255) frequency = 255; - String locale = null; + final Locale locale; - // TODO: Verify if this is the best way to get the current locale if (localeType == LOCALE_TYPE_CURRENT) { - locale = Locale.getDefault().toString(); + locale = Locale.getDefault(); + } else { + locale = null; } - ContentValues values = new ContentValues(4); + + addWord(context, word, frequency, null, locale); + } + + /** Adds a word to the dictionary, with the given frequency and the specified + * locale type. + * + * @param context the current application context + * @param word the word to add to the dictionary. This should not be null or + * empty. + * @param shortcut optional shortcut spelling for this word. When the shortcut + * is typed, the word may be suggested by applications that support it. May be null. + * @param locale the locale to insert the word for, or null to insert the word + * for all locales. + */ + public static void addWord(Context context, String word, + int frequency, String shortcut, Locale locale) { + final ContentResolver resolver = context.getContentResolver(); + + if (TextUtils.isEmpty(word)) { + return; + } + + if (frequency < FREQUENCY_MIN) frequency = FREQUENCY_MIN; + if (frequency > FREQUENCY_MAX) frequency = FREQUENCY_MAX; + + final int COLUMN_COUNT = 5; + ContentValues values = new ContentValues(COLUMN_COUNT); values.put(WORD, word); values.put(FREQUENCY, frequency); - values.put(LOCALE, locale); + values.put(LOCALE, null == locale ? null : locale.toString()); values.put(APP_ID, 0); // TODO: Get App UID + values.put(SHORTCUT, shortcut); Uri result = resolver.insert(CONTENT_URI, values); // It's ok if the insert doesn't succeed because the word diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index c184c11..a52e2ba 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -109,6 +109,9 @@ class MeasuredText { for (int i = 0; i < spans.length; i++) { int startInPara = spanned.getSpanStart(spans[i]) - start; int endInPara = spanned.getSpanEnd(spans[i]) - start; + // The span interval may be larger and must be restricted to [start, end[ + if (startInPara < 0) startInPara = 0; + if (endInPara > len) endInPara = len; for (int j = startInPara; j < endInPara; j++) { mChars[j] = '\uFFFC'; } diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 4ec4bc4..30bb447 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -280,8 +280,6 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme if (isSelecting(buffer)) { buffer.removeSpan(LAST_TAP_DOWN); Selection.extendSelection(buffer, offset); - } else if (!widget.shouldIgnoreActionUpEvent()) { - Selection.setSelection(buffer, offset); } MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index 519b980..a43d36c 100644 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -57,6 +57,13 @@ public class DisplayMetrics { public static final int DENSITY_XHIGH = 320; /** + * Standard quantized DPI for extra-extra-high-density screens. Applications + * should not generally worry about this density; relying on XHIGH graphics + * being scaled up to it should be sufficient for almost all cases. + */ + public static final int DENSITY_XXHIGH = 480; + + /** * The reference density used throughout the system. */ public static final int DENSITY_DEFAULT = DENSITY_MEDIUM; diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java new file mode 100644 index 0000000..641d1b4 --- /dev/null +++ b/core/java/android/util/LocalLog.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.text.format.Time; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * @hide + */ +public final class LocalLog { + + private LinkedList<String> mLog; + private int mMaxLines; + private Time mNow; + + public LocalLog(int maxLines) { + mLog = new LinkedList<String>(); + mMaxLines = maxLines; + mNow = new Time(); + } + + public synchronized void log(String msg) { + if (mMaxLines > 0) { + mNow.setToNow(); + mLog.add(mNow.format("%H:%M:%S") + " - " + msg); + while (mLog.size() > mMaxLines) mLog.remove(); + } + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + Iterator<String> itr = mLog.listIterator(0); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } +} diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 761a788..c08a402 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -22,6 +22,7 @@ import android.graphics.ColorFilter; import android.graphics.DrawFilter; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; import android.graphics.Path; import android.graphics.Picture; import android.graphics.PorterDuff; @@ -546,6 +547,7 @@ class GLES20Canvas extends HardwareCanvas { private static native void nSetMatrix(int renderer, int matrix); + @SuppressWarnings("deprecation") @Override public void getMatrix(Matrix matrix) { nGetMatrix(mRenderer, matrix.native_instance); @@ -658,8 +660,17 @@ class GLES20Canvas extends HardwareCanvas { @Override public void setDrawFilter(DrawFilter filter) { mFilter = filter; + if (filter == null) { + nResetPaintFilter(mRenderer); + } else if (filter instanceof PaintFlagsDrawFilter) { + PaintFlagsDrawFilter flagsFilter = (PaintFlagsDrawFilter) filter; + nSetupPaintFilter(mRenderer, flagsFilter.clearBits, flagsFilter.setBits); + } } + private static native void nResetPaintFilter(int renderer); + private static native void nSetupPaintFilter(int renderer, int clearBits, int setBits); + @Override public DrawFilter getDrawFilter() { return mFilter; @@ -908,17 +919,42 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPicture(Picture picture) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + picture.endRecording(); + // TODO: Implement rendering } @Override public void drawPicture(Picture picture, Rect dst) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + save(); + translate(dst.left, dst.top); + if (picture.getWidth() > 0 && picture.getHeight() > 0) { + scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight()); + } + drawPicture(picture); + restore(); } @Override public void drawPicture(Picture picture, RectF dst) { - throw new UnsupportedOperationException(); + if (picture.createdFromStream) { + return; + } + + save(); + translate(dst.left, dst.top); + if (picture.getWidth() > 0 && picture.getHeight() > 0) { + scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight()); + } + drawPicture(picture); + restore(); } @Override @@ -943,6 +979,7 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDrawPoints(int renderer, float[] points, int offset, int count, int paint); + @SuppressWarnings("deprecation") @Override public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { if (index < 0 || index + count > text.length || count * 2 > pos.length) { @@ -960,6 +997,7 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDrawPosText(int renderer, char[] text, int index, int count, float[] pos, int paint); + @SuppressWarnings("deprecation") @Override public void drawPosText(String text, float[] pos, Paint paint) { if (text.length() * 2 > pos.length) { diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 4592ae6..1c9cbbf 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -238,6 +238,15 @@ public abstract class HardwareRenderer { private static native void nSetupShadersDiskCache(String cacheFile); /** + * Notifies EGL that the frame is about to be rendered. + */ + private static void beginFrame() { + nBeginFrame(); + } + + private static native void nBeginFrame(); + + /** * Interface used to receive callbacks whenever a view is drawn by * a hardware renderer instance. */ @@ -808,6 +817,7 @@ public abstract class HardwareRenderer { } void onPreDraw(Rect dirty) { + } void onPostDraw() { @@ -832,6 +842,8 @@ public abstract class HardwareRenderer { dirty = null; } + beginFrame(); + onPreDraw(dirty); HardwareCanvas canvas = mCanvas; diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 93a9d50..c54d09e 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -229,4 +229,9 @@ interface IWindowManager * Device has a software navigation bar (separate from the status bar). */ boolean hasNavigationBar(); + + /** + * Lock the device immediately. + */ + void lockNow(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index a9d6cdf..8cac57d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -7996,84 +7996,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** - * @hide - */ - public void setFastTranslationX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationX = x; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastTranslationY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationY = y; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationX = x - mLeft; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mTranslationY = y - mTop; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastScaleX(float x) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mScaleX = x; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastScaleY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mScaleY = y; - info.mMatrixDirty = true; - } - - /** - * @hide - */ - public void setFastAlpha(float alpha) { - ensureTransformationInfo(); - mTransformationInfo.mAlpha = alpha; - } - - /** - * @hide - */ - public void setFastRotationY(float y) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - info.mRotationY = y; - info.mMatrixDirty = true; - } - - /** * Hit rectangle in parent's coordinates * * @param outRect The hit rectangle of the view. @@ -8650,37 +8572,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** - * @hide - */ - public void fastInvalidate() { - if (skipInvalidate()) { - return; - } - if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) || - (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID || - (mPrivateFlags & INVALIDATED) != INVALIDATED) { - if (mParent instanceof View) { - ((View) mParent).mPrivateFlags |= INVALIDATED; - } - mPrivateFlags &= ~DRAWN; - mPrivateFlags |= DIRTY; - mPrivateFlags |= INVALIDATED; - mPrivateFlags &= ~DRAWING_CACHE_VALID; - if (mParent != null && mAttachInfo != null) { - if (mAttachInfo.mHardwareAccelerated) { - mParent.invalidateChild(this, null); - } else { - final Rect r = mAttachInfo.mTmpInvalRect; - r.set(0, 0, mRight - mLeft, mBottom - mTop); - // Don't call invalidate -- we don't want to internally scroll - // our own bounds - mParent.invalidateChild(this, r); - } - } - } - } - - /** * Used to indicate that the parent of this view should clear its caches. This functionality * is used to force the parent to rebuild its display list (when hardware-accelerated), * which is necessary when various parent-managed properties of the view change, such as diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index c53fc6b..7fd3389 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -185,7 +185,8 @@ public final class ViewTreeObserver { mTouchableInsets = TOUCHABLE_INSETS_FRAME; } - @Override public boolean equals(Object o) { + @Override + public boolean equals(Object o) { try { if (o == null) { return false; @@ -357,10 +358,26 @@ public final class ViewTreeObserver { * @param victim The callback to remove * * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @deprecated Use #removeOnGlobalLayoutListener instead * * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener) */ + @Deprecated public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) { + removeOnGlobalLayoutListener(victim); + } + + /** + * Remove a previously installed global layout callback + * + * @param victim The callback to remove + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener) + */ + public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) { checkIsAlive(); if (mOnGlobalLayoutListeners == null) { return; diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 994565a..6ec2e8d 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -1029,6 +1029,11 @@ public interface WindowManagerPolicy { public boolean hasNavigationBar(); /** + * Lock the device now. + */ + public void lockNow(); + + /** * Print the WindowManagerPolicy's state into the given stream. * * @param prefix Text to print at the front of each line. diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java index c3c74a7..c28b220 100755 --- a/core/java/android/view/WindowOrientationListener.java +++ b/core/java/android/view/WindowOrientationListener.java @@ -21,6 +21,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.util.FloatMath; import android.util.Log; import android.util.Slog; @@ -48,6 +49,8 @@ public abstract class WindowOrientationListener { private static final boolean DEBUG = false; private static final boolean localLOGV = DEBUG || false; + private static final boolean USE_GRAVITY_SENSOR = false; + private SensorManager mSensorManager; private boolean mEnabled; private int mRate; @@ -79,7 +82,8 @@ public abstract class WindowOrientationListener { private WindowOrientationListener(Context context, int rate) { mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); mRate = rate; - mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mSensor = mSensorManager.getDefaultSensor(USE_GRAVITY_SENSOR + ? Sensor.TYPE_GRAVITY : Sensor.TYPE_ACCELEROMETER); if (mSensor != null) { // Create listener only if sensors do exist mSensorEventListener = new SensorEventListenerImpl(this); @@ -179,7 +183,7 @@ public abstract class WindowOrientationListener { * cartesian space because the orientation calculations are sensitive to the * absolute magnitude of the acceleration. In particular, there are singularities * in the calculation as the magnitude approaches 0. By performing the low-pass - * filtering early, we can eliminate high-frequency impulses systematically. + * filtering early, we can eliminate most spurious high-frequency impulses due to noise. * * - Convert the acceleromter vector from cartesian to spherical coordinates. * Since we're dealing with rotation of the device, this is the sensible coordinate @@ -204,11 +208,17 @@ public abstract class WindowOrientationListener { * new orientation proposal. * * Details are explained inline. + * + * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for + * signal processing background. */ static final class SensorEventListenerImpl implements SensorEventListener { // We work with all angles in degrees in this class. private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI); + // Number of nanoseconds per millisecond. + private static final long NANOS_PER_MS = 1000000; + // Indices into SensorEvent.values for the accelerometer sensor. private static final int ACCELEROMETER_DATA_X = 0; private static final int ACCELEROMETER_DATA_Y = 1; @@ -216,38 +226,41 @@ public abstract class WindowOrientationListener { private final WindowOrientationListener mOrientationListener; - /* State for first order low-pass filtering of accelerometer data. - * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for - * signal processing background. - */ - - private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds - private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; - - // The current proposal. We wait for the proposal to be stable for a - // certain amount of time before accepting it. - // - // The basic idea is to ignore intermediate poses of the device while the - // user is picking up, putting down or turning the device. - private int mProposalRotation; - private long mProposalAgeMS; - - // A historical trace of tilt and orientation angles. Used to determine whether - // the device posture has settled down. - private static final int HISTORY_SIZE = 20; - private int mHistoryIndex; // index of most recent sample - private int mHistoryLength; // length of historical trace - private final long[] mHistoryTimestampMS = new long[HISTORY_SIZE]; - private final float[] mHistoryMagnitudes = new float[HISTORY_SIZE]; - private final int[] mHistoryTiltAngles = new int[HISTORY_SIZE]; - private final int[] mHistoryOrientationAngles = new int[HISTORY_SIZE]; + // The minimum amount of time that a predicted rotation must be stable before it + // is accepted as a valid rotation proposal. This value can be quite small because + // the low-pass filter already suppresses most of the noise so we're really just + // looking for quick confirmation that the last few samples are in agreement as to + // the desired orientation. + private static final long PROPOSAL_SETTLE_TIME_NANOS = 40 * NANOS_PER_MS; + + // The minimum amount of time that must have elapsed since the device last exited + // the flat state (time since it was picked up) before the proposed rotation + // can change. + private static final long PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS = 500 * NANOS_PER_MS; + + // The mininum amount of time that must have elapsed since the device stopped + // swinging (time since device appeared to be in the process of being put down + // or put away into a pocket) before the proposed rotation can change. + private static final long PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS = 300 * NANOS_PER_MS; + + // If the tilt angle remains greater than the specified angle for a minimum of + // the specified time, then the device is deemed to be lying flat + // (just chillin' on a table). + private static final float FLAT_ANGLE = 75; + private static final long FLAT_TIME_NANOS = 1000 * NANOS_PER_MS; + + // If the tilt angle has increased by at least delta degrees within the specified amount + // of time, then the device is deemed to be swinging away from the user + // down towards flat (tilt = 90). + private static final float SWING_AWAY_ANGLE_DELTA = 20; + private static final long SWING_TIME_NANOS = 300 * NANOS_PER_MS; // The maximum sample inter-arrival time in milliseconds. // If the acceleration samples are further apart than this amount in time, we reset the // state of the low-pass filter and orientation properties. This helps to handle // boundary conditions when the device is turned on, wakes from suspend or there is // a significant gap in samples. - private static final float MAX_FILTER_DELTA_TIME_MS = 1000; + private static final long MAX_FILTER_DELTA_TIME_NANOS = 1000 * NANOS_PER_MS; // The acceleration filter time constant. // @@ -267,8 +280,10 @@ public abstract class WindowOrientationListener { // // Filtering adds latency proportional the time constant (inversely proportional // to the cutoff frequency) so we don't want to make the time constant too - // large or we can lose responsiveness. - private static final float FILTER_TIME_CONSTANT_MS = 100.0f; + // large or we can lose responsiveness. Likewise we don't want to make it too + // small or we do a poor job suppressing acceleration spikes. + // Empirically, 100ms seems to be too small and 500ms is too large. + private static final float FILTER_TIME_CONSTANT_MS = 200.0f; /* State for orientation detection. */ @@ -286,9 +301,9 @@ public abstract class WindowOrientationListener { // // In both cases, we postpone choosing an orientation. private static final float MIN_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY * 0.5f; + SensorManager.STANDARD_GRAVITY * 0.3f; private static final float MAX_ACCELERATION_MAGNITUDE = - SensorManager.STANDARD_GRAVITY * 1.5f; + SensorManager.STANDARD_GRAVITY * 1.25f; // Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e. // when screen is facing the sky or ground), we completely ignore orientation data. @@ -306,10 +321,10 @@ public abstract class WindowOrientationListener { // The ideal tilt angle is 0 (when the device is vertical) so the limits establish // how close to vertical the device must be in order to change orientation. private static final int[][] TILT_TOLERANCE = new int[][] { - /* ROTATION_0 */ { -20, 70 }, - /* ROTATION_90 */ { -20, 60 }, - /* ROTATION_180 */ { -20, 50 }, - /* ROTATION_270 */ { -20, 60 } + /* ROTATION_0 */ { -25, 70 }, + /* ROTATION_90 */ { -25, 65 }, + /* ROTATION_180 */ { -25, 60 }, + /* ROTATION_270 */ { -25, 65 } }; // The gap angle in degrees between adjacent orientation angles for hysteresis. @@ -319,29 +334,38 @@ public abstract class WindowOrientationListener { // orientation. private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45; - // The number of milliseconds for which the device posture must be stable - // before we perform an orientation change. If the device appears to be rotating - // (being picked up, put down) then we keep waiting until it settles. - private static final int SETTLE_TIME_MS = 200; + // Timestamp and value of the last accelerometer sample. + private long mLastFilteredTimestampNanos; + private float mLastFilteredX, mLastFilteredY, mLastFilteredZ; + + // The last proposed rotation, -1 if unknown. + private int mProposedRotation; + + // Value of the current predicted rotation, -1 if unknown. + private int mPredictedRotation; + + // Timestamp of when the predicted rotation most recently changed. + private long mPredictedRotationTimestampNanos; - // The maximum change in magnitude that can occur during the settle time. - // Tuning this constant particularly helps to filter out situations where the - // device is being picked up or put down by the user. - private static final float SETTLE_MAGNITUDE_MAX_DELTA = - SensorManager.STANDARD_GRAVITY * 0.2f; + // Timestamp when the device last appeared to be flat for sure (the flat delay elapsed). + private long mFlatTimestampNanos; - // The maximum change in tilt angle that can occur during the settle time. - private static final int SETTLE_TILT_ANGLE_MAX_DELTA = 5; + // Timestamp when the device last appeared to be swinging. + private long mSwingTimestampNanos; - // The maximum change in orientation angle that can occur during the settle time. - private static final int SETTLE_ORIENTATION_ANGLE_MAX_DELTA = 5; + // History of observed tilt angles. + private static final int TILT_HISTORY_SIZE = 40; + private float[] mTiltHistory = new float[TILT_HISTORY_SIZE]; + private long[] mTiltHistoryTimestampNanos = new long[TILT_HISTORY_SIZE]; + private int mTiltHistoryIndex; public SensorEventListenerImpl(WindowOrientationListener orientationListener) { mOrientationListener = orientationListener; + reset(); } public int getProposedRotation() { - return mProposalAgeMS >= SETTLE_TIME_MS ? mProposalRotation : -1; + return mProposedRotation; } @Override @@ -359,8 +383,9 @@ public abstract class WindowOrientationListener { float z = event.values[ACCELEROMETER_DATA_Z]; if (log) { - Slog.v(TAG, "Raw acceleration vector: " + - "x=" + x + ", y=" + y + ", z=" + z); + Slog.v(TAG, "Raw acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z + + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); } // Apply a low-pass filter to the acceleration up vector in cartesian space. @@ -368,14 +393,16 @@ public abstract class WindowOrientationListener { // or when we see values of (0, 0, 0) which indicates that we polled the // accelerometer too soon after turning it on and we don't have any data yet. final long now = event.timestamp; - final float timeDeltaMS = (now - mLastTimestamp) * 0.000001f; - boolean skipSample; - if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS + final long then = mLastFilteredTimestampNanos; + final float timeDeltaMS = (now - then) * 0.000001f; + final boolean skipSample; + if (now < then + || now > then + MAX_FILTER_DELTA_TIME_NANOS || (x == 0 && y == 0 && z == 0)) { if (log) { Slog.v(TAG, "Resetting orientation listener."); } - clearProposal(); + reset(); skipSample = true; } else { final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS); @@ -383,27 +410,28 @@ public abstract class WindowOrientationListener { y = alpha * (y - mLastFilteredY) + mLastFilteredY; z = alpha * (z - mLastFilteredZ) + mLastFilteredZ; if (log) { - Slog.v(TAG, "Filtered acceleration vector: " + - "x=" + x + ", y=" + y + ", z=" + z); + Slog.v(TAG, "Filtered acceleration vector: " + + "x=" + x + ", y=" + y + ", z=" + z + + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z)); } skipSample = false; } - mLastTimestamp = now; + mLastFilteredTimestampNanos = now; mLastFilteredX = x; mLastFilteredY = y; mLastFilteredZ = z; - final int oldProposedRotation = getProposedRotation(); + boolean isFlat = false; + boolean isSwinging = false; if (!skipSample) { // Calculate the magnitude of the acceleration vector. - final float magnitude = (float) Math.sqrt(x * x + y * y + z * z); + final float magnitude = FloatMath.sqrt(x * x + y * y + z * z); if (magnitude < MIN_ACCELERATION_MAGNITUDE || magnitude > MAX_ACCELERATION_MAGNITUDE) { if (log) { - Slog.v(TAG, "Ignoring sensor data, magnitude out of range: " - + "magnitude=" + magnitude); + Slog.v(TAG, "Ignoring sensor data, magnitude out of range."); } - clearProposal(); + clearPredictedRotation(); } else { // Calculate the tilt angle. // This is the angle between the up vector and the x-y plane (the plane of @@ -414,14 +442,25 @@ public abstract class WindowOrientationListener { final int tiltAngle = (int) Math.round( Math.asin(z / magnitude) * RADIANS_TO_DEGREES); + // Determine whether the device appears to be flat or swinging. + if (isFlat(now)) { + isFlat = true; + mFlatTimestampNanos = now; + } + if (isSwinging(now, tiltAngle)) { + isSwinging = true; + mSwingTimestampNanos = now; + } + addTiltHistoryEntry(now, tiltAngle); + // If the tilt angle is too close to horizontal then we cannot determine // the orientation angle of the screen. if (Math.abs(tiltAngle) > MAX_TILT) { if (log) { Slog.v(TAG, "Ignoring sensor data, tilt angle too high: " - + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle); + + "tiltAngle=" + tiltAngle); } - clearProposal(); + clearPredictedRotation(); } else { // Calculate the orientation angle. // This is the angle between the x-y projection of the up vector onto @@ -439,89 +478,93 @@ public abstract class WindowOrientationListener { nearestRotation = 0; } - // Determine the proposed orientation. - // The confidence of the proposal is 1.0 when it is ideal and it - // decays exponentially as the proposal moves further from the ideal - // angle, tilt and magnitude of the proposed orientation. - if (!isTiltAngleAcceptable(nearestRotation, tiltAngle) - || !isOrientationAngleAcceptable(nearestRotation, + // Determine the predicted orientation. + if (isTiltAngleAcceptable(nearestRotation, tiltAngle) + && isOrientationAngleAcceptable(nearestRotation, orientationAngle)) { + updatePredictedRotation(now, nearestRotation); if (log) { - Slog.v(TAG, "Ignoring sensor data, no proposal: " - + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle); + Slog.v(TAG, "Predicted: " + + "tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle + + ", predictedRotation=" + mPredictedRotation + + ", predictedRotationAgeMS=" + + ((now - mPredictedRotationTimestampNanos) + * 0.000001f)); } - clearProposal(); } else { if (log) { - Slog.v(TAG, "Proposal: " - + "magnitude=" + magnitude - + ", tiltAngle=" + tiltAngle - + ", orientationAngle=" + orientationAngle - + ", proposalRotation=" + mProposalRotation); + Slog.v(TAG, "Ignoring sensor data, no predicted rotation: " + + "tiltAngle=" + tiltAngle + + ", orientationAngle=" + orientationAngle); } - updateProposal(nearestRotation, now / 1000000L, - magnitude, tiltAngle, orientationAngle); + clearPredictedRotation(); } } } } + // Determine new proposed rotation. + final int oldProposedRotation = mProposedRotation; + if (mPredictedRotation < 0 || isPredictedRotationAcceptable(now)) { + mProposedRotation = mPredictedRotation; + } + // Write final statistics about where we are in the orientation detection process. - final int proposedRotation = getProposedRotation(); if (log) { - final float proposalConfidence = Math.min( - mProposalAgeMS * 1.0f / SETTLE_TIME_MS, 1.0f); Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation - + ", proposedRotation=" + proposedRotation + + ", proposedRotation=" + mProposedRotation + + ", predictedRotation=" + mPredictedRotation + ", timeDeltaMS=" + timeDeltaMS - + ", proposalRotation=" + mProposalRotation - + ", proposalAgeMS=" + mProposalAgeMS - + ", proposalConfidence=" + proposalConfidence); + + ", isFlat=" + isFlat + + ", isSwinging=" + isSwinging + + ", timeUntilSettledMS=" + remainingMS(now, + mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) + + ", timeUntilFlatDelayExpiredMS=" + remainingMS(now, + mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) + + ", timeUntilSwingDelayExpiredMS=" + remainingMS(now, + mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS)); } // Tell the listener. - if (proposedRotation != oldProposedRotation && proposedRotation >= 0) { + if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) { if (log) { - Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + proposedRotation + Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + mProposedRotation + ", oldProposedRotation=" + oldProposedRotation); } - mOrientationListener.onProposedRotationChanged(proposedRotation); + mOrientationListener.onProposedRotationChanged(mProposedRotation); } } /** - * Returns true if the tilt angle is acceptable for a proposed - * orientation transition. + * Returns true if the tilt angle is acceptable for a given predicted rotation. */ - private boolean isTiltAngleAcceptable(int proposedRotation, - int tiltAngle) { - return tiltAngle >= TILT_TOLERANCE[proposedRotation][0] - && tiltAngle <= TILT_TOLERANCE[proposedRotation][1]; + private boolean isTiltAngleAcceptable(int rotation, int tiltAngle) { + return tiltAngle >= TILT_TOLERANCE[rotation][0] + && tiltAngle <= TILT_TOLERANCE[rotation][1]; } /** - * Returns true if the orientation angle is acceptable for a proposed - * orientation transition. + * Returns true if the orientation angle is acceptable for a given predicted rotation. * * This function takes into account the gap between adjacent orientations * for hysteresis. */ - private boolean isOrientationAngleAcceptable(int proposedRotation, int orientationAngle) { + private boolean isOrientationAngleAcceptable(int rotation, int orientationAngle) { // If there is no current rotation, then there is no gap. // The gap is used only to introduce hysteresis among advertised orientation // changes to avoid flapping. final int currentRotation = mOrientationListener.mCurrentRotation; if (currentRotation >= 0) { - // If the proposed rotation is the same or is counter-clockwise adjacent, - // then we set a lower bound on the orientation angle. + // If the specified rotation is the same or is counter-clockwise adjacent + // to the current rotation, then we set a lower bound on the orientation angle. // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90, // then we want to check orientationAngle > 45 + GAP / 2. - if (proposedRotation == currentRotation - || proposedRotation == (currentRotation + 1) % 4) { - int lowerBound = proposedRotation * 90 - 45 + if (rotation == currentRotation + || rotation == (currentRotation + 1) % 4) { + int lowerBound = rotation * 90 - 45 + ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (proposedRotation == 0) { + if (rotation == 0) { if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) { return false; } @@ -532,15 +575,15 @@ public abstract class WindowOrientationListener { } } - // If the proposed rotation is the same or is clockwise adjacent, + // If the specified rotation is the same or is clockwise adjacent, // then we set an upper bound on the orientation angle. - // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_270, + // For example, if currentRotation is ROTATION_0 and rotation is ROTATION_270, // then we want to check orientationAngle < 315 - GAP / 2. - if (proposedRotation == currentRotation - || proposedRotation == (currentRotation + 3) % 4) { - int upperBound = proposedRotation * 90 + 45 + if (rotation == currentRotation + || rotation == (currentRotation + 3) % 4) { + int upperBound = rotation * 90 + 45 - ADJACENT_ORIENTATION_ANGLE_GAP / 2; - if (proposedRotation == 0) { + if (rotation == 0) { if (orientationAngle <= 45 && orientationAngle > upperBound) { return false; } @@ -554,58 +597,97 @@ public abstract class WindowOrientationListener { return true; } - private void clearProposal() { - mProposalRotation = -1; - mProposalAgeMS = 0; - } + /** + * Returns true if the predicted rotation is ready to be advertised as a + * proposed rotation. + */ + private boolean isPredictedRotationAcceptable(long now) { + // The predicted rotation must have settled long enough. + if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) { + return false; + } + + // The last flat state (time since picked up) must have been sufficiently long ago. + if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) { + return false; + } - private void updateProposal(int rotation, long timestampMS, - float magnitude, int tiltAngle, int orientationAngle) { - if (mProposalRotation != rotation) { - mProposalRotation = rotation; - mHistoryIndex = 0; - mHistoryLength = 0; + // The last swing state (time since last movement to put down) must have been + // sufficiently long ago. + if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) { + return false; } - final int index = mHistoryIndex; - mHistoryTimestampMS[index] = timestampMS; - mHistoryMagnitudes[index] = magnitude; - mHistoryTiltAngles[index] = tiltAngle; - mHistoryOrientationAngles[index] = orientationAngle; - mHistoryIndex = (index + 1) % HISTORY_SIZE; - if (mHistoryLength < HISTORY_SIZE) { - mHistoryLength += 1; + // Looks good! + return true; + } + + private void reset() { + mLastFilteredTimestampNanos = Long.MIN_VALUE; + mProposedRotation = -1; + mFlatTimestampNanos = Long.MIN_VALUE; + mSwingTimestampNanos = Long.MIN_VALUE; + clearPredictedRotation(); + clearTiltHistory(); + } + + private void clearPredictedRotation() { + mPredictedRotation = -1; + mPredictedRotationTimestampNanos = Long.MIN_VALUE; + } + + private void updatePredictedRotation(long now, int rotation) { + if (mPredictedRotation != rotation) { + mPredictedRotation = rotation; + mPredictedRotationTimestampNanos = now; } + } - long age = 0; - for (int i = 1; i < mHistoryLength; i++) { - final int olderIndex = (index + HISTORY_SIZE - i) % HISTORY_SIZE; - if (Math.abs(mHistoryMagnitudes[olderIndex] - magnitude) - > SETTLE_MAGNITUDE_MAX_DELTA) { + private void clearTiltHistory() { + mTiltHistoryTimestampNanos[0] = Long.MIN_VALUE; + mTiltHistoryIndex = 1; + } + + private void addTiltHistoryEntry(long now, float tilt) { + mTiltHistory[mTiltHistoryIndex] = tilt; + mTiltHistoryTimestampNanos[mTiltHistoryIndex] = now; + mTiltHistoryIndex = (mTiltHistoryIndex + 1) % TILT_HISTORY_SIZE; + mTiltHistoryTimestampNanos[mTiltHistoryIndex] = Long.MIN_VALUE; + } + + private boolean isFlat(long now) { + for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { + if (mTiltHistory[i] < FLAT_ANGLE) { break; } - if (angleAbsoluteDelta(mHistoryTiltAngles[olderIndex], - tiltAngle) > SETTLE_TILT_ANGLE_MAX_DELTA) { - break; + if (mTiltHistoryTimestampNanos[i] + FLAT_TIME_NANOS <= now) { + // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS. + return true; } - if (angleAbsoluteDelta(mHistoryOrientationAngles[olderIndex], - orientationAngle) > SETTLE_ORIENTATION_ANGLE_MAX_DELTA) { + } + return false; + } + + private boolean isSwinging(long now, float tilt) { + for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) { + if (mTiltHistoryTimestampNanos[i] + SWING_TIME_NANOS < now) { break; } - age = timestampMS - mHistoryTimestampMS[olderIndex]; - if (age >= SETTLE_TIME_MS) { - break; + if (mTiltHistory[i] + SWING_AWAY_ANGLE_DELTA <= tilt) { + // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS. + return true; } } - mProposalAgeMS = age; + return false; } - private static int angleAbsoluteDelta(int a, int b) { - int delta = Math.abs(a - b); - if (delta > 180) { - delta = 360 - delta; - } - return delta; + private int nextTiltHistoryIndex(int index) { + index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1; + return mTiltHistoryTimestampNanos[index] != Long.MIN_VALUE ? index : -1; + } + + private static float remainingMS(long now, long until) { + return now >= until ? 0 : (until - now) * 0.000001f; } } } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 5ec1ec3..bd02d62 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -193,10 +193,12 @@ public class BaseInputConnection implements InputConnection { /** * The default implementation performs the deletion around the current * selection position of the editable text. + * @param beforeLength + * @param afterLength */ - public boolean deleteSurroundingText(int leftLength, int rightLength) { - if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength - + " / " + rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength + + " / " + afterLength); final Editable content = getEditable(); if (content == null) return false; @@ -226,17 +228,17 @@ public class BaseInputConnection implements InputConnection { int deleted = 0; - if (leftLength > 0) { - int start = a - leftLength; + if (beforeLength > 0) { + int start = a - beforeLength; if (start < 0) start = 0; content.delete(start, a); deleted = a - start; } - if (rightLength > 0) { + if (afterLength > 0) { b = b - deleted; - int end = b + rightLength; + int end = b + afterLength; if (end > content.length()) end = content.length(); content.delete(b, end); diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index a6639d1..3563d4d 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -138,19 +138,21 @@ public interface InputConnection { int flags); /** - * Delete <var>leftLength</var> characters of text before the current cursor - * position, and delete <var>rightLength</var> characters of text after the - * current cursor position, excluding composing text. + * Delete <var>beforeLength</var> characters of text before the current cursor + * position, and delete <var>afterLength</var> characters of text after the + * current cursor position, excluding composing text. Before and after refer + * to the order of the characters in the string, not to their visual representation. * - * @param leftLength The number of characters to be deleted before the + * + * @param beforeLength The number of characters to be deleted before the * current cursor position. - * @param rightLength The number of characters to be deleted after the + * @param afterLength The number of characters to be deleted after the * current cursor position. - * + * * @return Returns true on success, false if the input connection is no longer * valid. */ - public boolean deleteSurroundingText(int leftLength, int rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength); /** * Set composing text around the current cursor position with the given text, diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 690ea85..a48473e 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -62,8 +62,8 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.getExtractedText(request, flags); } - public boolean deleteSurroundingText(int leftLength, int rightLength) { - return mTarget.deleteSurroundingText(leftLength, rightLength); + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + return mTarget.deleteSurroundingText(beforeLength, afterLength); } public boolean setComposingText(CharSequence text, int newCursorPosition) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index b41e6f5..0985e14 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -651,19 +651,7 @@ public final class InputMethodManager { } } - if (mServedInputConnection != null) { - // We need to tell the previously served view that it is no - // longer the input target, so it can reset its state. Schedule - // this call on its window's Handler so it will be on the correct - // thread and outside of our lock. - Handler vh = mServedView.getHandler(); - if (vh != null) { - // This will result in a call to reportFinishInputConnection() - // below. - vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION, - mServedInputConnection)); - } - } + notifyInputConnectionFinished(); mServedView = null; mCompletions = null; @@ -671,7 +659,25 @@ public final class InputMethodManager { clearConnectionLocked(); } } - + + /** + * Notifies the served view that the current InputConnection will no longer be used. + */ + private void notifyInputConnectionFinished() { + if (mServedView != null && mServedInputConnection != null) { + // We need to tell the previously served view that it is no + // longer the input target, so it can reset its state. Schedule + // this call on its window's Handler so it will be on the correct + // thread and outside of our lock. + Handler vh = mServedView.getHandler(); + if (vh != null) { + // This will result in a call to reportFinishInputConnection() below. + vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION, + mServedInputConnection)); + } + } + } + /** * Called from the FINISH_INPUT_CONNECTION message above. * @hide @@ -681,7 +687,7 @@ public final class InputMethodManager { ic.finishComposingText(); } } - + public void displayCompletions(View view, CompletionInfo[] completions) { checkFocus(); synchronized (mH) { @@ -831,7 +837,7 @@ public final class InputMethodManager { * shown with {@link #SHOW_FORCED}. */ public static final int HIDE_NOT_ALWAYS = 0x0002; - + /** * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)} * without a result: request to hide the soft input window from the @@ -993,7 +999,7 @@ public final class InputMethodManager { tba.fieldId = view.getId(); InputConnection ic = view.onCreateInputConnection(tba); if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); - + synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. @@ -1012,6 +1018,8 @@ public final class InputMethodManager { // Hook 'em up and let 'er rip. mCurrentTextBoxAttribute = tba; mServedConnecting = false; + // Notify the served view that its previous input connection is finished + notifyInputConnectionFinished(); mServedInputConnection = ic; IInputContext servedContext; if (ic != null) { @@ -1115,7 +1123,7 @@ public final class InputMethodManager { } } - void scheduleCheckFocusLocked(View view) { + static void scheduleCheckFocusLocked(View view) { Handler vh = view.getHandler(); if (vh != null && !vh.hasMessages(ViewRootImpl.CHECK_FOCUS)) { // This will result in a call to checkFocus() below. diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index 21364c1..bc0557e 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -198,6 +198,10 @@ public class HTML5VideoFullScreen extends HTML5VideoView // Call into the native to ask for the state, if still in play mode, // this will trigger the video to play. mProxy.dispatchOnRestoreState(); + + if (getStartWhenPrepared()) { + mPlayer.start(); + } } public boolean fullScreenExited() { diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java index 1d8bda7..73166cb 100644 --- a/core/java/android/webkit/HTML5VideoView.java +++ b/core/java/android/webkit/HTML5VideoView.java @@ -194,20 +194,9 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { mPlayer.setOnInfoListener(proxy); } - // Normally called immediately after setVideoURI. But for full screen, - // this should be after surface holder created - public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) { - // SurfaceTexture will be created lazily here for inline mode - decideDisplayMode(); - - setOnCompletionListener(proxy); - setOnPreparedListener(proxy); - setOnErrorListener(proxy); - setOnInfoListener(proxy); - // When there is exception, we could just bail out silently. - // No Video will be played though. Write the stack for debug + public void prepareDataCommon(HTML5VideoViewProxy proxy) { try { - mPlayer.setDataSource(mProxy.getContext(), mUri, mHeaders); + mPlayer.setDataSource(proxy.getContext(), mUri, mHeaders); mPlayer.prepareAsync(); } catch (IllegalArgumentException e) { e.printStackTrace(); @@ -219,6 +208,25 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { mCurrentState = STATE_NOTPREPARED; } + public void reprepareData(HTML5VideoViewProxy proxy) { + mPlayer.reset(); + prepareDataCommon(proxy); + } + + // Normally called immediately after setVideoURI. But for full screen, + // this should be after surface holder created + public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) { + // SurfaceTexture will be created lazily here for inline mode + decideDisplayMode(); + + setOnCompletionListener(proxy); + setOnPreparedListener(proxy); + setOnErrorListener(proxy); + setOnInfoListener(proxy); + + prepareDataCommon(proxy); + } + // Common code public int getVideoLayerId() { @@ -324,4 +332,14 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { return false; } + private boolean m_startWhenPrepared = false; + + public void setStartWhenPrepared(boolean willPlay) { + m_startWhenPrepared = willPlay; + } + + public boolean getStartWhenPrepared() { + return m_startWhenPrepared; + } + } diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index 1c09bb9..d306c86 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -182,6 +182,21 @@ class HTML5VideoViewProxy extends Handler if (mHTML5VideoView != null) { currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); backFromFullScreenMode = mHTML5VideoView.fullScreenExited(); + + // When playing video back to back in full screen mode, + // javascript will switch the src and call play. + // In this case, we can just reuse the same full screen view, + // and play the video after prepared. + if (mHTML5VideoView.isFullScreenMode() + && !backFromFullScreenMode + && currentVideoLayerId != videoLayerId + && mCurrentProxy != proxy) { + mCurrentProxy = proxy; + mHTML5VideoView.setStartWhenPrepared(true); + mHTML5VideoView.setVideoURI(url, proxy); + mHTML5VideoView.reprepareData(proxy); + return; + } } if (backFromFullScreenMode diff --git a/core/java/android/webkit/SelectActionModeCallback.java b/core/java/android/webkit/SelectActionModeCallback.java index 8c174aa..cdf20f6 100644 --- a/core/java/android/webkit/SelectActionModeCallback.java +++ b/core/java/android/webkit/SelectActionModeCallback.java @@ -17,6 +17,7 @@ package android.webkit; import android.app.SearchManager; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.provider.Browser; @@ -27,11 +28,16 @@ import android.view.MenuItem; class SelectActionModeCallback implements ActionMode.Callback { private WebView mWebView; private ActionMode mActionMode; + private boolean mIsTextSelected = true; void setWebView(WebView webView) { mWebView = webView; } + void setTextSelected(boolean isTextSelected) { + mIsTextSelected = isTextSelected; + } + void finish() { // It is possible that onCreateActionMode was never called, in the case // where there is no ActionBar, for example. @@ -52,17 +58,25 @@ class SelectActionModeCallback implements ActionMode.Callback { mode.setTitle(allowText ? context.getString(com.android.internal.R.string.textSelectionCABTitle) : null); - if (!mode.isUiFocusable()) { - // If the action mode UI we're running in isn't capable of taking window focus - // the user won't be able to type into the find on page UI. Disable this functionality. - // (Note that this should only happen in floating dialog windows.) - // This can be removed once we can handle multiple focusable windows at a time - // in a better way. - final MenuItem findOnPageItem = menu.findItem(com.android.internal.R.id.find); - if (findOnPageItem != null) { - findOnPageItem.setVisible(false); - } - } + // If the action mode UI we're running in isn't capable of taking window focus + // the user won't be able to type into the find on page UI. Disable this functionality. + // (Note that this should only happen in floating dialog windows.) + // This can be removed once we can handle multiple focusable windows at a time + // in a better way. + ClipboardManager cm = (ClipboardManager)(context + .getSystemService(Context.CLIPBOARD_SERVICE)); + boolean isFocusable = mode.isUiFocusable(); + boolean isEditable = mWebView.focusCandidateIsEditableText(); + boolean canPaste = isEditable && cm.hasPrimaryClip() && isFocusable; + boolean canFind = !isEditable && isFocusable; + boolean canCut = isEditable && mIsTextSelected && isFocusable; + boolean canCopy = mIsTextSelected; + boolean canWebSearch = mIsTextSelected; + setMenuVisibility(menu, canFind, com.android.internal.R.id.find); + setMenuVisibility(menu, canPaste, com.android.internal.R.id.paste); + setMenuVisibility(menu, canCut, com.android.internal.R.id.cut); + setMenuVisibility(menu, canCopy, com.android.internal.R.id.copy); + setMenuVisibility(menu, canWebSearch, com.android.internal.R.id.websearch); mActionMode = mode; return true; } @@ -75,11 +89,21 @@ class SelectActionModeCallback implements ActionMode.Callback { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch(item.getItemId()) { + case android.R.id.cut: + mWebView.cutSelection(); + mode.finish(); + break; + case android.R.id.copy: mWebView.copySelection(); mode.finish(); break; + case android.R.id.paste: + mWebView.pasteFromClipboard(); + mode.finish(); + break; + case com.android.internal.R.id.share: String selection = mWebView.getSelection(); Browser.sendString(mWebView.getContext(), selection); @@ -113,4 +137,11 @@ class SelectActionModeCallback implements ActionMode.Callback { public void onDestroyActionMode(ActionMode mode) { mWebView.selectionDone(); } + + private void setMenuVisibility(Menu menu, boolean visible, int resourceId) { + final MenuItem item = menu.findItem(resourceId); + if (item != null) { + item.setVisible(visible); + } + } } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index c3b6416..b255c57 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -20,6 +20,7 @@ import android.annotation.Widget; import android.app.ActivityManager; import android.app.AlertDialog; import android.content.BroadcastReceiver; +import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentCallbacks2; import android.content.Context; @@ -59,6 +60,10 @@ import android.os.StrictMode; import android.os.SystemClock; import android.provider.Settings; import android.speech.tts.TextToSpeech; +import android.text.Editable; +import android.text.InputType; +import android.text.Selection; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; @@ -362,39 +367,126 @@ public class WebView extends AbsoluteLayout } /** - * InputConnection used for ContentEditable. This captures the 'delete' - * commands and sends delete key presses. + * InputConnection used for ContentEditable. This captures changes + * to the text and sends them either as key strokes or text changes. */ private class WebViewInputConnection extends BaseInputConnection { + // Used for mapping characters to keys typed. + private KeyCharacterMap mKeyCharacterMap; + public WebViewInputConnection() { - super(WebView.this, false); + super(WebView.this, true); + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + Editable editable = getEditable(); + int start = getComposingSpanStart(editable); + int end = getComposingSpanEnd(editable); + if (start < 0 || end < 0) { + start = Selection.getSelectionStart(editable); + end = Selection.getSelectionEnd(editable); + } + if (end < start) { + int temp = end; + end = start; + start = temp; + } + setNewText(start, end, text); + return super.setComposingText(text, newCursorPosition); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + setComposingText(text, newCursorPosition); + finishComposingText(); + return true; + } + + @Override + public boolean deleteSurroundingText(int leftLength, int rightLength) { + Editable editable = getEditable(); + int cursorPosition = Selection.getSelectionEnd(editable); + int startDelete = Math.max(0, cursorPosition - leftLength); + int endDelete = Math.min(editable.length(), + cursorPosition + rightLength); + setNewText(startDelete, endDelete, ""); + return super.deleteSurroundingText(leftLength, rightLength); + } + + /** + * Sends a text change to webkit indirectly. If it is a single- + * character add or delete, it sends it as a key stroke. If it cannot + * be represented as a key stroke, it sends it as a field change. + * @param start The start offset (inclusive) of the text being changed. + * @param end The end offset (exclusive) of the text being changed. + * @param text The new text to replace the changed text. + */ + private void setNewText(int start, int end, CharSequence text) { + Editable editable = getEditable(); + CharSequence original = editable.subSequence(start, end); + boolean isCharacterAdd = false; + boolean isCharacterDelete = false; + int textLength = text.length(); + int originalLength = original.length(); + if (textLength > originalLength) { + isCharacterAdd = (textLength == originalLength + 1) + && TextUtils.regionMatches(text, 0, original, 0, + originalLength); + } else if (originalLength > textLength) { + isCharacterDelete = (textLength == originalLength - 1) + && TextUtils.regionMatches(text, 0, original, 0, + textLength); + } + if (isCharacterAdd) { + sendCharacter(text.charAt(textLength - 1)); + mTextGeneration++; + } else if (isCharacterDelete) { + sendDeleteKey(); + mTextGeneration++; + } else if (textLength != originalLength || + !TextUtils.regionMatches(text, 0, original, 0, + textLength)) { + // Send a message so that key strokes and text replacement + // do not come out of order. + Message replaceMessage = mPrivateHandler.obtainMessage( + REPLACE_TEXT, start, end, text.toString()); + mPrivateHandler.sendMessage(replaceMessage); + } + } + + /** + * Send a single character to the WebView as a key down and up event. + * @param c The character to be sent. + */ + private void sendCharacter(char c) { + if (mKeyCharacterMap == null) { + mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + } + char[] chars = new char[1]; + chars[0] = c; + KeyEvent[] events = mKeyCharacterMap.getEvents(chars); + if (events != null) { + for (KeyEvent event : events) { + sendKeyEvent(event); + } + } } - private void sendKeyPress(int keyCode) { + /** + * Send the delete character as a key down and up event. + */ + private void sendDeleteKey() { long eventTime = SystemClock.uptimeMillis(); sendKeyEvent(new KeyEvent(eventTime, eventTime, - KeyEvent.ACTION_DOWN, keyCode, 0, 0, + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD)); sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, - KeyEvent.ACTION_UP, keyCode, 0, 0, + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD)); } - - @Override - public boolean deleteSurroundingText(int leftLength, int rightLength) { - // Look for one-character delete and send it as a key press. - if (leftLength == 1 && rightLength == 0) { - sendKeyPress(KeyEvent.KEYCODE_DEL); - } else if (leftLength == 0 && rightLength == 1){ - sendKeyPress(KeyEvent.KEYCODE_FORWARD_DEL); - } else if (mWebViewCore != null) { - mWebViewCore.sendMessage(EventHub.DELETE_SURROUNDING_TEXT, - leftLength, rightLength); - } - return super.deleteSurroundingText(leftLength, rightLength); - } } @@ -422,7 +514,7 @@ public class WebView extends AbsoluteLayout private final Rect mViewRectViewport = new Rect(); private final RectF mVisibleContentRect = new RectF(); private boolean mGLViewportEmpty = false; - WebViewInputConnection mInputConnection = new WebViewInputConnection(); + WebViewInputConnection mInputConnection = null; /** @@ -712,13 +804,11 @@ public class WebView extends AbsoluteLayout static boolean sDisableNavcache = false; // the color used to highlight the touch rectangles - private static final int HIGHLIGHT_COLOR = 0x6633b5e5; - // the round corner for the highlight path - private static final float TOUCH_HIGHLIGHT_ARC = 5.0f; + static final int HIGHLIGHT_COLOR = 0x6633b5e5; // the region indicating where the user touched on the screen private Region mTouchHighlightRegion = new Region(); // the paint for the touch highlight - private Paint mTouchHightlightPaint; + private Paint mTouchHightlightPaint = new Paint(); // debug only private static final boolean DEBUG_TOUCH_HIGHLIGHT = true; private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000; @@ -799,6 +889,10 @@ public class WebView extends AbsoluteLayout static final int UPDATE_ZOOM_DENSITY = 139; static final int EXIT_FULLSCREEN_VIDEO = 140; + static final int COPY_TO_CLIPBOARD = 141; + static final int INIT_EDIT_FIELD = 142; + static final int REPLACE_TEXT = 143; + private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; private static final int LAST_PACKAGE_MSG_ID = HIT_TEST_RESULT; @@ -4430,10 +4524,6 @@ public class WebView extends AbsoluteLayout Rect r = mTouchHighlightRegion.getBounds(); postInvalidateDelayed(delay, r.left, r.top, r.right, r.bottom); } else { - if (mTouchHightlightPaint == null) { - mTouchHightlightPaint = new Paint(); - mTouchHightlightPaint.setColor(HIGHLIGHT_COLOR); - } RegionIterator iter = new RegionIterator(mTouchHighlightRegion); Rect r = new Rect(); while (iter.next(r)) { @@ -4526,6 +4616,11 @@ public class WebView extends AbsoluteLayout final boolean isSelecting = selectText(); if (isSelecting) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else if (focusCandidateIsEditableText()) { + mSelectCallback = new SelectActionModeCallback(); + mSelectCallback.setWebView(this); + mSelectCallback.setTextSelected(false); + startActionMode(mSelectCallback); } return isSelecting; } @@ -4948,15 +5043,19 @@ public class WebView extends AbsoluteLayout } @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN + outAttrs.inputType = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_NORMAL; + | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE + | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; + outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; + + if (mInputConnection == null) { + mInputConnection = new WebViewInputConnection(); + } + outAttrs.initialCapsMode = mInputConnection.getCursorCapsMode(InputType.TYPE_CLASS_TEXT); return mInputConnection; } @@ -5737,12 +5836,49 @@ public class WebView extends AbsoluteLayout ClipboardManager cm = (ClipboardManager)getContext() .getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(selection); + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.COPY_TEXT, handles); } invalidate(); // remove selection region and pointer return copiedSomething; } /** + * Cut the selected text into the clipboard + * + * @hide This is an implementation detail + */ + public void cutSelection() { + copySelection(); + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles); + } + + /** + * Paste text from the clipboard to the cursor position. + * + * @hide This is an implementation detail + */ + public void pasteFromClipboard() { + ClipboardManager cm = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = cm.getPrimaryClip(); + if (clipData != null) { + ClipData.Item clipItem = clipData.getItemAt(0); + CharSequence pasteText = clipItem.getText(); + if (pasteText != null) { + int[] handles = new int[4]; + nativeGetSelectionHandles(mNativeClass, handles); + mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles); + mWebViewCore.sendMessage(EventHub.INSERT_TEXT, + pasteText.toString()); + } + } + } + + /** * @hide This is an implementation detail. */ public SearchBox getSearchBox() { @@ -8874,7 +9010,7 @@ public class WebView extends AbsoluteLayout case HIT_TEST_RESULT: WebKitHitTest hit = (WebKitHitTest) msg.obj; mFocusedNode = hit; - setTouchHighlightRects(hit != null ? hit.mTouchRects : null); + setTouchHighlightRects(hit); if (hit == null) { mInitialHitTestResult = null; } else { @@ -8922,6 +9058,35 @@ public class WebView extends AbsoluteLayout nativeSelectAt(msg.arg1, msg.arg2); break; + case COPY_TO_CLIPBOARD: + copyToClipboard((String) msg.obj); + break; + + case INIT_EDIT_FIELD: + if (mInputConnection != null) { + mTextGeneration = 0; + String text = (String)msg.obj; + mInputConnection.beginBatchEdit(); + Editable editable = mInputConnection.getEditable(); + editable.replace(0, editable.length(), text); + int start = msg.arg1; + int end = msg.arg2; + mInputConnection.setComposingRegion(end, end); + mInputConnection.setSelection(start, end); + mInputConnection.endBatchEdit(); + } + break; + + case REPLACE_TEXT:{ + String text = (String)msg.obj; + int start = msg.arg1; + int end = msg.arg2; + int cursorPosition = start + text.length(); + replaceTextfieldText(start, end, text, + cursorPosition, cursorPosition); + break; + } + default: super.handleMessage(msg); break; @@ -8929,12 +9094,14 @@ public class WebView extends AbsoluteLayout } } - private void setTouchHighlightRects(Rect[] rects) { + private void setTouchHighlightRects(WebKitHitTest hit) { + Rect[] rects = hit != null ? hit.mTouchRects : null; if (!mTouchHighlightRegion.isEmpty()) { invalidate(mTouchHighlightRegion.getBounds()); mTouchHighlightRegion.setEmpty(); } if (rects != null) { + mTouchHightlightPaint.setColor(hit.mTapHighlightColor); for (Rect rect : rects) { Rect viewRect = contentToViewRect(rect); // some sites, like stories in nytimes.com, set @@ -9045,10 +9212,13 @@ public class WebView extends AbsoluteLayout */ private void updateTextSelectionFromMessage(int nodePointer, int textGeneration, WebViewCore.TextSelectionData data) { - if (inEditingMode() - && mWebTextView.isSameTextField(nodePointer) - && textGeneration == mTextGeneration) { - mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd); + if (textGeneration == mTextGeneration) { + if (inEditingMode() + && mWebTextView.isSameTextField(nodePointer)) { + mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd); + } else if (mInputConnection != null){ + mInputConnection.setSelection(data.mStart, data.mEnd); + } } } @@ -9598,6 +9768,18 @@ public class WebView extends AbsoluteLayout } /** + * Copy text into the clipboard. This is called indirectly from + * WebViewCore. + * @param text The text to put into the clipboard. + */ + private void copyToClipboard(String text) { + ClipboardManager cm = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getTitle(), text); + cm.setPrimaryClip(clip); + } + + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. * @hide @@ -9683,6 +9865,23 @@ public class WebView extends AbsoluteLayout return nativeTileProfilingGetFloat(frame, tile, key); } + /** + * Checks the focused content for an editable text field. This can be + * text input or ContentEditable. + * @return true if the focused item is an editable text field. + */ + boolean focusCandidateIsEditableText() { + boolean isEditable = false; + // TODO: reverse sDisableNavcache so that its name is positive + boolean isNavcacheEnabled = !sDisableNavcache; + if (isNavcacheEnabled) { + isEditable = nativeFocusCandidateIsEditableText(mNativeClass); + } else if (mFocusedNode != null) { + isEditable = mFocusedNode.mEditable; + } + return isEditable; + } + private native int nativeCacheHitFramePointer(); private native boolean nativeCacheHitIsPlugin(); private native Rect nativeCacheHitNodeBounds(); @@ -9728,6 +9927,7 @@ public class WebView extends AbsoluteLayout /* package */ native boolean nativeFocusCandidateIsPassword(); private native boolean nativeFocusCandidateIsRtlText(); private native boolean nativeFocusCandidateIsTextInput(); + private native boolean nativeFocusCandidateIsEditableText(int nativeClass); /* package */ native int nativeFocusCandidateMaxLength(); /* package */ native boolean nativeFocusCandidateIsAutoComplete(); /* package */ native boolean nativeFocusCandidateIsSpellcheck(); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index bfca07c..fe51581 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -647,18 +647,6 @@ public final class WebViewCore { int end, int textGeneration); /** - * Delete text near the cursor. - * @param nativeClass The pointer to the native class (mNativeClass) - * @param leftLength The number of characters to the left of the cursor to - * delete - * @param rightLength The number of characters to the right of the cursor - * to delete. - */ - private native void nativeDeleteSurroundingText(int nativeClass, - int leftLength, - int rightLength); - - /** * Set the selection to (start, end) in the focused textfield. If start and * end are out of order, swap them. * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass @@ -881,6 +869,7 @@ public final class WebViewCore { String mTitle; Rect[] mTouchRects; boolean mEditable; + int mTapHighlightColor = WebView.HIGHLIGHT_COLOR; // These are the input values that produced this hit test int mHitTestX; @@ -1125,6 +1114,11 @@ public final class WebViewCore { // private message ids private static final int DESTROY = 200; + // for cut & paste + static final int COPY_TEXT = 210; + static final int DELETE_TEXT = 211; + static final int INSERT_TEXT = 212; + // Private handler for WebCore messages. private Handler mHandler; // Message queue for containing messages before the WebCore thread is @@ -1570,11 +1564,6 @@ public final class WebViewCore { deleteSelectionData.mStart, deleteSelectionData.mEnd, msg.arg1); break; - case DELETE_SURROUNDING_TEXT: - nativeDeleteSurroundingText(mNativeClass, - msg.arg1, msg.arg2); - break; - case SET_SELECTION: nativeSetSelection(mNativeClass, msg.arg1, msg.arg2); break; @@ -1736,6 +1725,28 @@ public final class WebViewCore { Rect rect = (Rect) msg.obj; nativeScrollLayer(mNativeClass, nativeLayer, rect); + break; + + case DELETE_TEXT: { + int[] handles = (int[]) msg.obj; + nativeDeleteText(mNativeClass, handles[0], + handles[1], handles[2], handles[3]); + break; + } + case COPY_TEXT: { + int[] handles = (int[]) msg.obj; + String copiedText = nativeGetText(mNativeClass, + handles[0], handles[1], handles[2], + handles[3]); + if (copiedText != null) { + mWebView.mPrivateHandler.obtainMessage(WebView.COPY_TO_CLIPBOARD, copiedText) + .sendToTarget(); + } + break; + } + case INSERT_TEXT: + nativeInsertText(mNativeClass, (String) msg.obj); + break; } } }; @@ -2711,6 +2722,15 @@ public final class WebViewCore { WebView.FIND_AGAIN).sendToTarget(); } + // called by JNI + private void initEditField(String text, int start, int end) { + if (mWebView == null) { + return; + } + Message.obtain(mWebView.mPrivateHandler, + WebView.INIT_EDIT_FIELD, start, end, text).sendToTarget(); + } + private native void nativeUpdateFrameCacheIfLoading(int nativeClass); private native void nativeRevealSelection(int nativeClass); private native String nativeRequestLabel(int nativeClass, int framePtr, @@ -2975,4 +2995,35 @@ public final class WebViewCore { private native void nativeAutoFillForm(int nativeClass, int queryId); private native void nativeScrollLayer(int nativeClass, int layer, Rect rect); + + /** + * Deletes editable text between two points. Note that the selection may + * differ from the WebView's selection because the algorithms for selecting + * text differs for non-LTR text. Any text that isn't editable will be + * left unchanged. + * @param nativeClass The pointer to the native class (mNativeClass) + * @param startX The X position of the top-left selection point. + * @param startY The Y position of the top-left selection point. + * @param endX The X position of the bottom-right selection point. + * @param endY The Y position of the bottom-right selection point. + */ + private native void nativeDeleteText(int nativeClass, + int startX, int startY, int endX, int endY); + /** + * Inserts text at the current cursor position. If the currently-focused + * node does not have a cursor position then this function does nothing. + */ + private native void nativeInsertText(int nativeClass, String text); + /** + * Gets the text between two selection points. Note that the selection + * may differ from the WebView's selection because the algorithms for + * selecting text differs for non-LTR text. + * @param nativeClass The pointer to the native class (mNativeClass) + * @param startX The X position of the top-left selection point. + * @param startY The Y position of the top-left selection point. + * @param endX The X position of the bottom-right selection point. + * @param endY The Y position of the bottom-right selection point. + */ + private native String nativeGetText(int nativeClass, + int startX, int startY, int endX, int endY); } diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index 60b24bc..be6b4e2 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -33,8 +33,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; import android.widget.ActivityChooserModel.ActivityChooserModelClient; /** @@ -366,7 +364,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod getListPopupWindow().dismiss(); ViewTreeObserver viewTreeObserver = getViewTreeObserver(); if (viewTreeObserver.isAlive()) { - viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); + viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener); } } return true; @@ -400,7 +398,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod } ViewTreeObserver viewTreeObserver = getViewTreeObserver(); if (viewTreeObserver.isAlive()) { - viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener); + viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener); } mIsAttachedToWindow = false; } @@ -547,6 +545,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod position = mAdapter.getShowDefaultActivity() ? position : position + 1; Intent launchIntent = mAdapter.getDataModel().chooseActivity(position); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } } @@ -564,6 +563,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity); Intent launchIntent = mAdapter.getDataModel().chooseActivity(index); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } } else if (view == mExpandActivityOverflowButton) { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index e226d37..bb00049 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -558,7 +558,9 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> mCurrentWindowEnd = newWindowEnd; mCurrentWindowStartUnbounded = newWindowStartUnbounded; if (mRemoteViewsAdapter != null) { - mRemoteViewsAdapter.setVisibleRangeHint(mCurrentWindowStart, mCurrentWindowEnd); + int adapterStart = modulo(mCurrentWindowStart, adapterCount); + int adapterEnd = modulo(mCurrentWindowEnd, adapterCount); + mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd); } } requestLayout(); diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 07523e3..f7a6b27 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -1085,10 +1085,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe for (int i = 0; i < count; i++) { if (adapter.isEnabled(i)) { - realCount++; Object item = adapter.getItem(i); long id = adapter.getItemId(i); - completions[i] = new CompletionInfo(id, i, convertSelectionToString(item)); + completions[realCount] = new CompletionInfo(id, realCount, + convertSelectionToString(item)); + realCount++; } } diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index a210f0b..d395fb2 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -55,12 +55,12 @@ import com.android.internal.R; /** * A widget that enables the user to select a number form a predefined range. - * The widget presents an input filed and up and down buttons for selecting the + * The widget presents an input field and up and down buttons for selecting the * current value. Pressing/long pressing the up and down buttons increments and - * decrements the current value respectively. Touching the input filed shows a + * decrements the current value respectively. Touching the input field shows a * scroll wheel, tapping on which while shown and not moving allows direct edit * of the current value. Sliding motions up or down hide the buttons and the - * input filed, show the scroll wheel, and rotate the latter. Flinging is + * input field, show the scroll wheel, and rotate the latter. Flinging is * also supported. The widget enables mapping from positions to strings such * that instead the position index the corresponding string is displayed. * <p> @@ -71,6 +71,11 @@ import com.android.internal.R; public class NumberPicker extends LinearLayout { /** + * The number of items show in the selector wheel. + */ + public static final int SELECTOR_WHEEL_ITEM_COUNT = 5; + + /** * The default update interval during long press. */ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; @@ -1137,14 +1142,17 @@ public class NumberPicker extends LinearLayout { * items shown on the selector wheel) the selector wheel wrapping is * enabled. * </p> - * + * <p> + * <strong>Note:</strong> If the number of items, i.e. the range + * ({@link #getMaxValue()} - {@link #getMinValue()}) is less than + * {@link #SELECTOR_WHEEL_ITEM_COUNT}, the selector wheel will not + * wrap. Hence, in such a case calling this method is a NOP. + * </p> * @param wrapSelectorWheel Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelectorWheel) { - if (wrapSelectorWheel && (mMaxValue - mMinValue) < mSelectorIndices.length) { - throw new IllegalStateException("Range less than selector items count."); - } - if (wrapSelectorWheel != mWrapSelectorWheel) { + final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; + if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { mWrapSelectorWheel = wrapSelectorWheel; updateIncrementAndDecrementButtonsVisibilityState(); } diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index bb27b73..22e9ef1 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -279,6 +279,7 @@ public class ShareActionProvider extends ActionProvider { final int itemId = item.getItemId(); Intent launchIntent = dataModel.chooseActivity(itemId); if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); mContext.startActivity(launchIntent); } return true; diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index a106159..a9aec82 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -102,7 +102,8 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextServicesManager = (TextServicesManager) mTextView.getContext(). getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); - if (!mTextServicesManager.isSpellCheckerEnabled()) { + if (!mTextServicesManager.isSpellCheckerEnabled() + || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { mSpellCheckerSession = null; } else { mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 02144a8..b82a632 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -342,6 +342,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; + private Runnable mShowSuggestionRunnable; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; @@ -357,7 +358,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private float mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; - private final int mSquaredTouchSlopDistance; // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; @@ -443,15 +443,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, null); } - public TextView(Context context, - AttributeSet attrs) { + public TextView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.textViewStyle); } @SuppressWarnings("deprecation") - public TextView(Context context, - AttributeSet attrs, - int defStyle) { + public TextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mText = ""; @@ -1134,10 +1131,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLongClickable(longClickable); prepareCursorControllers(); - - final ViewConfiguration viewConfiguration = ViewConfiguration.get(context); - final int touchSlop = viewConfiguration.getScaledTouchSlop(); - mSquaredTouchSlopDistance = touchSlop * touchSlop; } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -3202,8 +3195,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int n = mFilters.length; for (int i = 0; i < n; i++) { - CharSequence out = mFilters[i].filter(text, 0, text.length(), - EMPTY_SPANNED, 0, 0); + CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0); if (out != null) { text = out; } @@ -4522,6 +4514,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mSelectionModifierCursorController.onDetached(); } + if (mShowSuggestionRunnable != null) { + removeCallbacks(mShowSuggestionRunnable); + } + hideControllers(); resetResolvedDrawables(); @@ -5273,10 +5269,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener state.handleUpEvent(event); } if (event.isTracking() && !event.isCanceled()) { - if (isInSelectionMode) { - stopSelectionActionMode(); - return true; - } + stopSelectionActionMode(); + return true; } } } @@ -5621,11 +5615,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); } - @Override public boolean onCheckIsTextEditor() { + @Override + public boolean onCheckIsTextEditor() { return mInputType != EditorInfo.TYPE_NULL; } - @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { if (mInputMethodState == null) { mInputMethodState = new InputMethodState(); @@ -6799,6 +6795,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) mTextDisplayListIsValid = false; + } + /** * Returns true if anything changed. */ @@ -8339,6 +8341,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener getSelectionController().onTouchEvent(event); } + if (mShowSuggestionRunnable != null) { + removeCallbacks(mShowSuggestionRunnable); + } + if (action == MotionEvent.ACTION_DOWN) { mLastDownPositionX = event.getX(); mLastDownPositionY = event.getY(); @@ -8362,7 +8368,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && - !shouldIgnoreActionUpEvent() && isFocused(); + !mIgnoreActionUpEvent && isFocused(); if ((mMovement != null || onCheckIsTextEditor()) && isEnabled() && mText instanceof Spannable && mLayout != null) { @@ -8379,7 +8385,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(), getSelectionEnd(), ClickableSpan.class); - if (links.length != 0) { + if (links.length > 0) { links[0].onClick(this); handled = true; } @@ -8402,8 +8408,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (!extractedTextModeWillBeStarted()) { if (isCursorInsideEasyCorrectionSpan()) { - showSuggestions(); + if (mShowSuggestionRunnable == null) { + mShowSuggestionRunnable = new Runnable() { + public void run() { + showSuggestions(); + } + }; + } + postDelayed(mShowSuggestionRunnable, + ViewConfiguration.getDoubleTapTimeout()); } else if (hasInsertionController()) { + // Move cursor + final int offset = getOffsetForPosition(event.getX(), event.getY()); + Selection.setSelection((Spannable) mText, offset); getInsertionController().show(); } } @@ -8538,17 +8555,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mIgnoreActionUpEvent = true; } - /** - * This method is only valid during a touch event. - * - * @return true when the ACTION_UP event should be ignored, false otherwise. - * - * @hide - */ - public boolean shouldIgnoreActionUpEvent() { - return mIgnoreActionUpEvent; - } - @Override public boolean onTrackballEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && @@ -8929,14 +8935,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wordIterator.setCharSequence(mText, minOffset, maxOffset); selectionStart = wordIterator.getBeginning(minOffset); - if (selectionStart == BreakIterator.DONE) return false; - selectionEnd = wordIterator.getEnd(maxOffset); - if (selectionEnd == BreakIterator.DONE) return false; - if (selectionStart == selectionEnd) { + if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || + selectionStart == selectionEnd) { // Possible when the word iterator does not properly handle the text's language - long range = getCharRange(selectionStart); + long range = getCharRange(minOffset); selectionStart = extractRangeStartFromLong(range); selectionEnd = extractRangeEndFromLong(range); } @@ -9248,7 +9252,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean vibrate = true; if (super.performLongClick()) { - mDiscardNextActionUp = true; handled = true; } @@ -10157,8 +10160,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean willExtract = extractedTextModeWillBeStarted(); - // Do not start the action mode when extracted text will show up full screen, thus - // immediately hiding the newly created action bar, which would be visually distracting. + // Do not start the action mode when extracted text will show up full screen, which would + // immediately hide the newly created action bar and would be visually distracting. if (!willExtract) { ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); mSelectionActionMode = startActionMode(actionModeCallback); @@ -10184,7 +10187,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - private void stopSelectionActionMode() { + /** + * @hide + */ + protected void stopSelectionActionMode() { if (mSelectionActionMode != null) { // This will hide the mSelectionModifierCursorController mSelectionActionMode.finish(); @@ -10798,7 +10804,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final float deltaX = mDownPositionX - ev.getRawX(); final float deltaY = mDownPositionY - ev.getRawY(); final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + final int touchSlop = viewConfiguration.getScaledTouchSlop(); + + if (distanceSquared < touchSlop * touchSlop) { if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) { // Tapping on the handle dismisses the displayed action popup mActionPopupWindow.hide(); @@ -11012,7 +11023,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Double tap detection private long mPreviousTapUpTime = 0; - private float mPreviousTapPositionX, mPreviousTapPositionY; + private float mDownPositionX, mDownPositionY; + private boolean mGestureStayedInTapRegion; SelectionModifierCursorController() { resetTouchOffsets(); @@ -11075,20 +11087,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); // Double tap detection - long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; - if (duration <= ViewConfiguration.getDoubleTapTimeout() && - isPositionOnText(x, y)) { - final float deltaX = x - mPreviousTapPositionX; - final float deltaY = y - mPreviousTapPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { - startSelectionActionMode(); - mDiscardNextActionUp = true; + if (mGestureStayedInTapRegion) { + long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; + if (duration <= ViewConfiguration.getDoubleTapTimeout()) { + final float deltaX = x - mDownPositionX; + final float deltaY = y - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); + boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop; + + if (stayedInArea && isPositionOnText(x, y)) { + startSelectionActionMode(); + mDiscardNextActionUp = true; + } } } - mPreviousTapPositionX = x; - mPreviousTapPositionY = y; + mDownPositionX = x; + mDownPositionY = y; + mGestureStayedInTapRegion = true; break; case MotionEvent.ACTION_POINTER_DOWN: @@ -11101,6 +11121,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } break; + case MotionEvent.ACTION_MOVE: + if (mGestureStayedInTapRegion) { + final float deltaX = event.getX() - mDownPositionX; + final float deltaY = event.getY() - mDownPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + TextView.this.getContext()); + int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); + + if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { + mGestureStayedInTapRegion = false; + } + } + break; + case MotionEvent.ACTION_UP: mPreviousTapUpTime = SystemClock.uptimeMillis(); break; diff --git a/core/java/com/android/internal/backup/BackupConstants.java b/core/java/com/android/internal/backup/BackupConstants.java index 906b5d5..4c276b7 100644 --- a/core/java/com/android/internal/backup/BackupConstants.java +++ b/core/java/com/android/internal/backup/BackupConstants.java @@ -24,4 +24,5 @@ public class BackupConstants { public static final int TRANSPORT_ERROR = 1; public static final int TRANSPORT_NOT_INITIALIZED = 2; public static final int AGENT_ERROR = 3; + public static final int AGENT_UNKNOWN = 4; } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 9c45dc6..6a99a2b 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -243,7 +243,7 @@ public class ZygoteInit { private static void preloadClasses() { final VMRuntime runtime = VMRuntime.getRuntime(); - InputStream is = ZygoteInit.class.getClassLoader().getResourceAsStream( + InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream( PRELOADED_CLASSES); if (is == null) { Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + "."); diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index edeb2a8..d1aa1ce 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -142,6 +142,14 @@ public class ArrayUtils return false; } + public static long total(long[] array) { + long total = 0; + for (long value : array) { + total += value; + } + return total; + } + /** * Appends an element to a copy of the array and returns the copy. * @param array The original array, or null to represent an empty array. diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java index 3ce95e7..8a8f315 100644 --- a/core/java/com/android/internal/util/FileRotator.java +++ b/core/java/com/android/internal/util/FileRotator.java @@ -17,9 +17,9 @@ package com.android.internal.util; import android.os.FileUtils; +import android.util.Slog; -import com.android.internal.util.FileRotator.Reader; -import com.android.internal.util.FileRotator.Writer; +import com.android.internal.util.FileRotator.Rewriter; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -41,12 +41,15 @@ import libcore.io.IoUtils; * Instead of manipulating files directly, users implement interfaces that * perform operations on {@link InputStream} and {@link OutputStream}. This * enables atomic rewriting of file contents in - * {@link #combineActive(Reader, Writer, long)}. + * {@link #rewriteActive(Rewriter, long)}. * <p> * Users must periodically call {@link #maybeRotate(long)} to perform actual * rotation. Not inherently thread safe. */ public class FileRotator { + private static final String TAG = "FileRotator"; + private static final boolean LOGD = true; + private final File mBasePath; private final String mPrefix; private final long mRotateAgeMillis; @@ -73,6 +76,15 @@ public class FileRotator { } /** + * External class that reads existing data from given {@link InputStream}, + * then writes any modified data to {@link OutputStream}. + */ + public interface Rewriter extends Reader, Writer { + public void reset(); + public boolean shouldWrite(); + } + + /** * Create a file rotator. * * @param basePath Directory under which all files will be placed. @@ -96,6 +108,8 @@ public class FileRotator { if (!name.startsWith(mPrefix)) continue; if (name.endsWith(SUFFIX_BACKUP)) { + if (LOGD) Slog.d(TAG, "recovering " + name); + final File backupFile = new File(mBasePath, name); final File file = new File( mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length())); @@ -104,6 +118,8 @@ public class FileRotator { backupFile.renameTo(file); } else if (name.endsWith(SUFFIX_NO_BACKUP)) { + if (LOGD) Slog.d(TAG, "recovering " + name); + final File noBackupFile = new File(mBasePath, name); final File file = new File( mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length())); @@ -116,26 +132,95 @@ public class FileRotator { } /** - * Atomically combine data with existing data in currently active file. - * Maintains a backup during write, which is restored if the write fails. + * Delete all files managed by this rotator. */ - public void combineActive(Reader reader, Writer writer, long currentTimeMillis) + public void deleteAll() { + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // delete each file that matches parser + new File(mBasePath, name).delete(); + } + } + + /** + * Process currently active file, first reading any existing data, then + * writing modified data. Maintains a backup during write, which is restored + * if the write fails. + */ + public void rewriteActive(Rewriter rewriter, long currentTimeMillis) throws IOException { final String activeName = getActiveName(currentTimeMillis); + rewriteSingle(rewriter, activeName); + } - final File file = new File(mBasePath, activeName); + @Deprecated + public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis) + throws IOException { + rewriteActive(new Rewriter() { + /** {@inheritDoc} */ + public void reset() { + // ignored + } + + /** {@inheritDoc} */ + public void read(InputStream in) throws IOException { + reader.read(in); + } + + /** {@inheritDoc} */ + public boolean shouldWrite() { + return true; + } + + /** {@inheritDoc} */ + public void write(OutputStream out) throws IOException { + writer.write(out); + } + }, currentTimeMillis); + } + + /** + * Process all files managed by this rotator, usually to rewrite historical + * data. Each file is processed atomically. + */ + public void rewriteAll(Rewriter rewriter) throws IOException { + final FileInfo info = new FileInfo(mPrefix); + for (String name : mBasePath.list()) { + if (!info.parse(name)) continue; + + // process each file that matches parser + rewriteSingle(rewriter, name); + } + } + + /** + * Process a single file atomically, first reading any existing data, then + * writing modified data. Maintains a backup during write, which is restored + * if the write fails. + */ + private void rewriteSingle(Rewriter rewriter, String name) throws IOException { + if (LOGD) Slog.d(TAG, "rewriting " + name); + + final File file = new File(mBasePath, name); final File backupFile; + rewriter.reset(); + if (file.exists()) { // read existing data - readFile(file, reader); + readFile(file, rewriter); + + // skip when rewriter has nothing to write + if (!rewriter.shouldWrite()) return; // backup existing data during write - backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP); + backupFile = new File(mBasePath, name + SUFFIX_BACKUP); file.renameTo(backupFile); try { - writeFile(file, writer); + writeFile(file, rewriter); // write success, delete backup backupFile.delete(); @@ -148,11 +233,11 @@ public class FileRotator { } else { // create empty backup during write - backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP); + backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP); backupFile.createNewFile(); try { - writeFile(file, writer); + writeFile(file, rewriter); // write success, delete empty backup backupFile.delete(); @@ -176,6 +261,8 @@ public class FileRotator { // read file when it overlaps if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) { + if (LOGD) Slog.d(TAG, "reading matching " + name); + final File file = new File(mBasePath, name); readFile(file, reader); } @@ -224,16 +311,20 @@ public class FileRotator { if (!info.parse(name)) continue; if (info.isActive()) { - // found active file; rotate if old enough - if (info.startMillis < rotateBefore) { + if (info.startMillis <= rotateBefore) { + // found active file; rotate if old enough + if (LOGD) Slog.d(TAG, "rotating " + name); + info.endMillis = currentTimeMillis; final File file = new File(mBasePath, name); final File destFile = new File(mBasePath, info.build()); file.renameTo(destFile); } - } else if (info.endMillis < deleteBefore) { + } else if (info.endMillis <= deleteBefore) { // found rotated file; delete if old enough + if (LOGD) Slog.d(TAG, "deleting " + name); + final File file = new File(mBasePath, name); file.delete(); } diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java new file mode 100644 index 0000000..3dd2284 --- /dev/null +++ b/core/java/com/android/internal/util/IndentingPrintWriter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import java.io.PrintWriter; +import java.io.Writer; + +/** + * Lightweight wrapper around {@link PrintWriter} that automatically indents + * newlines based on internal state. Delays writing indent until first actual + * write on a newline, enabling indent modification after newline. + */ +public class IndentingPrintWriter extends PrintWriter { + private final String mIndent; + + private StringBuilder mBuilder = new StringBuilder(); + private String mCurrent = new String(); + private boolean mEmptyLine = true; + + public IndentingPrintWriter(Writer writer, String indent) { + super(writer); + mIndent = indent; + } + + public void increaseIndent() { + mBuilder.append(mIndent); + mCurrent = mBuilder.toString(); + } + + public void decreaseIndent() { + mBuilder.delete(0, mIndent.length()); + mCurrent = mBuilder.toString(); + } + + @Override + public void println() { + super.println(); + mEmptyLine = true; + } + + @Override + public void write(char[] buf, int offset, int count) { + if (mEmptyLine) { + mEmptyLine = false; + super.print(mCurrent); + } + super.write(buf, offset, count); + } +} diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index a235d9a..9024d8d 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -387,9 +387,9 @@ public class InputConnectionWrapper implements InputConnection { } } - public boolean deleteSurroundingText(int leftLength, int rightLength) { + public boolean deleteSurroundingText(int beforeLength, int afterLength) { try { - mIInputContext.deleteSurroundingText(leftLength, rightLength); + mIInputContext.deleteSurroundingText(beforeLength, afterLength); return true; } catch (RemoteException e) { return false; diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 32e733b..9579bce 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -35,6 +35,11 @@ public class EditableInputConnection extends BaseInputConnection { private final TextView mTextView; + // Keeps track of nested begin/end batch edit to ensure this connection always has a + // balanced impact on its associated TextView. + // A negative value means that this connection has been finished by the InputMethodManager. + private int mBatchEditNesting; + public EditableInputConnection(TextView textview) { super(textview, true); mTextView = textview; @@ -48,19 +53,35 @@ public class EditableInputConnection extends BaseInputConnection { } return null; } - + @Override public boolean beginBatchEdit() { - mTextView.beginBatchEdit(); - return true; + synchronized(this) { + if (mBatchEditNesting >= 0) { + mTextView.beginBatchEdit(); + mBatchEditNesting++; + return true; + } + } + return false; } - + @Override public boolean endBatchEdit() { - mTextView.endBatchEdit(); - return true; + synchronized(this) { + if (mBatchEditNesting > 0) { + // When the connection is reset by the InputMethodManager and finishComposingText + // is called, some endBatchEdit calls may still be asynchronously received from the + // IME. Do not take these into account, thus ensuring that this IC's final + // contribution to mTextView's nested batch edit count is zero. + mTextView.endBatchEdit(); + mBatchEditNesting--; + return true; + } + } + return false; } - + @Override public boolean clearMetaKeyStates(int states) { final Editable content = getEditable(); @@ -76,7 +97,24 @@ public class EditableInputConnection extends BaseInputConnection { } return true; } - + + @Override + public boolean finishComposingText() { + final boolean superResult = super.finishComposingText(); + synchronized(this) { + if (mBatchEditNesting < 0) { + // The connection was already finished + return false; + } + while (mBatchEditNesting > 0) { + endBatchEdit(); + } + // Will prevent any further calls to begin or endBatchEdit + mBatchEditNesting = -1; + } + return superResult; + } + @Override public boolean commitCompletion(CompletionInfo text) { if (DEBUG) Log.v(TAG, "commitCompletion " + text); |
