diff options
Diffstat (limited to 'core/java/android')
64 files changed, 2906 insertions, 1921 deletions
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index b59e9dc..1bed706 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -426,13 +426,9 @@ public final class PendingIntent implements Parcelable { */ @Override public boolean equals(Object otherObj) { - if (otherObj == null) { - return false; - } - try { + if (otherObj instanceof PendingIntent) { return mTarget.asBinder().equals(((PendingIntent)otherObj) .mTarget.asBinder()); - } catch (ClassCastException e) { } return false; } @@ -442,6 +438,13 @@ public final class PendingIntent implements Parcelable { return mTarget.asBinder().hashCode(); } + @Override + public String toString() { + return "PendingIntent{" + + Integer.toHexString(System.identityHashCode(this)) + + " target " + (mTarget != null ? mTarget.asBinder() : null) + "}"; + } + public int describeContents() { return 0; } diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 5744ddc..7b8256c 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -31,8 +31,6 @@ import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -64,7 +62,6 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import java.lang.ref.WeakReference; -import java.util.List; import java.util.concurrent.atomic.AtomicLong; /** @@ -538,9 +535,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS testIntent = mVoiceAppSearchIntent; } if (testIntent != null) { - List<ResolveInfo> list = getContext().getPackageManager(). - queryIntentActivities(testIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (list.size() > 0) { + ResolveInfo ri = getContext().getPackageManager(). + resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (ri != null) { visibility = View.VISIBLE; } } diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 72692f4..a6a436f 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -327,11 +327,15 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } /** - * Print the Service's state into the given stream. + * Print the Service's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity service <yourservicename>". + * This is distinct from "dumpsys <servicename>", which only works for + * named system services and which invokes the {@link IBinder#dump} method + * on the {@link IBinder} interface registered with ServiceManager. * * @param fd The raw file descriptor that the dump is being sent to. * @param writer The PrintWriter to which you should dump your state. This will be - * closed for you after you return. + * closed for you after you return. * @param args additional arguments to the dump request. */ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { diff --git a/core/java/android/bluetooth/ScoSocket.java b/core/java/android/bluetooth/ScoSocket.java index 75b3329..a43a08b 100644 --- a/core/java/android/bluetooth/ScoSocket.java +++ b/core/java/android/bluetooth/ScoSocket.java @@ -16,17 +16,12 @@ package android.bluetooth; -import android.content.Context; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; -import java.io.IOException; -import java.lang.Thread; - - /** * The Android Bluetooth API is not finalized, and *will* change. Use at your * own risk. @@ -56,7 +51,7 @@ public class ScoSocket { private int mConnectedCode; private int mClosedCode; - private WakeLock mWakeLock; // held while STATE_CONNECTING or STATE_CONNECTED + private WakeLock mWakeLock; // held while in STATE_CONNECTING static { classInitNative(); @@ -130,6 +125,7 @@ public class ScoSocket { public synchronized void close() { if (DBG) log(this + " SCO OBJECT close() mState = " + mState); + acquireWakeLock(); mState = STATE_CLOSED; closeNative(); releaseWakeLock(); @@ -152,19 +148,16 @@ public class ScoSocket { mState = STATE_CLOSED; } mHandler.obtainMessage(mConnectedCode, mState, -1, this).sendToTarget(); - if (result < 0) { - releaseWakeLock(); - } + releaseWakeLock(); } private synchronized void onAccepted(int result) { if (VDBG) log("onAccepted() " + this); if (mState != STATE_ACCEPT) { - if (DBG) log("Strange state" + this); + if (DBG) log("Strange state " + this); return; } if (result >= 0) { - acquireWakeLock(); mState = STATE_CONNECTED; } else { mState = STATE_CLOSED; @@ -184,13 +177,13 @@ public class ScoSocket { private void acquireWakeLock() { if (!mWakeLock.isHeld()) { mWakeLock.acquire(); - if (VDBG) log("mWakeLock.acquire()" + this); + if (VDBG) log("mWakeLock.acquire() " + this); } } private void releaseWakeLock() { if (mWakeLock.isHeld()) { - if (VDBG) log("mWakeLock.release()" + this); + if (VDBG) log("mWakeLock.release() " + this); mWakeLock.release(); } } diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java index ee08eea..08f6191 100644 --- a/core/java/android/content/BroadcastReceiver.java +++ b/core/java/android/content/BroadcastReceiver.java @@ -79,7 +79,7 @@ import android.util.Log; * <p>The BroadcastReceiver class (when launched as a component through * a manifest's {@link android.R.styleable#AndroidManifestReceiver <receiver>} * tag) is an important part of an - * <a href="{@docRoot}intro/lifecycle.html">application's overall lifecycle</a>.</p> + * <a href="{@docRoot}guide/topics/fundamentals.html#lcycles">application's overall lifecycle</a>.</p> * * <p>Topics covered here: * <ol> @@ -135,7 +135,7 @@ import android.util.Log; * tag in their <code>AndroidManifest.xml</code>) will be able to send an * Intent to the receiver. * - * <p>See the <a href="{@docRoot}devel/security.html">Security Model</a> + * <p>See the <a href="{@docRoot}guide/topics/security/security.html">Security and Permissions</a> * document for more information on permissions and security in general. * * <a name="ProcessLifecycle"></a> diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 226c5ab..3a64cee 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -41,8 +41,8 @@ import java.io.FileNotFoundException; * multiple applications you can use a database directly via * {@link android.database.sqlite.SQLiteDatabase}. * - * <p>See <a href="{@docRoot}devel/data/contentproviders.html">this page</a> for more information on - * content providers.</p> + * <p>For more information, read <a href="{@docRoot}guide/topics/providers/content-providers.html">Content + * Providers</a>.</p> * * <p>When a request is made via * a {@link ContentResolver} the system inspects the authority of the given URI and passes the @@ -226,9 +226,9 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Return the name of the permission required for read-only access to * this content provider. This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. */ public final String getReadPermission() { return mReadPermission; @@ -248,9 +248,9 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Return the name of the permission required for read/write access to * this content provider. This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. */ public final String getWritePermission() { return mWritePermission; @@ -273,9 +273,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * Receives a query request from a client in a local process, and * returns a Cursor. This is called internally by the {@link ContentResolver}. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * <p> * Example client call:<p> * <pre>// Request a specific record. @@ -330,9 +330,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * <code>vnd.android.cursor.item</code> for a single record, * or <code>vnd.android.cursor.dir/</code> for multiple items. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * @param uri the URI to query. * @return a MIME type string, or null if there is no type. @@ -344,9 +344,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} * after inserting. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the - * Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * @param uri The content:// URI of the insertion request. * @param values A set of column_name/value pairs to add to the database. * @return The URI for the newly inserted item. @@ -359,9 +359,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} * after inserting. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * @param uri The content:// URI of the insertion request. * @param values An array of sets of column_name/value pairs to add to the database. @@ -382,9 +382,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyDelete()} * after deleting. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the - * Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * <p>The implementation is responsible for parsing out a row ID at the end * of the URI, if a specific row is being deleted. That is, the client would @@ -405,9 +405,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} * after updating. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the - * Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * @param uri The URI to query. This can potentially have a record ID if this * is an update request for a specific record. @@ -422,9 +422,9 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Open a file blob associated with a content URI. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the - * Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * <p>Returns a * ParcelFileDescriptor, from which you can obtain a @@ -507,9 +507,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * This is intended for use by the sync system. If null then this * content provider is considered not syncable. * This method can be called from multiple - * threads, as described in the - * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of - * the Application Model overview</a>. + * threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: + * Processes and Threads</a>. * * @return the SyncAdapter that is to be used by this ContentProvider, or null * if this ContentProvider is not syncable diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c4d3f9d..c1c3b49 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1766,9 +1766,9 @@ public class Intent implements Parcelable { * next task activity) defines an atomic group of activities that the * user can move to. Tasks can be moved to the foreground and background; * all of the activities inside of a particular task always remain in - * the same order. See the - * <a href="{@docRoot}intro/appmodel.html">Application Model</a> - * documentation for more details on tasks. + * the same order. See + * <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. * * <p>This flag is generally used by activities that want * to present a "launcher" style behavior: they give the user a list of @@ -1801,9 +1801,8 @@ public class Intent implements Parcelable { * <p>This flag is ignored if * {@link #FLAG_ACTIVITY_NEW_TASK} is not set. * - * <p>See the - * <a href="{@docRoot}intro/appmodel.html">Application Model</a> - * documentation for more details on tasks. + * <p>See <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. */ public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 0x08000000; /** @@ -1831,9 +1830,8 @@ public class Intent implements Parcelable { * especially useful, for example, when launching an activity from the * notification manager. * - * <p>See the - * <a href="{@docRoot}intro/appmodel.html">Application Model</a> - * documentation for more details on tasks. + * <p>See <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. */ public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x04000000; /** @@ -3919,8 +3917,8 @@ public class Intent implements Parcelable { * FLAG_RECEIVER_* flags are all for use with * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}. * - * <p>See the <a href="{@docRoot}intro/appmodel.html">Application Model</a> - * documentation for important information on how some of these options impact + * <p>See the <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> documentation for important information on how some of these options impact * the behavior of your application. * * @param flags The desired flags. @@ -4195,14 +4193,11 @@ public class Intent implements Parcelable { @Override public boolean equals(Object obj) { - Intent other; - try { - other = ((FilterComparison)obj).mIntent; - } catch (ClassCastException e) { - return false; + if (obj instanceof FilterComparison) { + Intent other = ((FilterComparison)obj).mIntent; + return mIntent.filterEquals(other); } - - return mIntent.filterEquals(other); + return false; } @Override diff --git a/core/java/android/content/package.html b/core/java/android/content/package.html index 7b3e8cf..dd5360f 100644 --- a/core/java/android/content/package.html +++ b/core/java/android/content/package.html @@ -50,9 +50,9 @@ an application's resources and transfer data between applications.</p> <p>This topic includes a terminology list associated with resources, and a series of examples of using resources in code. For a complete guide on creating and - using resources, see the document on <a href="{@docRoot}devel/resources-i18n.html">Resources + using resources, see the document on <a href="{@docRoot}guide/topics/resources/resources-i18n.html">Resources and Internationalization</a>. For a reference on the supported Android resource types, - see <a href="{@docRoot}reference/available-resources.html">Available Resource Types</a>.</p> + see <a href="{@docRoot}guide/topics/resources/available-resources.html">Available Resource Types</a>.</p> <p>The Android resource system keeps track of all non-code assets associated with an application. You use the {@link android.content.res.Resources Resources} class to access your @@ -175,7 +175,8 @@ download files with new appearances.</p> <p>This section gives a few quick examples you can use to make your own resources. For more details on how to define and use resources, see <a - href="{@docRoot}devel/resources-i18n.html">Resources</a>. </p> + href="{@docRoot}guide/topics/resources/resources-i18n.html">Resources and + Internationalization</a>. </p> <a name="UsingSystemResources"></a> <h4>Using System Resources</h4> diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 994afc8..d9326f2 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -29,6 +29,20 @@ public class PackageInfo implements Parcelable { public String versionName; /** + * The shared user ID name of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_sharedUserId sharedUserId} + * attribute. + */ + public String sharedUserId; + + /** + * The shared user ID label of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_sharedUserLabel sharedUserLabel} + * attribute. + */ + public int sharedUserLabel; + + /** * Information collected from the <application> tag, or null if * there was none. */ @@ -130,6 +144,8 @@ public class PackageInfo implements Parcelable { dest.writeString(packageName); dest.writeInt(versionCode); dest.writeString(versionName); + dest.writeString(sharedUserId); + dest.writeInt(sharedUserLabel); if (applicationInfo != null) { dest.writeInt(1); applicationInfo.writeToParcel(dest, parcelableFlags); @@ -163,6 +179,8 @@ public class PackageInfo implements Parcelable { packageName = source.readString(); versionCode = source.readInt(); versionName = source.readString(); + sharedUserId = source.readString(); + sharedUserLabel = source.readInt(); int hasApp = source.readInt(); if (hasApp != 0) { applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index e08f1d1..4ae8b08 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -101,6 +101,8 @@ public class PackageParser { pi.packageName = p.packageName; pi.versionCode = p.mVersionCode; pi.versionName = p.mVersionName; + pi.sharedUserId = p.mSharedUserId; + pi.sharedUserLabel = p.mSharedUserLabel; pi.applicationInfo = p.applicationInfo; if ((flags&PackageManager.GET_GIDS) != 0) { pi.gids = gids; @@ -585,6 +587,8 @@ public class PackageParser { return null; } pkg.mSharedUserId = str.intern(); + pkg.mSharedUserLabel = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); } sa.recycle(); @@ -2045,6 +2049,9 @@ public class PackageParser { // The shared user id that this package wants to use. public String mSharedUserId; + // The shared user label that this package wants to use. + public int mSharedUserLabel; + // Signatures that were read from the package. public Signature mSignatures[]; diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 2ff7294..10f3806 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -84,6 +84,7 @@ public class DatabaseUtils { code = 9; } else { reply.writeException(e); + Log.e(TAG, "Writing exception to parcel", e); return; } reply.writeInt(code); diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index d9f10a9..52f8209 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -102,7 +102,7 @@ public class ExtractEditText extends EditText { * highlight and cursor will be displayed. */ @Override public boolean hasWindowFocus() { - return true; + return this.isEnabled() ? true : false; } /** @@ -110,7 +110,7 @@ public class ExtractEditText extends EditText { * highlight and cursor will be displayed. */ @Override public boolean isFocused() { - return true; + return this.isEnabled() ? true : false; } /** @@ -118,7 +118,6 @@ public class ExtractEditText extends EditText { * highlight and cursor will be displayed. */ @Override public boolean hasFocus() { - return true; + return this.isEnabled() ? true : false; } - } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index c884120..4be1fc7 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -229,9 +229,11 @@ public class InputMethodService extends AbstractInputMethodService { InputConnection mStartedInputConnection; EditorInfo mInputEditorInfo; + int mShowInputFlags; boolean mShowInputRequested; boolean mLastShowInputRequested; int mCandidatesVisibility; + CompletionInfo[] mCurCompletions; boolean mShowInputForced; @@ -328,6 +330,7 @@ public class InputMethodService extends AbstractInputMethodService { */ public void hideSoftInput() { if (DEBUG) Log.v(TAG, "hideSoftInput()"); + mShowInputFlags = 0; mShowInputRequested = false; mShowInputForced = false; hideWindow(); @@ -338,7 +341,10 @@ public class InputMethodService extends AbstractInputMethodService { */ public void showSoftInput(int flags) { if (DEBUG) Log.v(TAG, "showSoftInput()"); - onShowRequested(flags); + mShowInputFlags = 0; + if (onShowInputRequested(flags, false)) { + showWindow(true); + } } } @@ -364,6 +370,7 @@ public class InputMethodService extends AbstractInputMethodService { if (!isEnabled()) { return; } + mCurCompletions = completions; onDisplayCompletions(completions); } @@ -557,8 +564,9 @@ public class InputMethodService extends AbstractInputMethodService { super.onConfigurationChanged(newConfig); boolean visible = mWindowVisible; + int showFlags = mShowInputFlags; boolean showingInput = mShowInputRequested; - boolean showingForced = mShowInputForced; + CompletionInfo[] completions = mCurCompletions; initViews(); mInputViewStarted = false; mCandidatesViewStarted = false; @@ -567,19 +575,24 @@ public class InputMethodService extends AbstractInputMethodService { getCurrentInputEditorInfo(), true); } if (visible) { - if (showingForced) { - // If we are showing the full soft keyboard, then go through - // this path to take care of current decisions about fullscreen - // etc. - onShowRequested(InputMethod.SHOW_FORCED|InputMethod.SHOW_EXPLICIT); - } else if (showingInput) { - // If we are showing the full soft keyboard, then go through - // this path to take care of current decisions about fullscreen - // etc. - onShowRequested(InputMethod.SHOW_EXPLICIT); - } else { - // Otherwise just put it back for its candidates. + if (showingInput) { + // If we were last showing the soft keyboard, try to do so again. + if (onShowInputRequested(showFlags, true)) { + showWindow(true); + if (completions != null) { + mCurCompletions = completions; + onDisplayCompletions(completions); + } + } else { + hideWindow(); + } + } else if (mCandidatesVisibility == View.VISIBLE) { + // If the candidates are currently visible, make sure the + // window is shown for them. showWindow(false); + } else { + // Otherwise hide the window. + hideWindow(); } } } @@ -1065,36 +1078,42 @@ public class InputMethodService extends AbstractInputMethodService { * The system has decided that it may be time to show your input method. * This is called due to a corresponding call to your * {@link InputMethod#showSoftInput(int) InputMethod.showSoftInput(int)} - * method. The default implementation simply calls - * {@link #showWindow(boolean)}, except if the - * {@link InputMethod#SHOW_EXPLICIT InputMethod.SHOW_EXPLICIT} flag is - * not set and the input method is running in fullscreen mode. + * method. The default implementation uses + * {@link #onEvaluateInputViewShown()}, {@link #onEvaluateFullscreenMode()}, + * and the current configuration to decide whether the input view should + * be shown at this point. * * @param flags Provides additional information about the show request, * as per {@link InputMethod#showSoftInput(int) InputMethod.showSoftInput(int)}. + * @param configChange This is true if we are re-showing due to a + * configuration change. + * @return Returns true to indicate that the window should be shown. */ - public void onShowRequested(int flags) { + public boolean onShowInputRequested(int flags, boolean configChange) { if (!onEvaluateInputViewShown()) { - return; + return false; } if ((flags&InputMethod.SHOW_EXPLICIT) == 0) { - if (onEvaluateFullscreenMode()) { + if (!configChange && onEvaluateFullscreenMode()) { // Don't show if this is not explicitly requested by the user and // the input method is fullscreen. That would be too disruptive. - return; + // However, we skip this change for a config change, since if + // the IME is already shown we do want to go into fullscreen + // mode at this point. + return false; } Configuration config = getResources().getConfiguration(); if (config.keyboard != Configuration.KEYBOARD_NOKEYS) { // And if the device has a hard keyboard, even if it is // currently hidden, don't show the input method implicitly. // These kinds of devices don't need it that much. - return; + return false; } } if ((flags&InputMethod.SHOW_FORCED) != 0) { mShowInputForced = true; } - showWindow(true); + return true; } public void showWindow(boolean showInput) { @@ -1106,7 +1125,6 @@ public class InputMethodService extends AbstractInputMethodService { + " mInputStarted=" + mInputStarted); boolean doShowInput = false; boolean wasVisible = mWindowVisible; - boolean wasCreated = mWindowCreated; mWindowVisible = true; if (!mShowInputRequested) { if (mInputStarted) { @@ -1240,6 +1258,7 @@ public class InputMethodService extends AbstractInputMethodService { } mInputStarted = false; mStartedInputConnection = null; + mCurCompletions = null; } void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) { @@ -1550,7 +1569,11 @@ public class InputMethodService extends AbstractInputMethodService { eet.setInputType(inputType); eet.setHint(mInputEditorInfo.hintText); if (mExtractedText != null) { + eet.setEnabled(true); eet.setExtractedText(mExtractedText); + } else { + eet.setEnabled(false); + eet.setText(""); } } finally { eet.finishInternalChanges(); @@ -1586,7 +1609,8 @@ public class InputMethodService extends AbstractInputMethodService { p.println(" mShowInputRequested=" + mShowInputRequested + " mLastShowInputRequested=" + mLastShowInputRequested - + " mShowInputForced=" + mShowInputForced); + + " mShowInputForced=" + mShowInputForced + + " mShowInputFlags=0x" + Integer.toHexString(mShowInputFlags)); p.println(" mCandidatesVisibility=" + mCandidatesVisibility + " mFullscreenApplied=" + mFullscreenApplied + " mIsFullscreen=" + mIsFullscreen); diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java index 228acbe..6a560ce 100755 --- a/core/java/android/inputmethodservice/Keyboard.java +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -132,7 +132,19 @@ public class Keyboard { /** Keyboard mode, or zero, if none. */ private int mKeyboardMode; + + // Variables for pre-computing nearest keys. + private static final int GRID_WIDTH = 10; + private static final int GRID_HEIGHT = 5; + private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT; + private int mCellWidth; + private int mCellHeight; + private int[][] mGridNeighbors; + private int mProximityThreshold; + /** Number of key widths from current touch point to search for nearest keys. */ + private static float SEARCH_DISTANCE = 1.4f; + /** * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. * Some of the key size defaults can be overridden per row from what the {@link Keyboard} @@ -637,6 +649,52 @@ public class Keyboard { public int getShiftKeyIndex() { return mShiftKeyIndex; } + + private void computeNearestNeighbors() { + // Round-up so we don't have any pixels outside the grid + mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH; + mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT; + mGridNeighbors = new int[GRID_SIZE][]; + int[] indices = new int[mKeys.size()]; + final int gridWidth = GRID_WIDTH * mCellWidth; + final int gridHeight = GRID_HEIGHT * mCellHeight; + for (int x = 0; x < gridWidth; x += mCellWidth) { + for (int y = 0; y < gridHeight; y += mCellHeight) { + int count = 0; + for (int i = 0; i < mKeys.size(); i++) { + final Key key = mKeys.get(i); + if (key.squaredDistanceFrom(x, y) < mProximityThreshold || + key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold || + key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) + < mProximityThreshold || + key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) { + indices[count++] = i; + } + } + int [] cell = new int[count]; + System.arraycopy(indices, 0, cell, 0, count); + mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell; + } + } + } + + /** + * Returns the indices of the keys that are closest to the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the array of integer indices for the nearest keys to the given point. If the given + * point is out of range, then an array of size zero is returned. + */ + public int[] getNearestKeys(int x, int y) { + if (mGridNeighbors == null) computeNearestNeighbors(); + if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) { + int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth); + if (index < GRID_SIZE) { + return mGridNeighbors[index]; + } + } + return new int[0]; + } protected Row createRowFromXml(Resources res, XmlResourceParser parser) { return new Row(res, this, parser); @@ -738,6 +796,8 @@ public class Keyboard { mDefaultVerticalGap = getDimensionOrFraction(a, com.android.internal.R.styleable.Keyboard_verticalGap, mDisplayHeight, 0); + mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE); + mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison a.recycle(); } diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index b8bd10d..886e688 100755 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -159,6 +159,9 @@ public class KeyboardView extends View implements View.OnClickListener { private static final int MSG_REPEAT = 3; private static final int MSG_LONGPRESS = 4; + private static final int DELAY_BEFORE_PREVIEW = 70; + private static final int DELAY_AFTER_PREVIEW = 60; + private int mVerticalCorrection; private int mProximityThreshold; @@ -219,7 +222,7 @@ public class KeyboardView extends View implements View.OnClickListener { public void handleMessage(Message msg) { switch (msg.what) { case MSG_SHOW_PREVIEW: - mPreviewText.setVisibility(VISIBLE); + showKey(msg.arg1); break; case MSG_REMOVE_PREVIEW: mPreviewText.setVisibility(INVISIBLE); @@ -234,7 +237,6 @@ public class KeyboardView extends View implements View.OnClickListener { openPopupIfRequired((MotionEvent) msg.obj); break; } - } }; @@ -533,10 +535,10 @@ public class KeyboardView extends View implements View.OnClickListener { dimensionSum += Math.min(key.width, key.height) + key.gap; } if (dimensionSum < 0 || length == 0) return; - mProximityThreshold = (int) (dimensionSum * 1.5f / length); + mProximityThreshold = (int) (dimensionSum * 1.4f / length); mProximityThreshold *= mProximityThreshold; // Square it } - + @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -647,9 +649,10 @@ public class KeyboardView extends View implements View.OnClickListener { int closestKey = NOT_A_KEY; int closestKeyDist = mProximityThreshold + 1; java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); - final int keyCount = keys.length; + int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); + final int keyCount = nearestKeyIndices.length; for (int i = 0; i < keyCount; i++) { - final Key key = keys[i]; + final Key key = keys[nearestKeyIndices[i]]; int dist = 0; boolean isInside = key.isInside(x,y); if (((mProximityCorrectOn @@ -660,7 +663,7 @@ public class KeyboardView extends View implements View.OnClickListener { final int nCodes = key.codes.length; if (dist < closestKeyDist) { closestKeyDist = dist; - closestKey = i; + closestKey = nearestKeyIndices[i]; } if (allKeys == null) continue; @@ -674,9 +677,6 @@ public class KeyboardView extends View implements View.OnClickListener { allKeys.length - j - nCodes); for (int c = 0; c < nCodes; c++) { allKeys[j + c] = key.codes[c]; - if (shifted) { - //allKeys[j + c] = Character.toUpperCase(key.codes[c]); - } mDistances[j + c] = dist; } break; @@ -685,7 +685,7 @@ public class KeyboardView extends View implements View.OnClickListener { } if (isInside) { - primaryIndex = i; + primaryIndex = nearestKeyIndices[i]; } } if (primaryIndex == NOT_A_KEY) { @@ -696,7 +696,7 @@ public class KeyboardView extends View implements View.OnClickListener { private void detectAndSendKey(int x, int y, long eventTime) { int index = mCurrentKey; - if (index != NOT_A_KEY) { + if (index != NOT_A_KEY && index < mKeys.length) { final Key key = mKeys[index]; if (key.text != null) { for (int i = 0; i < key.text.length(); i++) { @@ -763,70 +763,83 @@ public class KeyboardView extends View implements View.OnClickListener { if (previewPopup.isShowing()) { if (keyIndex == NOT_A_KEY) { mHandler.sendMessageDelayed(mHandler - .obtainMessage(MSG_REMOVE_PREVIEW), 60); + .obtainMessage(MSG_REMOVE_PREVIEW), + DELAY_AFTER_PREVIEW); } } if (keyIndex != NOT_A_KEY) { - Key key = keys[keyIndex]; - if (key.icon != null) { - mPreviewText.setCompoundDrawables(null, null, null, - key.iconPreview != null ? key.iconPreview : key.icon); - mPreviewText.setText(null); - } else { - mPreviewText.setCompoundDrawables(null, null, null, null); - mPreviewText.setText(getPreviewText(key)); - if (key.label.length() > 1 && key.codes.length < 2) { - mPreviewText.setTextSize(mLabelTextSize); - mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); - } else { - mPreviewText.setTextSize(mPreviewTextSizeLarge); - mPreviewText.setTypeface(Typeface.DEFAULT); - } - } - mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width - + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); - final int popupHeight = mPreviewHeight; - LayoutParams lp = mPreviewText.getLayoutParams(); - if (lp != null) { - lp.width = popupWidth; - lp.height = popupHeight; - } - if (!mPreviewCentered) { - mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; - mPopupPreviewY = key.y - popupHeight + mPreviewOffset; - } else { - // TODO: Fix this if centering is brought back - mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; - mPopupPreviewY = - mPreviewText.getMeasuredHeight(); - } - mHandler.removeMessages(MSG_REMOVE_PREVIEW); - if (mOffsetInWindow == null) { - mOffsetInWindow = new int[2]; - getLocationInWindow(mOffsetInWindow); - mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero - mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero - } - // Set the preview background state - mPreviewText.getBackground().setState( - key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - if (previewPopup.isShowing()) { - previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], - mPopupPreviewY + mOffsetInWindow[1], - popupWidth, popupHeight); + if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { + // Show right away, if it's already visible and finger is moving around + showKey(keyIndex); } else { - previewPopup.setWidth(popupWidth); - previewPopup.setHeight(popupHeight); - previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, - mPopupPreviewX + mOffsetInWindow[0], - mPopupPreviewY + mOffsetInWindow[1]); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), + DELAY_BEFORE_PREVIEW); } - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), - ViewConfiguration.getTapTimeout()); } } } + + private void showKey(final int keyIndex) { + final PopupWindow previewPopup = mPreviewPopup; + final Key[] keys = mKeys; + Key key = keys[keyIndex]; + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(mLabelTextSize); + mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); + } else { + mPreviewText.setTextSize(mPreviewTextSizeLarge); + mPreviewText.setTypeface(Typeface.DEFAULT); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.removeMessages(MSG_REMOVE_PREVIEW); + if (mOffsetInWindow == null) { + mOffsetInWindow = new int[2]; + getLocationInWindow(mOffsetInWindow); + mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero + mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero + } + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1], + popupWidth, popupHeight); + } else { + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1]); + } + mPreviewText.setVisibility(VISIBLE); + } private void invalidateKey(int keyIndex) { if (keyIndex < 0 || keyIndex >= mKeys.length) { diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 017b14d..6f9d6c6 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1,18 +1,20 @@ package android.os; -import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Formatter; import java.util.Map; +import android.util.Log; +import android.util.Printer; import android.util.SparseArray; /** * A class providing access to battery usage statistics, including information on * wakelocks, processes, packages, and services. All times are represented in microseconds * except where indicated otherwise. + * @hide */ -public abstract class BatteryStats { +public abstract class BatteryStats implements Parcelable { /** * A constant indicating a partial wake lock timer. @@ -96,6 +98,11 @@ public abstract class BatteryStats { * @return a time in microseconds */ public abstract long getTotalTime(long now, int which); + + /** + * Temporary for debugging. + */ + public abstract void logState(); } /** @@ -154,10 +161,10 @@ public abstract class BatteryStats { public abstract long getTcpBytesSent(int which); public static abstract class Sensor { - /** - * {@hide} - */ - public abstract String getName(); + // Magic sensor number for the GPS. + public static final int GPS = -10000; + + public abstract int getHandle(); public abstract Timer getSensorTime(); } @@ -260,6 +267,11 @@ public abstract class BatteryStats { public abstract long getPluggedScreenOnTime(); /** + * Return whether we are currently running on battery. + */ + public abstract boolean getIsOnBattery(); + + /** * Returns a SparseArray containing the statistics for each uid. */ public abstract SparseArray<? extends Uid> getUidStats(); @@ -457,7 +469,7 @@ public abstract class BatteryStats { * @param pw * @param which */ - private final void dumpCheckinLocked(FileDescriptor fd, PrintWriter pw, int which) { + private final void dumpCheckinLocked(PrintWriter pw, int which) { long uSecTime = SystemClock.elapsedRealtime() * 1000; final long uSecNow = getBatteryUptime(uSecTime); @@ -578,35 +590,18 @@ public abstract class BatteryStats { } @SuppressWarnings("unused") - private final void dumpLocked(FileDescriptor fd, PrintWriter pw, String prefix, int which) { + private final void dumpLocked(Printer pw, String prefix, int which) { long uSecTime = SystemClock.elapsedRealtime() * 1000; final long uSecNow = getBatteryUptime(uSecTime); StringBuilder sb = new StringBuilder(128); - switch (which) { - case STATS_TOTAL: - pw.println(prefix + "Current and Historic Battery Usage Statistics:"); - pw.println(prefix + " System starts: " + getStartCount()); - break; - case STATS_LAST: - pw.println(prefix + "Last Battery Usage Statistics:"); - break; - case STATS_UNPLUGGED: - pw.println(prefix + "Last Unplugged Battery Usage Statistics:"); - break; - case STATS_CURRENT: - pw.println(prefix + "Current Battery Usage Statistics:"); - break; - default: - throw new IllegalArgumentException("which = " + which); - } long batteryUptime = computeBatteryUptime(uSecNow, which); long batteryRealtime = computeBatteryRealtime(getBatteryRealtime(uSecTime), which); long elapsedRealtime = computeRealtime(uSecTime, which); long uptime = computeUptime(SystemClock.uptimeMillis() * 1000, which); pw.println(prefix - + " On battery: " + formatTimeMs(batteryUptime / 1000) + "(" + + " Time on battery: " + formatTimeMs(batteryUptime / 1000) + "(" + formatRatioLocked(batteryUptime, elapsedRealtime) + ") uptime, " + formatTimeMs(batteryRealtime / 1000) + "(" @@ -618,7 +613,7 @@ public abstract class BatteryStats { + "uptime, " + formatTimeMs(elapsedRealtime / 1000) + "realtime"); - + pw.println(" "); SparseArray<? extends Uid> uidStats = getUidStats(); @@ -629,8 +624,12 @@ public abstract class BatteryStats { pw.println(prefix + " #" + uid + ":"); boolean uidActivity = false; - pw.println(prefix + " Network: " + u.getTcpBytesReceived(which) + " bytes received, " - + u.getTcpBytesSent(which) + " bytes sent"); + long tcpReceived = u.getTcpBytesReceived(which); + long tcpSent = u.getTcpBytesSent(which); + if (tcpReceived != 0 || tcpSent != 0) { + pw.println(prefix + " Network: " + tcpReceived + " bytes received, " + + tcpSent + " bytes sent"); + } Map<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); if (wakelocks.size() > 0) { @@ -648,7 +647,9 @@ public abstract class BatteryStats { "partial", which, linePrefix); linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), uSecNow, "window", which, linePrefix); - if (linePrefix.equals(": ")) { + if (!linePrefix.equals(": ")) { + sb.append(" realtime"); + } else { sb.append(": (nothing executed)"); } pw.println(sb.toString()); @@ -665,23 +666,30 @@ public abstract class BatteryStats { sb.setLength(0); sb.append(prefix); sb.append(" Sensor "); - sb.append(sensorNumber); + int handle = se.getHandle(); + if (handle == Uid.Sensor.GPS) { + sb.append("GPS"); + } else { + sb.append(handle); + } + sb.append(": "); Timer timer = se.getSensorTime(); if (timer != null) { // Convert from microseconds to milliseconds with rounding long totalTime = (timer.getTotalTime(uSecNow, which) + 500) / 1000; int count = timer.getCount(which); + //timer.logState(); if (totalTime != 0) { - sb.append(": "); sb.append(formatTimeMs(totalTime)); - sb.append(' '); - sb.append('('); + sb.append("realtime ("); sb.append(count); sb.append(" times)"); + } else { + sb.append("(not used)"); } } else { - sb.append(": (none used)"); + sb.append("(not used)"); } pw.println(sb.toString()); @@ -734,8 +742,9 @@ public abstract class BatteryStats { int launches = ss.getLaunches(which); if (startTime != 0 || starts != 0 || launches != 0) { pw.println(prefix + " Service " + sent.getKey() + ":"); - pw.println(prefix + " Time spent started: " - + formatTimeMs(startTime / 1000)); + pw.println(prefix + " Created for: " + + formatTimeMs(startTime / 1000) + + " uptime"); pw.println(prefix + " Starts: " + starts + ", launches: " + launches); apkActivity = true; @@ -757,36 +766,30 @@ public abstract class BatteryStats { /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * - * @param fd a FileDescriptor, currently unused. - * @param pw a PrintWriter to receive the dump output. - * @param args an array of Strings, currently unused. + * @param pw a Printer to receive the dump output. */ @SuppressWarnings("unused") - public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) { - boolean isCheckin = false; - if (args != null) { - for (String arg : args) { - if ("-c".equals(arg)) { - isCheckin = true; - break; - } - } - } - synchronized (this) { - if (isCheckin) { - dumpCheckinLocked(fd, pw, STATS_TOTAL); - dumpCheckinLocked(fd, pw, STATS_LAST); - dumpCheckinLocked(fd, pw, STATS_UNPLUGGED); - dumpCheckinLocked(fd, pw, STATS_CURRENT); - } else { - dumpLocked(fd, pw, "", STATS_TOTAL); - pw.println(""); - dumpLocked(fd, pw, "", STATS_LAST); - pw.println(""); - dumpLocked(fd, pw, "", STATS_UNPLUGGED); - pw.println(""); - dumpLocked(fd, pw, "", STATS_CURRENT); - } - } + public void dumpLocked(Printer pw) { + pw.println("Total Statistics (Current and Historic):"); + pw.println(" System starts: " + getStartCount() + + ", currently on battery: " + getIsOnBattery()); + dumpLocked(pw, "", STATS_TOTAL); + pw.println(""); + pw.println("Last Run Statistics (Previous run of system):"); + dumpLocked(pw, "", STATS_LAST); + pw.println(""); + pw.println("Current Battery Statistics (Currently running system):"); + dumpLocked(pw, "", STATS_CURRENT); + pw.println(""); + pw.println("Unplugged Statistics (Since last unplugged from power):"); + dumpLocked(pw, "", STATS_UNPLUGGED); + } + + @SuppressWarnings("unused") + public void dumpCheckinLocked(PrintWriter pw, String[] args) { + dumpCheckinLocked(pw, STATS_TOTAL); + dumpCheckinLocked(pw, STATS_LAST); + dumpCheckinLocked(pw, STATS_UNPLUGGED); + dumpCheckinLocked(pw, STATS_CURRENT); } } diff --git a/core/java/android/os/ICheckinService.aidl b/core/java/android/os/ICheckinService.aidl index 11becc4..e56b55d 100644 --- a/core/java/android/os/ICheckinService.aidl +++ b/core/java/android/os/ICheckinService.aidl @@ -26,7 +26,13 @@ import android.os.IParentalControlCallback; * {@hide} */ interface ICheckinService { - /** Synchronously attempt a checkin with the server, return true on success. */ + /** Synchronously attempt a checkin with the server, return true + * on success. + * @throws IllegalStateException whenever an error occurs. The + * cause of the exception will be the real exception: + * IOException for network errors, JSONException for invalid + * server responses, etc. + */ boolean checkin(); /** Direct submission of crash data; returns after writing the crash. */ diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index e48f152..5486920 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -29,4 +29,5 @@ interface IPowerManager void setStayOnSetting(int val); long getScreenOnTime(); void preventScreenOn(boolean prevent); + void setScreenBrightnessOverride(int brightness); } diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java index 02ab1da..14c0054 100644 --- a/core/java/android/preference/PreferenceGroupAdapter.java +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -242,7 +242,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn mHasReturnedViewTypeCount = true; } - return mPreferenceClassNames.size(); + return Math.max(1, mPreferenceClassNames.size()); } } diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 5767c65..688e19a 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -97,6 +97,7 @@ public final class Checkin { SETUP_RETRIES_EXHAUSTED, SETUP_SERVER_ERROR, SETUP_SERVER_TIMEOUT, + SETUP_NO_DATA_NETWORK, SYSTEM_APP_NOT_RESPONDING, SYSTEM_BOOT, SYSTEM_LAST_KMSG, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c6a7b40..4a784c8 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2321,6 +2321,13 @@ public final class Settings { public static final String GMAIL_SEND_IMMEDIATELY = "gmail_send_immediately"; /** + * Controls whether gmail buffers server responses. Possible values are "memory", for a + * memory-based buffer, or "file", for a temp-file-based buffer. All other values + * (including not set) disable buffering. + */ + public static final String GMAIL_BUFFER_SERVER_RESPONSE = "gmail_buffer_server_response"; + + /** * Hostname of the GTalk server. */ public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname"; diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java index 7c15045..86d5a1e 100644 --- a/core/java/android/server/BluetoothDeviceService.java +++ b/core/java/android/server/BluetoothDeviceService.java @@ -111,9 +111,19 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native int isEnabledNative(); /** - * Disable bluetooth. Returns true on success. + * Bring down bluetooth and disable BT in settings. Returns true on success. */ - public synchronized boolean disable() { + public boolean disable() { + return disable(true); + } + + /** + * Bring down bluetooth. Returns true on success. + * + * @param saveSetting If true, disable BT in settings + * + */ + public synchronized boolean disable(boolean saveSetting) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); @@ -137,7 +147,9 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { mContext.sendBroadcast(intent, BLUETOOTH_PERM); mIsEnabled = false; - persistBluetoothOnSetting(false); + if (saveSetting) { + persistBluetoothOnSetting(false); + } mIsDiscovering = false; intent = new Intent(BluetoothIntent.DISABLED_ACTION); mContext.sendBroadcast(intent, BLUETOOTH_PERM); @@ -145,13 +157,27 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { } /** + * Bring up bluetooth, asynchronously, and enable BT in settings. + * This turns on/off the underlying hardware. + * + * @return True on success (so far), guaranteeing the callback with be + * notified when complete. + */ + public boolean enable(IBluetoothDeviceCallback callback) { + return enable(callback, true); + } + + /** * Enable this Bluetooth device, asynchronously. * This turns on/off the underlying hardware. * - * @return True on success (so far), guarenteeing the callback with be + * @param saveSetting If true, enable BT in settings + * + * @return True on success (so far), guaranteeing the callback with be * notified when complete. */ - public synchronized boolean enable(IBluetoothDeviceCallback callback) { + public synchronized boolean enable(IBluetoothDeviceCallback callback, + boolean saveSetting) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); @@ -165,7 +191,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { if (mEnableThread != null && mEnableThread.isAlive()) { return false; } - mEnableThread = new EnableThread(callback); + mEnableThread = new EnableThread(callback, saveSetting); mEnableThread.start(); return true; } @@ -189,8 +215,10 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private class EnableThread extends Thread { private final IBluetoothDeviceCallback mEnableCallback; - public EnableThread(IBluetoothDeviceCallback callback) { + private final boolean mSaveSetting; + public EnableThread(IBluetoothDeviceCallback callback, boolean saveSetting) { mEnableCallback = callback; + mSaveSetting = saveSetting; } public void run() { boolean res = (enableNative() == 0); @@ -208,7 +236,9 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { if (res) { mIsEnabled = true; - persistBluetoothOnSetting(true); + if (mSaveSetting) { + persistBluetoothOnSetting(true); + } mIsDiscovering = false; Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION); mBondState.loadBondState(); @@ -952,9 +982,9 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { // If bluetooth is currently expected to be on, then enable or disable bluetooth if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { if (enabled) { - enable(null); + enable(null, false); } else { - disable(); + disable(false); } } } diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index b5e4090..187ec2c 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -359,10 +359,10 @@ class BluetoothEventLoop { private void onGetRemoteServiceChannelResult(String address, int channel) { IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address); if (callback != null) { + mGetRemoteServiceChannelCallbacks.remove(address); try { callback.onGetRemoteServiceChannelResult(address, channel); } catch (RemoteException e) {} - mGetRemoteServiceChannelCallbacks.remove(address); } } diff --git a/core/java/android/speech/srec/MicrophoneInputStream.java b/core/java/android/speech/srec/MicrophoneInputStream.java index 160a003..fab77a9 100644 --- a/core/java/android/speech/srec/MicrophoneInputStream.java +++ b/core/java/android/speech/srec/MicrophoneInputStream.java @@ -1,5 +1,5 @@ /*---------------------------------------------------------------------------* - * MicrophoneInputStream.java * + * MicrophoneInputStream.java * * * * Copyright 2007 Nuance Communciations, Inc. * * * @@ -45,8 +45,12 @@ public final class MicrophoneInputStream extends InputStream { */ public MicrophoneInputStream(int sampleRate, int fifoDepth) throws IOException { mAudioRecord = AudioRecordNew(sampleRate, fifoDepth); - if (mAudioRecord == 0) throw new IllegalStateException("not open"); - AudioRecordStart(mAudioRecord); + if (mAudioRecord == 0) throw new IOException("AudioRecord constructor failed - busy?"); + int status = AudioRecordStart(mAudioRecord); + if (status != 0) { + close(); + throw new IOException("AudioRecord start failed: " + status); + } } @Override @@ -99,7 +103,7 @@ public final class MicrophoneInputStream extends InputStream { // AudioRecord JNI interface // private static native int AudioRecordNew(int sampleRate, int fifoDepth); - private static native void AudioRecordStart(int audioRecord); + private static native int AudioRecordStart(int audioRecord); private static native int AudioRecordRead(int audioRecord, byte[] b, int offset, int length) throws IOException; private static native void AudioRecordStop(int audioRecord) throws IOException; private static native void AudioRecordDelete(int audioRecord) throws IOException; diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index 5bf9b20..daa99c2 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -394,6 +394,7 @@ public class Time { * * @param s the string to parse * @return true if the resulting time value is in UTC time + * @throws android.util.TimeFormatException if s cannot be parsed. */ public boolean parse(String s) { if (nativeParse(s)) { diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java index 2eebc0d..efb88a0 100644 --- a/core/java/android/text/style/ImageSpan.java +++ b/core/java/android/text/style/ImageSpan.java @@ -44,8 +44,9 @@ public class ImageSpan extends DynamicDrawableSpan { public ImageSpan(Bitmap b, int verticalAlignment) { super(verticalAlignment); mDrawable = new BitmapDrawable(b); - mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), - mDrawable.getIntrinsicHeight()); + int width = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); + mDrawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0); } public ImageSpan(Drawable d) { diff --git a/core/java/android/util/SparseIntArray.java b/core/java/android/util/SparseIntArray.java index 610cfd4..9ab3b53 100644 --- a/core/java/android/util/SparseIntArray.java +++ b/core/java/android/util/SparseIntArray.java @@ -73,13 +73,20 @@ public class SparseIntArray { int i = binarySearch(mKeys, 0, mSize, key); if (i >= 0) { - System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1)); - System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 1)); - mSize--; + removeAt(i); } } /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** * Adds a mapping from the specified key to the specified value, * replacing the previous mapping from the specified key if there * was one. diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index a472689..679c683 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -163,7 +163,7 @@ public class GestureDetector { private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); // TODO make new double-tap timeout, and define its events (i.e. either time // between down-down or time between up-down) - private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getJumpTapTimeout(); + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); // constants for Message.what used by GestureHandler below private static final int SHOW_PRESS = 1; @@ -174,11 +174,13 @@ public class GestureDetector { private final OnGestureListener mListener; private OnDoubleTapListener mDoubleTapListener; + private boolean mStillDown; private boolean mInLongPress; private boolean mAlwaysInTapRegion; private boolean mAlwaysInBiggerTapRegion; private MotionEvent mCurrentDownEvent; + private MotionEvent mPreviousUpEvent; /** * True when the user is still touching for the second tap (down, move, and @@ -217,7 +219,8 @@ public class GestureDetector { break; case TAP: - if (mDoubleTapListener != null) { + // If the user's finger is still down, do not count it as a tap + if (mDoubleTapListener != null && !mStillDown) { mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); } break; @@ -378,8 +381,10 @@ public class GestureDetector { switch (action) { case MotionEvent.ACTION_DOWN: if (mDoubleTapListener != null) { - mHandler.removeMessages(TAP); - if (mCurrentDownEvent != null && isConsideredDoubleTap(mCurrentDownEvent, ev)) { + boolean hadTapMessage = mHandler.hasMessages(TAP); + if (hadTapMessage) mHandler.removeMessages(TAP); + if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && + isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { // This is a second tap mIsDoubleTapping = true; handled = mDoubleTapListener.onDoubleTapEvent(ev); @@ -394,6 +399,7 @@ public class GestureDetector { mCurrentDownEvent = MotionEvent.obtain(ev); mAlwaysInTapRegion = true; mAlwaysInBiggerTapRegion = true; + mStillDown = true; mInLongPress = false; if (mIsLongpressEnabled) { @@ -422,6 +428,7 @@ public class GestureDetector { mLastMotionX = x; mLastMotionY = y; mAlwaysInTapRegion = false; + mHandler.removeMessages(TAP); mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } @@ -436,6 +443,7 @@ public class GestureDetector { break; case MotionEvent.ACTION_UP: + mStillDown = false; MotionEvent currentUpEvent = MotionEvent.obtain(ev); if (mIsDoubleTapping) { handled = mDoubleTapListener.onDoubleTapEvent(ev); @@ -461,6 +469,7 @@ public class GestureDetector { handled = mListener.onFling(mCurrentDownEvent, currentUpEvent, velocityX, velocityY); } } + mPreviousUpEvent = MotionEvent.obtain(ev); mVelocityTracker.recycle(); mVelocityTracker = null; mHandler.removeMessages(SHOW_PRESS); @@ -472,6 +481,7 @@ public class GestureDetector { mHandler.removeMessages(TAP); mVelocityTracker.recycle(); mVelocityTracker = null; + mStillDown = false; if (mInLongPress) { mInLongPress = false; break; @@ -480,12 +490,13 @@ public class GestureDetector { return handled; } - private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent secondDown) { + private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, + MotionEvent secondDown) { if (!mAlwaysInBiggerTapRegion) { return false; } - if (secondDown.getEventTime() - firstDown.getEventTime() > DOUBLE_TAP_TIMEOUT) { + if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { return false; } @@ -495,6 +506,7 @@ public class GestureDetector { } private void dispatchLongPress() { + mHandler.removeMessages(TAP); mInLongPress = true; mListener.onLongPress(mCurrentDownEvent); } diff --git a/core/java/android/view/OrientationEventListener.java b/core/java/android/view/OrientationEventListener.java new file mode 100755 index 0000000..cddec11 --- /dev/null +++ b/core/java/android/view/OrientationEventListener.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Config; +import android.util.Log; + +/** + * Helper class for receiving notifications from the SensorManager when + * the orientation of the device has changed. + */ +public abstract class OrientationEventListener { + private static final String TAG = "OrientationEventListener"; + private static final boolean DEBUG = false; + private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private int mOrientation = ORIENTATION_UNKNOWN; + private SensorManager mSensorManager; + private boolean mEnabled = false; + private int mRate; + private Sensor mSensor; + private SensorEventListener mSensorEventListener; + private OrientationListener mOldListener; + + /** + * Returned from onOrientationChanged when the device orientation cannot be determined + * (typically when the device is in a close to flat position). + * + * @see #onOrientationChanged + */ + public static final int ORIENTATION_UNKNOWN = -1; + + /** + * Creates a new OrientationEventListener. + * + * @param context for the OrientationEventListener. + */ + public OrientationEventListener(Context context) { + this(context, SensorManager.SENSOR_DELAY_NORMAL); + } + + /** + * Creates a new OrientationEventListener. + * + * @param context for the OrientationEventListener. + * @param rate at which sensor events are processed (see also + * {@link android.hardware.SensorManager SensorManager}). Use the default + * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL + * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. + */ + public OrientationEventListener(Context context, int rate) { + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mRate = rate; + mSensorEventListener = new SensorEventListenerImpl(); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + void registerListener(OrientationListener lis) { + mOldListener = lis; + } + + /** + * Enables the OrientationEventListener so it will monitor the sensor and call + * {@link #onOrientationChanged} when the device orientation changes. + */ + public void enable() { + if (mEnabled == false) { + if (localLOGV) Log.d(TAG, "OrientationEventListener enabled"); + mSensorManager.registerListener(mSensorEventListener, mSensor, mRate); + mEnabled = true; + } + } + + /** + * Disables the OrientationEventListener. + */ + public void disable() { + if (mEnabled == true) { + if (localLOGV) Log.d(TAG, "OrientationEventListener disabled"); + mSensorManager.unregisterListener(mSensorEventListener); + mEnabled = false; + } + } + + class SensorEventListenerImpl implements SensorEventListener { + private static final int _DATA_X = 0; + private static final int _DATA_Y = 1; + private static final int _DATA_Z = 2; + + public void onSensorChanged(SensorEvent event) { + float[] values = event.values; + int orientation = ORIENTATION_UNKNOWN; + float X = -values[_DATA_X]; + float Y = -values[_DATA_Y]; + float Z = -values[_DATA_Z]; + float magnitude = X*X + Y*Y; + // Don't trust the angle if the magnitude is small compared to the y value + if (magnitude * 4 >= Z*Z) { + float OneEightyOverPi = 57.29577957855f; + float angle = (float)Math.atan2(-Y, X) * OneEightyOverPi; + orientation = 90 - (int)Math.round(angle); + // normalize to 0 - 359 range + while (orientation >= 360) { + orientation -= 360; + } + while (orientation < 0) { + orientation += 360; + } + } + if (mOldListener != null) { + mOldListener.onSensorChanged(Sensor.TYPE_ACCELEROMETER, event.values); + } + if (orientation != mOrientation) { + mOrientation = orientation; + onOrientationChanged(orientation); + } + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } + } + + /** + * Called when the orientation of the device has changed. + * orientation parameter is in degrees, ranging from 0 to 359. + * orientation is 0 degrees when the device is oriented in its natural position, + * 90 degrees when its left side is at the top, 180 degrees when it is upside down, + * and 270 degrees when its right side is to the top. + * {@link #ORIENTATION_UNKNOWN} is returned when the device is close to flat + * and the orientation cannot be determined. + * + * @param orientation The new orientation of the device. + * + * @see #ORIENTATION_UNKNOWN + */ + abstract public void onOrientationChanged(int orientation); +} diff --git a/core/java/android/view/OrientationListener.java b/core/java/android/view/OrientationListener.java index 974c2e8..ce8074e 100644 --- a/core/java/android/view/OrientationListener.java +++ b/core/java/android/view/OrientationListener.java @@ -18,23 +18,16 @@ package android.view; import android.content.Context; import android.hardware.SensorListener; -import android.hardware.SensorManager; -import android.util.Config; -import android.util.Log; /** * Helper class for receiving notifications from the SensorManager when * the orientation of the device has changed. + * @deprecated use {@link android.view.OrientationEventListener} instead. + * This class internally uses the OrientationEventListener. */ +@Deprecated public abstract class OrientationListener implements SensorListener { - - private static final String TAG = "OrientationListener"; - private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; - private SensorManager mSensorManager; - private int mOrientation = ORIENTATION_UNKNOWN; - private boolean mEnabled = false; - private int mRate; + private OrientationEventListener mOrientationEventLis; /** * Returned from onOrientationChanged when the device orientation cannot be determined @@ -42,7 +35,7 @@ public abstract class OrientationListener implements SensorListener { * * @see #onOrientationChanged */ - public static final int ORIENTATION_UNKNOWN = -1; + public static final int ORIENTATION_UNKNOWN = OrientationEventListener.ORIENTATION_UNKNOWN; /** * Creates a new OrientationListener. @@ -50,8 +43,7 @@ public abstract class OrientationListener implements SensorListener { * @param context for the OrientationListener. */ public OrientationListener(Context context) { - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - mRate = SensorManager.SENSOR_DELAY_NORMAL; + mOrientationEventLis = new OrientationEventListenerInternal(context); } /** @@ -64,78 +56,55 @@ public abstract class OrientationListener implements SensorListener { * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. */ public OrientationListener(Context context, int rate) { - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - mRate = rate; + mOrientationEventLis = new OrientationEventListenerInternal(context, rate); } - + + class OrientationEventListenerInternal extends OrientationEventListener { + OrientationEventListenerInternal(Context context) { + super(context); + } + + OrientationEventListenerInternal(Context context, int rate) { + super(context, rate); + // register so that onSensorChanged gets invoked + registerListener(OrientationListener.this); + } + + public void onOrientationChanged(int orientation) { + OrientationListener.this.onOrientationChanged(orientation); + } + } + /** * Enables the OrientationListener so it will monitor the sensor and call * {@link #onOrientationChanged} when the device orientation changes. */ public void enable() { - if (mEnabled == false) { - if (localLOGV) Log.d(TAG, "OrientationListener enabled"); - mSensorManager.registerListener(this, SensorManager.SENSOR_ACCELEROMETER, mRate); - mEnabled = true; - } + mOrientationEventLis.enable(); } /** * Disables the OrientationListener. */ public void disable() { - if (mEnabled == true) { - if (localLOGV) Log.d(TAG, "OrientationListener disabled"); - mSensorManager.unregisterListener(this); - mEnabled = false; - } + mOrientationEventLis.disable(); } - - /** - * - */ + + public void onAccuracyChanged(int sensor, int accuracy) { + } + public void onSensorChanged(int sensor, float[] values) { - int orientation = ORIENTATION_UNKNOWN; - float X = values[SensorManager.RAW_DATA_X]; - float Y = values[SensorManager.RAW_DATA_Y]; - float Z = values[SensorManager.RAW_DATA_Z]; - float magnitude = X*X + Y*Y; - // Don't trust the angle if the magnitude is small compared to the y value - if (magnitude * 4 >= Z*Z) { - float OneEightyOverPi = 57.29577957855f; - float angle = (float)Math.atan2(-Y, X) * OneEightyOverPi; - orientation = 90 - (int)Math.round(angle); - // normalize to 0 - 359 range - while (orientation >= 360) { - orientation -= 360; - } - while (orientation < 0) { - orientation += 360; - } - } - - if (orientation != mOrientation) { - mOrientation = orientation; - onOrientationChanged(orientation); - } + // just ignore the call here onOrientationChanged is invoked anyway } - public void onAccuracyChanged(int sensor, int accuracy) { - // TODO Auto-generated method stub - } /** - * Called when the orientation of the device has changed. - * orientation parameter is in degrees, ranging from 0 to 359. - * orientation is 0 degrees when the device is oriented in its natural position, - * 90 degrees when its left side is at the top, 180 degrees when it is upside down, - * and 270 degrees when its right side is to the top. - * {@link #ORIENTATION_UNKNOWN} is returned when the device is close to flat - * and the orientation cannot be determined. - * + * Look at {@link android.view.OrientationEventListener#onOrientationChanged} + * for method description and usage * @param orientation The new orientation of the device. * * @see #ORIENTATION_UNKNOWN */ abstract public void onOrientationChanged(int orientation); + } diff --git a/core/java/android/os/HandlerInterface.java b/core/java/android/view/RemotableViewMethod.java index 62dc273..4318290 100644 --- a/core/java/android/os/HandlerInterface.java +++ b/core/java/android/view/RemotableViewMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,22 @@ * limitations under the License. */ -package android.os; +package android.view; -/** - * @hide - * @deprecated +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @hide + * This annotation indicates that a method on a subclass of View + * is alllowed to be used with the {@link android.widget.RemoteViews} mechanism. */ -public interface HandlerInterface -{ - void handleMessage(Message msg); +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface RemotableViewMethod { } + + diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1d5e7cd..5ed3a7e 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2674,6 +2674,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. * @attr ref android.R.styleable#View_visibility */ + @RemotableViewMethod public void setVisibility(int visibility) { setFlags(visibility, VISIBILITY_MASK); if (mBGDrawable != null) mBGDrawable.setVisible(visibility == VISIBLE, false); @@ -4016,6 +4017,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ protected void onScrollChanged(int l, int t, int oldl, int oldt) { mBackgroundSizeChanged = true; + + final AttachInfo ai = mAttachInfo; + if (ai != null) { + ai.mViewScrollChanged = true; + } } /** @@ -7948,6 +7954,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { boolean mViewVisibilityChanged; /** + * Set to true if a view has been scrolled. + */ + boolean mViewScrollChanged; + + /** * Global to the view hierarchy used as a temporary for dealing with * x/y points in the transparent region computations. */ diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 7153ea1..2f7b0d1 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -67,6 +67,13 @@ public class ViewConfiguration { * considered to be a tap. */ private static final int JUMP_TAP_TIMEOUT = 500; + + /** + * Defines the duration in milliseconds between the first tap's up event and + * the second tap's down event for an interaction to be considered a + * double-tap. + */ + private static final int DOUBLE_TAP_TIMEOUT = 300; /** * Defines the duration in milliseconds we want to display zoom controls in response @@ -82,7 +89,7 @@ public class ViewConfiguration { /** * Distance a touch can wander before we think the user is scrolling in pixels */ - private static final int TOUCH_SLOP = 12; + private static final int TOUCH_SLOP = 25; /** * Distance between the first touch and second touch to still be considered a double tap @@ -257,6 +264,16 @@ public class ViewConfiguration { } /** + * @return Defines the duration in milliseconds between the first tap's up event and + * the second tap's down event for an interaction to be considered a + * double-tap. + * @hide pending API council + */ + public static int getDoubleTapTimeout() { + return DOUBLE_TAP_TIMEOUT; + } + + /** * @return Inset in pixels to look for touchable content when the user touches the edge of the * screen * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index c758662..70cc2a9 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -27,6 +27,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.RectF; import android.os.Parcelable; +import android.os.SystemClock; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; @@ -1023,6 +1024,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override void dispatchDetachedFromWindow() { + // If we still have a motion target, we are still in the process of + // dispatching motion events to a child; we need to get rid of that + // child to avoid dispatching events to it after the window is torn + // down. To make sure we keep the child in a consistent state, we + // first send it an ACTION_CANCEL motion event. + if (mMotionTarget != null) { + final long now = SystemClock.uptimeMillis(); + final MotionEvent event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + mMotionTarget.dispatchTouchEvent(event); + event.recycle(); + mMotionTarget = null; + } + final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { @@ -1331,6 +1346,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final Animation a = child.getAnimation(); boolean concatMatrix = false; + final int childWidth = cr - cl; + final int childHeight = cb - ct; + if (a != null) { if (mInvalidateRegion == null) { mInvalidateRegion = new RectF(); @@ -1339,8 +1357,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final boolean initialized = a.isInitialized(); if (!initialized) { - a.initialize(cr - cl, cb - ct, getWidth(), getHeight()); - a.initializeInvalidateRegion(cl, ct, cr, cb); + a.initialize(childWidth, childHeight, getWidth(), getHeight()); + a.initializeInvalidateRegion(0, 0, childWidth, childHeight); child.onAnimationStart(); } @@ -1364,7 +1382,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager invalidate(cl, ct, cr, cb); } } else { - a.getInvalidateRegion(cl, ct, cr, cb, region, transformToApply); + a.getInvalidateRegion(0, 0, childWidth, childHeight, region, transformToApply); // The child need to draw an animation, potentially offscreen, so // make sure we do not cancel invalidate requests @@ -1372,8 +1390,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Enlarge the invalidate region to account for rounding errors // in Animation#getInvalidateRegion(); Using 0.5f is unfortunately // not enough for some types of animations (e.g. scale down.) - invalidate((int) (region.left - 1.0f), (int) (region.top - 1.0f), - (int) (region.right + 1.0f), (int) (region.bottom + 1.0f)); + final int left = cl + (int) (region.left - 1.0f); + final int top = ct + (int) (region.top - 1.0f); + invalidate(left, top, + left + (int) (region.width() + 1.0f), + top + (int) (region.height() + 1.0f)); } } } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == @@ -1453,9 +1474,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) { if (hasNoCache) { - canvas.clipRect(sx, sy, sx + cr - cl, sy + cb - ct); + canvas.clipRect(sx, sy, sx + childWidth, sy + childHeight); } else { - canvas.clipRect(0, 0, cr - cl, cb - ct); + canvas.clipRect(0, 0, childWidth, childHeight); } } diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index ccfa6bf..db8829f 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -33,6 +33,7 @@ import android.util.Config; import android.util.Log; import android.util.EventLog; import android.util.SparseArray; +import android.util.DisplayMetrics; import android.view.View.MeasureSpec; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; @@ -184,6 +185,7 @@ public final class ViewRoot extends Handler implements ViewParent, */ AudioManager mAudioManager; + private final float mDensity; public ViewRoot(Context context) { super(); @@ -226,6 +228,7 @@ public final class ViewRoot extends Handler implements ViewParent, mAdded = false; mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); mViewConfiguration = ViewConfiguration.get(context); + mDensity = context.getResources().getDisplayMetrics().density; } @Override @@ -1077,6 +1080,11 @@ public final class ViewRoot extends Handler implements ViewParent, } scrollToRectOrFocus(null, false); + + if (mAttachInfo.mViewScrollChanged) { + mAttachInfo.mViewScrollChanged = false; + mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); + } int yoff; final boolean scrolling = mScroller != null @@ -1090,7 +1098,7 @@ public final class ViewRoot extends Handler implements ViewParent, mCurScrollY = yoff; fullRedrawNeeded = true; } - + Rect dirty = mDirty; if (mUseGL) { if (!dirty.isEmpty()) { @@ -1126,7 +1134,6 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (fullRedrawNeeded) dirty.union(0, 0, mWidth, mHeight); @@ -1138,22 +1145,22 @@ public final class ViewRoot extends Handler implements ViewParent, + surface + " surface.isValid()=" + surface.isValid()); } - if (!dirty.isEmpty()) { - Canvas canvas; - try { - canvas = surface.lockCanvas(dirty); - // TODO: Do this in native - canvas.setDensityScale(mView.getResources().getDisplayMetrics().density); - } catch (Surface.OutOfResourcesException e) { - Log.e("ViewRoot", "OutOfResourcesException locking surface", e); - // TODO: we should ask the window manager to do something! - // for now we just do nothing - return; - } + Canvas canvas; + try { + canvas = surface.lockCanvas(dirty); + // TODO: Do this in native + canvas.setDensityScale(mDensity); + } catch (Surface.OutOfResourcesException e) { + Log.e("ViewRoot", "OutOfResourcesException locking surface", e); + // TODO: we should ask the window manager to do something! + // for now we just do nothing + return; + } - long startTime; + try { + if (!dirty.isEmpty()) { + long startTime; - try { if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v("ViewRoot", "Surface " + surface + " drawing to bitmap w=" + canvas.getWidth() + ", h=" + canvas.getHeight()); @@ -1169,7 +1176,7 @@ public final class ViewRoot extends Handler implements ViewParent, // properly re-composite its drawing on a transparent // background. This automatically respects the clip/dirty region if (!canvas.isOpaque()) { - canvas.drawColor(0, PorterDuff.Mode.CLEAR); + canvas.drawColor(0xff0000ff, PorterDuff.Mode.CLEAR); } else if (yoff != 0) { // If we are applying an offset, we need to clear the area // where the offset doesn't appear to avoid having garbage @@ -1192,35 +1199,18 @@ public final class ViewRoot extends Handler implements ViewParent, sDrawTime = now; } - } finally { - surface.unlockCanvasAndPost(canvas); - } - - if (PROFILE_DRAWING) { - EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); - } - - if (LOCAL_LOGV) { - Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost"); + if (PROFILE_DRAWING) { + EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); + } } - } else if (mWidth == 0 || mHeight == 0) { - // This is a special case where a window dimension is 0 -- we - // normally wouldn't draw anything because we have an empty - // dirty rect, but the surface flinger may be waiting for us to - // draw the window before it stops freezing the screen, so we - // need to diddle it like this to keep it from getting stuck. - Canvas canvas; - try { - canvas = surface.lockCanvas(dirty); - } catch (Surface.OutOfResourcesException e) { - Log.e("ViewRoot", "OutOfResourcesException locking surface", e); - // TODO: we should ask the window manager to do something! - // for now we just do nothing - return; - } + } finally { surface.unlockCanvasAndPost(canvas); } + + if (LOCAL_LOGV) { + Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost"); + } if (scrolling) { mFullRedrawNeeded = true; @@ -1401,14 +1391,22 @@ public final class ViewRoot extends Handler implements ViewParent, void dispatchDetachedFromWindow() { if (Config.LOGV) Log.v("ViewRoot", "Detaching in " + this + " of " + mSurface); + if (mView != null) { mView.dispatchDetachedFromWindow(); } + mView = null; mAttachInfo.mRootView = null; + if (mUseGL) { destroyGL(); } + + try { + sWindowSession.remove(mWindow); + } catch (RemoteException e) { + } } /** @@ -1609,16 +1607,20 @@ public final class ViewRoot extends Handler implements ViewParent, } } } + + InputMethodManager imm = InputMethodManager.peekInstance(); if (mView != null) { + if (hasWindowFocus && imm != null) { + imm.startGettingWindowFocus(); + } mView.dispatchWindowFocusChanged(hasWindowFocus); } // Note: must be done after the focus change callbacks, // so all of the view state is set up correctly. if (hasWindowFocus) { - InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { - imm.onWindowFocus(mView.findFocus(), + imm.onWindowFocus(mView, mView.findFocus(), mWindowAttributes.softInputMode, !mHasHadWindowFocus, mWindowAttributes.flags); } @@ -2289,10 +2291,6 @@ public final class ViewRoot extends Handler implements ViewParent, } if (mAdded) { mAdded = false; - try { - sWindowSession.remove(mWindow); - } catch (RemoteException e) { - } if (immediate) { dispatchDetachedFromWindow(); } else if (mView != null) { diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index 05f5fa2..47b52e4 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -35,6 +35,7 @@ public final class ViewTreeObserver { private ArrayList<OnPreDrawListener> mOnPreDrawListeners; private ArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners; private ArrayList<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners; + private ArrayList<OnScrollChangedListener> mOnScrollChangedListeners; private boolean mAlive = true; @@ -99,6 +100,20 @@ public final class ViewTreeObserver { } /** + * Interface definition for a callback to be invoked when + * something in the view tree has been scrolled. + * + * @hide pending API council approval + */ + public interface OnScrollChangedListener { + /** + * Callback method to be invoked when something in the view tree + * has been scrolled. + */ + public void onScrollChanged(); + } + + /** * Parameters used with OnComputeInternalInsetsListener. * {@hide pending API Council approval} */ @@ -361,6 +376,44 @@ public final class ViewTreeObserver { } /** + * Register a callback to be invoked when a view has been scrolled. + * + * @param listener The callback to add + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @hide pending API council approval + */ + public void addOnScrollChangedListener(OnScrollChangedListener listener) { + checkIsAlive(); + + if (mOnScrollChangedListeners == null) { + mOnScrollChangedListeners = new ArrayList<OnScrollChangedListener>(); + } + + mOnScrollChangedListeners.add(listener); + } + + /** + * Remove a previously installed scroll-changed callback + * + * @param victim The callback to remove + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @see #addOnScrollChangedListener(OnScrollChangedListener) + * + * @hide pending API council approval + */ + public void removeOnScrollChangedListener(OnScrollChangedListener victim) { + checkIsAlive(); + if (mOnScrollChangedListeners == null) { + return; + } + mOnScrollChangedListeners.remove(victim); + } + + /** * Register a callback to be invoked when the invoked when the touch mode changes. * * @param listener The callback to add @@ -525,6 +578,19 @@ public final class ViewTreeObserver { } /** + * Notifies registered listeners that something has scrolled. + */ + final void dispatchOnScrollChanged() { + final ArrayList<OnScrollChangedListener> listeners = mOnScrollChangedListeners; + + if (listeners != null) { + for (OnScrollChangedListener scl : mOnScrollChangedListeners) { + scl.onScrollChanged(); + } + } + } + + /** * Returns whether there are listeners for computing internal insets. */ final boolean hasComputeInternalInsetsListeners() { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 406af3e3..b87cc42 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -513,26 +513,33 @@ public interface WindowManager extends ViewManager { /** * Visibility state for {@link #softInputMode}: please hide any soft input - * area. + * area when normally appropriate (when the user is navigating + * forward to your window). */ public static final int SOFT_INPUT_STATE_HIDDEN = 2; /** + * Visibility state for {@link #softInputMode}: please always hide any + * soft input area when this window receives focus. + */ + public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3; + + /** * Visibility state for {@link #softInputMode}: please show the soft * input area when normally appropriate (when the user is navigating * forward to your window). */ - public static final int SOFT_INPUT_STATE_VISIBLE = 3; + public static final int SOFT_INPUT_STATE_VISIBLE = 4; /** * Visibility state for {@link #softInputMode}: please always make the * soft input area visible when this window receives input focus. */ - public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 4; + public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5; /** * Mask for {@link #softInputMode} of the bits that determine the - * way that the window should be adjusted to accomodate the soft + * way that the window should be adjusted to accommodate the soft * input window. */ public static final int SOFT_INPUT_MASK_ADJUST = 0xf0; @@ -634,6 +641,14 @@ public interface WindowManager extends ViewManager { public float dimAmount = 1.0f; /** + * This can be used to override the user's preferred brightness of + * the screen. A value of less than 0, the default, means to use the + * preferred screen brightness. 0 to 1 adjusts the brightness from + * dark to full bright. + */ + public float screenBrightness = -1.0f; + + /** * Identifier for this window. This will usually be filled in for * you. */ @@ -729,6 +744,7 @@ public interface WindowManager extends ViewManager { out.writeInt(windowAnimations); out.writeFloat(alpha); out.writeFloat(dimAmount); + out.writeFloat(screenBrightness); out.writeStrongBinder(token); out.writeString(packageName); TextUtils.writeToParcel(mTitle, out, parcelableFlags); @@ -763,6 +779,7 @@ public interface WindowManager extends ViewManager { windowAnimations = in.readInt(); alpha = in.readFloat(); dimAmount = in.readFloat(); + screenBrightness = in.readFloat(); token = in.readStrongBinder(); packageName = in.readString(); mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); @@ -780,6 +797,7 @@ public interface WindowManager extends ViewManager { public static final int MEMORY_TYPE_CHANGED = 1<<8; public static final int SOFT_INPUT_MODE_CHANGED = 1<<9; public static final int SCREEN_ORIENTATION_CHANGED = 1<<10; + public static final int SCREEN_BRIGHTNESS_CHANGED = 1<<11; public final int copyFrom(LayoutParams o) { int changes = 0; @@ -874,6 +892,10 @@ public interface WindowManager extends ViewManager { dimAmount = o.dimAmount; changes |= DIM_AMOUNT_CHANGED; } + if (screenBrightness != o.screenBrightness) { + screenBrightness = o.screenBrightness; + changes |= SCREEN_BRIGHTNESS_CHANGED; + } if (screenOrientation != o.screenOrientation) { screenOrientation = o.screenOrientation; diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java new file mode 100755 index 0000000..4aa3f7a --- /dev/null +++ b/core/java/android/view/WindowOrientationListener.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Config; +import android.util.Log; + +/** + * A special helper class used by the WindowManager + * for receiving notifications from the SensorManager when + * the orientation of the device has changed. + * @hide + */ +public abstract class WindowOrientationListener { + private static final String TAG = "WindowOrientationListener"; + private static final boolean DEBUG = false; + private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private int mOrientation = ORIENTATION_UNKNOWN; + private SensorManager mSensorManager; + private boolean mEnabled = false; + private int mRate; + private Sensor mSensor; + private SensorEventListener mSensorEventListener; + + /** + * Returned from onOrientationChanged when the device orientation cannot be determined + * (typically when the device is in a close to flat position). + * + * @see #onOrientationChanged + */ + public static final int ORIENTATION_UNKNOWN = -1; + /* + * Returned when the device is almost lying flat on a surface + */ + public static final int ORIENTATION_FLAT = -2; + + /** + * Creates a new WindowOrientationListener. + * + * @param context for the WindowOrientationListener. + */ + public WindowOrientationListener(Context context) { + this(context, SensorManager.SENSOR_DELAY_NORMAL); + } + + /** + * Creates a new WindowOrientationListener. + * + * @param context for the WindowOrientationListener. + * @param rate at which sensor events are processed (see also + * {@link android.hardware.SensorManager SensorManager}). Use the default + * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL + * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. + */ + public WindowOrientationListener(Context context, int rate) { + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mRate = rate; + mSensorEventListener = new SensorEventListenerImpl(); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + /** + * Enables the WindowOrientationListener so it will monitor the sensor and call + * {@link #onOrientationChanged} when the device orientation changes. + */ + public void enable() { + if (mEnabled == false) { + if (localLOGV) Log.d(TAG, "WindowOrientationListener enabled"); + mSensorManager.registerListener(mSensorEventListener, mSensor, mRate); + mEnabled = true; + } + } + + /** + * Disables the WindowOrientationListener. + */ + public void disable() { + if (mEnabled == true) { + if (localLOGV) Log.d(TAG, "WindowOrientationListener disabled"); + mSensorManager.unregisterListener(mSensorEventListener); + mEnabled = false; + } + } + + class SensorEventListenerImpl implements SensorEventListener { + private static final int _DATA_X = 0; + private static final int _DATA_Y = 1; + private static final int _DATA_Z = 2; + + public void onSensorChanged(SensorEvent event) { + float[] values = event.values; + int orientation = ORIENTATION_UNKNOWN; + float X = values[_DATA_X]; + float Y = values[_DATA_Y]; + float Z = values[_DATA_Z]; + float OneEightyOverPi = 57.29577957855f; + float gravity = (float) Math.sqrt(X*X+Y*Y+Z*Z); + float zyangle = Math.abs((float)Math.asin(Z/gravity)*OneEightyOverPi); + // The device is considered flat if the angle is more than 75 + // if the angle is less than 40, its considered too flat to switch + // orientation. if the angle is between 40 - 75, the orientation is unknown + if (zyangle < 40) { + // Check orientation only if the phone is flat enough + // Don't trust the angle if the magnitude is small compared to the y value + float angle = (float)Math.atan2(Y, -X) * OneEightyOverPi; + orientation = 90 - (int)Math.round(angle); + // normalize to 0 - 359 range + while (orientation >= 360) { + orientation -= 360; + } + while (orientation < 0) { + orientation += 360; + } + } else if (zyangle >= 75){ + orientation = ORIENTATION_FLAT; + } + + if (orientation != mOrientation) { + mOrientation = orientation; + onOrientationChanged(orientation); + } + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } + } + + /** + * Called when the orientation of the device has changed. + * orientation parameter is in degrees, ranging from 0 to 359. + * orientation is 0 degrees when the device is oriented in its natural position, + * 90 degrees when its left side is at the top, 180 degrees when it is upside down, + * and 270 degrees when its right side is to the top. + * {@link #ORIENTATION_UNKNOWN} is returned when the device is close to flat + * and the orientation cannot be determined. + * + * @param orientation The new orientation of the device. + * + * @see #ORIENTATION_UNKNOWN + */ + abstract public void onOrientationChanged(int orientation); +} diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index c96b3e5..b9c8ec3 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -179,6 +179,7 @@ public abstract class Animation implements Cloneable { private boolean mOneMoreTime = true; RectF mPreviousRegion = new RectF(); + RectF mRegion = new RectF(); Transformation mTransformation = new Transformation(); Transformation mPreviousTransformation = new Transformation(); @@ -226,6 +227,7 @@ public abstract class Animation implements Cloneable { protected Animation clone() throws CloneNotSupportedException { final Animation animation = (Animation) super.clone(); animation.mPreviousRegion = new RectF(); + animation.mRegion = new RectF(); animation.mTransformation = new Transformation(); animation.mPreviousTransformation = new Transformation(); return animation; @@ -799,14 +801,15 @@ public abstract class Animation implements Cloneable { public void getInvalidateRegion(int left, int top, int right, int bottom, RectF invalidate, Transformation transformation) { + final RectF tempRegion = mRegion; final RectF previousRegion = mPreviousRegion; invalidate.set(left, top, right, bottom); transformation.getMatrix().mapRect(invalidate); + tempRegion.set(invalidate); invalidate.union(previousRegion); - previousRegion.set(left, top, right, bottom); - transformation.getMatrix().mapRect(previousRegion); + previousRegion.set(tempRegion); final Transformation tempTransformation = mTransformation; final Transformation previousTransformation = mPreviousTransformation; diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 9509b15..6fbc174 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -525,9 +525,6 @@ public class BaseInputConnection implements InputConnection { setComposingSpans(sp); } - // Adjust newCursorPosition to be relative the start of the text. - newCursorPosition += a; - if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \"" + text + "\", composing=" + composing + ", type=" + text.getClass().getCanonicalName()); @@ -540,11 +537,21 @@ public class BaseInputConnection implements InputConnection { TextUtils.dumpSpans(text, lp, " "); } - content.replace(a, b, text); + // Position the cursor appropriately, so that after replacing the + // desired range of text it will be located in the correct spot. + // This allows us to deal with filters performing edits on the text + // we are providing here. + if (newCursorPosition > 0) { + newCursorPosition += b - 1; + } else { + newCursorPosition += a; + } if (newCursorPosition < 0) newCursorPosition = 0; if (newCursorPosition > content.length()) newCursorPosition = content.length(); Selection.setSelection(content, newCursorPosition); + + content.replace(a, b, text); if (DEBUG) { LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 13173f6..530127d 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -148,8 +148,14 @@ public interface InputConnection { * object to the text. {#link android.text.SpannableString} and * {#link android.text.SpannableStringBuilder} are two * implementations of the interface {#link android.text.Spanned}. - * @param newCursorPosition The new cursor position within the - * <var>text</var>. + * @param newCursorPosition The new cursor position around the text. If + * > 0, this is relative to the end of the text - 1; if <= 0, this + * is relative to the start of the text. So a value of 1 will + * always advance you to the position after the full text being + * inserted. Note that this means you can't position the cursor + * within the text, because the editor can make modifications to + * the text you are providing so it is not possible to correctly + * specify locations there. * * @return Returns true on success, false if the input connection is no longer * valid. @@ -170,8 +176,14 @@ public interface InputConnection { * automatically. * * @param text The committed text. - * @param newCursorPosition The new cursor position within the - * <var>text</var>. + * @param newCursorPosition The new cursor position around the text. If + * > 0, this is relative to the end of the text - 1; if <= 0, this + * is relative to the start of the text. So a value of 1 will + * always advance you to the position after the full text being + * inserted. Note that this means you can't position the cursor + * within the text, because the editor can make modifications to + * the text you are providing so it is not possible to correctly + * specify locations there. * * * @return Returns true on success, false if the input connection is no longer diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index fe14166..91fa211 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -226,6 +226,10 @@ public final class InputMethodManager { * regardless of the state of setting that up. */ View mServedView; + /* + * Keep track of the view that was set when our window gained focus. + */ + View mWindowFocusedView; /** * For evaluating the state after a focus change, this is the view that * had focus. @@ -466,8 +470,8 @@ public final class InputMethodManager { } /** @hide */ - public void setFullscreenMode(boolean enabled) { - mFullscreenMode = true; + public void setFullscreenMode(boolean fullScreen) { + mFullscreenMode = fullScreen; } /** @@ -828,19 +832,23 @@ public final class InputMethodManager { */ public void focusIn(View view) { synchronized (mH) { - if (DEBUG) Log.v(TAG, "focusIn: " + view); - // Okay we have a new view that is being served. - if (mServedView != view) { - mCurrentTextBoxAttribute = null; - } - mServedView = view; - mCompletions = null; - mServedConnecting = true; + focusInLocked(view); } startInputInner(); } + void focusInLocked(View view) { + if (DEBUG) Log.v(TAG, "focusIn: " + view); + // Okay we have a new view that is being served. + if (mServedView != view) { + mCurrentTextBoxAttribute = null; + } + mServedView = view; + mCompletions = null; + mServedConnecting = true; + } + /** * Call this when a view loses focus. * @hide @@ -908,16 +916,28 @@ public final class InputMethodManager { * Called by ViewRoot the first time it gets window focus. * @hide */ - public void onWindowFocus(View focusedView, int softInputMode, + public void onWindowFocus(View rootView, View focusedView, int softInputMode, boolean first, int windowFlags) { + boolean needStartInput = false; synchronized (mH) { if (DEBUG) Log.v(TAG, "onWindowFocus: " + focusedView + " softInputMode=" + softInputMode + " first=" + first + " flags=#" + Integer.toHexString(windowFlags)); + if (mWindowFocusedView == null) { + focusInLocked(focusedView != null ? focusedView : rootView); + needStartInput = true; + } + } + + if (needStartInput) { + startInputInner(); + } + + synchronized (mH) { try { final boolean isTextEditor = focusedView != null && - focusedView.onCheckIsTextEditor(); + focusedView.onCheckIsTextEditor(); mService.windowGainedFocus(mClient, focusedView != null, isTextEditor, softInputMode, first, windowFlags); } catch (RemoteException e) { @@ -925,6 +945,13 @@ public final class InputMethodManager { } } + /** @hide */ + public void startGettingWindowFocus() { + synchronized (mH) { + mWindowFocusedView = null; + } + } + /** * Report the current selection range. */ diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index 1dd37be..451af6d 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -51,7 +51,7 @@ class BrowserFrame extends Handler { private final Context mContext; private final WebViewDatabase mDatabase; private final WebViewCore mWebViewCore; - private boolean mLoadInitFromJava; + /* package */ boolean mLoadInitFromJava; private int mLoadType; private boolean mFirstLayoutDone = true; private boolean mCommitted = true; diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index 3694969..dfae17d 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -575,7 +575,7 @@ class LoadListener extends Handler implements EventHandler { mRequestHandle.getMethod().equals("POST")) { sendMessageInternal(obtainMessage( MSG_LOCATION_CHANGED_REQUEST)); - } else if (mMethod.equals("POST")) { + } else if (mMethod != null && mMethod.equals("POST")) { sendMessageInternal(obtainMessage( MSG_LOCATION_CHANGED_REQUEST)); } else { diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index 9af30c5..c2620a5 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -291,6 +291,25 @@ import java.util.ArrayList; } /** + * Create a fake touch up event at (x,y) with respect to this TextDialog. + * This is used by WebView to act as though a touch event which happened + * before we placed the TextDialog actually hit it, so that it can place + * the cursor accordingly. + */ + /* package */ void fakeTouchEvent(float x, float y) { + // We need to ensure that there is a Layout, since the Layout is used + // in determining where to place the cursor. + if (getLayout() == null) { + measure(mWidthSpec, mHeightSpec); + } + // Create a fake touch up, which is used to place the cursor. + MotionEvent ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, + x, y, 0); + onTouchEvent(ev); + ev.recycle(); + } + + /** * Determine whether this TextDialog currently represents the node * represented by ptr. * @param ptr Pointer to a node to compare to. @@ -461,9 +480,8 @@ import java.util.ArrayList; */ public void setAdapterCustom(AutoCompleteAdapter adapter) { if (adapter != null) { - adapter.setTextView(this); - } else { setInputType(EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); + adapter.setTextView(this); } super.setAdapter(adapter); } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index bdbf38a..4d9a8fb 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -45,6 +45,8 @@ import android.util.AttributeSet; import android.util.Config; import android.util.Log; import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -234,6 +236,9 @@ public class WebView extends AbsoluteLayout */ VelocityTracker mVelocityTracker; + private static boolean mShowZoomRingTutorial = true; + private static final int ZOOM_RING_TUTORIAL_DURATION = 3000; + /** * Touch mode */ @@ -284,9 +289,7 @@ public class WebView extends AbsoluteLayout // In the browser, if it switches out of tap too soon, jump tap won't work. private static final int TAP_TIMEOUT = 200; // The duration in milliseconds we will wait to see if it is a double tap. - // With a limited survey, the time between the first tap up and the second - // tap down in the double tap case is around 70ms - 120ms. - private static final int DOUBLE_TAP_TIMEOUT = 200; + private static final int DOUBLE_TAP_TIMEOUT = 250; // This should be ViewConfiguration.getLongPressTimeout() // But system time out is 500ms, which is too short for the browser. // With a short timeout, it's difficult to treat trigger a short press. @@ -315,6 +318,9 @@ public class WebView extends AbsoluteLayout private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore + static int MAX_FLOAT_CONTENT_WIDTH = 480; + private int mMinContentWidth; + // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; @@ -348,6 +354,7 @@ public class WebView extends AbsoluteLayout private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6; private static final int SWITCH_TO_ENTER = 7; private static final int RESUME_WEBCORE_UPDATE = 8; + private static final int DISMISS_ZOOM_RING_TUTORIAL = 9; //! arg1=x, arg2=y static final int SCROLL_TO_MSG_ID = 10; @@ -370,12 +377,46 @@ public class WebView extends AbsoluteLayout static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // obj=Rect in doc coordinates static final int INVAL_RECT_MSG_ID = 26; + + static final String[] HandlerDebugString = { + "REMEMBER_PASSWORD", // = 1; + "NEVER_REMEMBER_PASSWORD", // = 2; + "SWITCH_TO_SHORTPRESS", // = 3; + "SWITCH_TO_LONGPRESS", // = 4; + "RELEASE_SINGLE_TAP", // = 5; + "UPDATE_TEXT_ENTRY_ADAPTER", // = 6; + "SWITCH_TO_ENTER", // = 7; + "RESUME_WEBCORE_UPDATE", // = 8; + "9", + "SCROLL_TO_MSG_ID", // = 10; + "SCROLL_BY_MSG_ID", // = 11; + "SPAWN_SCROLL_TO_MSG_ID", // = 12; + "SYNC_SCROLL_TO_MSG_ID", // = 13; + "NEW_PICTURE_MSG_ID", // = 14; + "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; + "WEBCORE_INITIALIZED_MSG_ID", // = 16; + "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; + "DID_FIRST_LAYOUT_MSG_ID", // = 18; + "RECOMPUTE_FOCUS_MSG_ID", // = 19; + "NOTIFY_FOCUS_SET_MSG_ID", // = 20; + "MARK_NODE_INVALID_ID", // = 21; + "UPDATE_CLIPBOARD", // = 22; + "LONG_PRESS_ENTER", // = 23; + "PREVENT_TOUCH_ID", // = 24; + "WEBCORE_NEED_TOUCH_EVENTS", // = 25; + "INVAL_RECT_MSG_ID" // = 26; + }; // width which view is considered to be fully zoomed out static final int ZOOM_OUT_WIDTH = 1024; - private static final float DEFAULT_MAX_ZOOM_SCALE = 4; - private static final float DEFAULT_MIN_ZOOM_SCALE = 0.25f; + private static final float MAX_ZOOM_RING_ANGLE = (float) (Math.PI * 2 / 3); + private static final int ZOOM_RING_STEPS = 4; + private static final float ZOOM_RING_ANGLE_UNIT = MAX_ZOOM_RING_ANGLE + / ZOOM_RING_STEPS; + + private static final float DEFAULT_MAX_ZOOM_SCALE = 2; + private static final float DEFAULT_MIN_ZOOM_SCALE = (float) 1/3; // scale limit, which can be set through viewport meta tag in the web page private float mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; private float mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; @@ -505,6 +546,8 @@ public class WebView extends AbsoluteLayout } private ZoomRingController mZoomRingController; + private ImageView mZoomRingOverview; + private Animation mZoomRingOverviewExitAnimation; // These keep track of the center point of the zoom ring. They are used to // determine the point around which we should zoom. @@ -519,51 +562,83 @@ public class WebView extends AbsoluteLayout // in this callback } + public void onBeginPan() { + setZoomOverviewVisible(false); + } + public boolean onPan(int deltaX, int deltaY) { return pinScrollBy(deltaX, deltaY, false, 0); } + public void onEndPan() { + } + public void onVisibilityChanged(boolean visible) { if (visible) { - mZoomControls.show(false, canZoomScrollOut()); - } else { - mZoomControls.hide(); + switchOutDrawHistory(); + float angle = 0f; + if (mActualScale > 1) { + angle = -(float) Math.round(ZOOM_RING_STEPS + * (mActualScale - 1) / (mMaxZoomScale - 1)) + / ZOOM_RING_STEPS; + } else if (mActualScale < 1) { + angle = (float) Math.round(ZOOM_RING_STEPS + * (1 - mActualScale) / (1 - mMinZoomScale)) + / ZOOM_RING_STEPS; + } + mZoomRingController.setThumbAngle(angle * MAX_ZOOM_RING_ANGLE); + + // Show the zoom overview tab on the ring + setZoomOverviewVisible(true); } } - - public void onBeginDrag(float startAngle) { + + public void onBeginDrag() { mPreviewZoomOnly = true; + setZoomOverviewVisible(false); } - public void onEndDrag(float endAngle) { + public void onEndDrag() { mPreviewZoomOnly = false; setNewZoomScale(mActualScale, true); } public boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle) { - - if (mZoomScale == mMinZoomScale && deltaZoomLevel < 0 || - mZoomScale == mMaxZoomScale && deltaZoomLevel > 0 || - deltaZoomLevel == 0) { + if (deltaZoomLevel < 0 + && Math.abs(mActualScale - mMinZoomScale) < 0.01f + || deltaZoomLevel > 0 + && Math.abs(mActualScale - mMaxZoomScale) < 0.01f + || deltaZoomLevel == 0) { return false; } mZoomCenterX = (float) centerX; mZoomCenterY = (float) centerY; - while (deltaZoomLevel != 0) { - if (deltaZoomLevel > 0) { - if (!zoomIn()) return false; - deltaZoomLevel--; + float scale = 1.0f; + if (curAngle > (float) Math.PI) + curAngle -= (float) 2 * Math.PI; + if (curAngle > 0) { + if (curAngle >= MAX_ZOOM_RING_ANGLE) { + scale = mMinZoomScale; + } else { + scale = 1 - (float) Math.round(curAngle + / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS + * (1 - mMinZoomScale); + } + } else if (curAngle < 0) { + if (curAngle <= -MAX_ZOOM_RING_ANGLE) { + scale = mMaxZoomScale; } else { - if (!zoomOut()) return false; - deltaZoomLevel++; + scale = 1 + (float) Math.round(-curAngle + / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS + * (mMaxZoomScale - 1); } } - + zoomWithPreview(scale); return true; } - + public void onSimpleZoom(boolean zoomIn) { if (zoomIn) { zoomIn(); @@ -571,6 +646,7 @@ public class WebView extends AbsoluteLayout zoomOut(); } } + }; /** @@ -610,7 +686,14 @@ public class WebView extends AbsoluteLayout mFocusData.mY = 0; mScroller = new Scroller(context); mZoomRingController = new ZoomRingController(context, this); + mZoomRingController.setResetThumbAutomatically(false); + mZoomRingController.setThumbClockwiseBound( + (float) (2 * Math.PI - MAX_ZOOM_RING_ANGLE)); + mZoomRingController.setThumbCounterclockwiseBound(MAX_ZOOM_RING_ANGLE); mZoomRingController.setCallback(mZoomListener); + mZoomRingController.setZoomRingTrack( + com.android.internal.R.drawable.zoom_ring_track_absolute); + createZoomRingOverviewTab(); } private void init() { @@ -625,6 +708,63 @@ public class WebView extends AbsoluteLayout mMinLockSnapReverseDistance = slop; } + private void createZoomRingOverviewTab() { + Context context = getContext(); + + mZoomRingOverviewExitAnimation = AnimationUtils.loadAnimation(context, + com.android.internal.R.anim.fade_out); + + mZoomRingOverview = new ImageView(context); + mZoomRingOverview.setBackgroundResource( + com.android.internal.R.drawable.zoom_ring_overview_tab); + mZoomRingOverview.setImageResource(com.android.internal.R.drawable.btn_zoom_page); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER); + // TODO: magic constant that's based on the zoom ring radius + some offset + lp.topMargin = 208; + mZoomRingOverview.setLayoutParams(lp); + mZoomRingOverview.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // Hide the zoom ring + mZoomRingController.setVisible(false); + zoomScrollOut(); + }}); + + // Measure the overview View to figure out its height + mZoomRingOverview.forceLayout(); + mZoomRingOverview.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + ViewGroup container = mZoomRingController.getContainer(); + // Find the index of the zoom ring in the container + View zoomRing = container.findViewById(mZoomRingController.getZoomRingId()); + int zoomRingIndex; + for (zoomRingIndex = container.getChildCount() - 1; zoomRingIndex >= 0; zoomRingIndex--) { + if (container.getChildAt(zoomRingIndex) == zoomRing) break; + } + // Add the overview tab below the zoom ring (so we don't steal its events) + container.addView(mZoomRingOverview, zoomRingIndex); + // Since we use margins to adjust the vertical placement of the tab, the widget + // ends up getting clipped off. Ensure the container is big enough for + // us. + int myHeight = mZoomRingOverview.getMeasuredHeight() + lp.topMargin / 2; + // Multiplied by 2 b/c the zoom ring needs to be centered on the screen + container.setMinimumHeight(myHeight * 2); + } + + private void setZoomOverviewVisible(boolean visible) { + int newVisibility = visible ? View.VISIBLE : View.INVISIBLE; + if (mZoomRingOverview.getVisibility() == newVisibility) return; + + if (!visible) { + mZoomRingOverview.startAnimation(mZoomRingOverviewExitAnimation); + } + mZoomRingOverview.setVisibility(newVisibility); + } + /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { boolean rVal = false; @@ -1653,7 +1793,8 @@ public class WebView extends AbsoluteLayout * @return true if new values were sent */ private boolean sendViewSizeZoom() { - int newWidth = Math.round(getViewWidth() * mInvActualScale); + int viewWidth = getViewWidth(); + int newWidth = Math.round(viewWidth * mInvActualScale); int newHeight = Math.round(getViewHeight() * mInvActualScale); /* * Because the native side may have already done a layout before the @@ -1669,7 +1810,7 @@ public class WebView extends AbsoluteLayout // Avoid sending another message if the dimensions have not changed. if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) { mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, - newWidth, newHeight, new Float(mActualScale)); + newWidth, newHeight, new Integer(viewWidth)); mLastWidthSent = newWidth; mLastHeightSent = newHeight; return true; @@ -1968,7 +2109,7 @@ public class WebView extends AbsoluteLayout // Scale from content to view coordinates, and pin. // Also called by jni webview.cpp - private void setContentScrollBy(int cx, int cy) { + private void setContentScrollBy(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. @@ -1992,10 +2133,10 @@ public class WebView extends AbsoluteLayout // vertical scroll? // Log.d(LOGTAG, "setContentScrollBy cy=" + cy); if (cy == 0 && cx != 0) { - pinScrollBy(cx, 0, true, 0); + pinScrollBy(cx, 0, animate, 0); } } else { - pinScrollBy(cx, cy, true, 0); + pinScrollBy(cx, cy, animate, 0); } } @@ -2205,7 +2346,8 @@ public class WebView extends AbsoluteLayout // state. // If mNativeClass is 0, we should not reach here, so we do not // need to check it again. - nativeRecordButtons(mTouchMode == TOUCH_SHORTPRESS_START_MODE + nativeRecordButtons(hasFocus() && hasWindowFocus(), + mTouchMode == TOUCH_SHORTPRESS_START_MODE || mTrackballDown || mGotEnterDown, false); drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); } @@ -2254,6 +2396,8 @@ public class WebView extends AbsoluteLayout invalidate(); } else { zoomScale = mZoomScale; + // set mZoomScale to be 0 as we have done animation + mZoomScale = 0; } float scale = (mActualScale - zoomScale) * mInvActualScale; float tx = scale * (mZoomCenterX + mScrollX); @@ -2775,6 +2919,17 @@ public class WebView extends AbsoluteLayout getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEntry, 0); mTextEntry.enableScrollOnScreen(true); + // Now we need to fake a touch event to place the cursor where the + // user touched. + AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) + mTextEntry.getLayoutParams(); + if (lp != null) { + // Take the last touch and adjust for the location of the + // TextDialog. + float x = mLastTouchX - lp.x; + float y = mLastTouchY - lp.y; + mTextEntry.fakeTouchEvent(x, y); + } } private void updateTextEntry() { @@ -2987,7 +3142,9 @@ public class WebView extends AbsoluteLayout mGotEnterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); - nativeRecordButtons(true, true); + // Already checked mNativeClass, so we do not need to check it + // again. + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); return true; } // Bubble up the key event as WebView doesn't handle it @@ -3198,6 +3355,9 @@ public class WebView extends AbsoluteLayout ViewGroup p = (ViewGroup) parent; p.setOnHierarchyChangeListener(null); } + + // Clean up the zoom ring + mZoomRingController.setVisible(false); } // Implementation for OnHierarchyChangeListener @@ -3234,16 +3394,25 @@ public class WebView extends AbsoluteLayout if (mNeedsUpdateTextEntry) { updateTextEntry(); } + if (mNativeClass != 0) { + nativeRecordButtons(true, false, true); + } } else { // If our window gained focus, but we do not have it, do not // draw the focus ring. mDrawFocusRing = false; + // We do not call nativeRecordButtons here because we assume + // that when we lost focus, or window focus, it got called with + // false for the first parameter } } else { // If our window has lost focus, stop drawing the focus ring mDrawFocusRing = false; mGotKeyDown = false; mShiftIsPressed = false; + if (mNativeClass != 0) { + nativeRecordButtons(false, false, true); + } } invalidate(); super.onWindowFocusChanged(hasWindowFocus); @@ -3264,12 +3433,22 @@ public class WebView extends AbsoluteLayout updateTextEntry(); mNeedsUpdateTextEntry = false; } + if (mNativeClass != 0) { + nativeRecordButtons(true, false, true); + } + //} else { + // The WebView has gained focus while we do not have + // windowfocus. When our window lost focus, we should have + // called nativeRecordButtons(false...) } } else { // When we lost focus, unless focus went to the TextView (which is // true if we are in editing mode), stop drawing the focus ring. if (!inEditingMode()) { mDrawFocusRing = false; + if (mNativeClass != 0) { + nativeRecordButtons(false, false, true); + } } mGotKeyDown = false; } @@ -3285,6 +3464,22 @@ public class WebView extends AbsoluteLayout // the new zoom ring controller mZoomCenterX = getViewWidth() * .5f; mZoomCenterY = getViewHeight() * .5f; + + // update mMinZoomScale + if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) { + boolean atMin = Math.abs(mActualScale - mMinZoomScale) < 0.01f; + mMinZoomScale = (float) getViewWidth() / mMinContentWidth; + if (atMin) { + // if the WebView was at the minimum zoom scale, keep it. e,g., + // the WebView was at the minimum zoom scale at the portrait + // mode, rotate it to the landscape modifying the scale to the + // new minimum zoom scale, when rotating back, we would like to + // keep the minimum zoom scale instead of keeping the same scale + // as normally we do. + mActualScale = mMinZoomScale; + } + } + // we always force, in case our height changed, in which case we still // want to send the notification over to webkit setNewZoomScale(mActualScale, true); @@ -3340,6 +3535,14 @@ public class WebView extends AbsoluteLayout return false; } + if (mShowZoomRingTutorial && mMinZoomScale < mMaxZoomScale) { + ZoomRingController.showZoomTutorialOnce(mContext); + mShowZoomRingTutorial = false; + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(DISMISS_ZOOM_RING_TUTORIAL), + ZOOM_RING_TUTORIAL_DURATION); + } + if (LOGV_ENABLED) { Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode=" + mTouchMode); @@ -3749,7 +3952,7 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(SWITCH_TO_ENTER); mTrackballDown = true; if (mNativeClass != 0) { - nativeRecordButtons(true, true); + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); } if (time - mLastFocusTime <= TRACKBALL_TIMEOUT && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { @@ -4148,6 +4351,9 @@ public class WebView extends AbsoluteLayout }); zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() { public void onClick(View v) { + // Hide the zoom ring + mZoomRingController.setVisible(false); + mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -4200,6 +4406,12 @@ public class WebView extends AbsoluteLayout } } + // Called by JNI to handle a touch on a node representing an email address, + // address, or phone number + private void overrideLoading(String url) { + mCallbackProxy.uiOverrideUrlLoading(url); + } + @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; @@ -4372,6 +4584,11 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); + } switch (msg.what) { case REMEMBER_PASSWORD: { mDatabase.setUsernamePassword( @@ -4413,7 +4630,7 @@ public class WebView extends AbsoluteLayout , KeyEvent.KEYCODE_ENTER)); break; case SCROLL_BY_MSG_ID: - setContentScrollBy(msg.arg1, msg.arg2); + setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj); break; case SYNC_SCROLL_TO_MSG_ID: if (mUserScroll) { @@ -4451,6 +4668,11 @@ public class WebView extends AbsoluteLayout 0, 0); } } + mMinContentWidth = msg.arg1; + if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) { + mMinZoomScale = (float) getViewWidth() + / mMinContentWidth; + } // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to // WebCore matches the view size of the picture we just @@ -4638,6 +4860,10 @@ public class WebView extends AbsoluteLayout } break; + case DISMISS_ZOOM_RING_TUTORIAL: + mZoomRingController.finishZoomTutorial(); + break; + default: super.handleMessage(msg); break; @@ -5018,8 +5244,8 @@ public class WebView extends AbsoluteLayout private native void nativeRecomputeFocus(); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. - private native void nativeRecordButtons(boolean pressed, - boolean invalidate); + private native void nativeRecordButtons(boolean focused, + boolean pressed, boolean invalidate); private native void nativeResetFocus(); private native void nativeResetNavClipBounds(); private native void nativeSelectBestAt(Rect rect); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index b979032..45113ab 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -271,6 +271,11 @@ final class WebViewCore { static native String nativeFindAddress(String addr); /** + * Rebuild the nav cache if the dom changed. + */ + private native void nativeCheckNavCache(); + + /** * Empty the picture set. */ private native void nativeClearContent(); @@ -317,7 +322,7 @@ final class WebViewCore { should this be called nativeSetViewPortSize? */ private native void nativeSetSize(int width, int height, int screenWidth, - float scale); + float scale, int realScreenWidth, int screenHeight); private native int nativeGetContentMinPrefWidth(); @@ -501,6 +506,51 @@ final class WebViewCore { int mY; } + static final String[] HandlerDebugString = { + "LOAD_URL", // = 100; + "STOP_LOADING", // = 101; + "RELOAD", // = 102; + "KEY_DOWN", // = 103; + "KEY_UP", // = 104; + "VIEW_SIZE_CHANGED", // = 105; + "GO_BACK_FORWARD", // = 106; + "SET_SCROLL_OFFSET", // = 107; + "RESTORE_STATE", // = 108; + "PAUSE_TIMERS", // = 109; + "RESUME_TIMERS", // = 110; + "CLEAR_CACHE", // = 111; + "CLEAR_HISTORY", // = 112; + "SET_SELECTION", // = 113; + "REPLACE_TEXT", // = 114; + "PASS_TO_JS", // = 115; + "SET_GLOBAL_BOUNDS", // = 116; + "UPDATE_CACHE_AND_TEXT_ENTRY", // = 117; + "CLICK", // = 118; + "119", + "DOC_HAS_IMAGES", // = 120; + "SET_SNAP_ANCHOR", // = 121; + "DELETE_SELECTION", // = 122; + "LISTBOX_CHOICES", // = 123; + "SINGLE_LISTBOX_CHOICE", // = 124; + "125", + "SET_BACKGROUND_COLOR", // = 126; + "UNBLOCK_FOCUS", // = 127; + "SAVE_DOCUMENT_STATE", // = 128; + "GET_SELECTION", // = 129; + "WEBKIT_DRAW", // = 130; + "SYNC_SCROLL", // = 131; + "REFRESH_PLUGINS", // = 132; + "SPLIT_PICTURE_SET", // = 133; + "CLEAR_CONTENT", // = 134; + "SET_FINAL_FOCUS", // = 135; + "SET_KIT_FOCUS", // = 136; + "REQUEST_FOCUS_HREF", // = 137; + "ADD_JS_INTERFACE", // = 138; + "LOAD_DATA", // = 139; + "TOUCH_UP", // = 140; + "TOUCH_EVENT", // = 141; + }; + class EventHub { // Message Ids static final int LOAD_URL = 100; @@ -595,6 +645,11 @@ final class WebViewCore { mHandler = new Handler() { @Override public void handleMessage(Message msg) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, msg.what < LOAD_URL || msg.what + > TOUCH_EVENT ? Integer.toString(msg.what) + : HandlerDebugString[msg.what - LOAD_URL]); + } switch (msg.what) { case WEBKIT_DRAW: webkitDraw(); @@ -675,7 +730,7 @@ final class WebViewCore { case VIEW_SIZE_CHANGED: viewSizeChanged(msg.arg1, msg.arg2, - ((Float) msg.obj).floatValue()); + ((Integer) msg.obj).intValue()); break; case SET_SCROLL_OFFSET: @@ -1131,12 +1186,22 @@ final class WebViewCore { private int mCurrentViewWidth = 0; private int mCurrentViewHeight = 0; + // Define a minimum screen width so that we won't wrap the paragraph to one + // word per line during zoom-in. + private static final int MIN_SCREEN_WIDTH = 160; + // notify webkit that our virtual view size changed size (after inv-zoom) - private void viewSizeChanged(int w, int h, float scale) { + private void viewSizeChanged(int w, int h, int viewWidth) { if (LOGV_ENABLED) Log.v(LOGTAG, "CORE onSizeChanged"); + if (w == 0) { + Log.w(LOGTAG, "skip viewSizeChanged as w is 0"); + return; + } + float scale = (float) viewWidth / w; if (mSettings.getUseWideViewPort() && (w < mViewportWidth || mViewportWidth == -1)) { int width = mViewportWidth; + int screenWidth = Math.max(w, MIN_SCREEN_WIDTH); if (mViewportWidth == -1) { if (mSettings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NORMAL) { @@ -1154,12 +1219,21 @@ final class WebViewCore { * In the worse case, the native width will be adjusted when * next zoom or screen orientation change happens. */ - width = Math.max(w, nativeGetContentMinPrefWidth()); + int minContentWidth = nativeGetContentMinPrefWidth(); + if (minContentWidth > WebView.MAX_FLOAT_CONTENT_WIDTH) { + // keep the same width and screen width so that there is + // no reflow when zoom-out + width = minContentWidth; + screenWidth = Math.min(screenWidth, viewWidth); + } else { + width = Math.max(w, minContentWidth); + } } } - nativeSetSize(width, Math.round((float) width * h / w), w, scale); + nativeSetSize(width, Math.round((float) width * h / w), + screenWidth, scale, w, h); } else { - nativeSetSize(w, h, w, scale); + nativeSetSize(w, h, w, scale, w, h); } // Remember the current width and height boolean needInvalidate = (mCurrentViewWidth == 0); @@ -1219,7 +1293,9 @@ final class WebViewCore { draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); Message.obtain(mWebView.mPrivateHandler, - WebView.NEW_PICTURE_MSG_ID, draw).sendToTarget(); + WebView.NEW_PICTURE_MSG_ID, nativeGetContentMinPrefWidth(), + 0, draw).sendToTarget(); + nativeCheckNavCache(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { // as we have the new picture, try to sync the scroll position Message.obtain(mWebView.mPrivateHandler, @@ -1324,7 +1400,9 @@ final class WebViewCore { for (int i = 0; i < size; i++) { list.getItemAtIndex(i).inflate(mBrowserFrame.mNativeFrame); } + mBrowserFrame.mLoadInitFromJava = true; list.restoreIndex(mBrowserFrame.mNativeFrame, index); + mBrowserFrame.mLoadInitFromJava = false; } //------------------------------------------------------------------------- @@ -1349,14 +1427,15 @@ final class WebViewCore { } // called by JNI - private void contentScrollBy(int dx, int dy) { + private void contentScrollBy(int dx, int dy, boolean animate) { if (!mBrowserFrame.firstLayoutDone()) { // Will this happen? If yes, we need to do something here. return; } if (mWebView != null) { Message.obtain(mWebView.mPrivateHandler, - WebView.SCROLL_BY_MSG_ID, dx, dy).sendToTarget(); + WebView.SCROLL_BY_MSG_ID, dx, dy, + new Boolean(animate)).sendToTarget(); } } @@ -1461,7 +1540,7 @@ final class WebViewCore { // current scale mEventHub.sendMessage(Message.obtain(null, EventHub.VIEW_SIZE_CHANGED, mWebView.mLastWidthSent, - mWebView.mLastHeightSent, -1.0f)); + mWebView.mLastHeightSent, new Integer(-1))); } mBrowserFrame.didFirstLayout(); diff --git a/core/java/android/webkit/gears/HttpRequestAndroid.java b/core/java/android/webkit/gears/HttpRequestAndroid.java deleted file mode 100644 index 30f855f..0000000 --- a/core/java/android/webkit/gears/HttpRequestAndroid.java +++ /dev/null @@ -1,745 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.net.http.Headers; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.util.Log; -import android.webkit.CacheManager; -import android.webkit.CacheManager.CacheResult; -import android.webkit.CookieManager; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import org.apache.http.impl.cookie.DateUtils; -import org.apache.http.util.CharArrayBuffer; - -import java.io.*; -import java.net.*; -import java.util.*; -import javax.net.ssl.*; - -/** - * Performs the underlying HTTP/HTTPS GET and POST requests. - * <p> These are performed synchronously (blocking). The caller should - * ensure that it is in a background thread if asynchronous behavior - * is required. All data is pushed, so there is no need for JNI native - * callbacks. - * <p> This uses the java.net.HttpURLConnection class to perform most - * of the underlying network activity. The Android brower's cache, - * android.webkit.CacheManager, is also used when caching is enabled, - * and updated with new data. The android.webkit.CookieManager is also - * queried and updated as necessary. - * <p> The public interface is designed to be called by native code - * through JNI, and to simplify coding none of the public methods will - * surface a checked exception. Unchecked exceptions may still be - * raised but only if the system is in an ill state, such as out of - * memory. - * <p> TODO: This isn't plumbed into LocalServer yet. Mutually - * dependent on LocalServer - will attach the two together once both - * are submitted. - */ -public final class HttpRequestAndroid { - /** Debug logging tag. */ - private static final String LOG_TAG = "Gears-J"; - /** HTTP response header line endings are CR-LF style. */ - private static final String HTTP_LINE_ENDING = "\r\n"; - /** Safe MIME type to use whenever it isn't specified. */ - private static final String DEFAULT_MIME_TYPE = "text/plain"; - /** Case-sensitive header keys */ - public static final String KEY_CONTENT_LENGTH = "Content-Length"; - public static final String KEY_EXPIRES = "Expires"; - public static final String KEY_LAST_MODIFIED = "Last-Modified"; - public static final String KEY_ETAG = "ETag"; - public static final String KEY_LOCATION = "Location"; - public static final String KEY_CONTENT_TYPE = "Content-Type"; - /** Number of bytes to send and receive on the HTTP connection in - * one go. */ - private static final int BUFFER_SIZE = 4096; - /** The first element of the String[] value in a headers map is the - * unmodified (case-sensitive) key. */ - public static final int HEADERS_MAP_INDEX_KEY = 0; - /** The second element of the String[] value in a headers map is the - * associated value. */ - public static final int HEADERS_MAP_INDEX_VALUE = 1; - - /** Enable/disable all logging in this class. */ - private static boolean logEnabled = false; - /** The underlying HTTP or HTTPS network connection. */ - private HttpURLConnection connection; - /** HTTP body stream, setup after connection. */ - private InputStream inputStream; - /** The complete response line e.g "HTTP/1.0 200 OK" */ - private String responseLine; - /** Request headers, as a lowercase key -> [ unmodified key, value ] map. */ - private Map<String, String[]> requestHeaders = - new HashMap<String, String[]>(); - /** Response headers, as a lowercase key -> [ unmodified key, value ] map. */ - private Map<String, String[]> responseHeaders; - /** True if the child thread is in performing blocking IO. */ - private boolean inBlockingOperation = false; - /** True when the thread acknowledges the abort. */ - private boolean abortReceived = false; - /** The URL used for createCacheResult() */ - private String cacheResultUrl; - /** CacheResult being saved into, if inserting a new cache entry. */ - private CacheResult cacheResult; - /** Initialized by initChildThread(). Used to target abort(). */ - private Thread childThread; - - /** - * Convenience debug function. Calls Android logging mechanism. - * @param str String to log to the Android console. - */ - private static void log(String str) { - if (logEnabled) { - Log.i(LOG_TAG, str); - } - } - - /** - * Turn on/off logging in this class. - * @param on Logging enable state. - */ - public static void enableLogging(boolean on) { - logEnabled = on; - } - - /** - * Initialize childThread using the TLS value of - * Thread.currentThread(). Called on start up of the native child - * thread. - */ - public synchronized void initChildThread() { - childThread = Thread.currentThread(); - } - - /** - * Analagous to the native-side HttpRequest::open() function. This - * initializes an underlying java.net.HttpURLConnection, but does - * not go to the wire. On success, this enables a call to send() to - * initiate the transaction. - * - * @param method The HTTP method, e.g GET or POST. - * @param url The URL to open. - * @return True on success with a complete HTTP response. - * False on failure. - */ - public synchronized boolean open(String method, String url) { - if (logEnabled) - log("open " + method + " " + url); - // Reset the response between calls to open(). - inputStream = null; - responseLine = null; - responseHeaders = null; - if (!method.equals("GET") && !method.equals("POST")) { - log("Method " + method + " not supported"); - return false; - } - // Setup the connection. This doesn't go to the wire yet - it - // doesn't block. - try { - URL url_object = new URL(url); - // Check that the protocol is indeed HTTP(S). - String protocol = url_object.getProtocol(); - if (protocol == null) { - log("null protocol for URL " + url); - return false; - } - protocol = protocol.toLowerCase(); - if (!"http".equals(protocol) && !"https".equals(protocol)) { - log("Url has wrong protocol: " + url); - return false; - } - - connection = (HttpURLConnection) url_object.openConnection(); - connection.setRequestMethod(method); - // Manually follow redirects. - connection.setInstanceFollowRedirects(false); - // Manually cache. - connection.setUseCaches(false); - // Enable data output in POST method requests. - connection.setDoOutput(method.equals("POST")); - // Enable data input in non-HEAD method requests. - // TODO: HEAD requests not tested. - connection.setDoInput(!method.equals("HEAD")); - if (connection instanceof javax.net.ssl.HttpsURLConnection) { - // Verify the certificate matches the origin. - ((HttpsURLConnection) connection).setHostnameVerifier( - new StrictHostnameVerifier()); - } - return true; - } catch (IOException e) { - log("Got IOException in open: " + e.toString()); - return false; - } - } - - /** - * Interrupt a blocking IO operation. This will cause the child - * thread to expediently return from an operation if it was stuck at - * the time. Note that this inherently races, and unfortunately - * requires the caller to loop. - */ - public synchronized void interrupt() { - if (childThread == null) { - log("interrupt() called but no child thread"); - return; - } - synchronized (this) { - if (inBlockingOperation) { - log("Interrupting blocking operation"); - childThread.interrupt(); - } else { - log("Nothing to interrupt"); - } - } - } - - /** - * Set a header to send with the HTTP request. Will not take effect - * on a transaction already in progress. The key is associated - * case-insensitive, but stored case-sensitive. - * @param name The name of the header, e.g "Set-Cookie". - * @param value The value for this header, e.g "text/html". - */ - public synchronized void setRequestHeader(String name, String value) { - String[] mapValue = { name, value }; - requestHeaders.put(name.toLowerCase(), mapValue); - } - - /** - * Returns the value associated with the given request header. - * @param name The name of the request header, non-null, case-insensitive. - * @return The value associated with the request header, or null if - * not set, or error. - */ - public synchronized String getRequestHeader(String name) { - String[] value = requestHeaders.get(name.toLowerCase()); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } - - /** - * Returns the value associated with the given response header. - * @param name The name of the response header, non-null, case-insensitive. - * @return The value associated with the response header, or null if - * not set or error. - */ - public synchronized String getResponseHeader(String name) { - if (responseHeaders != null) { - String[] value = responseHeaders.get(name.toLowerCase()); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } else { - log("getResponseHeader() called but response not received"); - return null; - } - } - - /** - * Set a response header and associated value. The key is associated - * case-insensitively, but stored case-sensitively. - * @param name Case sensitive request header key. - * @param value The associated value. - */ - private void setResponseHeader(String name, String value) { - if (logEnabled) - log("Set response header " + name + ": " + value); - String mapValue[] = { name, value }; - responseHeaders.put(name.toLowerCase(), mapValue); - } - - /** - * Apply the contents of the Map requestHeaders to the connection - * object. Calls to setRequestHeader() after this will not affect - * the connection. - */ - private synchronized void applyRequestHeadersToConnection() { - Iterator<String[]> it = requestHeaders.values().iterator(); - while (it.hasNext()) { - // Set the key case-sensitive. - String[] entry = it.next(); - connection.setRequestProperty( - entry[HEADERS_MAP_INDEX_KEY], - entry[HEADERS_MAP_INDEX_VALUE]); - } - } - - /** - * Return all response headers, separated by CR-LF line endings, and - * ending with a trailing blank line. This mimics the format of the - * raw response header up to but not including the body. - * @return A string containing the entire response header. - */ - public synchronized String getAllResponseHeaders() { - if (responseHeaders == null) { - log("getAllResponseHeaders() called but response not received"); - return null; - } - String result = new String(); - Iterator<String[]> it = responseHeaders.values().iterator(); - while (it.hasNext()) { - String[] entry = it.next(); - // Output the "key: value" lines. - result += entry[HEADERS_MAP_INDEX_KEY] + ": " - + entry[HEADERS_MAP_INDEX_VALUE] + HTTP_LINE_ENDING; - } - result += HTTP_LINE_ENDING; - return result; - } - - /** - * Get the complete response line of the HTTP request. Only valid on - * completion of the transaction. - * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK". - */ - public synchronized String getResponseLine() { - return responseLine; - } - - /** - * Get the cookie for the given URL. - * @param url The fully qualified URL. - * @return A string containing the cookie for the URL if it exists, - * or null if not. - */ - public static String getCookieForUrl(String url) { - // Get the cookie for this URL, set as a header - return CookieManager.getInstance().getCookie(url); - } - - /** - * Set the cookie for the given URL. - * @param url The fully qualified URL. - * @param cookie The new cookie value. - * @return A string containing the cookie for the URL if it exists, - * or null if not. - */ - public static void setCookieForUrl(String url, String cookie) { - // Get the cookie for this URL, set as a header - CookieManager.getInstance().setCookie(url, cookie); - } - - /** - * Perform a request using LocalServer if possible. Initializes - * class members so that receive() will obtain data from the stream - * provided by the response. - * @param url The fully qualified URL to try in LocalServer. - * @return True if the url was found and is now setup to receive. - * False if not found, with no side-effect. - */ - public synchronized boolean useLocalServerResult(String url) { - UrlInterceptHandlerGears handler = UrlInterceptHandlerGears.getInstance(); - if (handler == null) { - return false; - } - UrlInterceptHandlerGears.ServiceResponse serviceResponse = - handler.getServiceResponse(url, requestHeaders); - if (serviceResponse == null) { - log("No response in LocalServer"); - return false; - } - // LocalServer will handle this URL. Initialize stream and - // response. - inputStream = serviceResponse.getInputStream(); - responseLine = serviceResponse.getStatusLine(); - responseHeaders = serviceResponse.getResponseHeaders(); - if (logEnabled) - log("Got response from LocalServer: " + responseLine); - return true; - } - - /** - * Perform a request using the cache result if present. Initializes - * class members so that receive() will obtain data from the cache. - * @param url The fully qualified URL to try in the cache. - * @return True is the url was found and is now setup to receive - * from cache. False if not found, with no side-effect. - */ - public synchronized boolean useCacheResult(String url) { - // Try the browser's cache. CacheManager wants a Map<String, String>. - Map<String, String> cacheRequestHeaders = new HashMap<String, String>(); - Iterator<Map.Entry<String, String[]>> it = - requestHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<String, String[]> entry = it.next(); - cacheRequestHeaders.put( - entry.getKey(), - entry.getValue()[HEADERS_MAP_INDEX_VALUE]); - } - CacheResult cacheResult = - CacheManager.getCacheFile(url, cacheRequestHeaders); - if (cacheResult == null) { - if (logEnabled) - log("No CacheResult for " + url); - return false; - } - if (logEnabled) - log("Got CacheResult from browser cache"); - // Check for expiry. -1 is "never", otherwise milliseconds since 1970. - // Can be compared to System.currentTimeMillis(). - long expires = cacheResult.getExpires(); - if (expires >= 0 && System.currentTimeMillis() >= expires) { - log("CacheResult expired " - + (System.currentTimeMillis() - expires) - + " milliseconds ago"); - // Cache hit has expired. Do not return it. - return false; - } - // Setup the inputStream to come from the cache. - inputStream = cacheResult.getInputStream(); - if (inputStream == null) { - // Cache result may have gone away. - log("No inputStream for CacheResult " + url); - return false; - } - // Cache hit. Parse headers. - synthesizeHeadersFromCacheResult(cacheResult); - return true; - } - - /** - * Take the limited set of headers in a CacheResult and synthesize - * response headers. - * @param cacheResult A CacheResult to populate responseHeaders with. - */ - private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) { - int statusCode = cacheResult.getHttpStatusCode(); - // The status message is informal, so we can greatly simplify it. - String statusMessage; - if (statusCode >= 200 && statusCode < 300) { - statusMessage = "OK"; - } else if (statusCode >= 300 && statusCode < 400) { - statusMessage = "MOVED"; - } else { - statusMessage = "UNAVAILABLE"; - } - // Synthesize the response line. - responseLine = "HTTP/1.1 " + statusCode + " " + statusMessage; - if (logEnabled) - log("Synthesized " + responseLine); - // Synthesize the returned headers from cache. - responseHeaders = new HashMap<String, String[]>(); - String contentLength = Long.toString(cacheResult.getContentLength()); - setResponseHeader(KEY_CONTENT_LENGTH, contentLength); - long expires = cacheResult.getExpires(); - if (expires >= 0) { - // "Expires" header is valid and finite. Milliseconds since 1970 - // epoch, formatted as RFC-1123. - String expiresString = DateUtils.formatDate(new Date(expires)); - setResponseHeader(KEY_EXPIRES, expiresString); - } - String lastModified = cacheResult.getLastModified(); - if (lastModified != null) { - // Last modification time of the page. Passed end-to-end, but - // not used by us. - setResponseHeader(KEY_LAST_MODIFIED, lastModified); - } - String eTag = cacheResult.getETag(); - if (eTag != null) { - // Entity tag. A kind of GUID to identify identical resources. - setResponseHeader(KEY_ETAG, eTag); - } - String location = cacheResult.getLocation(); - if (location != null) { - // If valid, refers to the location of a redirect. - setResponseHeader(KEY_LOCATION, location); - } - String mimeType = cacheResult.getMimeType(); - if (mimeType == null) { - // Use a safe default MIME type when none is - // specified. "text/plain" is safe to render in the browser - // window (even if large) and won't be intepreted as anything - // that would cause execution. - mimeType = DEFAULT_MIME_TYPE; - } - String encoding = cacheResult.getEncoding(); - // Encoding may not be specified. No default. - String contentType = mimeType; - if (encoding != null && encoding.length() > 0) { - contentType += "; charset=" + encoding; - } - setResponseHeader(KEY_CONTENT_TYPE, contentType); - } - - /** - * Create a CacheResult for this URL. This enables the repsonse body - * to be sent in calls to appendCacheResult(). - * @param url The fully qualified URL to add to the cache. - * @param responseCode The response code returned for the request, e.g 200. - * @param mimeType The MIME type of the body, e.g "text/plain". - * @param encoding The encoding, e.g "utf-8". Use "" for unknown. - */ - public synchronized boolean createCacheResult( - String url, int responseCode, String mimeType, String encoding) { - if (logEnabled) - log("Making cache entry for " + url); - // Take the headers and parse them into a format needed by - // CacheManager. - Headers cacheHeaders = new Headers(); - Iterator<Map.Entry<String, String[]>> it = - responseHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<String, String[]> entry = it.next(); - // Headers.parseHeader() expects lowercase keys. - String keyValue = entry.getKey() + ": " - + entry.getValue()[HEADERS_MAP_INDEX_VALUE]; - CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length()); - buffer.append(keyValue); - // Parse it into the header container. - cacheHeaders.parseHeader(buffer); - } - cacheResult = CacheManager.createCacheFile( - url, responseCode, cacheHeaders, mimeType, true); - if (cacheResult != null) { - if (logEnabled) - log("Saving into cache"); - cacheResult.setEncoding(encoding); - cacheResultUrl = url; - return true; - } else { - log("Couldn't create cacheResult"); - return false; - } - } - - /** - * Add data from the response body to the CacheResult created with - * createCacheResult(). - * @param data A byte array of the next sequential bytes in the - * response body. - * @param bytes The number of bytes to write from the start of - * the array. - * @return True if all bytes successfully written, false on failure. - */ - public synchronized boolean appendCacheResult(byte[] data, int bytes) { - if (cacheResult == null) { - log("appendCacheResult() called without a CacheResult initialized"); - return false; - } - try { - cacheResult.getOutputStream().write(data, 0, bytes); - } catch (IOException ex) { - log("Got IOException writing cache data: " + ex); - return false; - } - return true; - } - - /** - * Save the completed CacheResult into the CacheManager. This must - * have been created first with createCacheResult(). - * @return Returns true if the entry has been successfully saved. - */ - public synchronized boolean saveCacheResult() { - if (cacheResult == null || cacheResultUrl == null) { - log("Tried to save cache result but createCacheResult not called"); - return false; - } - if (logEnabled) - log("Saving cache result"); - CacheManager.saveCacheFile(cacheResultUrl, cacheResult); - cacheResult = null; - cacheResultUrl = null; - return true; - } - - /** - * Perform an HTTP request on the network. The underlying - * HttpURLConnection is connected to the remote server and the - * response headers are received. - * @return True if the connection succeeded and headers have been - * received. False on connection failure. - */ - public boolean connectToRemote() { - synchronized (this) { - // Transfer a snapshot of our internally maintained map of request - // headers to the connection object. - applyRequestHeadersToConnection(); - // Note blocking I/O so abort() can interrupt us. - inBlockingOperation = true; - } - boolean success; - try { - if (logEnabled) - log("Connecting to remote"); - connection.connect(); - if (logEnabled) - log("Connected"); - success = true; - } catch (IOException e) { - log("Got IOException in connect(): " + e.toString()); - success = false; - } finally { - synchronized (this) { - // No longer blocking. - inBlockingOperation = false; - } - } - return success; - } - - /** - * Receive all headers from the server and populate - * responseHeaders. This converts from the slightly odd format - * returned by java.net.HttpURLConnection to a simpler - * java.util.Map. - * @return True if headers are successfully received, False on - * connection error. - */ - public synchronized boolean parseHeaders() { - responseHeaders = new HashMap<String, String[]>(); - /* HttpURLConnection contains a null terminated list of - * key->value response pairs. If the key is null, then the value - * contains the complete status line. If both key and value are - * null for an index, we've reached the end. - */ - for (int i = 0; ; ++i) { - String key = connection.getHeaderFieldKey(i); - String value = connection.getHeaderField(i); - if (logEnabled) - log("header " + key + " -> " + value); - if (key == null && value == null) { - // End of list. - break; - } else if (key == null) { - // The pair with null key has the complete status line in - // the value, e.g "HTTP/1.0 200 OK". - responseLine = value; - } else if (value != null) { - // If key and value are non-null, this is a response pair, e.g - // "Content-Length" -> "5". Use setResponseHeader() to - // correctly deal with case-insensitivity of the key. - setResponseHeader(key, value); - } else { - // The key is non-null but value is null. Unexpected - // condition. - return false; - } - } - return true; - } - - /** - * Receive the next sequential bytes of the response body after - * successful connection. This will receive up to the size of the - * provided byte array. If there is no body, this will return 0 - * bytes on the first call after connection. - * @param buf A pre-allocated byte array to receive data into. - * @return The number of bytes from the start of the array which - * have been filled, 0 on EOF, or negative on error. - */ - public int receive(byte[] buf) { - if (inputStream == null) { - // If this is the first call, setup the InputStream. This may - // fail if there were headers, but no body returned by the - // server. - try { - inputStream = connection.getInputStream(); - } catch (IOException inputException) { - log("Failed to connect InputStream: " + inputException); - // Not unexpected. For example, 404 response return headers, - // and sometimes a body with a detailed error. Try the error - // stream. - inputStream = connection.getErrorStream(); - if (inputStream == null) { - // No error stream either. Treat as a 0 byte response. - log("No InputStream"); - return 0; // EOF. - } - } - } - synchronized (this) { - // Note blocking I/O so abort() can interrupt us. - inBlockingOperation = true; - } - int ret; - try { - int got = inputStream.read(buf); - if (got > 0) { - // Got some bytes, not EOF. - ret = got; - } else { - // EOF. - inputStream.close(); - ret = 0; - } - } catch (IOException e) { - // An abort() interrupts us by calling close() on our stream. - log("Got IOException in inputStream.read(): " + e.toString()); - ret = -1; - } finally { - synchronized (this) { - // No longer blocking. - inBlockingOperation = false; - } - } - return ret; - } - - /** - * For POST method requests, send a stream of data provided by the - * native side in repeated callbacks. - * @param data A byte array containing the data to sent, or null - * if indicating EOF. - * @param bytes The number of bytes from the start of the array to - * send, or 0 if indicating EOF. - * @return True if all bytes were successfully sent, false on error. - */ - public boolean sendPostData(byte[] data, int bytes) { - synchronized (this) { - // Note blocking I/O so abort() can interrupt us. - inBlockingOperation = true; - } - boolean success; - try { - OutputStream outputStream = connection.getOutputStream(); - if (data == null && bytes == 0) { - outputStream.close(); - } else { - outputStream.write(data, 0, bytes); - } - success = true; - } catch (IOException e) { - log("Got IOException in post: " + e.toString()); - success = false; - } finally { - synchronized (this) { - // No longer blocking. - inBlockingOperation = false; - } - } - return success; - } -} diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java index 288240e..2a5cbe9 100644 --- a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java +++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java @@ -64,11 +64,11 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { /** The unmodified (case-sensitive) key in the headers map is the * same index as used by HttpRequestAndroid. */ public static final int HEADERS_MAP_INDEX_KEY = - HttpRequestAndroid.HEADERS_MAP_INDEX_KEY; + ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_KEY; /** The associated value in the headers map is the same index as * used by HttpRequestAndroid. */ public static final int HEADERS_MAP_INDEX_VALUE = - HttpRequestAndroid.HEADERS_MAP_INDEX_VALUE; + ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_VALUE; /** * Object passed to the native side, containing information about @@ -382,7 +382,7 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { // browser's cache for too long. long now_ms = System.currentTimeMillis(); String expires = DateUtils.formatDate(new Date(now_ms + CACHE_EXPIRY_MS)); - response.setResponseHeader(HttpRequestAndroid.KEY_EXPIRES, expires); + response.setResponseHeader(ApacheHttpRequestAndroid.KEY_EXPIRES, expires); // The browser is only interested in a small subset of headers, // contained in a Headers object. Iterate the map of all headers // and add them to Headers. diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c012e25..f362e22 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -41,7 +41,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.WindowManagerImpl; +import android.view.inputmethod.InputMethodManager; import android.view.ContextMenu.ContextMenuInfo; import com.android.internal.R; @@ -425,6 +425,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mTouchSlop; + private float mDensityScale; + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. @@ -567,7 +569,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ @Override protected boolean isVerticalScrollBarHidden() { - return mFastScroller != null ? mFastScroller.isVisible() : false; + return mFastScroller != null && mFastScroller.isVisible(); } /** @@ -709,6 +711,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te setScrollingCacheEnabled(true); mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + mDensityScale = getContext().getResources().getDisplayMetrics().density; } private void useDefaultSelector() { @@ -891,14 +894,26 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } // Don't restore the type filter window when there is no keyboard - int keyboardHidden = getContext().getResources().getConfiguration().keyboardHidden; - if (keyboardHidden != Configuration.KEYBOARDHIDDEN_YES) { + if (acceptFilter()) { String filterText = ss.filter; setFilterText(filterText); } + requestLayout(); } + private boolean acceptFilter() { + final Context context = mContext; + final Configuration configuration = context.getResources().getConfiguration(); + final boolean keyboardShowing = configuration.keyboardHidden != + Configuration.KEYBOARDHIDDEN_YES; + final boolean hasKeyboard = configuration.keyboard != Configuration.KEYBOARD_NOKEYS; + final InputMethodManager inputManager = (InputMethodManager) + context.getSystemService(Context.INPUT_METHOD_SERVICE); + return (hasKeyboard && keyboardShowing) || + (!hasKeyboard && !inputManager.isFullscreenMode()); + } + /** * Sets the initial value for the text filter. * @param filterText The text to use for the filter. @@ -906,6 +921,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setTextFilterEnabled */ public void setFilterText(String filterText) { + // TODO: Should we check for acceptFilter()? if (mTextFilterEnabled && filterText != null && filterText.length() > 0) { createTextFilter(false); // This is going to call our listener onTextChanged, but we might not @@ -1076,6 +1092,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mInLayout = false; } + /** + * @hide + */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + final boolean changed = super.setFrame(left, top, right, bottom); + + // Reposition the popup when the frame has changed. This includes + // translating the widget, not just changing its dimension. The + // filter popup needs to follow the widget. + if (mFiltered && changed && getWindowVisibility() == View.VISIBLE && mPopup != null && + mPopup.isShowing()) { + positionPopup(true); + } + + return changed; + } + protected void layoutChildren() { } @@ -2587,10 +2621,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te clearScrollingCache(); mSpecificTop = selectedTop; selectedPos = lookForSelectablePosition(selectedPos, down); - if (selectedPos >= 0) { + if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { mLayoutMode = LAYOUT_SPECIFIC; setSelectionInt(selectedPos); invokeOnItemScrollListener(); + } else { + selectedPos = INVALID_POSITION; } reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); @@ -2727,19 +2763,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private void showPopup() { // Make sure we have a window before showing the popup if (getWindowVisibility() == View.VISIBLE) { - int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight(); - final int[] xy = new int[2]; - getLocationOnScreen(xy); - // TODO: The 20 below should come from the theme and be expressed in dip - final float scale = getContext().getResources().getDisplayMetrics().density; - int bottomGap = screenHeight - xy[1] - getHeight() + (int) (scale * 20); - mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, - xy[0], bottomGap); + positionPopup(false); // Make sure we get focus if we are showing the popup checkFocus(); } } + private void positionPopup(boolean update) { + int screenHeight = getResources().getDisplayMetrics().heightPixels; + final int[] xy = new int[2]; + getLocationOnScreen(xy); + // TODO: The 20 below should come from the theme and be expressed in dip + // TODO: And the gravity should be defined in the theme as well + final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); + if (!update) { + mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + xy[0], bottomGap); + } else { + mPopup.update(xy[0], bottomGap, -1, -1); + } + } + /** * What is the distance between the source and destination rectangles given the direction of * focus navigation between them? The direction basically helps figure out more quickly what is @@ -2831,7 +2875,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } - if (okToSend) { + if (okToSend && acceptFilter()) { createTextFilter(true); KeyEvent forwardEvent = event; @@ -2873,6 +2917,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTextFilter.addTextChangedListener(this); p.setFocusable(false); p.setTouchable(false); + p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); p.setContentView(mTextFilter); p.setWidth(LayoutParams.WRAP_CONTENT); p.setHeight(LayoutParams.WRAP_CONTENT); diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 7086ae2..369221e 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -46,6 +46,18 @@ import java.util.Locale; public class Chronometer extends TextView { private static final String TAG = "Chronometer"; + /** + * A callback that notifies when the chronometer has incremented on its own. + */ + public interface OnChronometerTickListener { + + /** + * Notification that the chronometer has changed. + */ + void onChronometerTick(Chronometer chronometer); + + } + private long mBase; private boolean mVisible; private boolean mStarted; @@ -56,6 +68,7 @@ public class Chronometer extends TextView { private Locale mFormatterLocale; private Object[] mFormatterArgs = new Object[1]; private StringBuilder mFormatBuilder; + private OnChronometerTickListener mOnChronometerTickListener; /** * Initialize this Chronometer object. @@ -99,6 +112,7 @@ public class Chronometer extends TextView { * * @param base Use the {@link SystemClock#elapsedRealtime} time base. */ + @android.view.RemotableViewMethod public void setBase(long base) { mBase = base; updateText(SystemClock.elapsedRealtime()); @@ -122,6 +136,7 @@ public class Chronometer extends TextView { * * @param format the format string. */ + @android.view.RemotableViewMethod public void setFormat(String format) { mFormat = format; if (format != null && mFormatBuilder == null) { @@ -137,6 +152,23 @@ public class Chronometer extends TextView { } /** + * Sets the listener to be called when the chronometer changes. + * + * @param listener The listener. + */ + public void setOnChronometerTickListener(OnChronometerTickListener listener) { + mOnChronometerTickListener = listener; + } + + /** + * @return The listener (may be null) that is listening for chronometer change + * events. + */ + public OnChronometerTickListener getOnChronometerTickListener() { + return mOnChronometerTickListener; + } + + /** * Start counting up. This does not affect the base as set from {@link #setBase}, just * the view display. * @@ -161,6 +193,15 @@ public class Chronometer extends TextView { updateRunning(); } + /** + * The same as calling {@link #start} or {@link #stop}. + */ + @android.view.RemotableViewMethod + public void setStarted(boolean started) { + mStarted = started; + updateRunning(); + } + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -216,8 +257,15 @@ public class Chronometer extends TextView { public void handleMessage(Message m) { if (mStarted) { updateText(SystemClock.elapsedRealtime()); + dispatchChronometerTick(); sendMessageDelayed(Message.obtain(), 1000); } } }; + + void dispatchChronometerTick() { + if (mOnChronometerTickListener != null) { + mOnChronometerTickListener.onChronometerTick(this); + } + } } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index b4ed3ba..8aafee2 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -93,6 +93,7 @@ public class FrameLayout extends ViewGroup { * * @attr ref android.R.styleable#FrameLayout_foregroundGravity */ + @android.view.RemotableViewMethod public void setForegroundGravity(int foregroundGravity) { if (mForegroundGravity != foregroundGravity) { if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -348,6 +349,7 @@ public class FrameLayout extends ViewGroup { * * @attr ref android.R.styleable#FrameLayout_measureAllChildren */ + @android.view.RemotableViewMethod public void setMeasureAllChildren(boolean measureAll) { mMeasureAllChildren = measureAll; } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 4ae322e..94d1bd1 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -193,6 +193,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_adjustViewBounds */ + @android.view.RemotableViewMethod public void setAdjustViewBounds(boolean adjustViewBounds) { mAdjustViewBounds = adjustViewBounds; if (adjustViewBounds) { @@ -217,6 +218,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_maxWidth */ + @android.view.RemotableViewMethod public void setMaxWidth(int maxWidth) { mMaxWidth = maxWidth; } @@ -238,6 +240,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_maxHeight */ + @android.view.RemotableViewMethod public void setMaxHeight(int maxHeight) { mMaxHeight = maxHeight; } @@ -256,6 +259,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_src */ + @android.view.RemotableViewMethod public void setImageResource(int resId) { if (mUri != null || mResource != resId) { updateDrawable(null); @@ -272,6 +276,7 @@ public class ImageView extends View { * * @param uri The Uri of an image */ + @android.view.RemotableViewMethod public void setImageURI(Uri uri) { if (mResource != 0 || (mUri != uri && @@ -306,6 +311,7 @@ public class ImageView extends View { * * @param bm The bitmap to set */ + @android.view.RemotableViewMethod public void setImageBitmap(Bitmap bm) { // if this is used frequently, may handle bitmaps explicitly // to reduce the intermediate drawable object @@ -327,6 +333,7 @@ public class ImageView extends View { resizeFromDrawable(); } + @android.view.RemotableViewMethod public void setImageLevel(int level) { mLevel = level; if (mDrawable != null) { diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 85a7339..a9822f8 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -136,6 +136,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_baselineAligned */ + @android.view.RemotableViewMethod public void setBaselineAligned(boolean baselineAligned) { mBaselineAligned = baselineAligned; } @@ -208,6 +209,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex */ + @android.view.RemotableViewMethod public void setBaselineAlignedChildIndex(int i) { if ((i < 0) || (i >= getChildCount())) { throw new IllegalArgumentException("base aligned child index out " @@ -265,6 +267,7 @@ public class LinearLayout extends ViewGroup { * to 0.0f if the weight sum should be computed from the children's * layout_weight */ + @android.view.RemotableViewMethod public void setWeightSum(float weightSum) { mWeightSum = Math.max(0.0f, weightSum); } @@ -1149,6 +1152,7 @@ public class LinearLayout extends ViewGroup { * * @attr ref android.R.styleable#LinearLayout_gravity */ + @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -1164,6 +1168,7 @@ public class LinearLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { @@ -1172,6 +1177,7 @@ public class LinearLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setVerticalGravity(int verticalGravity) { final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 9c7f600..4e5989c 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -2179,6 +2179,10 @@ public class ListView extends AbsListView { && !isViewAncestorOf(selectedView, this)) { selectedView = null; hideSelector(); + + // but we don't want to set the ressurect position (that would make subsequent + // unhandled key events bring back the item we just scrolled off!) + mResurrectToPosition = INVALID_POSITION; } if (needToRedraw) { diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index dada105..4a5cea1 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -24,6 +24,8 @@ import android.view.View; import android.view.WindowManager; import android.view.Gravity; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; @@ -33,6 +35,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import java.lang.ref.WeakReference; + /** * <p>A popup window that can be used to display an arbitrary view. The popup * windows is a floating container that appears on top of the current @@ -109,7 +113,23 @@ public class PopupWindow { private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { com.android.internal.R.attr.state_above_anchor }; - + + private WeakReference<View> mAnchor; + private OnScrollChangedListener mOnScrollChangedListener = + new OnScrollChangedListener() { + public void onScrollChanged() { + View anchor = mAnchor.get(); + if (anchor != null && mPopupView != null) { + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + + mAboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff); + update(p.x, p.y, -1, -1, true); + } + } + }; + private int mAnchorXoff, mAnchorYoff; + /** * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> * @@ -579,6 +599,8 @@ public class PopupWindow { return; } + unregisterForScrollChanged(); + mIsShowing = true; mIsDropdown = false; @@ -617,6 +639,8 @@ public class PopupWindow { * the popup in its entirety, this method tries to find a parent scroll * view to scroll. If no parent scroll view can be scrolled, the bottom-left * corner of the popup is pinned at the top left corner of the anchor view.</p> + * <p>If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly.</p> * * @param anchor the view on which to pin the popup window * @@ -627,6 +651,8 @@ public class PopupWindow { return; } + registerForScrollChanged(anchor, xoff, yoff); + mIsShowing = true; mIsDropdown = true; @@ -894,6 +920,8 @@ public class PopupWindow { */ public void dismiss() { if (isShowing() && mPopupView != null) { + unregisterForScrollChanged(); + mWindowManager.removeView(mPopupView); if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { ((ViewGroup) mPopupView).removeView(mContentView); @@ -962,6 +990,25 @@ public class PopupWindow { * @param height the new height, can be -1 to ignore */ public void update(int x, int y, int width, int height) { + update(x, y, width, height, false); + } + + /** + * <p>Updates the position and the dimension of the popup window. Width and + * height can be set to -1 to update location only. Calling this function + * also updates the window with the current popup state as + * described for {@link #update()}.</p> + * + * @param x the new x location + * @param y the new y location + * @param width the new width, can be -1 to ignore + * @param height the new height, can be -1 to ignore + * @param force reposition the window even if the specified position + * already seems to correspond to the LayoutParams + * + * @hide pending API council approval + */ + public void update(int x, int y, int width, int height, boolean force) { if (width != -1) { mLastWidth = width; setWidth(width); @@ -979,7 +1026,7 @@ public class PopupWindow { WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); - boolean update = false; + boolean update = force; final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth; if (width != -1 && p.width != finalWidth) { @@ -1039,6 +1086,8 @@ public class PopupWindow { * height can be set to -1 to update location only. Calling this function * also updates the window with the current popup state as * described for {@link #update()}.</p> + * <p>If the view later scrolls to move <code>anchor</code> to a different + * location, the popup will be moved correspondingly.</p> * * @param anchor the popup's anchor view * @param xoff x offset from the view's left edge @@ -1051,6 +1100,12 @@ public class PopupWindow { return; } + WeakReference<View> oldAnchor = mAnchor; + if (oldAnchor == null || oldAnchor.get() != anchor || + mAnchorXoff != xoff || mAnchorYoff != yoff) { + registerForScrollChanged(anchor, xoff, yoff); + } + WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); @@ -1065,10 +1120,10 @@ public class PopupWindow { mPopupHeight = height; } - findDropDownPosition(anchor, p, xoff, yoff); + mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); update(p.x, p.y, width, height); } - + /** * Listener that is called when this popup window is dismissed. */ @@ -1078,7 +1133,33 @@ public class PopupWindow { */ public void onDismiss(); } - + + private void unregisterForScrollChanged() { + WeakReference<View> anchorRef = mAnchor; + View anchor = null; + if (anchorRef != null) { + anchor = anchorRef.get(); + } + if (anchor != null) { + ViewTreeObserver vto = anchor.getViewTreeObserver(); + vto.removeOnScrollChangedListener(mOnScrollChangedListener); + } + mAnchor = null; + } + + private void registerForScrollChanged(View anchor, int xoff, int yoff) { + unregisterForScrollChanged(); + + mAnchor = new WeakReference<View>(anchor); + ViewTreeObserver vto = anchor.getViewTreeObserver(); + if (vto != null) { + vto.addOnScrollChangedListener(mOnScrollChangedListener); + } + + mAnchorXoff = xoff; + mAnchorYoff = yoff; + } + private class PopupViewContainer extends FrameLayout { public PopupViewContainer(Context context) { diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 434e9f3..dd2570a 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -344,6 +344,7 @@ public class ProgressBar extends View { * * @param indeterminate true to enable the indeterminate mode */ + @android.view.RemotableViewMethod public synchronized void setIndeterminate(boolean indeterminate) { if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) { mIndeterminate = indeterminate; @@ -529,6 +530,7 @@ public class ProgressBar extends View { setProgress(progress, false); } + @android.view.RemotableViewMethod synchronized void setProgress(int progress, boolean fromUser) { if (mIndeterminate) { return; @@ -560,6 +562,7 @@ public class ProgressBar extends View { * @see #getSecondaryProgress() * @see #incrementSecondaryProgressBy(int) */ + @android.view.RemotableViewMethod public synchronized void setSecondaryProgress(int secondaryProgress) { if (mIndeterminate) { return; @@ -633,6 +636,7 @@ public class ProgressBar extends View { * @see #setProgress(int) * @see #setSecondaryProgress(int) */ + @android.view.RemotableViewMethod public synchronized void setMax(int max) { if (max < 0) { max = 0; diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 9ded52b..ba63ec3 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -168,6 +168,7 @@ public class RelativeLayout extends ViewGroup { * * @attr ref android.R.styleable#RelativeLayout_ignoreGravity */ + @android.view.RemotableViewMethod public void setIgnoreGravity(int viewId) { mIgnoreGravity = viewId; } @@ -183,6 +184,7 @@ public class RelativeLayout extends ViewGroup { * * @attr ref android.R.styleable#RelativeLayout_gravity */ + @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { @@ -198,6 +200,7 @@ public class RelativeLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { @@ -206,6 +209,7 @@ public class RelativeLayout extends ViewGroup { } } + @android.view.RemotableViewMethod public void setVerticalGravity(int verticalGravity) { final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 25afee8..e000d2e 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -31,6 +31,7 @@ import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; +import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater.Filter; @@ -38,10 +39,13 @@ import android.view.View.OnClickListener; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import java.lang.Class; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; @@ -80,19 +84,22 @@ public class RemoteViews implements Parcelable, Filter { /** - * This annotation indicates that a subclass of View is alllowed to be used with the - * {@link android.widget.RemoteViews} mechanism. + * This annotation indicates that a subclass of View is alllowed to be used + * with the {@link android.widget.RemoteViews} mechanism. */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface RemoteView { } - + /** * Exception to send when something goes wrong executing an action * */ public static class ActionException extends RuntimeException { + public ActionException(Exception ex) { + super(ex); + } public ActionException(String message) { super(message); } @@ -110,274 +117,7 @@ public class RemoteViews implements Parcelable, Filter { return 0; } }; - - /** - * Equivalent to calling View.setVisibility - */ - private class SetViewVisibility extends Action { - public SetViewVisibility(int id, int vis) { - viewId = id; - visibility = vis; - } - - public SetViewVisibility(Parcel parcel) { - viewId = parcel.readInt(); - visibility = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(visibility); - } - - @Override - public void apply(View root) { - View target = root.findViewById(viewId); - if (target != null) { - target.setVisibility(visibility); - } - } - - private int viewId; - private int visibility; - public final static int TAG = 0; - } - - /** - * Equivalent to calling TextView.setText - */ - private class SetTextViewText extends Action { - public SetTextViewText(int id, CharSequence t) { - viewId = id; - text = t; - } - - public SetTextViewText(Parcel parcel) { - viewId = parcel.readInt(); - text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - TextUtils.writeToParcel(text, dest, flags); - } - - @Override - public void apply(View root) { - TextView target = (TextView) root.findViewById(viewId); - if (target != null) { - target.setText(text); - } - } - - int viewId; - CharSequence text; - public final static int TAG = 1; - } - - /** - * Equivalent to calling ImageView.setResource - */ - private class SetImageViewResource extends Action { - public SetImageViewResource(int id, int src) { - viewId = id; - srcId = src; - } - - public SetImageViewResource(Parcel parcel) { - viewId = parcel.readInt(); - srcId = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(srcId); - } - - @Override - public void apply(View root) { - ImageView target = (ImageView) root.findViewById(viewId); - Drawable d = mContext.getResources().getDrawable(srcId); - if (target != null) { - target.setImageDrawable(d); - } - } - - int viewId; - int srcId; - public final static int TAG = 2; - } - - /** - * Equivalent to calling ImageView.setImageURI - */ - private class SetImageViewUri extends Action { - public SetImageViewUri(int id, Uri u) { - viewId = id; - uri = u; - } - - public SetImageViewUri(Parcel parcel) { - viewId = parcel.readInt(); - uri = Uri.CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - Uri.writeToParcel(dest, uri); - } - - @Override - public void apply(View root) { - ImageView target = (ImageView) root.findViewById(viewId); - if (target != null) { - target.setImageURI(uri); - } - } - - int viewId; - Uri uri; - public final static int TAG = 3; - } - - /** - * Equivalent to calling ImageView.setImageBitmap - */ - private class SetImageViewBitmap extends Action { - public SetImageViewBitmap(int id, Bitmap src) { - viewId = id; - bitmap = src; - } - - public SetImageViewBitmap(Parcel parcel) { - viewId = parcel.readInt(); - bitmap = Bitmap.CREATOR.createFromParcel(parcel); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - if (bitmap != null) { - bitmap.writeToParcel(dest, flags); - } - } - - @Override - public void apply(View root) { - if (bitmap != null) { - ImageView target = (ImageView) root.findViewById(viewId); - Drawable d = new BitmapDrawable(bitmap); - if (target != null) { - target.setImageDrawable(d); - } - } - } - int viewId; - Bitmap bitmap; - public final static int TAG = 4; - } - - /** - * Equivalent to calling Chronometer.setBase, Chronometer.setFormat, - * and Chronometer.start/stop. - */ - private class SetChronometer extends Action { - public SetChronometer(int id, long base, String format, boolean running) { - this.viewId = id; - this.base = base; - this.format = format; - this.running = running; - } - - public SetChronometer(Parcel parcel) { - viewId = parcel.readInt(); - base = parcel.readLong(); - format = parcel.readString(); - running = parcel.readInt() != 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeLong(base); - dest.writeString(format); - dest.writeInt(running ? 1 : 0); - } - - @Override - public void apply(View root) { - Chronometer target = (Chronometer) root.findViewById(viewId); - if (target != null) { - target.setBase(base); - target.setFormat(format); - if (running) { - target.start(); - } else { - target.stop(); - } - } - } - - int viewId; - boolean running; - long base; - String format; - - public final static int TAG = 5; - } - - /** - * Equivalent to calling ProgressBar.setMax, ProgressBar.setProgress and - * ProgressBar.setIndeterminate - */ - private class SetProgressBar extends Action { - public SetProgressBar(int id, int max, int progress, boolean indeterminate) { - this.viewId = id; - this.progress = progress; - this.max = max; - this.indeterminate = indeterminate; - } - - public SetProgressBar(Parcel parcel) { - viewId = parcel.readInt(); - progress = parcel.readInt(); - max = parcel.readInt(); - indeterminate = parcel.readInt() != 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(progress); - dest.writeInt(max); - dest.writeInt(indeterminate ? 1 : 0); - } - - @Override - public void apply(View root) { - ProgressBar target = (ProgressBar) root.findViewById(viewId); - if (target != null) { - target.setIndeterminate(indeterminate); - if (!indeterminate) { - target.setMax(max); - target.setProgress(progress); - } - } - } - - int viewId; - boolean indeterminate; - int progress; - int max; - - public final static int TAG = 6; - } - /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} @@ -421,7 +161,7 @@ public class RemoteViews implements Parcelable, Filter { int viewId; PendingIntent pendingIntent; - public final static int TAG = 7; + public final static int TAG = 1; } /** @@ -511,92 +251,215 @@ public class RemoteViews implements Parcelable, Filter { PorterDuff.Mode filterMode; int level; - public final static int TAG = 8; + public final static int TAG = 3; } /** - * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}. + * Base class for the reflection actions. */ - private class SetTextColor extends Action { - public SetTextColor(int id, int color) { - this.viewId = id; - this.color = color; - } - - public SetTextColor(Parcel parcel) { - viewId = parcel.readInt(); - color = parcel.readInt(); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(color); - } - - @Override - public void apply(View root) { - final View target = root.findViewById(viewId); - if (target instanceof TextView) { - final TextView textView = (TextView) target; - textView.setTextColor(color); + private class ReflectionAction extends Action { + static final int TAG = 2; + + static final int BOOLEAN = 1; + static final int BYTE = 2; + static final int SHORT = 3; + static final int INT = 4; + static final int LONG = 5; + static final int FLOAT = 6; + static final int DOUBLE = 7; + static final int CHAR = 8; + static final int STRING = 9; + static final int CHAR_SEQUENCE = 10; + static final int URI = 11; + static final int BITMAP = 12; + + int viewId; + String methodName; + int type; + Object value; + + ReflectionAction(int viewId, String methodName, int type, Object value) { + this.viewId = viewId; + this.methodName = methodName; + this.type = type; + this.value = value; + } + + ReflectionAction(Parcel in) { + this.viewId = in.readInt(); + this.methodName = in.readString(); + this.type = in.readInt(); + if (false) { + Log.d("RemoteViews", "read viewId=0x" + Integer.toHexString(this.viewId) + + " methodName=" + this.methodName + " type=" + this.type); + } + switch (this.type) { + case BOOLEAN: + this.value = in.readInt() != 0; + break; + case BYTE: + this.value = in.readByte(); + break; + case SHORT: + this.value = (short)in.readInt(); + break; + case INT: + this.value = in.readInt(); + break; + case LONG: + this.value = in.readLong(); + break; + case FLOAT: + this.value = in.readFloat(); + break; + case DOUBLE: + this.value = in.readDouble(); + break; + case CHAR: + this.value = (char)in.readInt(); + break; + case STRING: + this.value = in.readString(); + break; + case CHAR_SEQUENCE: + this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + break; + case URI: + this.value = Uri.CREATOR.createFromParcel(in); + break; + case BITMAP: + this.value = Bitmap.CREATOR.createFromParcel(in); + break; + default: + break; } } - - int viewId; - int color; - public final static int TAG = 9; - } - - /** - * Equivalent to calling {@link android.widget.ViewFlipper#startFlipping()} - * or {@link android.widget.ViewFlipper#stopFlipping()} along with - * {@link android.widget.ViewFlipper#setFlipInterval(int)}. - */ - private class SetFlipping extends Action { - public SetFlipping(int id, boolean flipping, int milliseconds) { - this.viewId = id; - this.flipping = flipping; - this.milliseconds = milliseconds; - } - - public SetFlipping(Parcel parcel) { - viewId = parcel.readInt(); - flipping = parcel.readInt() != 0; - milliseconds = parcel.readInt(); + public void writeToParcel(Parcel out, int flags) { + out.writeInt(TAG); + out.writeInt(this.viewId); + out.writeString(this.methodName); + out.writeInt(this.type); + if (false) { + Log.d("RemoteViews", "write viewId=0x" + Integer.toHexString(this.viewId) + + " methodName=" + this.methodName + " type=" + this.type); + } + switch (this.type) { + case BOOLEAN: + out.writeInt(((Boolean)this.value).booleanValue() ? 1 : 0); + break; + case BYTE: + out.writeByte(((Byte)this.value).byteValue()); + break; + case SHORT: + out.writeInt(((Short)this.value).shortValue()); + break; + case INT: + out.writeInt(((Integer)this.value).intValue()); + break; + case LONG: + out.writeLong(((Long)this.value).longValue()); + break; + case FLOAT: + out.writeFloat(((Float)this.value).floatValue()); + break; + case DOUBLE: + out.writeDouble(((Double)this.value).doubleValue()); + break; + case CHAR: + out.writeInt((int)((Character)this.value).charValue()); + break; + case STRING: + out.writeString((String)this.value); + break; + case CHAR_SEQUENCE: + TextUtils.writeToParcel((CharSequence)this.value, out, flags); + break; + case URI: + ((Uri)this.value).writeToParcel(out, flags); + break; + case BITMAP: + ((Bitmap)this.value).writeToParcel(out, flags); + break; + default: + break; + } } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TAG); - dest.writeInt(viewId); - dest.writeInt(flipping ? 1 : 0); - dest.writeInt(milliseconds); + + private Class getParameterType() { + switch (this.type) { + case BOOLEAN: + return boolean.class; + case BYTE: + return byte.class; + case SHORT: + return short.class; + case INT: + return int.class; + case LONG: + return long.class; + case FLOAT: + return float.class; + case DOUBLE: + return double.class; + case CHAR: + return char.class; + case STRING: + return String.class; + case CHAR_SEQUENCE: + return CharSequence.class; + case URI: + return Uri.class; + case BITMAP: + return Bitmap.class; + default: + return null; + } } - + @Override public void apply(View root) { - final View target = root.findViewById(viewId); - if (target instanceof ViewFlipper) { - final ViewFlipper flipper = (ViewFlipper) target; - if (milliseconds != -1) { - flipper.setFlipInterval(milliseconds); - } - if (flipping) { - flipper.startFlipping(); - } else { - flipper.stopFlipping(); + final View view = root.findViewById(viewId); + if (view == null) { + throw new ActionException("can't find view: 0x" + Integer.toHexString(viewId)); + } + + Class param = getParameterType(); + if (param == null) { + throw new ActionException("bad type: " + this.type); + } + + Class klass = view.getClass(); + Method method = null; + try { + method = klass.getMethod(this.methodName, getParameterType()); + } + catch (NoSuchMethodException ex) { + throw new ActionException("view: " + klass.getName() + " doesn't have method: " + + this.methodName + "(" + param.getName() + ")"); + } + + if (!method.isAnnotationPresent(RemotableViewMethod.class)) { + throw new ActionException("view: " + klass.getName() + + " can't use method with RemoteViews: " + + this.methodName + "(" + param.getName() + ")"); + } + + try { + if (false) { + Log.d("RemoteViews", "view: " + klass.getName() + " calling method: " + + this.methodName + "(" + param.getName() + ") with " + + (this.value == null ? "null" : this.value.getClass().getName())); } + method.invoke(view, this.value); + } + catch (Exception ex) { + throw new ActionException(ex); } } - - int viewId; - boolean flipping; - int milliseconds; - - public final static int TAG = 10; } + /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. @@ -623,41 +486,17 @@ public class RemoteViews implements Parcelable, Filter { for (int i=0; i<count; i++) { int tag = parcel.readInt(); switch (tag) { - case SetViewVisibility.TAG: - mActions.add(new SetViewVisibility(parcel)); - break; - case SetTextViewText.TAG: - mActions.add(new SetTextViewText(parcel)); - break; - case SetImageViewResource.TAG: - mActions.add(new SetImageViewResource(parcel)); - break; - case SetImageViewUri.TAG: - mActions.add(new SetImageViewUri(parcel)); - break; - case SetImageViewBitmap.TAG: - mActions.add(new SetImageViewBitmap(parcel)); - break; - case SetChronometer.TAG: - mActions.add(new SetChronometer(parcel)); - break; - case SetProgressBar.TAG: - mActions.add(new SetProgressBar(parcel)); - break; case SetOnClickPendingIntent.TAG: mActions.add(new SetOnClickPendingIntent(parcel)); break; case SetDrawableParameters.TAG: mActions.add(new SetDrawableParameters(parcel)); break; - case SetTextColor.TAG: - mActions.add(new SetTextColor(parcel)); - break; - case SetFlipping.TAG: - mActions.add(new SetFlipping(parcel)); + case ReflectionAction.TAG: + mActions.add(new ReflectionAction(parcel)); break; default: - throw new ActionException("Tag " + tag + "not found"); + throw new ActionException("Tag " + tag + " not found"); } } } @@ -690,7 +529,7 @@ public class RemoteViews implements Parcelable, Filter { * @param visibility The new visibility for the view */ public void setViewVisibility(int viewId, int visibility) { - addAction(new SetViewVisibility(viewId, visibility)); + setInt(viewId, "setVisibility", visibility); } /** @@ -700,7 +539,7 @@ public class RemoteViews implements Parcelable, Filter { * @param text The new text for the view */ public void setTextViewText(int viewId, CharSequence text) { - addAction(new SetTextViewText(viewId, text)); + setCharSequence(viewId, "setText", text); } /** @@ -710,7 +549,7 @@ public class RemoteViews implements Parcelable, Filter { * @param srcId The new resource id for the drawable */ public void setImageViewResource(int viewId, int srcId) { - addAction(new SetImageViewResource(viewId, srcId)); + setInt(viewId, "setImageResource", srcId); } /** @@ -720,7 +559,7 @@ public class RemoteViews implements Parcelable, Filter { * @param uri The Uri for the image */ public void setImageViewUri(int viewId, Uri uri) { - addAction(new SetImageViewUri(viewId, uri)); + setUri(viewId, "setImageURI", uri); } /** @@ -730,7 +569,7 @@ public class RemoteViews implements Parcelable, Filter { * @param bitmap The new Bitmap for the drawable */ public void setImageViewBitmap(int viewId, Bitmap bitmap) { - addAction(new SetImageViewBitmap(viewId, bitmap)); + setBitmap(viewId, "setImageBitmap", bitmap); } /** @@ -745,16 +584,20 @@ public class RemoteViews implements Parcelable, Filter { * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}. * @param format The Chronometer format string, or null to * simply display the timer value. - * @param running True if you want the clock to be running, false if not. + * @param started True if you want the clock to be started, false if not. */ - public void setChronometer(int viewId, long base, String format, boolean running) { - addAction(new SetChronometer(viewId, base, format, running)); + public void setChronometer(int viewId, long base, String format, boolean started) { + setLong(viewId, "setBase", base); + setString(viewId, "setFormat", format); + setBoolean(viewId, "setStarted", started); } /** * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax}, * {@link ProgressBar#setProgress ProgressBar.setProgress}, and * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate} + * + * If indeterminate is true, then the values for max and progress are ignored. * * @param viewId The id of the view whose text should change * @param max The 100% value for the progress bar @@ -764,7 +607,11 @@ public class RemoteViews implements Parcelable, Filter { */ public void setProgressBar(int viewId, int max, int progress, boolean indeterminate) { - addAction(new SetProgressBar(viewId, max, progress, indeterminate)); + setBoolean(viewId, "setIndeterminate", indeterminate); + if (!indeterminate) { + setInt(viewId, "setMax", max); + setInt(viewId, "setProgress", progress); + } } /** @@ -780,6 +627,7 @@ public class RemoteViews implements Parcelable, Filter { } /** + * @hide * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given @@ -818,23 +666,55 @@ public class RemoteViews implements Parcelable, Filter { * focused) to be this color. */ public void setTextColor(int viewId, int color) { - addAction(new SetTextColor(viewId, color)); + setInt(viewId, "setTextColor", color); } - /** - * Equivalent to calling {@link android.widget.ViewFlipper#startFlipping()} - * or {@link android.widget.ViewFlipper#stopFlipping()} along with - * {@link android.widget.ViewFlipper#setFlipInterval(int)}. - * - * @param viewId The id of the view to apply changes to - * @param flipping True means we should - * {@link android.widget.ViewFlipper#startFlipping()}, otherwise - * {@link android.widget.ViewFlipper#stopFlipping()}. - * @param milliseconds How long to wait before flipping to the next view, or - * -1 to leave unchanged. - */ - public void setFlipping(int viewId, boolean flipping, int milliseconds) { - addAction(new SetFlipping(viewId, flipping, milliseconds)); + public void setBoolean(int viewId, String methodName, boolean value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BOOLEAN, value)); + } + + public void setByte(int viewId, String methodName, byte value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BYTE, value)); + } + + public void setShort(int viewId, String methodName, short value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.SHORT, value)); + } + + public void setInt(int viewId, String methodName, int value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value)); + } + + public void setLong(int viewId, String methodName, long value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.LONG, value)); + } + + public void setFloat(int viewId, String methodName, float value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value)); + } + + public void setDouble(int viewId, String methodName, double value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.DOUBLE, value)); + } + + public void setChar(int viewId, String methodName, char value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR, value)); + } + + public void setString(int viewId, String methodName, String value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.STRING, value)); + } + + public void setCharSequence(int viewId, String methodName, CharSequence value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value)); + } + + public void setUri(int viewId, String methodName, Uri value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.URI, value)); + } + + public void setBitmap(int viewId, String methodName, Bitmap value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP, value)); } /** diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index da4a077..dc2c70d 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -405,7 +405,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); * Specify a label and icon as the tab indicator. */ public TabSpec setIndicator(CharSequence label, Drawable icon) { - mIndicatorStrategy = new LabelAndIconIndicatorStategy(label, icon); + mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon); return this; } @@ -497,12 +497,12 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); /** * How we create a tab indicator that has a label and an icon */ - private class LabelAndIconIndicatorStategy implements IndicatorStrategy { + private class LabelAndIconIndicatorStrategy implements IndicatorStrategy { private final CharSequence mLabel; private final Drawable mIcon; - private LabelAndIconIndicatorStategy(CharSequence label, Drawable icon) { + private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) { mLabel = label; mIcon = icon; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 2ae5d4e..bdc54ff 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -87,6 +87,7 @@ import android.view.ViewDebug; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; import android.view.animation.AnimationUtils; +import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -1521,6 +1522,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textSize */ + @android.view.RemotableViewMethod public void setTextSize(float size) { setTextSize(TypedValue.COMPLEX_UNIT_SP, size); } @@ -1572,6 +1574,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textScaleX */ + @android.view.RemotableViewMethod public void setTextScaleX(float size) { if (size != mTextPaint.getTextScaleX()) { mTextPaint.setTextScaleX(size); @@ -1620,6 +1623,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColor */ + @android.view.RemotableViewMethod public void setTextColor(int color) { mTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1662,6 +1666,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorHighlight */ + @android.view.RemotableViewMethod public void setHighlightColor(int color) { if (mHighlightColor != color) { mHighlightColor = color; @@ -1703,6 +1708,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_autoLink */ + @android.view.RemotableViewMethod public final void setAutoLinkMask(int mask) { mAutoLinkMask = mask; } @@ -1715,6 +1721,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_linksClickable */ + @android.view.RemotableViewMethod public final void setLinksClickable(boolean whether) { mLinksClickable = whether; } @@ -1751,6 +1758,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorHint */ + @android.view.RemotableViewMethod public final void setHintTextColor(int color) { mHintTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1789,6 +1797,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_textColorLink */ + @android.view.RemotableViewMethod public final void setLinkTextColor(int color) { mLinkTextColor = ColorStateList.valueOf(color); updateTextColors(); @@ -1876,6 +1885,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * reflows the text if they are different from the old flags. * @see Paint#setFlags */ + @android.view.RemotableViewMethod public void setPaintFlags(int flags) { if (mTextPaint.getFlags() != flags) { mTextPaint.setFlags(flags); @@ -1909,6 +1919,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minLines */ + @android.view.RemotableViewMethod public void setMinLines(int minlines) { mMinimum = minlines; mMinMode = LINES; @@ -1922,6 +1933,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minHeight */ + @android.view.RemotableViewMethod public void setMinHeight(int minHeight) { mMinimum = minHeight; mMinMode = PIXELS; @@ -1935,6 +1947,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxLines */ + @android.view.RemotableViewMethod public void setMaxLines(int maxlines) { mMaximum = maxlines; mMaxMode = LINES; @@ -1948,6 +1961,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxHeight */ + @android.view.RemotableViewMethod public void setMaxHeight(int maxHeight) { mMaximum = maxHeight; mMaxMode = PIXELS; @@ -1961,6 +1975,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_lines */ + @android.view.RemotableViewMethod public void setLines(int lines) { mMaximum = mMinimum = lines; mMaxMode = mMinMode = LINES; @@ -1976,6 +1991,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_height */ + @android.view.RemotableViewMethod public void setHeight(int pixels) { mMaximum = mMinimum = pixels; mMaxMode = mMinMode = PIXELS; @@ -1989,6 +2005,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minEms */ + @android.view.RemotableViewMethod public void setMinEms(int minems) { mMinWidth = minems; mMinWidthMode = EMS; @@ -2002,6 +2019,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_minWidth */ + @android.view.RemotableViewMethod public void setMinWidth(int minpixels) { mMinWidth = minpixels; mMinWidthMode = PIXELS; @@ -2015,6 +2033,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxEms */ + @android.view.RemotableViewMethod public void setMaxEms(int maxems) { mMaxWidth = maxems; mMaxWidthMode = EMS; @@ -2028,6 +2047,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_maxWidth */ + @android.view.RemotableViewMethod public void setMaxWidth(int maxpixels) { mMaxWidth = maxpixels; mMaxWidthMode = PIXELS; @@ -2041,6 +2061,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_ems */ + @android.view.RemotableViewMethod public void setEms(int ems) { mMaxWidth = mMinWidth = ems; mMaxWidthMode = mMinWidthMode = EMS; @@ -2056,6 +2077,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_width */ + @android.view.RemotableViewMethod public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; @@ -2321,6 +2343,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_freezesText */ + @android.view.RemotableViewMethod public void setFreezesText(boolean freezesText) { mFreezesText = freezesText; } @@ -2366,6 +2389,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_text */ + @android.view.RemotableViewMethod public final void setText(CharSequence text) { setText(text, mBufferType); } @@ -2378,6 +2402,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @see #setText(CharSequence) */ + @android.view.RemotableViewMethod public final void setTextKeepState(CharSequence text) { setTextKeepState(text, mBufferType); } @@ -2648,6 +2673,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + @android.view.RemotableViewMethod public final void setText(int resid) { setText(getContext().getResources().getText(resid)); } @@ -2666,6 +2692,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_hint */ + @android.view.RemotableViewMethod public final void setHint(CharSequence hint) { mHint = TextUtils.stringOrSpannedString(hint); @@ -2686,6 +2713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_hint */ + @android.view.RemotableViewMethod public final void setHint(int resid) { setHint(getContext().getResources().getText(resid)); } @@ -2896,6 +2924,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * <code>error</code> is <code>null</code>, the error message and icon * will be cleared. */ + @android.view.RemotableViewMethod public void setError(CharSequence error) { if (error == null) { setError(null, null); @@ -2954,10 +2983,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mPopup == null) { LayoutInflater inflater = LayoutInflater.from(getContext()); - TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, + final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, null); - mPopup = new PopupWindow(err, 200, 50); + mPopup = new PopupWindow(err, 200, 50) { + private boolean mAbove = false; + + @Override + public void update(int x, int y, int w, int h, boolean force) { + super.update(x, y, w, h, force); + + boolean above = isAboveAnchor(); + if (above != mAbove) { + mAbove = above; + + if (above) { + err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above); + } else { + err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error); + } + } + } + }; mPopup.setFocusable(false); } @@ -5094,6 +5141,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_singleLine */ + @android.view.RemotableViewMethod public void setSingleLine(boolean singleLine) { if ((mInputType&EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { @@ -5168,6 +5216,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_selectAllOnFocus */ + @android.view.RemotableViewMethod public void setSelectAllOnFocus(boolean selectAllOnFocus) { mSelectAllOnFocus = selectAllOnFocus; @@ -5181,6 +5230,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_cursorVisible */ + @android.view.RemotableViewMethod public void setCursorVisible(boolean visible) { mCursorVisible = visible; invalidate(); @@ -5730,6 +5780,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener startStopMarquee(hasWindowFocus); } + /** + * Use {@link BaseInputConnection#removeComposingSpans + * BaseInputConnection.removeComposingSpans()} to remove any IME composing + * state from this text view. + */ + public void clearComposingText() { + if (mText instanceof Spannable) { + BaseInputConnection.removeComposingSpans((Spannable)mText); + } + } + @Override public void setSelected(boolean selected) { boolean wasSelected = isSelected(); diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index e20bfdf..8a7946b 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -31,7 +31,6 @@ import android.widget.RemoteViews.RemoteView; * * @attr ref android.R.styleable#ViewFlipper_flipInterval */ -@RemoteView public class ViewFlipper extends ViewAnimator { private int mFlipInterval = 3000; private boolean mKeepFlipping = false; @@ -56,6 +55,7 @@ public class ViewFlipper extends ViewAnimator { * @param milliseconds * time in milliseconds */ + @android.view.RemotableViewMethod public void setFlipInterval(int milliseconds) { mFlipInterval = milliseconds; } diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java index be3b1fb..22881b3 100644 --- a/core/java/android/widget/ZoomRing.java +++ b/core/java/android/widget/ZoomRing.java @@ -7,7 +7,11 @@ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.RotateDrawable; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; @@ -19,10 +23,8 @@ import android.view.ViewConfiguration; public class ZoomRing extends View { // TODO: move to ViewConfiguration? - static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getJumpTapTimeout(); + static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); // TODO: get from theme - private static final int DISABLED_ALPHA = 160; - private static final String TAG = "ZoomRing"; // TODO: Temporary until the trail is done @@ -32,15 +34,26 @@ public class ZoomRing extends View { private static final int THUMB_DISTANCE = 63; /** To avoid floating point calculations, we multiply radians by this value. */ - public static final int RADIAN_INT_MULTIPLIER = 100000000; + public static final int RADIAN_INT_MULTIPLIER = 10000; + public static final int RADIAN_INT_ERROR = 100; /** PI using our multiplier. */ public static final int PI_INT_MULTIPLIED = (int) (Math.PI * RADIAN_INT_MULTIPLIER); + public static final int TWO_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED * 2; /** PI/2 using our multiplier. */ private static final int HALF_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED / 2; private int mZeroAngle = HALF_PI_INT_MULTIPLIED * 3; + + private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 8; + private static final int THUMB_DRAG_SLOP = PI_INT_MULTIPLIED / 12; - private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 4; + /** + * Includes error because we compare this to the result of + * getDelta(getClosestTickeAngle(..), oldAngle) which ends up having some + * rounding error. + */ + private static final int MAX_ABS_JUMP_DELTA_ANGLE = (2 * PI_INT_MULTIPLIED / 3) + + RADIAN_INT_ERROR; /** The cached X of our center. */ private int mCenterX; @@ -49,10 +62,13 @@ public class ZoomRing extends View { /** The angle of the thumb (in int radians) */ private int mThumbAngle; - private boolean mIsThumbAngleValid; private int mThumbHalfWidth; private int mThumbHalfHeight; - + + private int mThumbCwBound = Integer.MIN_VALUE; + private int mThumbCcwBound = Integer.MIN_VALUE; + private boolean mEnforceMaxAbsJump = true; + /** The inner radius of the track. */ private int mBoundInnerRadiusSquared = 0; /** The outer radius of the track. */ @@ -63,8 +79,23 @@ public class ZoomRing extends View { private boolean mDrawThumb = true; private Drawable mThumbDrawable; - + + /** Shown beneath the thumb if we can still zoom in. */ + private Drawable mThumbPlusArrowDrawable; + /** Shown beneath the thumb if we can still zoom out. */ + private Drawable mThumbMinusArrowDrawable; + private static final int THUMB_ARROWS_FADE_DURATION = 300; + private long mThumbArrowsFadeStartTime; + private int mThumbArrowsAlpha = 255; + private static final int MODE_IDLE = 0; + + /** + * User has his finger down somewhere on the ring (besides the thumb) and we + * are waiting for him to move the slop amount before considering him in the + * drag thumb state. + */ + private static final int MODE_WAITING_FOR_DRAG_THUMB = 5; private static final int MODE_DRAG_THUMB = 1; /** * User has his finger down, but we are waiting for him to pass the touch @@ -74,24 +105,47 @@ public class ZoomRing extends View { private static final int MODE_WAITING_FOR_MOVE_ZOOM_RING = 4; private static final int MODE_MOVE_ZOOM_RING = 2; private static final int MODE_TAP_DRAG = 3; + /** Ignore the touch interaction. Reset to MODE_IDLE after up/cancel. */ + private static final int MODE_IGNORE_UNTIL_UP = 6; private int mMode; - private long mPreviousDownTime; + private long mPreviousUpTime; private int mPreviousDownX; private int mPreviousDownY; - private Disabler mDisabler = new Disabler(); - + private int mWaitingForDragThumbDownAngle; + private OnZoomRingCallback mCallback; private int mPreviousCallbackAngle; private int mCallbackThreshold = Integer.MAX_VALUE; private boolean mResetThumbAutomatically = true; private int mThumbDragStartAngle; + private final int mTouchSlop; + private Drawable mTrail; private double mAcculumalatedTrailAngle; - + + private Scroller mThumbScroller; + + private static final int MSG_THUMB_SCROLLER_TICK = 1; + private static final int MSG_THUMB_ARROWS_FADE_TICK = 2; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_THUMB_SCROLLER_TICK: + onThumbScrollerTick(); + break; + + case MSG_THUMB_ARROWS_FADE_TICK: + onThumbArrowsFadeTick(); + break; + } + } + }; + public ZoomRing(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); @@ -101,6 +155,10 @@ public class ZoomRing extends View { // TODO get drawables from style instead Resources res = context.getResources(); mThumbDrawable = res.getDrawable(R.drawable.zoom_ring_thumb); + mThumbPlusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_plus_arrow_rotatable). + mutate(); + mThumbMinusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus_arrow_rotatable). + mutate(); if (DRAW_TRAIL) { mTrail = res.getDrawable(R.drawable.zoom_ring_trail).mutate(); } @@ -108,7 +166,7 @@ public class ZoomRing extends View { // TODO: add padding to drawable setBackgroundResource(R.drawable.zoom_ring_track); // TODO get from style - setBounds(30, Integer.MAX_VALUE); + setRingBounds(30, Integer.MAX_VALUE); mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2; mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2; @@ -134,7 +192,7 @@ public class ZoomRing extends View { } // TODO: from XML too - public void setBounds(int innerRadius, int outerRadius) { + public void setRingBounds(int innerRadius, int outerRadius) { mBoundInnerRadiusSquared = innerRadius * innerRadius; if (mBoundInnerRadiusSquared < innerRadius) { // Prevent overflow @@ -148,7 +206,64 @@ public class ZoomRing extends View { } } + public void setThumbClockwiseBound(int angle) { + if (angle < 0) { + mThumbCwBound = Integer.MIN_VALUE; + } else { + mThumbCwBound = getClosestTickAngle(angle); + } + setEnforceMaxAbsJump(); + } + + public void setThumbCounterclockwiseBound(int angle) { + if (angle < 0) { + mThumbCcwBound = Integer.MIN_VALUE; + } else { + mThumbCcwBound = getClosestTickAngle(angle); + } + setEnforceMaxAbsJump(); + } + + private void setEnforceMaxAbsJump() { + // If there are bounds in both direction, there is no reason to restrict + // the amount that a user can absolute jump to + mEnforceMaxAbsJump = + mThumbCcwBound == Integer.MIN_VALUE || mThumbCwBound == Integer.MIN_VALUE; + } + + public int getThumbAngle() { + return mThumbAngle; + } + public void setThumbAngle(int angle) { + angle = getValidAngle(angle); + mPreviousCallbackAngle = getClosestTickAngle(angle); + setThumbAngleAuto(angle, false, false); + } + + /** + * Sets the thumb angle. If already animating, will continue the animation, + * otherwise it will do a direct jump. + * + * @param angle + * @param useDirection Whether to use the ccw parameter + * @param ccw Whether going counterclockwise (only used if useDirection is true) + */ + private void setThumbAngleAuto(int angle, boolean useDirection, boolean ccw) { + if (mThumbScroller == null + || mThumbScroller.isFinished() + || Math.abs(getDelta(angle, getThumbScrollerAngle())) < THUMB_GRAB_SLOP) { + setThumbAngleInt(angle); + } else { + if (useDirection) { + setThumbAngleAnimated(angle, 0, ccw); + } else { + setThumbAngleAnimated(angle, 0); + } + } + } + + private void setThumbAngleInt(int angle) { mThumbAngle = angle; int unoffsetAngle = angle + mZeroAngle; int thumbCenterX = (int) (Math.cos(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * @@ -161,6 +276,10 @@ public class ZoomRing extends View { thumbCenterX + mThumbHalfWidth, thumbCenterY + mThumbHalfHeight); + if (mThumbArrowsAlpha > 0) { + setThumbArrowsAngle(angle); + } + if (DRAW_TRAIL) { double degrees; degrees = Math.min(359.0, Math.abs(mAcculumalatedTrailAngle)); @@ -174,10 +293,66 @@ public class ZoomRing extends View { invalidate(); } + + /** + * + * @param angle + * @param duration The animation duration, or 0 for the default duration. + */ + public void setThumbAngleAnimated(int angle, int duration) { + // The angle when going from the current angle to the new angle + int deltaAngle = getDelta(mThumbAngle, angle); + // Counter clockwise if the new angle is more the current angle + boolean counterClockwise = deltaAngle > 0; + + if (deltaAngle > PI_INT_MULTIPLIED || deltaAngle < -PI_INT_MULTIPLIED) { + // It's quicker to go the other direction + counterClockwise = !counterClockwise; + } + + setThumbAngleAnimated(angle, duration, counterClockwise); + } + + public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) { + if (mThumbScroller == null) { + mThumbScroller = new Scroller(mContext); + } + + int startAngle = mThumbAngle; + int endAngle = getValidAngle(angle); + int deltaAngle = getDelta(startAngle, endAngle, counterClockwise); + if (startAngle + deltaAngle < 0) { + // Keep our angles positive + startAngle += TWO_PI_INT_MULTIPLIED; + } + + if (!mThumbScroller.isFinished()) { + duration = mThumbScroller.getDuration() - mThumbScroller.timePassed(); + } else if (duration == 0) { + duration = getAnimationDuration(deltaAngle); + } + mThumbScroller.startScroll(startAngle, 0, deltaAngle, 0, duration); + onThumbScrollerTick(); + } + + private int getAnimationDuration(int deltaAngle) { + if (deltaAngle < 0) deltaAngle *= -1; + return 300 + deltaAngle * 300 / RADIAN_INT_MULTIPLIER; + } + + private void onThumbScrollerTick() { + if (!mThumbScroller.computeScrollOffset()) return; + setThumbAngleInt(getThumbScrollerAngle()); + mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_TICK); + } + private int getThumbScrollerAngle() { + return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED; + } + public void resetThumbAngle(int angle) { mPreviousCallbackAngle = angle; - setThumbAngle(angle); + setThumbAngleInt(angle); } public void resetThumbAngle() { @@ -185,7 +360,7 @@ public class ZoomRing extends View { resetThumbAngle(0); } } - + public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mResetThumbAutomatically = resetThumbAutomatically; } @@ -214,6 +389,9 @@ public class ZoomRing extends View { if (DRAW_TRAIL) { mTrail.setBounds(0, 0, right - left, bottom - top); } + + mThumbPlusArrowDrawable.setBounds(0, 0, right - left, bottom - top); + mThumbMinusArrowDrawable.setBounds(0, 0, right - left, bottom - top); } @Override @@ -227,15 +405,13 @@ public class ZoomRing extends View { mMode = MODE_IDLE; mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE; mAcculumalatedTrailAngle = 0.0; - mIsThumbAngleValid = false; } public void setTapDragMode(boolean tapDragMode, int x, int y) { resetState(); mMode = tapDragMode ? MODE_TAP_DRAG : MODE_IDLE; - mIsThumbAngleValid = false; - if (tapDragMode && mCallback != null) { + if (tapDragMode) { onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY)); } } @@ -244,35 +420,44 @@ public class ZoomRing extends View { switch (action) { case MotionEvent.ACTION_DOWN: - if (mPreviousDownTime + DOUBLE_TAP_DISMISS_TIMEOUT >= time) { - if (mCallback != null) { - mCallback.onZoomRingDismissed(); - } - } else { - mPreviousDownTime = time; - mPreviousDownX = x; - mPreviousDownY = y; + mCallback.onUserInteractionStarted(); + + if (time - mPreviousUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT) { + mCallback.onZoomRingDismissed(true); } + + mPreviousDownX = x; + mPreviousDownY = y; resetState(); - return true; + // Fall through to code below switch (since the down is used for + // jumping to the touched tick) + break; case MotionEvent.ACTION_MOVE: + if (mMode == MODE_IGNORE_UNTIL_UP) return true; + // Fall through to code below switch break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - if (mCallback != null) { - if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - mCallback.onZoomRingSetMovableHintVisible(false); - if (mMode == MODE_MOVE_ZOOM_RING) { - mCallback.onZoomRingMovingStopped(); - } - } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - onThumbDragStopped(getAngle(x - mCenterX, y - mCenterY)); + if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { + mCallback.onZoomRingSetMovableHintVisible(false); + if (mMode == MODE_MOVE_ZOOM_RING) { + mCallback.onZoomRingMovingStopped(); + } + } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG || + mMode == MODE_WAITING_FOR_DRAG_THUMB) { + onThumbDragStopped(); + + if (mMode == MODE_DRAG_THUMB) { + // Animate back to a tick + setThumbAngleAnimated(mPreviousCallbackAngle, 0); } } - mDisabler.setEnabling(true); + + mPreviousUpTime = time; + mCallback.onUserInteractionStopped(); return true; default: @@ -283,18 +468,18 @@ public class ZoomRing extends View { int localX = x - mCenterX; int localY = y - mCenterY; boolean isTouchingThumb = true; - boolean isInBounds = true; + boolean isInRingBounds = true; + int touchAngle = getAngle(localX, localY); - int radiusSquared = localX * localX + localY * localY; if (radiusSquared < mBoundInnerRadiusSquared || radiusSquared > mBoundOuterRadiusSquared) { // Out-of-bounds isTouchingThumb = false; - isInBounds = false; + isInRingBounds = false; } - int deltaThumbAndTouch = getDelta(touchAngle, mThumbAngle); + int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? deltaThumbAndTouch : -deltaThumbAndTouch; if (isTouchingThumb && @@ -305,17 +490,68 @@ public class ZoomRing extends View { if (mMode == MODE_IDLE) { if (isTouchingThumb) { + // They grabbed the thumb mMode = MODE_DRAG_THUMB; + onThumbDragStarted(touchAngle); + + } else if (isInRingBounds) { + // They tapped somewhere else on the ring + int tickAngle = getClosestTickAngle(touchAngle); + + int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle); + int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); + + if (mEnforceMaxAbsJump) { + // Enforcing the max jump + if (deltaThumbAndTick > MAX_ABS_JUMP_DELTA_ANGLE || + deltaThumbAndTick < -MAX_ABS_JUMP_DELTA_ANGLE) { + // Trying to jump too far, ignore this touch interaction + mMode = MODE_IGNORE_UNTIL_UP; + return true; + } + + // Make sure we only let them jump within bounds + if (boundAngle != Integer.MIN_VALUE) { + tickAngle = boundAngle; + } + } else { + // Not enforcing the max jump, but we have to make sure + // we're getting to the tapped angle by going through the + // in-bounds region + if (boundAngle != Integer.MIN_VALUE) { + // Going this direction hits a bound, let's go the opposite direction + boolean oldDirectionIsCcw = deltaThumbAndTick > 0; + deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw); + boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); + if (boundAngle != Integer.MIN_VALUE) { + Log + .d( + TAG, + "Tapped somewhere where the shortest distance goes through a bound, but then the opposite direction also went through a bound!"); + } + } + } + + mMode = MODE_WAITING_FOR_DRAG_THUMB; + mWaitingForDragThumbDownAngle = touchAngle; + boolean ccw = deltaThumbAndTick > 0; + setThumbAngleAnimated(tickAngle, 0, ccw); + + // Our thumb scrolling animation takes us from mThumbAngle to tickAngle + onThumbDragStarted(mThumbAngle); + onThumbDragged(tickAngle, true, ccw); + } else { + // They tapped somewhere else mMode = MODE_WAITING_FOR_MOVE_ZOOM_RING; + mCallback.onZoomRingSetMovableHintVisible(true); } - if (mCallback != null) { - if (mMode == MODE_DRAG_THUMB) { - onThumbDragStarted(touchAngle); - } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - mCallback.onZoomRingSetMovableHintVisible(true); - } + } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB) { + int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle); + if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) && + isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) { + mMode = MODE_DRAG_THUMB; } } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { @@ -323,19 +559,14 @@ public class ZoomRing extends View { Math.abs(y - mPreviousDownY) > mTouchSlop) { /* Make sure the user has moved the slop amount before going into that mode. */ mMode = MODE_MOVE_ZOOM_RING; - - if (mCallback != null) { - mCallback.onZoomRingMovingStarted(); - } + mCallback.onZoomRingMovingStarted(); } } // Purposefully not an "else if" if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - if (isInBounds) { - onThumbDragged(touchAngle, mIsThumbAngleValid ? deltaThumbAndTouch : 0); - } else { - mIsThumbAngleValid = false; + if (isInRingBounds) { + onThumbDragged(touchAngle, false, false); } } else if (mMode == MODE_MOVE_ZOOM_RING) { onZoomRingMoved(rawX, rawY); @@ -344,55 +575,219 @@ public class ZoomRing extends View { return true; } - private int getDelta(int angle1, int angle2) { - int delta = angle1 - angle2; - - // Assume this is a result of crossing over the discontinuous 0 -> 2pi - if (delta > PI_INT_MULTIPLIED || delta < -PI_INT_MULTIPLIED) { - // Bring both the radians and previous angle onto a continuous range - if (angle1 < HALF_PI_INT_MULTIPLIED) { - // Same as deltaRadians = (radians + 2PI) - previousAngle - delta += PI_INT_MULTIPLIED * 2; - } else if (angle2 < HALF_PI_INT_MULTIPLIED) { - // Same as deltaRadians = radians - (previousAngle + 2PI) - delta -= PI_INT_MULTIPLIED * 2; + private boolean isDeltaInBounds(int startAngle, int deltaAngle) { + return getBoundIfExceeds(startAngle, deltaAngle) == Integer.MIN_VALUE; + } + + private int getBoundIfExceeds(int startAngle, int deltaAngle) { + if (deltaAngle > 0) { + // Counterclockwise movement + if (mThumbCcwBound != Integer.MIN_VALUE && + getDelta(startAngle, mThumbCcwBound, true) < deltaAngle) { + return mThumbCcwBound; + } + } else if (deltaAngle < 0) { + // Clockwise movement, both of these will be negative + int deltaThumbAndBound = getDelta(startAngle, mThumbCwBound, false); + if (mThumbCwBound != Integer.MIN_VALUE && + deltaThumbAndBound > deltaAngle) { + // Tapped outside of the bound in that direction + return mThumbCwBound; } } + + return Integer.MIN_VALUE; + } + + private int getDelta(int startAngle, int endAngle, boolean useDirection, boolean ccw) { + return useDirection ? getDelta(startAngle, endAngle, ccw) : getDelta(startAngle, endAngle); + } + + /** + * Gets the smallest delta between two angles, and infers the direction + * based on the shortest path between the two angles. If going from + * startAngle to endAngle is counterclockwise, the result will be positive. + * If it is clockwise, the result will be negative. + * + * @param startAngle The start angle. + * @param endAngle The end angle. + * @return The difference in angles. + */ + private int getDelta(int startAngle, int endAngle) { + int largerAngle, smallerAngle; + if (endAngle > startAngle) { + largerAngle = endAngle; + smallerAngle = startAngle; + } else { + largerAngle = startAngle; + smallerAngle = endAngle; + } - return delta; + int delta = largerAngle - smallerAngle; + if (delta <= PI_INT_MULTIPLIED) { + // If going clockwise, negate the delta + return startAngle == largerAngle ? -delta : delta; + } else { + // The other direction is the delta we want (it includes the + // discontinuous 0-2PI angle) + delta = TWO_PI_INT_MULTIPLIED - delta; + // If going clockwise, negate the delta + return startAngle == smallerAngle ? -delta : delta; + } } + /** + * Gets the delta between two angles in the direction specified. + * + * @param startAngle The start angle. + * @param endAngle The end angle. + * @param counterClockwise The direction to take when computing the delta. + * @return The difference in angles in the given direction. + */ + private int getDelta(int startAngle, int endAngle, boolean counterClockwise) { + int delta = endAngle - startAngle; + + if (!counterClockwise && delta > 0) { + // Crossed the discontinuous 0/2PI angle, take the leftover slice of + // the pie and negate it + return -TWO_PI_INT_MULTIPLIED + delta; + } else if (counterClockwise && delta < 0) { + // Crossed the discontinuous 0/2PI angle, take the leftover slice of + // the pie (and ensure it is positive) + return TWO_PI_INT_MULTIPLIED + delta; + } else { + return delta; + } + } + private void onThumbDragStarted(int startAngle) { + setThumbArrowsVisible(false); mThumbDragStartAngle = startAngle; - mCallback.onZoomRingThumbDraggingStarted(startAngle); + mCallback.onZoomRingThumbDraggingStarted(); } - - private void onThumbDragged(int touchAngle, int deltaAngle) { - mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); - int totalDeltaAngle = getDelta(touchAngle, mPreviousCallbackAngle); - if (totalDeltaAngle > mCallbackThreshold - || totalDeltaAngle < -mCallbackThreshold) { - if (mCallback != null) { - boolean canStillZoom = mCallback.onZoomRingThumbDragged( - totalDeltaAngle / mCallbackThreshold, - mThumbDragStartAngle, touchAngle); - mDisabler.setEnabling(canStillZoom); - - if (canStillZoom) { - // TODO: we're trying the haptics to see how it goes with - // users, so we're ignoring the settings (for now) - performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + + private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) { + boolean animateThumbToNewAngle = false; + + int totalDeltaAngle; + totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); + int fuzzyCallbackThreshold = (int) (mCallbackThreshold * 0.65f); + if (totalDeltaAngle >= fuzzyCallbackThreshold + || totalDeltaAngle <= -fuzzyCallbackThreshold) { + + if (!useDirection) { + // Set ccw to match the direction found by getDelta + ccw = totalDeltaAngle > 0; + } + + /* + * When the user slides the thumb through the tick that corresponds + * to a zoom bound, we don't want to abruptly stop there. Instead, + * let the user slide it to the next tick, and then animate it back + * to the original zoom bound tick. Because of this, we make sure + * the delta from the bound is more than halfway to the next tick. + * We make sure the bound is between the touch and the previous + * callback to ensure we just passed the bound. + */ + int oldTouchAngle = touchAngle; + if (ccw && mThumbCcwBound != Integer.MIN_VALUE) { + int deltaCcwBoundAndTouch = + getDelta(mThumbCcwBound, touchAngle, useDirection, true); + if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) { + // The touch has past a bound + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + touchAngle, useDirection, true); + if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { + // The bound is between the previous callback angle and the touch + touchAngle = mThumbCcwBound; + // We're moving the touch BACK to the bound, so opposite direction + ccw = false; + } + } + } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) { + // See block above for general comments + int deltaCwBoundAndTouch = + getDelta(mThumbCwBound, touchAngle, useDirection, false); + if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) { + int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, + touchAngle, useDirection, false); + /* + * Both of these will be negative since we got delta in + * clockwise direction, and we want the magnitude of + * deltaPreviousCbAndTouch to be greater than the magnitude + * of deltaCwBoundAndTouch + */ + if (deltaPreviousCbAndTouch <= deltaCwBoundAndTouch) { + touchAngle = mThumbCwBound; + ccw = true; + } + } + } + if (touchAngle != oldTouchAngle) { + // We bounded the touch angle + totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); + animateThumbToNewAngle = true; + mMode = MODE_IGNORE_UNTIL_UP; + } + + + // Prevent it from jumping too far + if (mEnforceMaxAbsJump) { + if (totalDeltaAngle <= -MAX_ABS_JUMP_DELTA_ANGLE) { + totalDeltaAngle = -MAX_ABS_JUMP_DELTA_ANGLE; + animateThumbToNewAngle = true; + } else if (totalDeltaAngle >= MAX_ABS_JUMP_DELTA_ANGLE) { + totalDeltaAngle = MAX_ABS_JUMP_DELTA_ANGLE; + animateThumbToNewAngle = true; } } - // Get the closest tick and lock on there - mPreviousCallbackAngle = getClosestTickAngle(touchAngle); + /* + * We need to cover the edge case of a user grabbing the thumb, + * going into the center of the widget, and then coming out from the + * center to an angle that's slightly below the angle he's trying to + * hit. If we do int division, we'll end up with one level lower + * than the one he was going for. + */ + int deltaLevels = Math.round((float) totalDeltaAngle / mCallbackThreshold); + if (deltaLevels != 0) { + boolean canStillZoom = mCallback.onZoomRingThumbDragged( + deltaLevels, mThumbDragStartAngle, touchAngle); + + // TODO: we're trying the haptics to see how it goes with + // users, so we're ignoring the settings (for now) + performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + + // Set the callback angle to the actual angle based on how many delta levels we gave + mPreviousCallbackAngle = getValidAngle( + mPreviousCallbackAngle + (deltaLevels * mCallbackThreshold)); + } } - setThumbAngle(touchAngle); - mIsThumbAngleValid = true; + int deltaAngle = getDelta(mThumbAngle, touchAngle, useDirection, ccw); + mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); + + if (animateThumbToNewAngle) { + if (useDirection) { + setThumbAngleAnimated(touchAngle, 0, ccw); + } else { + setThumbAngleAnimated(touchAngle, 0); + } + } else { + setThumbAngleAuto(touchAngle, useDirection, ccw); + } + } + + private int getValidAngle(int invalidAngle) { + if (invalidAngle < 0) { + return (invalidAngle % TWO_PI_INT_MULTIPLIED) + TWO_PI_INT_MULTIPLIED; + } else if (invalidAngle >= TWO_PI_INT_MULTIPLIED) { + return invalidAngle % TWO_PI_INT_MULTIPLIED; + } else { + return invalidAngle; + } } private int getClosestTickAngle(int angle) { @@ -403,12 +798,12 @@ public class ZoomRing extends View { return smallerAngle; } else { // Closer to the bigger angle (premodding) - return (smallerAngle + mCallbackThreshold) % (PI_INT_MULTIPLIED * 2); + return (smallerAngle + mCallbackThreshold) % TWO_PI_INT_MULTIPLIED; } } - private void onThumbDragStopped(int stopAngle) { - mCallback.onZoomRingThumbDraggingStopped(stopAngle); + private void onThumbDragStopped() { + mCallback.onZoomRingThumbDraggingStopped(); } private void onZoomRingMoved(int x, int y) { @@ -416,9 +811,7 @@ public class ZoomRing extends View { int deltaX = x - mPreviousWidgetDragX; int deltaY = y - mPreviousWidgetDragY; - if (mCallback != null) { - mCallback.onZoomRingMoved(deltaX, deltaY); - } + mCallback.onZoomRingMoved(deltaX, deltaY); } mPreviousWidgetDragX = x; @@ -429,11 +822,11 @@ public class ZoomRing extends View { public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus && mCallback != null) { - mCallback.onZoomRingDismissed(); + if (!hasWindowFocus) { + mCallback.onZoomRingDismissed(true); } } - + private int getAngle(int localX, int localY) { int radians = (int) (Math.atan2(localY, localX) * RADIAN_INT_MULTIPLIER); @@ -458,45 +851,65 @@ public class ZoomRing extends View { if (DRAW_TRAIL) { mTrail.draw(canvas); } + + // If we aren't near the bounds, draw the corresponding arrows + int callbackAngle = mPreviousCallbackAngle; + if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR || + callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) { + mThumbPlusArrowDrawable.draw(canvas); + } + if (callbackAngle < mThumbCcwBound - RADIAN_INT_ERROR || + callbackAngle > mThumbCcwBound + RADIAN_INT_ERROR) { + mThumbMinusArrowDrawable.draw(canvas); + } mThumbDrawable.draw(canvas); } } - - private class Disabler implements Runnable { - private static final int DELAY = 15; - private static final float ENABLE_RATE = 1.05f; - private static final float DISABLE_RATE = 0.95f; - - private int mAlpha = 255; - private boolean mEnabling; - - public int getAlpha() { - return mAlpha; - } - - public void setEnabling(boolean enabling) { - if ((enabling && mAlpha != 255) || (!enabling && mAlpha != DISABLED_ALPHA)) { - mEnabling = enabling; - post(this); - } + + private void setThumbArrowsAngle(int angle) { + int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED; + mThumbPlusArrowDrawable.setLevel(level); + mThumbMinusArrowDrawable.setLevel(level); + } + + public void setThumbArrowsVisible(boolean visible) { + if (visible) { + mThumbArrowsAlpha = 255; + mThumbPlusArrowDrawable.setAlpha(255); + mThumbMinusArrowDrawable.setAlpha(255); + invalidate(); + } else if (mThumbArrowsAlpha == 255) { + // Only start fade if we're fully visible (otherwise another fade is happening already) + mThumbArrowsFadeStartTime = SystemClock.elapsedRealtime(); + onThumbArrowsFadeTick(); } + } + + private void onThumbArrowsFadeTick() { + if (mThumbArrowsAlpha <= 0) return; - public void run() { - mAlpha *= mEnabling ? ENABLE_RATE : DISABLE_RATE; - if (mAlpha < DISABLED_ALPHA) { - mAlpha = DISABLED_ALPHA; - } else if (mAlpha > 255) { - mAlpha = 255; - } else { - // Still more to go - postDelayed(this, DELAY); - } - - getBackground().setAlpha(mAlpha); - invalidate(); + mThumbArrowsAlpha = (int) + (255 - (255 * (SystemClock.elapsedRealtime() - mThumbArrowsFadeStartTime) + / THUMB_ARROWS_FADE_DURATION)); + if (mThumbArrowsAlpha < 0) mThumbArrowsAlpha = 0; + mThumbPlusArrowDrawable.setAlpha(mThumbArrowsAlpha); + mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mThumbPlusArrowDrawable); + invalidateDrawable(mThumbMinusArrowDrawable); + + if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_TICK)) { + mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_TICK); } } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + setThumbArrowsAngle(mThumbAngle); + setThumbArrowsVisible(true); + } + public interface OnZoomRingCallback { void onZoomRingSetMovableHintVisible(boolean visible); @@ -504,11 +917,17 @@ public class ZoomRing extends View { boolean onZoomRingMoved(int deltaX, int deltaY); void onZoomRingMovingStopped(); - void onZoomRingThumbDraggingStarted(int startAngle); + void onZoomRingThumbDraggingStarted(); boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle); - void onZoomRingThumbDraggingStopped(int endAngle); + void onZoomRingThumbDraggingStopped(); - void onZoomRingDismissed(); + void onZoomRingDismissed(boolean dismissImmediately); + + void onUserInteractionStarted(); + void onUserInteractionStopped(); } + private static void printAngle(String angleName, int angle) { + Log.d(TAG, angleName + ": " + (long) angle * 180 / PI_INT_MULTIPLIED); + } } diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java index eb28767..31074b6 100644 --- a/core/java/android/widget/ZoomRingController.java +++ b/core/java/android/widget/ZoomRingController.java @@ -16,6 +16,8 @@ package android.widget; +import android.app.AlertDialog; +import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -27,7 +29,6 @@ import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.SystemClock; -import android.os.Vibrator; import android.provider.Settings; import android.util.Log; import android.view.Gravity; @@ -35,6 +36,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; +import android.view.Window; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.Animation; @@ -46,12 +48,16 @@ import android.view.animation.DecelerateInterpolator; /** * TODO: Docs * + * If you are using this with a custom View, please call + * {@link #setVisible(boolean) setVisible(false)} from the + * {@link View#onDetachedFromWindow}. + * * @hide */ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, View.OnTouchListener, View.OnKeyListener { - private static final int SHOW_TUTORIAL_TOAST_DELAY = 1000; + private static final int ZOOM_RING_RADIUS_INSET = 10; private static final int ZOOM_RING_RECENTERING_DURATION = 500; @@ -69,9 +75,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // TODO: scale px values based on latest from ViewConfiguration private static final int SECOND_TAP_TIMEOUT = 500; private static final int ZOOM_RING_DISMISS_DELAY = SECOND_TAP_TIMEOUT / 2; - private static final int SECOND_TAP_SLOP = 70; - private static final int SECOND_TAP_MOVE_SLOP = 15; - private static final int MAX_PAN_GAP = 30; + // TODO: view config? at least scaled + private static final int MAX_PAN_GAP = 20; + private static final int MAX_INITIATE_PAN_GAP = 10; + // TODO view config + private static final int INITIATE_PAN_DELAY = 400; private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; @@ -95,6 +103,23 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private FrameLayout mContainer; private LayoutParams mContainerLayoutParams; + /** + * The view (or null) that should receive touch events. This will get set if + * the touch down hits the container. It will be reset on the touch up. + */ + private View mTouchTargetView; + /** + * The {@link #mTouchTargetView}'s location in window, set on touch down. + */ + private int[] mTouchTargetLocationInWindow = new int[2]; + /** + * If the zoom ring is dismissed but the user is still in a touch + * interaction, we set this to true. This will ignore all touch events until + * up/cancel, and then set the owner's touch listener to null. + */ + private boolean mReleaseTouchListenerOnUp; + + /* * Tap-drag is an interaction where the user first taps and then (quickly) * does the clockwise or counter-clockwise drag. In reality, this is: (down, @@ -122,6 +147,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner = new Panner(); + private long mTouchingEdgeStartTime; + private boolean mPanningEnabledForThisInteraction; private ImageView mPanningArrows; private Animation mPanningArrowsEnterAnimation; @@ -162,26 +189,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * the UI thread so it will be exceuted AFTER the layout. This is the logic. */ private Runnable mPostedVisibleInitializer; - - // TODO: need a better way to persist this value, becuase right now this - // requires the WRITE_SETTINGS perimssion which the app may not have -// private Runnable mShowTutorialToast = new Runnable() { -// public void run() { -// if (Settings.System.getInt(mContext.getContentResolver(), -// SETTING_NAME_SHOWN_TOAST, 0) == 1) { -// return; -// } -// try { -// Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); -// } catch (SecurityException e) { -// // The app does not have permission to clear this flag, oh well! -// } -// -// Toast.makeText(mContext, -// com.android.internal.R.string.tutorial_double_tap_to_zoom_message, -// Toast.LENGTH_LONG).show(); -// } -// }; + + /** + * Only touch from the main thread. + */ + private static Dialog sTutorialDialog; + private static long sTutorialShowTime; + private static final int TUTORIAL_MIN_DISPLAY_TIME = 2000; private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); @@ -230,38 +244,37 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mOwnerView = ownerView; mZoomRing = new ZoomRing(context); + mZoomRing.setId(com.android.internal.R.id.zoomControls); mZoomRing.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER)); mZoomRing.setCallback(this); createPanningArrows(); - mContainer = new FrameLayout(context); - mContainer.setMeasureAllChildren(true); - mContainer.setOnTouchListener(this); - - mContainer.addView(mZoomRing); - mContainer.addView(mPanningArrows); - mContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - mContainerLayoutParams = new LayoutParams(); mContainerLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | + mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_NOT_FOCUSABLE | - LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_LAYOUT_NO_LIMITS; + LayoutParams.FLAG_LAYOUT_NO_LIMITS; mContainerLayoutParams.height = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.width = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.type = LayoutParams.TYPE_APPLICATION_PANEL; - mContainerLayoutParams.format = PixelFormat.TRANSLUCENT; + mContainerLayoutParams.format = PixelFormat.TRANSPARENT; // TODO: make a new animation for this mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Dialog; + + mContainer = new FrameLayout(context); + mContainer.setLayoutParams(mContainerLayoutParams); + mContainer.setMeasureAllChildren(true); + + mContainer.addView(mZoomRing); + mContainer.addView(mPanningArrows); mScroller = new Scroller(context, new DecelerateInterpolator()); mViewConfig = ViewConfiguration.get(context); - -// mHandler.postDelayed(mShowTutorialToast, SHOW_TUTORIAL_TOAST_DELAY); } private void createPanningArrows() { @@ -272,7 +285,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - mPanningArrows.setVisibility(View.GONE); + mPanningArrows.setVisibility(View.INVISIBLE); mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_in); @@ -291,6 +304,17 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setZoomCallbackThreshold(float callbackThreshold) { mZoomRing.setCallbackThreshold((int) (callbackThreshold * ZoomRing.RADIAN_INT_MULTIPLIER)); } + + /** + * Sets a drawable for the zoom ring track. + * + * @param drawable The drawable to use for the track. + * @hide Need a better way of doing this, but this one-off for browser so it + * can have its final look for the usability study + */ + public void setZoomRingTrack(int drawable) { + mZoomRing.setBackgroundResource(drawable); + } public void setCallback(OnZoomListener callback) { mCallback = callback; @@ -300,10 +324,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } + public void setThumbAngleAnimated(float angle) { + mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0); + } + public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mZoomRing.setResetThumbAutomatically(resetThumbAutomatically); } + public void setThumbClockwiseBound(float angle) { + mZoomRing.setThumbClockwiseBound(angle >= 0 ? + (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : + Integer.MIN_VALUE); + } + + public void setThumbCounterclockwiseBound(float angle) { + mZoomRing.setThumbCounterclockwiseBound(angle >= 0 ? + (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : + Integer.MIN_VALUE); + } + public boolean isVisible() { return mIsZoomRingVisible; } @@ -321,12 +361,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (mIsZoomRingVisible == visible) { return; } + mIsZoomRingVisible = visible; if (visible) { if (mContainerLayoutParams.token == null) { mContainerLayoutParams.token = mOwnerView.getWindowToken(); } - + mWindowManager.addView(mContainer, mContainerLayoutParams); if (mPostedVisibleInitializer == null) { @@ -340,6 +381,10 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // probably can only be retrieved after it's measured, which happens // after it's added). mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); + + if (mCallback != null) { + mCallback.onVisibilityChanged(true); + } } }; } @@ -349,24 +394,49 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // Handle configuration changes when visible mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); - // Steal key events from the owner + // Steal key/touches events from the owner mOwnerView.setOnKeyListener(this); + mOwnerView.setOnTouchListener(this); + mReleaseTouchListenerOnUp = false; } else { - // Don't want to steal any more keys + // Don't want to steal any more keys/touches mOwnerView.setOnKeyListener(null); + if (mTouchTargetView != null) { + // We are still stealing the touch events for this touch + // sequence, so release the touch listener later + mReleaseTouchListenerOnUp = true; + } else { + mOwnerView.setOnTouchListener(null); + } // No longer care about configuration changes mContext.unregisterReceiver(mConfigurationChangedReceiver); mWindowManager.removeView(mContainer); - } - - mIsZoomRingVisible = visible; - if (mCallback != null) { - mCallback.onVisibilityChanged(visible); + if (mCallback != null) { + mCallback.onVisibilityChanged(false); + } } + + } + + /** + * TODO: docs + * + * Notes: + * - Touch dispatching is different. Only direct children who are clickable are eligble for touch events. + * - Please ensure you set your View to INVISIBLE not GONE when hiding it. + * + * @return + */ + public FrameLayout getContainer() { + return mContainer; + } + + public int getZoomRingId() { + return mZoomRing.getId(); } private void dismissZoomRingDelayed(int delay) { @@ -484,77 +554,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mOwnerViewBounds, mTempRect); mCenteredContainerX = mTempRect.left; mCenteredContainerY = mTempRect.top; - } - // MOVE ALL THIS TO GESTURE DETECTOR -// public boolean onTouch(View v, MotionEvent event) { -// int action = event.getAction(); -// -// if (mListenForInvocation) { -// switch (mTouchMode) { -// case TOUCH_MODE_IDLE: { -// if (action == MotionEvent.ACTION_DOWN) { -// setFirstTap(event); -// } -// break; -// } -// -// case TOUCH_MODE_WAITING_FOR_SECOND_TAP: { -// switch (action) { -// case MotionEvent.ACTION_DOWN: -// if (isSecondTapWithinSlop(event)) { -// handleDoubleTapEvent(event); -// } else { -// setFirstTap(event); -// } -// break; -// -// case MotionEvent.ACTION_MOVE: -// int deltaX = (int) event.getX() - mFirstTapX; -// if (deltaX < -SECOND_TAP_MOVE_SLOP || -// deltaX > SECOND_TAP_MOVE_SLOP) { -// mTouchMode = TOUCH_MODE_IDLE; -// } else { -// int deltaY = (int) event.getY() - mFirstTapY; -// if (deltaY < -SECOND_TAP_MOVE_SLOP || -// deltaY > SECOND_TAP_MOVE_SLOP) { -// mTouchMode = TOUCH_MODE_IDLE; -// } -// } -// break; -// } -// break; -// } -// -// case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: -// case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: { -// handleDoubleTapEvent(event); -// break; -// } -// } -// -// if (action == MotionEvent.ACTION_CANCEL) { -// mTouchMode = TOUCH_MODE_IDLE; -// } -// } -// -// return false; -// } -// -// private void setFirstTap(MotionEvent event) { -// mFirstTapTime = event.getEventTime(); -// mFirstTapX = (int) event.getX(); -// mFirstTapY = (int) event.getY(); -// mTouchMode = TOUCH_MODE_WAITING_FOR_SECOND_TAP; -// } -// -// private boolean isSecondTapWithinSlop(MotionEvent event) { -// return mFirstTapTime + SECOND_TAP_TIMEOUT > event.getEventTime() && -// Math.abs((int) event.getX() - mFirstTapX) < SECOND_TAP_SLOP && -// Math.abs((int) event.getY() - mFirstTapY) < SECOND_TAP_SLOP; -// } - /** * Centers the point (in owner view's coordinates). */ @@ -575,16 +576,28 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingSetMovableHintVisible(boolean visible) { setPanningArrowsVisible(visible); } + + public void onUserInteractionStarted() { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + } + + public void onUserInteractionStopped() { + dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + } public void onZoomRingMovingStarted() { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); mScroller.abortAnimation(); + mPanningEnabledForThisInteraction = false; + mTouchingEdgeStartTime = 0; + if (mCallback != null) { + mCallback.onBeginPan(); + } } private void setPanningArrowsVisible(boolean visible) { mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation : mPanningArrowsExitAnimation); - mPanningArrows.setVisibility(visible ? View.VISIBLE : View.GONE); + mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } public boolean onZoomRingMoved(int deltaX, int deltaY) { @@ -611,37 +624,73 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mWindowManager.updateViewLayout(mContainer, lp); // Check for pan + boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; if (leftGap < MAX_PAN_GAP) { - mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); + if (shouldPan(leftGap)) { + mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); + } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); if (rightGap < MAX_PAN_GAP) { - mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); + if (shouldPan(rightGap)) { + mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); + } } else { mPanner.setHorizontalStrength(0); + horizontalPanning = false; } } int topGap = newZoomRingY - ownerBounds.top; if (topGap < MAX_PAN_GAP) { - mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); + if (shouldPan(topGap)) { + mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); + } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); if (bottomGap < MAX_PAN_GAP) { - mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); + if (shouldPan(bottomGap)) { + mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); + } } else { mPanner.setVerticalStrength(0); + if (!horizontalPanning) { + // Neither are panning, reset any timer to start pan mode + mTouchingEdgeStartTime = 0; + } } } return true; } + private boolean shouldPan(int gap) { + if (mPanningEnabledForThisInteraction) return true; + + if (gap < MAX_INITIATE_PAN_GAP) { + long time = SystemClock.elapsedRealtime(); + if (mTouchingEdgeStartTime != 0 && + mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) { + mPanningEnabledForThisInteraction = true; + return true; + } else if (mTouchingEdgeStartTime == 0) { + mTouchingEdgeStartTime = time; + } else { + } + } else { + // Moved away from the initiate pan gap, so reset the timer + mTouchingEdgeStartTime = 0; + } + return false; + } + public void onZoomRingMovingStopped() { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); mPanner.stop(); - setPanningArrowsVisible(false); + setPanningArrowsVisible(false); + if (mCallback != null) { + mCallback.onEndPan(); + } } private int getStrengthFromGap(int gap) { @@ -649,10 +698,9 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; } - public void onZoomRingThumbDraggingStarted(int startAngle) { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { - mCallback.onBeginDrag((float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER); + mCallback.onBeginDrag(); } } @@ -674,25 +722,122 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return false; } - public void onZoomRingThumbDraggingStopped(int endAngle) { - dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + public void onZoomRingThumbDraggingStopped() { if (mCallback != null) { - mCallback.onEndDrag((float) endAngle / ZoomRing.RADIAN_INT_MULTIPLIER); + mCallback.onEndDrag(); } } - public void onZoomRingDismissed() { - dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + public void onZoomRingDismissed(boolean dismissImmediately) { + if (dismissImmediately) { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + setVisible(false); + } else { + dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + } } - + + public void onRingDown(int tickAngle, int touchAngle) { + } + public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { - // If the user touches outside of the zoom ring, dismiss the zoom ring - dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + if (sTutorialDialog != null && sTutorialDialog.isShowing() && + SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) { + finishZoomTutorial(); + } + + int action = event.getAction(); + + if (mReleaseTouchListenerOnUp) { + // The ring was dismissed but we need to throw away all events until the up + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mOwnerView.setOnTouchListener(null); + mReleaseTouchListenerOnUp = false; + } + + // Eat this event return true; } - return false; + View targetView = mTouchTargetView; + + switch (action) { + case MotionEvent.ACTION_DOWN: + targetView = mTouchTargetView = + getViewForTouch((int) event.getRawX(), (int) event.getRawY()); + if (targetView != null) { + targetView.getLocationInWindow(mTouchTargetLocationInWindow); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchTargetView = null; + break; + } + + if (targetView != null) { + // The upperleft corner of the target view in raw coordinates + int targetViewRawX = mContainerLayoutParams.x + mTouchTargetLocationInWindow[0]; + int targetViewRawY = mContainerLayoutParams.y + mTouchTargetLocationInWindow[1]; + + MotionEvent containerEvent = MotionEvent.obtain(event); + // Convert the motion event into the target view's coordinates (from + // owner view's coordinates) + containerEvent.offsetLocation(mOwnerViewBounds.left - targetViewRawX, + mOwnerViewBounds.top - targetViewRawY); + boolean retValue = targetView.dispatchTouchEvent(containerEvent); + containerEvent.recycle(); + return retValue; + + } else { + if (action == MotionEvent.ACTION_DOWN) { + dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); + } + + return false; + } + } + + /** + * Returns the View that should receive a touch at the given coordinates. + * + * @param rawX The raw X. + * @param rawY The raw Y. + * @return The view that should receive the touches, or null if there is not one. + */ + private View getViewForTouch(int rawX, int rawY) { + // Check to see if it is touching the ring + int containerCenterX = mContainerLayoutParams.x + mContainer.getWidth() / 2; + int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2; + int distanceFromCenterX = rawX - containerCenterX; + int distanceFromCenterY = rawY - containerCenterY; + int zoomRingRadius = mZoomRingWidth / 2 - ZOOM_RING_RADIUS_INSET; + if (distanceFromCenterX * distanceFromCenterX + + distanceFromCenterY * distanceFromCenterY <= + zoomRingRadius * zoomRingRadius) { + return mZoomRing; + } + + // Check to see if it is touching any other clickable View. + // Reverse order so the child drawn on top gets first dibs. + int containerCoordsX = rawX - mContainerLayoutParams.x; + int containerCoordsY = rawY - mContainerLayoutParams.y; + Rect frame = mTempRect; + for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { + View child = mContainer.getChildAt(i); + if (child == mZoomRing || child.getVisibility() != View.VISIBLE || + !child.isClickable()) { + continue; + } + + child.getHitRect(frame); + if (frame.contains(containerCoordsX, containerCoordsY)) { + return child; + } + } + + return null; } /** Steals key events from the owner view. */ @@ -707,6 +852,8 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case KeyEvent.KEYCODE_DPAD_DOWN: // Keep the zoom alive a little longer dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); + // They started zooming, hide the thumb arrows + mZoomRing.setThumbArrowsVisible(false); if (mCallback != null && event.getAction() == KeyEvent.ACTION_DOWN) { mCallback.onSimpleZoom(keyCode == KeyEvent.KEYCODE_DPAD_UP); @@ -734,9 +881,14 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, ensureZoomRingIsCentered(); } + /* + * This is static so Activities can call this instead of the Views + * (Activities usually do not have a reference to the ZoomRingController + * instance.) + */ /** * Shows a "tutorial" (some text) to the user teaching her the new zoom - * invocation method. + * invocation method. Must call from the main thread. * <p> * It checks the global system setting to ensure this has not been seen * before. Furthermore, if the application does not have privilege to write @@ -757,20 +909,45 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, return; } + if (sTutorialDialog != null && sTutorialDialog.isShowing()) { + sTutorialDialog.dismiss(); + } + + sTutorialDialog = new AlertDialog.Builder(context) + .setMessage( + com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short) + .setIcon(0) + .create(); + + Window window = sTutorialDialog.getWindow(); + window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + + sTutorialDialog.show(); + sTutorialShowTime = SystemClock.elapsedRealtime(); + } + + public void finishZoomTutorial() { + if (sTutorialDialog == null) return; + + sTutorialDialog.dismiss(); + sTutorialDialog = null; + + // Record that they have seen the tutorial try { - Settings.System.putInt(cr, SETTING_NAME_SHOWN_TOAST, 1); + Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); } catch (SecurityException e) { /* * The app does not have permission to clear this global flag, make * sure the user does not see the message when he comes back to this * same app at least. */ + SharedPreferences sp = mContext.getSharedPreferences("_zoom", Context.MODE_PRIVATE); sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); } - - Toast.makeText(context, - com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short, - Toast.LENGTH_LONG).show(); } private class Panner implements Runnable { @@ -861,12 +1038,14 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } public interface OnZoomListener { - void onBeginDrag(float startAngle); + void onBeginDrag(); boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle); - void onEndDrag(float endAngle); + void onEndDrag(); void onSimpleZoom(boolean deltaZoomLevel); + void onBeginPan(); boolean onPan(int deltaX, int deltaY); + void onEndPan(); void onCenter(int x, int y); void onVisibilityChanged(boolean visible); } |