From 3001a035439d8134a7d70d796376d1dfbff3cdcd Mon Sep 17 00:00:00 2001
From: The Android Open Source Project 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
- * application's overall lifecycle.
Topics covered here: *
AndroidManifest.xml
) will be able to send an
* Intent to the receiver.
*
- * See the Security Model + *
See the Security and Permissions * document for more information on permissions and security in general. * * diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 226c5ab77ebc..3a64ceebc4fc 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}. * - *
See this page for more information on - * content providers.
+ *For more information, read Content + * Providers.
* *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 - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. */ 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 - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. */ 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 - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. *
* Example client call:
*
// Request a specific record. @@ -330,9 +330,9 @@ public abstract class ContentProvider implements ComponentCallbacks { *vnd.android.cursor.item
for a single record, * orvnd.android.cursor.dir/
for multiple items. * This method can be called from multiple - * threads, as described in the - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * * @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 - * Threading section of the - * Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * @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 - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * * @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 - * Threading section of the - * Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * *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 - * Threading section of the - * Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * * @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 - * Threading section of the - * Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * *
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 - * Threading section of - * the Application Model overview. + * threads, as described in + * Application Fundamentals: + * Processes and Threads. * * @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 c4d3f9d2ecee..c1c3b499b008 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 - * Application Model - * documentation for more details on tasks. + * the same order. See + * Application Fundamentals: + * Activities and Tasks for more details on tasks. * *
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 { *
This flag is ignored if * {@link #FLAG_ACTIVITY_NEW_TASK} is not set. * - *
See the - * Application Model - * documentation for more details on tasks. + *
See Application Fundamentals: + * Activities and Tasks 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. * - *
See the - * Application Model - * documentation for more details on tasks. + *
See Application Fundamentals: + * Activities and Tasks 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()}. * - *
See the Application Model - * documentation for important information on how some of these options impact + *
See the Application Fundamentals: + * Activities and Tasks 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 7b3e8cfae7b1..dd5360f651b4 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.
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 Resources + using resources, see the document on Resources and Internationalization. For a reference on the supported Android resource types, - see Available Resource Types.
+ see Available Resource Types.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.
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 Resources.
+ href="{@docRoot}guide/topics/resources/resources-i18n.html">Resources and + Internationalization.Using System Resources
diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 994afc8381ff..d9326f211b5b 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 e08f1d154dc2..4ae8b0884708 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 2ff72947879f..10f3806d0ec9 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 d9f10a96aac0..52f820928900 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 c8841201f11b..4be1fc798f88 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 228acbee97e0..6a560ce0150b 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 b8bd10dc44de..886e68829e1d 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 017b14da98a0..6f9d6c674d01 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"); + } Mapwakelocks = 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 11becc4869c7..e56b55d78cd5 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 e48f1523a6be..5486920249cf 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 02ab1da35fda..14c0054b868b 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 5767c65bb757..688e19a83c76 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 c6a7b407101c..4a784c8bd210 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 7c15045dbfb4..86d5a1e73bd3 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 b5e409027ce9..187ec2cf3a0b 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 160a0034a5dd..fab77a9b8a0a 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 5bf9b205a6c6..daa99c250bfa 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 2eebc0deb80e..efb88a0eb993 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 610cfd4a461a..9ab3b53169e3 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 a4726894c0cd..679c683cfeea 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 000000000000..cddec11a3563 --- /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 974c2e83565a..ce8074e2ad6f 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 similarity index 53% rename from core/java/android/os/HandlerInterface.java rename to core/java/android/view/RemotableViewMethod.java index 62dc273e32c2..4318290affc4 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 1d5e7cd49ca5..5ed3a7ea6471 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 7153ea1985c8..2f7b0d1a0068 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 c75866245215..70cc2a9c3390 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 ccfa6bf97fcf..db8829f9bc02 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 05f5fa2a1532..47b52e4a9ad7 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 mOnPreDrawListeners; private ArrayList mOnTouchModeChangeListeners; private ArrayList mOnComputeInternalInsetsListeners; + private ArrayList 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 (); + } + + 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 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 406af3e3d384..b87cc42f0e05 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 000000000000..4aa3f7aa783b --- /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 c96b3e5e9194..b9c8ec35637f 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 9509b15317eb..6fbc174bab49 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 13173f656397..530127d1ecee 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 - * text. + * @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 - * text. + * @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 fe1416680620..91fa2115d272 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 1dd37be06493..451af6d047cf 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 36949695b50a..dfae17da6da1 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 9af30c5b740c..c2620a512e4f 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 bdbf38a70369..4d9a8fbafda5 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 b979032f55de..45113ab000b7 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 30f855f0c892..000000000000 --- 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. - * 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. - *
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. - *
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. - *
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
+ *requestHeaders = - new HashMap (); - /** Response headers, as a lowercase key -> [ unmodified key, value ] map. */ - private Map 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 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 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 . - Map cacheRequestHeaders = new HashMap (); - Iterator > it = - requestHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry 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 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 > it = - responseHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry 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 (); - /* 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 288240ea8e2a..2a5cbe99e15c 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 c012e25cf0c5..f362e2202c5e 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 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 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 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 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 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 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 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 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 implements Te break; } - if (okToSend) { + if (okToSend && acceptFilter()) { createTextFilter(true); KeyEvent forwardEvent = event; @@ -2873,6 +2917,7 @@ public abstract class AbsListView extends AdapterView 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 7086ae2510bb..369221e8789a 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 b4ed3bad9a5a..8aafee2118f9 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 4ae322e1fb01..94d1bd175c7a 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 85a733916731..a9822f8bc2ce 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 9c7f600779f0..4e5989cb69b2 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 dada1051632a..4a5cea1cb028 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; + /** * 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
+ *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; + /** * Create a new empty, non focusable popup window of dimension (0,0).
* @@ -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.If the view later scrolls to move
* * @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); + } + + /** + *anchor
to a different + * location, the popup will be moved correspondingly.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()}.
+ * + * @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()}.If the view later scrolls to move
* * @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; } + WeakReferenceanchor
to a different + * location, the popup will be moved correspondingly.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 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 (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 434e9f3c3f91..dd2570afec7b 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 9ded52be3c51..ba63ec356b1a 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 25afee83a548..e000d2e22781 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 error is null
, 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 e20bfdf0f52f..8a7946b951b1 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 be3b1fbd527f..22881b3bab49 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 eb287670c7a1..31074b640269 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. ** 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); } diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index eb232c7ad03e..b502a6c13977 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -19,13 +19,11 @@ package com.android.internal.app; import com.android.internal.os.BatteryStatsImpl; interface IBatteryStats { - BatteryStatsImpl getStatistics(); + byte[] getStatistics(); void noteStartWakelock(int uid, String name, int type); void noteStopWakelock(int uid, String name, int type); - void noteStartSensor(int uid, String name, int sensor); - void noteStopSensor(int uid, String name, int sensor); - void noteRequestGpsOn(int uid); - void noteRequestGpsOff(int uid); + void noteStartSensor(int uid, int sensor); + void noteStopSensor(int uid, int sensor); void noteStartGps(int uid); void noteStopGps(int uid); void noteScreenOn(); diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index cbb65dc43679..558a122c7395 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -31,7 +31,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; /** @@ -39,7 +38,7 @@ import java.util.Map; * battery life. All times are represented in microseconds except where indicated * otherwise. */ -public final class BatteryStatsImpl extends BatteryStats implements Parcelable { +public final class BatteryStatsImpl extends BatteryStats { private static final String TAG = "BatteryStatsImpl"; @@ -47,7 +46,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 15; + private static final int VERSION = 23; private final File mFile; private final File mBackupFile; @@ -65,8 +64,13 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { final ArrayList
mPartialTimers = new ArrayList (); final ArrayList mFullTimers = new ArrayList (); final ArrayList mWindowTimers = new ArrayList (); - final ArrayList mSensorTimers = new ArrayList (); + final SparseArray > mSensorTimers + = new SparseArray >(); + // These are the objects that will want to do something when the device + // is unplugged from power. + final ArrayList mUnpluggables = new ArrayList (); + int mStartCount; long mBatteryUptime; @@ -90,6 +94,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { * in to power. */ boolean mOnBattery; + boolean mOnBatteryInternal; long mTrackBatteryPastUptime; long mTrackBatteryUptimeStart; long mTrackBatteryPastRealtime; @@ -98,8 +103,6 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { long mUnpluggedBatteryUptime; long mUnpluggedBatteryRealtime; - HashSet mGpsRequesters = new HashSet (); - long mLastWriteTime = 0; // Milliseconds // For debugging @@ -107,87 +110,100 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mFile = mBackupFile = null; } + public static interface Unpluggable { + void unplug(long batteryUptime, long batteryRealtime); + void plug(long batteryUptime, long batteryRealtime); + } + /** * State for keeping track of timing information. */ - public static final class Timer extends BatteryStats.Timer { - ArrayList mTimerPool; + public static final class Timer extends BatteryStats.Timer implements Unpluggable { + final int mType; + final ArrayList mTimerPool; - int mType; int mNesting; int mCount; int mLoadedCount; int mLastCount; + int mUnpluggedCount; - // Times are in microseconds for better accuracy when dividing by the lock count + // Times are in microseconds for better accuracy when dividing by the + // lock count, and are in "battery realtime" units. - long mTotalTime; // Add mUnpluggedTotalTime to get true value + long mTotalTime; long mLoadedTotalTime; long mLastTotalTime; - long mStartTime; long mUpdateTime; /** * The value of mTotalTime when unplug() was last called, initially 0. */ - long mTotalTimeAtLastUnplug; + long mUnpluggedTotalTime; - /** Constructor used for unmarshalling only. */ - Timer() {} + Timer(int type, ArrayList timerPool, + ArrayList unpluggables, Parcel in) { + mType = type; + mTimerPool = timerPool; + mCount = in.readInt(); + mLoadedCount = in.readInt(); + mLastCount = in.readInt(); + mUnpluggedCount = in.readInt(); + mTotalTime = in.readLong(); + mLoadedTotalTime = in.readLong(); + mLastTotalTime = in.readLong(); + mUpdateTime = in.readLong(); + mUnpluggedTotalTime = in.readLong(); + unpluggables.add(this); + } - Timer(int type, ArrayList timerPool) { + Timer(int type, ArrayList timerPool, + ArrayList unpluggables) { mType = type; mTimerPool = timerPool; + unpluggables.add(this); } - public void writeToParcel(Parcel out) { - out.writeInt(mType); - out.writeInt(mNesting); + public void writeToParcel(Parcel out, long batteryRealtime) { out.writeInt(mCount); out.writeInt(mLoadedCount); out.writeInt(mLastCount); - out.writeLong(mTotalTime); + out.writeInt(mUnpluggedCount); + out.writeLong(computeRunTimeLocked(batteryRealtime)); out.writeLong(mLoadedTotalTime); out.writeLong(mLastTotalTime); - out.writeLong(mStartTime); out.writeLong(mUpdateTime); - out.writeLong(mTotalTimeAtLastUnplug); + out.writeLong(mUnpluggedTotalTime); } - public void readFromParcel(Parcel in) { - mType = in.readInt(); - mNesting = in.readInt(); - mCount = in.readInt(); - mLoadedCount = in.readInt(); - mLastCount = in.readInt(); - mTotalTime = in.readLong(); - mLoadedTotalTime = in.readLong(); - mLastTotalTime = in.readLong(); - mStartTime = in.readLong(); - mUpdateTime = in.readLong(); - mTotalTimeAtLastUnplug = in.readLong(); - } - - private void unplug() { - mTotalTimeAtLastUnplug += mTotalTime; - mTotalTime = 0; + public void unplug(long batteryUptime, long batteryRealtime) { + mUnpluggedTotalTime = computeRunTimeLocked(batteryRealtime); + mUnpluggedCount = mCount; } + public void plug(long batteryUptime, long batteryRealtime) { + if (mNesting > 0) { + mTotalTime = computeRunTimeLocked(batteryRealtime); + mUpdateTime = batteryRealtime; + } + } + /** * Writes a possibly null Timer to a Parcel. * * @param out the Parcel to be written to. * @param timer a Timer, or null. */ - public static void writeTimerToParcel(Parcel out, Timer timer) { + public static void writeTimerToParcel(Parcel out, Timer timer, + long batteryRealtime) { if (timer == null) { out.writeInt(0); // indicates null return; } out.writeInt(1); // indicates non-null - timer.writeToParcel(out); + timer.writeToParcel(out, batteryRealtime); } @Override @@ -197,10 +213,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mLastTotalTime; } else { val = computeRunTimeLocked(now); - if (which != STATS_UNPLUGGED) { - val += mTotalTimeAtLastUnplug; - } - if ((which == STATS_CURRENT) || (which == STATS_UNPLUGGED)) { + if (which == STATS_UNPLUGGED) { + val -= mUnpluggedTotalTime; + } else if (which != STATS_TOTAL) { val -= mLoadedTotalTime; } } @@ -215,7 +230,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mLastCount; } else { val = mCount; - if ((which == STATS_CURRENT) || (which == STATS_UNPLUGGED)) { + if (which == STATS_UNPLUGGED) { + val -= mUnpluggedCount; + } else if (which != STATS_TOTAL) { val -= mLoadedCount; } } @@ -223,12 +240,23 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { return val; } + public void logState() { + Log.i("foo", "mNesting=" + mNesting + " mCount=" + mCount + + " mLoadedCount=" + mLoadedCount + " mLastCount=" + mLastCount + + " mUnpluggedCount=" + mUnpluggedCount); + Log.i("foo", "mTotalTime=" + mTotalTime + + " mLoadedTotalTime=" + mLoadedTotalTime); + Log.i("foo", "mLastTotalTime=" + mLastTotalTime + + " mUpdateTime=" + mUpdateTime); + } + void startRunningLocked(BatteryStatsImpl stats) { if (mNesting++ == 0) { - mStartTime = mUpdateTime = - stats.getBatteryUptimeLocked(SystemClock.elapsedRealtime() * 1000); - // Accumulate time to all other active counters with the current value of mCount - refreshTimersLocked(stats); + mUpdateTime = stats.getBatteryRealtimeLocked( + SystemClock.elapsedRealtime() * 1000); + // Accumulate time to all currently active timers before adding + // this new one to the pool. + refreshTimersLocked(stats, mTimerPool); // Add this timer to the active pool mTimerPool.add(this); // Increment the count @@ -242,38 +270,34 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { return; } if (--mNesting == 0) { - // Accumulate time to all active counters with the current value of mCount - refreshTimersLocked(stats); + // Accumulate time to all active counters, scaled by the total + // active in the pool, before taking this one out of the pool. + refreshTimersLocked(stats, mTimerPool); // Remove this timer from the active pool mTimerPool.remove(this); - // Decrement the count - mCount--; } } // Update the total time for all other running Timers with the same type as this Timer // due to a change in timer count - private void refreshTimersLocked(BatteryStatsImpl stats) { - for (Timer t : mTimerPool) { - t.updateTimeLocked(stats); - } - } - - /** - * Update totalTime and reset updateTime - * @param stats - */ - private void updateTimeLocked(BatteryStatsImpl stats) { - long realtime = SystemClock.elapsedRealtime() * 1000; - long heldTime = stats.getBatteryUptimeLocked(realtime) - mUpdateTime; - if (heldTime > 0) { - mTotalTime += heldTime / mCount; + private static void refreshTimersLocked(final BatteryStatsImpl stats, + final ArrayList pool) { + final long realtime = SystemClock.elapsedRealtime() * 1000; + final long batteryRealtime = stats.getBatteryRealtimeLocked(realtime); + final int N = pool.size(); + for (int i=N-1; i>= 0; i--) { + final Timer t = pool.get(i); + long heldTime = batteryRealtime - t.mUpdateTime; + if (heldTime > 0) { + t.mTotalTime += heldTime / N; + } + t.mUpdateTime = batteryRealtime; } - mUpdateTime = stats.getBatteryUptimeLocked(realtime); } - private long computeRunTimeLocked(long curBatteryUptime) { - return mTotalTime + (mNesting > 0 ? (curBatteryUptime - mUpdateTime) / mCount : 0); + private long computeRunTimeLocked(long curBatteryRealtime) { + return mTotalTime + (mNesting > 0 + ? (curBatteryRealtime - mUpdateTime) / mTimerPool.size() : 0); } void writeSummaryFromParcelLocked(Parcel out, long curBatteryUptime) { @@ -295,53 +319,41 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - public void unplugTcpCounters() { - final int NU = mUidStats.size(); - for (int iu = 0; iu < NU; iu++) { + public void doUnplug(long batteryUptime, long batteryRealtime) { + for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { Uid u = mUidStats.valueAt(iu); - u.mTcpBytesReceivedAtLastUnplug = u.getTcpBytesReceived(STATS_TOTAL); - u.mTcpBytesSentAtLastUnplug = u.getTcpBytesSent(STATS_TOTAL); + u.mStartedTcpBytesReceived = NetStat.getUidRxBytes(u.mUid); + u.mStartedTcpBytesSent = NetStat.getUidTxBytes(u.mUid); + u.mTcpBytesReceivedAtLastUnplug = u.mCurrentTcpBytesReceived; + u.mTcpBytesSentAtLastUnplug = u.mCurrentTcpBytesSent; + } + for (int i = mUnpluggables.size() - 1; i >= 0; i--) { + mUnpluggables.get(i).unplug(batteryUptime, batteryRealtime); } } - public void unplugTimers() { - ArrayList timers; - - timers = mPartialTimers; - for (int i = timers.size() - 1; i >= 0; i--) { - timers.get(i).unplug(); - } - timers = mFullTimers; - for (int i = timers.size() - 1; i >= 0; i--) { - timers.get(i).unplug(); - } - timers = mWindowTimers; - for (int i = timers.size() - 1; i >= 0; i--) { - timers.get(i).unplug(); + public void doPlug(long batteryUptime, long batteryRealtime) { + for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { + Uid u = mUidStats.valueAt(iu); + if (u.mStartedTcpBytesReceived >= 0) { + u.mCurrentTcpBytesReceived = u.computeCurrentTcpBytesReceived(); + u.mStartedTcpBytesReceived = -1; + } + if (u.mStartedTcpBytesSent >= 0) { + u.mCurrentTcpBytesSent = u.computeCurrentTcpBytesSent(); + u.mStartedTcpBytesSent = -1; + } } - timers = mSensorTimers; - for (int i = timers.size() - 1; i >= 0; i--) { - timers.get(i).unplug(); + for (int i = mUnpluggables.size() - 1; i >= 0; i--) { + mUnpluggables.get(i).plug(batteryUptime, batteryRealtime); } } public void noteStartGps(int uid) { - mGpsRequesters.add(uid); mUidStats.get(uid).noteStartGps(); } public void noteStopGps(int uid) { - mGpsRequesters.remove(uid); - mUidStats.get(uid).noteStopGps(); - } - - public void noteRequestGpsOn(int uid) { - mGpsRequesters.add(uid); - mUidStats.get(uid).noteStartGps(); - } - - public void noteRequestGpsOff(int uid) { - mGpsRequesters.remove(uid); mUidStats.get(uid).noteStopGps(); } @@ -349,7 +361,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { * When the device screen or battery state changes, update the appropriate "screen on time" * counter. */ - private void updateScreenOnTime(boolean screenOn) { + private void updateScreenOnTimeLocked(boolean screenOn) { if (!mScreenOn) { Log.w(TAG, "updateScreenOnTime without mScreenOn, ignored"); return; @@ -366,17 +378,17 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - public void noteScreenOn() { + public void noteScreenOnLocked() { mScreenOn = true; mLastScreenOnTimeMillis = SystemClock.elapsedRealtime(); } - public void noteScreenOff() { + public void noteScreenOffLocked() { if (!mScreenOn) { Log.w(TAG, "noteScreenOff without mScreenOn, ignored"); return; } - updateScreenOnTime(false); + updateScreenOnTimeLocked(false); mScreenOn = false; } @@ -388,8 +400,11 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { return mPluggedScreenOnTimeMillis; } - @Override - public SparseArray extends BatteryStats.Uid> getUidStats() { + @Override public boolean getIsOnBattery() { + return mOnBattery; + } + + @Override public SparseArray extends BatteryStats.Uid> getUidStats() { return mUidStats; } @@ -401,11 +416,16 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { final int mUid; long mLoadedTcpBytesReceived; long mLoadedTcpBytesSent; + long mCurrentTcpBytesReceived; + long mCurrentTcpBytesSent; long mTcpBytesReceivedAtLastUnplug; long mTcpBytesSentAtLastUnplug; - private final byte[] mBuf = new byte[16]; - + // These are not saved/restored when parcelling, since we want + // to return from the parcel with a snapshot of the state. + long mStartedTcpBytesReceived = -1; + long mStartedTcpBytesSent = -1; + /** * The statistics we have collected for this uid's wake locks. */ @@ -455,51 +475,56 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } public long getTcpBytesReceived(int which) { - long current = NetStat.getUidRxBytes(mUid); - - if (which == STATS_CURRENT) { - return current; - } else if (which == STATS_LAST) { + if (which == STATS_LAST) { return mLoadedTcpBytesReceived; - } else if (which == STATS_UNPLUGGED) { - return current - mTcpBytesReceivedAtLastUnplug; - } else if (which == STATS_TOTAL) { - return mLoadedTcpBytesReceived + current; } else { - throw new IllegalArgumentException("which = " + which); + long current = computeCurrentTcpBytesReceived(); + if (which == STATS_UNPLUGGED) { + current -= mTcpBytesReceivedAtLastUnplug; + } else if (which == STATS_TOTAL) { + current += mLoadedTcpBytesReceived; + } + return current; } } + public long computeCurrentTcpBytesReceived() { + return mCurrentTcpBytesReceived + (mStartedTcpBytesReceived >= 0 + ? (NetStat.getUidRxBytes(mUid) - mStartedTcpBytesReceived) : 0); + } + public long getTcpBytesSent(int which) { - long current = NetStat.getUidTxBytes(mUid); - - if (which == STATS_CURRENT) { - return current; - } else if (which == STATS_LAST) { + if (which == STATS_LAST) { return mLoadedTcpBytesSent; - } else if (which == STATS_UNPLUGGED) { - return current - mTcpBytesSentAtLastUnplug; - } else if (which == STATS_TOTAL) { - return mLoadedTcpBytesSent + current; } else { - throw new IllegalArgumentException("which = " + which); + long current = computeCurrentTcpBytesSent(); + if (which == STATS_UNPLUGGED) { + current -= mTcpBytesSentAtLastUnplug; + } else if (which == STATS_TOTAL) { + current += mLoadedTcpBytesSent; + } + return current; } } - - void writeToParcelLocked(Parcel out) { + + public long computeCurrentTcpBytesSent() { + return mCurrentTcpBytesSent + (mStartedTcpBytesSent >= 0 + ? (NetStat.getUidTxBytes(mUid) - mStartedTcpBytesSent) : 0); + } + + void writeToParcelLocked(Parcel out, long batteryRealtime) { out.writeInt(mWakelockStats.size()); for (Map.Entry wakelockEntry : mWakelockStats.entrySet()) { out.writeString(wakelockEntry.getKey()); Uid.Wakelock wakelock = wakelockEntry.getValue(); - wakelock.writeToParcelLocked(out); + wakelock.writeToParcelLocked(out, batteryRealtime); } out.writeInt(mSensorStats.size()); for (Map.Entry sensorEntry : mSensorStats.entrySet()) { out.writeInt(sensorEntry.getKey()); - out.writeString(sensorEntry.getValue().getName()); Uid.Sensor sensor = sensorEntry.getValue(); - sensor.writeToParcelLocked(out); + sensor.writeToParcelLocked(out, batteryRealtime); } out.writeInt(mProcessStats.size()); @@ -518,17 +543,19 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeLong(mLoadedTcpBytesReceived); out.writeLong(mLoadedTcpBytesSent); + out.writeLong(computeCurrentTcpBytesReceived()); + out.writeLong(computeCurrentTcpBytesSent()); out.writeLong(mTcpBytesReceivedAtLastUnplug); out.writeLong(mTcpBytesSentAtLastUnplug); } - void readFromParcelLocked(Parcel in) { + void readFromParcelLocked(ArrayList unpluggables, Parcel in) { int numWakelocks = in.readInt(); mWakelockStats.clear(); for (int j = 0; j < numWakelocks; j++) { String wakelockName = in.readString(); Uid.Wakelock wakelock = new Wakelock(); - wakelock.readFromParcelLocked(in); + wakelock.readFromParcelLocked(unpluggables, in); mWakelockStats.put(wakelockName, wakelock); } @@ -536,9 +563,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mSensorStats.clear(); for (int k = 0; k < numSensors; k++) { int sensorNumber = in.readInt(); - String name = in.readString(); - Uid.Sensor sensor = new Sensor(name); - sensor.readFromParcelLocked(in); + Uid.Sensor sensor = new Sensor(sensorNumber); + sensor.readFromParcelLocked(mUnpluggables, in); mSensorStats.put(sensorNumber, sensor); } @@ -562,6 +588,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mLoadedTcpBytesReceived = in.readLong(); mLoadedTcpBytesSent = in.readLong(); + mCurrentTcpBytesReceived = in.readLong(); + mCurrentTcpBytesSent = in.readLong(); mTcpBytesReceivedAtLastUnplug = in.readLong(); mTcpBytesSentAtLastUnplug = in.readLong(); } @@ -573,17 +601,17 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { /** * How long (in ms) this uid has been keeping the device partially awake. */ - Timer wakeTimePartial; + Timer mTimerPartial; /** * How long (in ms) this uid has been keeping the device fully awake. */ - Timer wakeTimeFull; + Timer mTimerFull; /** * How long (in ms) this uid has had a window keeping the device awake. */ - Timer wakeTimeWindow; + Timer mTimerWindow; /** * Reads a possibly null Timer from a Parcel. The timer is associated with the @@ -592,103 +620,85 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { * @param in the Parcel to be read from. * return a new Timer, or null. */ - private Timer readTimerFromParcel(Parcel in) { + private Timer readTimerFromParcel(int type, ArrayList pool, + ArrayList unpluggables, Parcel in) { if (in.readInt() == 0) { return null; } - Timer timer = new Timer(); - timer.readFromParcel(in); - // Set the timer pool for the timer according to its type - switch (timer.mType) { - case WAKE_TYPE_PARTIAL: - timer.mTimerPool = mPartialTimers; - break; - case WAKE_TYPE_FULL: - timer.mTimerPool = mFullTimers; - break; - case WAKE_TYPE_WINDOW: - timer.mTimerPool = mWindowTimers; - break; - } - // If the timer is active, add it to the pool - if (timer.mNesting > 0) { - timer.mTimerPool.add(timer); - } - return timer; + return new Timer(type, pool, unpluggables, in); } - void readFromParcelLocked(Parcel in) { - wakeTimePartial = readTimerFromParcel(in); - wakeTimeFull = readTimerFromParcel(in); - wakeTimeWindow = readTimerFromParcel(in); + void readFromParcelLocked(ArrayList unpluggables, Parcel in) { + mTimerPartial = readTimerFromParcel(WAKE_TYPE_PARTIAL, + mPartialTimers, unpluggables, in); + mTimerFull = readTimerFromParcel(WAKE_TYPE_FULL, + mFullTimers, unpluggables, in); + mTimerWindow = readTimerFromParcel(WAKE_TYPE_WINDOW, + mWindowTimers, unpluggables, in); } - void writeToParcelLocked(Parcel out) { - Timer.writeTimerToParcel(out, wakeTimePartial); - Timer.writeTimerToParcel(out, wakeTimeFull); - Timer.writeTimerToParcel(out, wakeTimeWindow); + void writeToParcelLocked(Parcel out, long batteryRealtime) { + Timer.writeTimerToParcel(out, mTimerPartial, batteryRealtime); + Timer.writeTimerToParcel(out, mTimerFull, batteryRealtime); + Timer.writeTimerToParcel(out, mTimerWindow, batteryRealtime); } @Override public Timer getWakeTime(int type) { switch (type) { - case WAKE_TYPE_FULL: return wakeTimeFull; - case WAKE_TYPE_PARTIAL: return wakeTimePartial; - case WAKE_TYPE_WINDOW: return wakeTimeWindow; + case WAKE_TYPE_FULL: return mTimerFull; + case WAKE_TYPE_PARTIAL: return mTimerPartial; + case WAKE_TYPE_WINDOW: return mTimerWindow; default: throw new IllegalArgumentException("type = " + type); } } } public final class Sensor extends BatteryStats.Uid.Sensor { - static final int GPS = -10000; // Treat GPS as a sensor - final String mName; - Timer sensorTime; + final int mHandle; + Timer mTimer; - public Sensor(String name) { - mName = name; + public Sensor(int handle) { + mHandle = handle; } - private Timer readTimerFromParcel(Parcel in) { + private Timer readTimerFromParcel(ArrayList unpluggables, + Parcel in) { if (in.readInt() == 0) { return null; } - Timer timer = new Timer(); - timer.readFromParcel(in); - // Set the timer pool for the timer - timer.mTimerPool = mSensorTimers; - - // If the timer is active, add it to the pool - if (timer.mNesting > 0) { - timer.mTimerPool.add(timer); + ArrayList pool = mSensorTimers.get(mHandle); + if (pool == null) { + pool = new ArrayList (); + mSensorTimers.put(mHandle, pool); } - return timer; + return new Timer(0, pool, unpluggables, in); } - void readFromParcelLocked(Parcel in) { - sensorTime = readTimerFromParcel(in); + void readFromParcelLocked(ArrayList unpluggables, Parcel in) { + mTimer = readTimerFromParcel(unpluggables, in); } - void writeToParcelLocked(Parcel out) { - Timer.writeTimerToParcel(out, sensorTime); + void writeToParcelLocked(Parcel out, long batteryRealtime) { + Timer.writeTimerToParcel(out, mTimer, batteryRealtime); } @Override public Timer getSensorTime() { - return sensorTime; + return mTimer; } - public String getName() { - return mName; + public int getHandle() { + return mHandle; } } /** * The statistics associated with a particular process. */ - public final class Proc extends BatteryStats.Uid.Proc { + public final class Proc extends BatteryStats.Uid.Proc implements Unpluggable { /** * Total time (in 1/100 sec) spent executing in user code. */ @@ -734,6 +744,34 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { */ int mLastStarts; + /** + * The amount of user time when last unplugged. + */ + long mUnpluggedUserTime; + + /** + * The amount of system time when last unplugged. + */ + long mUnpluggedSystemTime; + + /** + * The number of times the process has started before unplugged. + */ + int mUnpluggedStarts; + + Proc() { + mUnpluggables.add(this); + } + + public void unplug(long batteryUptime, long batteryRealtime) { + mUnpluggedUserTime = mUserTime; + mUnpluggedSystemTime = mSystemTime; + mUnpluggedStarts = mStarts; + } + + public void plug(long batteryUptime, long batteryRealtime) { + } + void writeToParcelLocked(Parcel out) { out.writeLong(mUserTime); out.writeLong(mSystemTime); @@ -744,6 +782,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeLong(mLastUserTime); out.writeLong(mLastSystemTime); out.writeInt(mLastStarts); + out.writeLong(mUnpluggedUserTime); + out.writeLong(mUnpluggedSystemTime); + out.writeInt(mUnpluggedStarts); } void readFromParcelLocked(Parcel in) { @@ -756,6 +797,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mLastUserTime = in.readLong(); mLastSystemTime = in.readLong(); mLastStarts = in.readInt(); + mUnpluggedUserTime = in.readLong(); + mUnpluggedSystemTime = in.readLong(); + mUnpluggedStarts = in.readInt(); } public BatteryStatsImpl getBatteryStats() { @@ -780,6 +824,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mUserTime; if (which == STATS_CURRENT) { val -= mLoadedUserTime; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedUserTime; } } return val; @@ -794,6 +840,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mSystemTime; if (which == STATS_CURRENT) { val -= mLoadedSystemTime; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedSystemTime; } } return val; @@ -808,6 +856,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mStarts; if (which == STATS_CURRENT) { val -= mLoadedStarts; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedStarts; } } return val; @@ -817,7 +867,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { /** * The statistics associated with a particular package. */ - public final class Pkg extends BatteryStats.Uid.Pkg { + public final class Pkg extends BatteryStats.Uid.Pkg implements Unpluggable { /** * Number of times this package has done something that could wake up the * device from sleep. @@ -837,14 +887,32 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { int mLastWakeups; /** + * Number of things that could wake up the device as of the + * last run. + */ + int mUnpluggedWakeups; + + /** * The statics we have collected for this package's services. */ final HashMap mServiceStats = new HashMap (); + Pkg() { + mUnpluggables.add(this); + } + + public void unplug(long batteryUptime, long batteryRealtime) { + mUnpluggedWakeups = mWakeups; + } + + public void plug(long batteryUptime, long batteryRealtime) { + } + void readFromParcelLocked(Parcel in) { mWakeups = in.readInt(); mLoadedWakeups = in.readInt(); mLastWakeups = in.readInt(); + mUnpluggedWakeups = in.readInt(); int numServs = in.readInt(); mServiceStats.clear(); @@ -861,6 +929,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeInt(mWakeups); out.writeInt(mLoadedWakeups); out.writeInt(mLastWakeups); + out.writeInt(mUnpluggedWakeups); out.writeInt(mServiceStats.size()); for (Map.Entry servEntry : mServiceStats.entrySet()) { @@ -885,6 +954,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mWakeups; if (which == STATS_CURRENT) { val -= mLoadedWakeups; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedWakeups; } } @@ -894,9 +965,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { /** * The statistics associated with a particular service. */ - public final class Serv extends BatteryStats.Uid.Pkg.Serv { + public final class Serv extends BatteryStats.Uid.Pkg.Serv implements Unpluggable { /** - * Total time (ms) the service has been left started. + * Total time (ms in battery uptime) the service has been left started. */ long mStartTime; @@ -917,13 +988,13 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { int mStarts; /** - * Total time (ms) the service has been left launched. + * Total time (ms in battery uptime) the service has been left launched. */ long mLaunchedTime; /** * If service has been launched and not yet exited, this is - * when it was launched. + * when it was launched (ms in battery uptime). */ long mLaunchedSince; @@ -938,7 +1009,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { int mLaunches; /** - * The amount of time spent started loaded from a previous save. + * The amount of time spent started loaded from a previous save + * (ms in battery uptime). */ long mLoadedStartTime; @@ -953,7 +1025,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { int mLoadedLaunches; /** - * The amount of time spent started as of the last run. + * The amount of time spent started as of the last run (ms + * in battery uptime). */ long mLastStartTime; @@ -967,6 +1040,35 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { */ int mLastLaunches; + /** + * The amount of time spent started when last unplugged (ms + * in battery uptime). + */ + long mUnpluggedStartTime; + + /** + * The number of starts when last unplugged. + */ + int mUnpluggedStarts; + + /** + * The number of launches when last unplugged. + */ + int mUnpluggedLaunches; + + Serv() { + mUnpluggables.add(this); + } + + public void unplug(long batteryUptime, long batteryRealtime) { + mUnpluggedStartTime = getStartTimeToNowLocked(batteryUptime); + mUnpluggedStarts = mStarts; + mUnpluggedLaunches = mLaunches; + } + + public void plug(long batteryUptime, long batteryRealtime) { + } + void readFromParcelLocked(Parcel in) { mStartTime = in.readLong(); mRunningSince = in.readLong(); @@ -982,6 +1084,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mLastStartTime = in.readLong(); mLastStarts = in.readInt(); mLastLaunches = in.readInt(); + mUnpluggedStartTime = in.readLong(); + mUnpluggedStarts = in.readInt(); + mUnpluggedLaunches = in.readInt(); } void writeToParcelLocked(Parcel out) { @@ -999,6 +1104,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeLong(mLastStartTime); out.writeInt(mLastStarts); out.writeInt(mLastLaunches); + out.writeLong(mUnpluggedStartTime); + out.writeInt(mUnpluggedStarts); + out.writeInt(mUnpluggedLaunches); } long getLaunchTimeToNowLocked(long batteryUptime) { @@ -1065,6 +1173,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mLaunches; if (which == STATS_CURRENT) { val -= mLoadedLaunches; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedLaunches; } } @@ -1080,6 +1190,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = getStartTimeToNowLocked(now); if (which == STATS_CURRENT) { val -= mLoadedStartTime; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedStartTime; } } @@ -1095,6 +1207,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { val = mStarts; if (which == STATS_CURRENT) { val -= mLoadedStarts; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedStarts; } } @@ -1167,24 +1281,24 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { Timer t = null; switch (type) { case WAKE_TYPE_PARTIAL: - t = wl.wakeTimePartial; + t = wl.mTimerPartial; if (t == null) { - t = new Timer(WAKE_TYPE_PARTIAL, mPartialTimers); - wl.wakeTimePartial = t; + t = new Timer(WAKE_TYPE_PARTIAL, mPartialTimers, mUnpluggables); + wl.mTimerPartial = t; } return t; case WAKE_TYPE_FULL: - t = wl.wakeTimeFull; + t = wl.mTimerFull; if (t == null) { - t = new Timer(WAKE_TYPE_FULL, mFullTimers); - wl.wakeTimeFull = t; + t = new Timer(WAKE_TYPE_FULL, mFullTimers, mUnpluggables); + wl.mTimerFull = t; } return t; case WAKE_TYPE_WINDOW: - t = wl.wakeTimeWindow; + t = wl.mTimerWindow; if (t == null) { - t = new Timer(WAKE_TYPE_WINDOW, mWindowTimers); - wl.wakeTimeWindow = t; + t = new Timer(WAKE_TYPE_WINDOW, mWindowTimers, mUnpluggables); + wl.mTimerWindow = t; } return t; default: @@ -1192,21 +1306,26 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - public Timer getSensorTimerLocked(String name, int sensor, boolean create) { - Integer sId = Integer.valueOf(sensor); - Sensor se = mSensorStats.get(sId); + public Timer getSensorTimerLocked(int sensor, boolean create) { + Sensor se = mSensorStats.get(sensor); if (se == null) { if (!create) { return null; } - se = new Sensor(name); - mSensorStats.put(sId, se); + se = new Sensor(sensor); + mSensorStats.put(sensor, se); } - Timer t = se.sensorTime; - if (t == null) { - t = new Timer(BatteryStats.SENSOR, mSensorTimers); - se.sensorTime = t; + Timer t = se.mTimer; + if (t != null) { + return t; } + ArrayList timers = mSensorTimers.get(sensor); + if (timers == null) { + timers = new ArrayList (); + mSensorTimers.put(sensor, timers); + } + t = new Timer(BatteryStats.SENSOR, timers, mUnpluggables); + se.mTimer = t; return t; } @@ -1224,30 +1343,30 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - public void noteStartSensor(String name, int sensor) { - Timer t = getSensorTimerLocked(name, sensor, true); + public void noteStartSensor(int sensor) { + Timer t = getSensorTimerLocked(sensor, true); if (t != null) { t.startRunningLocked(BatteryStatsImpl.this); } } - public void noteStopSensor(String name, int sensor) { + public void noteStopSensor(int sensor) { // Don't create a timer if one doesn't already exist - Timer t = getSensorTimerLocked(name, sensor, false); + Timer t = getSensorTimerLocked(sensor, false); if (t != null) { t.stopRunningLocked(BatteryStatsImpl.this); } } public void noteStartGps() { - Timer t = getSensorTimerLocked("GPS", Sensor.GPS, true); + Timer t = getSensorTimerLocked(Sensor.GPS, true); if (t != null) { t.startRunningLocked(BatteryStatsImpl.this); } } public void noteStopGps() { - Timer t = getSensorTimerLocked("GPS", Sensor.GPS, false); + Timer t = getSensorTimerLocked(Sensor.GPS, false); if (t != null) { t.stopRunningLocked(BatteryStatsImpl.this); } @@ -1262,12 +1381,11 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mFile = new File(filename); mBackupFile = new File(filename + ".bak"); mStartCount++; - mOnBattery = true; + mOnBattery = mOnBatteryInternal = false; mTrackBatteryPastUptime = 0; mTrackBatteryPastRealtime = 0; mUptimeStart = mTrackBatteryUptimeStart = SystemClock.uptimeMillis() * 1000; mRealtimeStart = mTrackBatteryRealtimeStart = SystemClock.elapsedRealtime() * 1000; - mUnpluggedBatteryUptime = getBatteryUptimeLocked(mUptimeStart); mUnpluggedBatteryRealtime = getBatteryRealtimeLocked(mRealtimeStart); } @@ -1290,25 +1408,25 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { synchronized(this) { if (mOnBattery != onBattery) { if (mScreenOn) { - updateScreenOnTime(true); + updateScreenOnTimeLocked(true); } + mOnBattery = mOnBatteryInternal = onBattery; + long uptime = SystemClock.uptimeMillis() * 1000; long mSecRealtime = SystemClock.elapsedRealtime(); long realtime = mSecRealtime * 1000; if (onBattery) { - mTrackBatteryUptimeStart = getBatteryUptime(uptime); - mTrackBatteryRealtimeStart = getBatteryRealtime(realtime); - unplugTcpCounters(); - unplugTimers(); - - mUnpluggedBatteryUptime = getBatteryUptime(uptime); - mUnpluggedBatteryRealtime = getBatteryRealtime(realtime); + mTrackBatteryUptimeStart = uptime; + mTrackBatteryRealtimeStart = realtime; + mUnpluggedBatteryUptime = getBatteryUptimeLocked(uptime); + mUnpluggedBatteryRealtime = getBatteryRealtimeLocked(realtime); + doUnplug(mUnpluggedBatteryUptime, mUnpluggedBatteryRealtime); } else { mTrackBatteryPastUptime += uptime - mTrackBatteryUptimeStart; mTrackBatteryPastRealtime += realtime - mTrackBatteryRealtimeStart; + doPlug(mUnpluggedBatteryUptime, mUnpluggedBatteryRealtime); } - mOnBattery = onBattery; if ((mLastWriteTime + (60 * 1000)) < mSecRealtime) { if (mFile != null) { writeLocked(); @@ -1359,7 +1477,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { case STATS_CURRENT: return uptime; case STATS_UNPLUGGED: - return uptime - mUnpluggedBatteryUptime; + return getBatteryUptimeLocked(curTime) - mUnpluggedBatteryUptime; } return 0; } @@ -1381,7 +1499,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { long getBatteryUptimeLocked(long curTime) { long time = mTrackBatteryPastUptime; - if (mOnBattery) { + if (mOnBatteryInternal) { time += curTime - mTrackBatteryUptimeStart; } return time; @@ -1398,7 +1516,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { long getBatteryRealtimeLocked(long curTime) { long time = mTrackBatteryPastRealtime; - if (mOnBattery) { + if (mOnBatteryInternal) { time += curTime - mTrackBatteryRealtimeStart; } return time; @@ -1554,8 +1672,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { private void readSummaryFromParcel(Parcel in) { final int version = in.readInt(); if (version != VERSION) { - Log.e("BatteryStats", "readFromParcel: version got " + version - + ", expected " + VERSION); + Log.w("BatteryStats", "readFromParcel: version got " + version + + ", expected " + VERSION + "; erasing old stats"); return; } @@ -1570,11 +1688,9 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mLastRealtime = in.readLong(); mStartCount++; - if (version >= 14) { - mBatteryScreenOnTimeMillis = in.readLong(); - mPluggedScreenOnTimeMillis = in.readLong(); - mScreenOn = false; - } + mBatteryScreenOnTimeMillis = in.readLong(); + mPluggedScreenOnTimeMillis = in.readLong(); + mScreenOn = false; final int NU = in.readInt(); for (int iu = 0; iu < NU; iu++) { @@ -1596,22 +1712,16 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - if (version >= 12) { - int NSE = in.readInt(); - for (int is = 0; is < NSE; is++) { - int seNumber = in.readInt(); - String seName = " "; - if (version >= 14) { - seName = in.readString(); - } - if (in.readInt() != 0) { - u.getSensorTimerLocked(seName, seNumber, true) - .readSummaryFromParcelLocked(in); - } + int NP = in.readInt(); + for (int is = 0; is < NP; is++) { + int seNumber = in.readInt(); + if (in.readInt() != 0) { + u.getSensorTimerLocked(seNumber, true) + .readSummaryFromParcelLocked(in); } } - int NP = in.readInt(); + NP = in.readInt(); for (int ip = 0; ip < NP; ip++) { String procName = in.readString(); Uid.Proc p = u.getProcessStatsLocked(procName); @@ -1642,10 +1752,8 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { } } - if (version >= 14) { - u.mLoadedTcpBytesReceived = in.readLong(); - u.mLoadedTcpBytesSent = in.readLong(); - } + u.mLoadedTcpBytesReceived = in.readLong(); + u.mLoadedTcpBytesSent = in.readLong(); } } @@ -1685,24 +1793,24 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeInt(NW); if (NW > 0) { for (Map.Entry ent - : u.mWakelockStats.entrySet()) { + : u.mWakelockStats.entrySet()) { out.writeString(ent.getKey()); Uid.Wakelock wl = ent.getValue(); - if (wl.wakeTimeFull != null) { + if (wl.mTimerFull != null) { out.writeInt(1); - wl.wakeTimeFull.writeSummaryFromParcelLocked(out, NOW); + wl.mTimerFull.writeSummaryFromParcelLocked(out, NOW); } else { out.writeInt(0); } - if (wl.wakeTimePartial != null) { + if (wl.mTimerPartial != null) { out.writeInt(1); - wl.wakeTimePartial.writeSummaryFromParcelLocked(out, NOW); + wl.mTimerPartial.writeSummaryFromParcelLocked(out, NOW); } else { out.writeInt(0); } - if (wl.wakeTimeWindow != null) { + if (wl.mTimerWindow != null) { out.writeInt(1); - wl.wakeTimeWindow.writeSummaryFromParcelLocked(out, NOW); + wl.mTimerWindow.writeSummaryFromParcelLocked(out, NOW); } else { out.writeInt(0); } @@ -1713,13 +1821,12 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeInt(NSE); if (NSE > 0) { for (Map.Entry ent - : u.mSensorStats.entrySet()) { + : u.mSensorStats.entrySet()) { out.writeInt(ent.getKey()); Uid.Sensor se = ent.getValue(); - out.writeString(se.getName()); - if (se.sensorTime != null) { + if (se.mTimer != null) { out.writeInt(1); - se.sensorTime.writeSummaryFromParcelLocked(out, NOW); + se.mTimer.writeSummaryFromParcelLocked(out, NOW); } else { out.writeInt(0); } @@ -1800,6 +1907,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { mRealtimeStart = in.readLong(); mLastRealtime = in.readLong(); mOnBattery = in.readInt() != 0; + mOnBatteryInternal = false; // we are no longer really running. mTrackBatteryPastUptime = in.readLong(); mTrackBatteryUptimeStart = in.readLong(); mTrackBatteryPastRealtime = in.readLong(); @@ -1815,7 +1923,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { for (int i = 0; i < numUids; i++) { int uid = in.readInt(); Uid u = new Uid(uid); - u.readFromParcelLocked(in); + u.readFromParcelLocked(mUnpluggables, in); mUidStats.append(uid, u); } } @@ -1826,6 +1934,11 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { @SuppressWarnings("unused") void writeToParcelLocked(Parcel out, int flags) { + final long uSecUptime = SystemClock.uptimeMillis() * 1000; + final long uSecRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryUptime = getBatteryUptimeLocked(uSecUptime); + final long batteryRealtime = getBatteryRealtimeLocked(uSecRealtime); + out.writeInt(MAGIC); out.writeInt(mStartCount); out.writeLong(mBatteryUptime); @@ -1853,7 +1966,7 @@ public final class BatteryStatsImpl extends BatteryStats implements Parcelable { out.writeInt(mUidStats.keyAt(i)); Uid uid = mUidStats.valueAt(i); - uid.writeToParcelLocked(out); + uid.writeToParcelLocked(out, batteryRealtime); } } diff --git a/core/java/com/android/internal/os/HandlerThread.java b/core/java/com/android/internal/os/HandlerThread.java deleted file mode 100644 index 1de6bfd407b3..000000000000 --- a/core/java/com/android/internal/os/HandlerThread.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.os; - -import android.os.Handler; -import android.os.HandlerInterface; -import android.os.Looper; - -/** - * Handy class for starting a new thread containing a Handler - * @hide - * @deprecated - */ -public class HandlerThread extends Thread -{ - Runnable mSetup; - HandlerInterface mhi; - Handler mh; - Throwable mtr; - final Object mMonitor = new Object(); - - public - HandlerThread(HandlerInterface h, Runnable setup, String name) - { - super(name); - - mhi = h; - mSetup = setup; - - synchronized (mMonitor) { - start(); - while (mh == null && mtr == null) { - try { - mMonitor.wait(); - } catch (InterruptedException ex) { - } - } - } - - if (mtr != null) { - throw new RuntimeException("exception while starting", mtr); - } - } - - @Override - public void - run() - { - synchronized(mMonitor) { - try { - Looper.prepare(); - mh = new HandlerHelper (mhi); - - if (mSetup != null) { - mSetup.run(); - mSetup = null; - } - } catch (RuntimeException exc) { - mtr = exc; - } - - mMonitor.notify(); - } - - if (mtr == null) { - Looper.loop(); - } - } - - public Handler - getHandler() - { - return mh; - } - -} - diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index ac72a20e6483..4f98cee688e1 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -1,7 +1,5 @@ package com.android.internal.view; -import com.android.internal.view.IInputContext; - import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -324,7 +322,7 @@ public class IInputConnectionWrapper extends IInputContext.Stub { Log.w(TAG, "showStatusIcon on inactive InputConnection"); return; } - ic.reportFullscreenMode(msg.arg1 != 1); + ic.reportFullscreenMode(msg.arg1 == 1); return; } case DO_PERFORM_PRIVATE_COMMAND: { diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 648d944cc7b8..44cf0ed1fcd3 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -96,4 +96,21 @@ public class EditableInputConnection extends BaseInputConnection { mTextView.onPrivateIMECommand(action, data); return true; } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + if (mTextView == null) { + return super.commitText(text, newCursorPosition); + } + + CharSequence errorBefore = mTextView.getError(); + boolean success = super.commitText(text, newCursorPosition); + CharSequence errorAfter = mTextView.getError(); + + if (errorAfter != null && errorBefore == errorAfter) { + mTextView.setError(null, null); + } + + return success; + } } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index ed1cd58c7385..c8b3ad48ec45 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -77,11 +77,10 @@ public class LockPatternUtils { public static final int MIN_PATTERN_REGISTER_FAIL = 3; private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; + private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; private final ContentResolver mContentResolver; - private long mLockoutDeadline = 0; - private static String sLockPatternFilename; /** @@ -270,12 +269,14 @@ public class LockPatternUtils { } /** - * Store the lockout deadline, meaning the user can't attempt his/her unlock - * pattern until the deadline has passed. Does not persist across reboots. - * @param deadline The elapsed real time in millis in future. + * Set and store the lockout deadline, meaning the user can't attempt his/her unlock + * pattern until the deadline has passed. + * @return the chosen deadline. */ - public void setLockoutAttemptDeadline(long deadline) { - mLockoutDeadline = deadline; + public long setLockoutAttemptDeadline() { + final long deadline = SystemClock.elapsedRealtime() + FAILED_ATTEMPT_TIMEOUT_MS; + setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline); + return deadline; } /** @@ -284,7 +285,12 @@ public class LockPatternUtils { * enter a pattern. */ public long getLockoutAttemptDeadline() { - return (mLockoutDeadline <= SystemClock.elapsedRealtime()) ? 0 : mLockoutDeadline; + final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L); + final long now = SystemClock.elapsedRealtime(); + if (deadline < now || deadline > (now + FAILED_ATTEMPT_TIMEOUT_MS)) { + return 0L; + } + return deadline; } /** @@ -341,5 +347,13 @@ public class LockPatternUtils { enabled ? 1 : 0); } + private long getLong(String systemSettingKey, long def) { + return android.provider.Settings.System.getLong(mContentResolver, systemSettingKey, def); + } + + private void setLong(String systemSettingKey, long value) { + android.provider.Settings.System.putLong(mContentResolver, systemSettingKey, value); + } + } diff --git a/core/java/com/android/internal/widget/TextProgressBar.java b/core/java/com/android/internal/widget/TextProgressBar.java new file mode 100644 index 000000000000..d06886563472 --- /dev/null +++ b/core/java/com/android/internal/widget/TextProgressBar.java @@ -0,0 +1,177 @@ +/* + * 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 com.android.internal.widget; + +import android.content.Context; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Chronometer; +import android.widget.Chronometer.OnChronometerTickListener; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.RemoteViews.RemoteView; + +/** + * Container that links together a {@link ProgressBar} and {@link Chronometer} + * as children. It subscribes to {@link Chronometer#OnChronometerTickListener} + * and updates the {@link ProgressBar} based on a preset finishing time. + * + * This widget expects to contain two children with specific ids + * {@link android.R.id.progress} and {@link android.R.id.text1}. + *
+ * If the {@link Chronometer} {@link android.R.attr#layout_width} is + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, then the + * {@link android.R.attr#gravity} will be used to automatically move it with + * respect to the {@link ProgressBar} position. For example, if + * {@link android.view.Gravity#LEFT} then the {@link Chronometer} will be placed + * just ahead of the leading edge of the {@link ProgressBar} position. + */ +@RemoteView +public class TextProgressBar extends RelativeLayout implements OnChronometerTickListener { + public static final String TAG = "TextProgressBar"; + + static final int CHRONOMETER_ID = android.R.id.text1; + static final int PROGRESSBAR_ID = android.R.id.progress; + + Chronometer mChronometer = null; + ProgressBar mProgressBar = null; + + long mDurationBase = -1; + int mDuration = -1; + + boolean mChronometerFollow = false; + int mChronometerGravity = Gravity.NO_GRAVITY; + + public TextProgressBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public TextProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TextProgressBar(Context context) { + super(context); + } + + /** + * Catch any interesting children when they are added. + */ + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + + int childId = child.getId(); + if (childId == CHRONOMETER_ID && child instanceof Chronometer) { + mChronometer = (Chronometer) child; + mChronometer.setOnChronometerTickListener(this); + + // Check if Chronometer should move with with ProgressBar + mChronometerFollow = (params.width == ViewGroup.LayoutParams.WRAP_CONTENT); + mChronometerGravity = (mChronometer.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK); + + } else if (childId == PROGRESSBAR_ID && child instanceof ProgressBar) { + mProgressBar = (ProgressBar) child; + } + } + + /** + * Set the expected termination time of the running {@link Chronometer}. + * This value is used to adjust the {@link ProgressBar} against the elapsed + * time. + *
+ * Call this after adjusting the {@link Chronometer} base, if + * necessary. + * + * @param durationBase Use the {@link SystemClock#elapsedRealtime} time + * base. + */ + public void setDurationBase(long durationBase) { + mDurationBase = durationBase; + + if (mProgressBar == null || mChronometer == null) { + throw new RuntimeException("Expecting child ProgressBar with id " + + "'android.R.id.progress' and Chronometer id 'android.R.id.text1'"); + } + + // Update the ProgressBar maximum relative to Chronometer base + mDuration = (int) (durationBase - mChronometer.getBase()); + mProgressBar.setMax(mDuration); + + } + + /** + * Callback when {@link Chronometer} changes, indicating that we should + * update the {@link ProgressBar} and change the layout if necessary. + */ + public void onChronometerTick(Chronometer chronometer) { + if (mProgressBar == null) { + throw new RuntimeException( + "Expecting child ProgressBar with id 'android.R.id.progress'"); + } + + // Stop Chronometer if we're past duration + long now = SystemClock.elapsedRealtime(); + if (now >= mDurationBase) { + mChronometer.stop(); + } + + // Update the ProgressBar status + int remaining = (int) (mDurationBase - now); + mProgressBar.setProgress(mDuration - remaining); + + // Move the Chronometer if gravity is set correctly + if (mChronometerFollow) { + RelativeLayout.LayoutParams params; + + // Calculate estimate of ProgressBar leading edge position + params = (RelativeLayout.LayoutParams) mProgressBar.getLayoutParams(); + int contentWidth = mProgressBar.getWidth() - (params.leftMargin + params.rightMargin); + int leadingEdge = ((contentWidth * mProgressBar.getProgress()) / + mProgressBar.getMax()) + params.leftMargin; + + // Calculate any adjustment based on gravity + int adjustLeft = 0; + int textWidth = mChronometer.getWidth(); + if (mChronometerGravity == Gravity.RIGHT) { + adjustLeft = -textWidth; + } else if (mChronometerGravity == Gravity.CENTER_HORIZONTAL) { + adjustLeft = -(textWidth / 2); + } + + // Limit margin to keep text inside ProgressBar bounds + leadingEdge += adjustLeft; + int rightLimit = contentWidth - params.rightMargin - textWidth; + if (leadingEdge < params.leftMargin) { + leadingEdge = params.leftMargin; + } else if (leadingEdge > rightLimit) { + leadingEdge = rightLimit; + } + + params = (RelativeLayout.LayoutParams) mChronometer.getLayoutParams(); + params.leftMargin = leadingEdge; + + // Request layout to move Chronometer + mChronometer.requestLayout(); + + } + } +} diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 79965b94313c..2f1f9b8c6419 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -51,9 +51,9 @@ struct fields_t { static fields_t fields; static Mutex sLock; +static jclass sCameraClass; struct callback_cookie { - jclass camera_class; jobject camera_ref; }; @@ -87,7 +87,7 @@ static void err_callback(status_t err, void *cookie) } LOGV("err_callback: camera_ref=%x, cookie=%x", (int)c->camera_ref, (int)cookie); - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kErrorCallback, error, 0, NULL); } @@ -115,7 +115,6 @@ static void android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobj jniThrowException(env, "java/lang/Exception", NULL); return; } - cookie->camera_class = (jclass)env->NewGlobalRef(clazz); // We use a weak reference so the Camera object can be garbage collected. // The reference is only used as a proxy for callbacks. @@ -157,7 +156,6 @@ static void android_hardware_Camera_release(JNIEnv *env, jobject thiz) if (cookie) { env->DeleteGlobalRef(cookie->camera_ref); - env->DeleteGlobalRef(cookie->camera_class); delete cookie; env->SetIntField(thiz, fields.listener_context, 0); } @@ -207,7 +205,7 @@ static void preview_callback(const sp
& mem, void *cookie) obj = array; - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kPreviewCallback, arg1, arg2, obj); env->DeleteLocalRef(array); } @@ -270,13 +268,11 @@ static void autofocus_callback_impl(bool success, void *cookie) return; } callback_cookie *c = (callback_cookie *)cookie; - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kAutoFocusCallback, success, 0, NULL); } - - static void android_hardware_Camera_autoFocus(JNIEnv *env, jobject thiz) { sp c = get_native_camera(env, thiz); @@ -301,7 +297,7 @@ static void jpeg_callback(const sp & mem, void *cookie) jobject obj = NULL; if (mem == NULL) { - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kJpegCallback, arg1, arg2, NULL); return; } @@ -331,7 +327,7 @@ static void jpeg_callback(const sp & mem, void *cookie) obj = array; - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kJpegCallback, arg1, arg2, obj); env->DeleteLocalRef(array); } @@ -344,7 +340,7 @@ static void shutter_callback_impl(void *cookie) return; } callback_cookie *c = (callback_cookie *)cookie; - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kShutterCallback, 0, 0, NULL); } @@ -357,7 +353,7 @@ static void raw_callback(const sp & mem __attribute__((unused)), return; } callback_cookie *c = (callback_cookie *)cookie; - env->CallStaticVoidMethod(c->camera_class, fields.post_event, + env->CallStaticVoidMethod(sCameraClass, fields.post_event, c->camera_ref, kRawCallback, 0, 0, NULL); } @@ -524,6 +520,7 @@ int register_android_hardware_Camera(JNIEnv *env) return -1; jclass clazz = env->FindClass("android/hardware/Camera"); + sCameraClass = (jclass)env->NewGlobalRef(clazz); fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative", "(Ljava/lang/Object;IIILjava/lang/Object;)V"); if (fields.post_event == NULL) { diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp index 6ca821d2fca5..d9effeed19aa 100644 --- a/core/jni/android_media_AudioTrack.cpp +++ b/core/jni/android_media_AudioTrack.cpp @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#define LOG_NDEBUG 0 +//#define LOG_NDEBUG 0 #define LOG_TAG "AudioTrack-JNI" @@ -164,8 +164,8 @@ android_media_AudioTrack_native_setup(JNIEnv *env, jobject thiz, jobject weak_th jint streamType, jint sampleRateInHertz, jint nbChannels, jint audioFormat, jint buffSizeInBytes, jint memoryMode) { - //LOGV("sampleRate=%d, audioFormat(from Java)=%d, nbChannels=%d, buffSize=%d", - // sampleRateInHertz, audioFormat, nbChannels, buffSizeInBytes); + LOGV("sampleRate=%d, audioFormat(from Java)=%d, nbChannels=%d, buffSize=%d", + sampleRateInHertz, audioFormat, nbChannels, buffSizeInBytes); int afSampleRate; int afFrameCount; @@ -210,7 +210,20 @@ android_media_AudioTrack_native_setup(JNIEnv *env, jobject thiz, jobject weak_th LOGE("Error creating AudioTrack: unsupported audio format."); return AUDIOTRACK_ERROR_SETUP_INVALIDFORMAT; } - + + // for the moment 8bitPCM in MODE_STATIC is not supported natively in the AudioTrack C++ class + // so we declare everything as 16bitPCM, the 8->16bit conversion for MODE_STATIC will be handled + // in android_media_AudioTrack_native_write() + if ((audioFormat == javaAudioTrackFields.PCM8) + && (memoryMode == javaAudioTrackFields.MODE_STATIC)) { + LOGV("android_media_AudioTrack_native_setup(): requesting MODE_STATIC for 8bit \ + buff size of %dbytes, switching to 16bit, buff size of %dbytes", + buffSizeInBytes, 2*buffSizeInBytes); + audioFormat = javaAudioTrackFields.PCM16; + // we will need twice the memory to store the data + buffSizeInBytes *= 2; + } + // compute the frame count int bytesPerSample = audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1; int format = audioFormat == javaAudioTrackFields.PCM16 ? @@ -387,13 +400,13 @@ android_media_AudioTrack_set_volume(JNIEnv *env, jobject thiz, jfloat leftVol, j // ---------------------------------------------------------------------------- static void android_media_AudioTrack_native_finalize(JNIEnv *env, jobject thiz) { - LOGV("android_media_AudioTrack_native_finalize jobject: %x\n", (int)thiz); + //LOGV("android_media_AudioTrack_native_finalize jobject: %x\n", (int)thiz); // delete the AudioTrack object AudioTrack *lpTrack = (AudioTrack *)env->GetIntField( thiz, javaAudioTrackFields.nativeTrackInJavaObj); if (lpTrack) { - LOGV("deleting lpTrack: %x\n", (int)lpTrack); + //LOGV("deleting lpTrack: %x\n", (int)lpTrack); lpTrack->stop(); delete lpTrack; } @@ -402,7 +415,7 @@ static void android_media_AudioTrack_native_finalize(JNIEnv *env, jobject thiz) AudioTrackJniStorage* pJniStorage = (AudioTrackJniStorage *)env->GetIntField( thiz, javaAudioTrackFields.jniData); if (pJniStorage) { - LOGV("deleting pJniStorage: %x\n", (int)pJniStorage); + //LOGV("deleting pJniStorage: %x\n", (int)pJniStorage); delete pJniStorage; } } @@ -422,7 +435,8 @@ static void android_media_AudioTrack_native_release(JNIEnv *env, jobject thiz) // ---------------------------------------------------------------------------- static jint android_media_AudioTrack_native_write(JNIEnv *env, jobject thiz, jbyteArray javaAudioData, - jint offsetInBytes, jint sizeInBytes) { + jint offsetInBytes, jint sizeInBytes, + jint javaAudioFormat) { jbyte* cAudioData = NULL; AudioTrack *lpTrack = NULL; //LOGV("android_media_AudioTrack_native_write(offset=%d, sizeInBytes=%d) called", @@ -453,8 +467,22 @@ static jint android_media_AudioTrack_native_write(JNIEnv *env, jobject thiz, if (lpTrack->sharedBuffer() == 0) { written = lpTrack->write(cAudioData + offsetInBytes, sizeInBytes); } else { - memcpy(lpTrack->sharedBuffer()->pointer(), cAudioData + offsetInBytes, sizeInBytes); - written = sizeInBytes; + if (javaAudioFormat == javaAudioTrackFields.PCM16) { + memcpy(lpTrack->sharedBuffer()->pointer(), cAudioData + offsetInBytes, sizeInBytes); + written = sizeInBytes; + } else if (javaAudioFormat == javaAudioTrackFields.PCM8) { + // cAudioData contains 8bit data we need to expand to 16bit before copying + // to the shared memory + int count = sizeInBytes; + int16_t *dst = (int16_t *)lpTrack->sharedBuffer()->pointer(); + const int8_t *src = (const int8_t *)(cAudioData + offsetInBytes); + while(count--) { + *dst++ = (int16_t)(*src++^0x80) << 8; + } + // even though we wrote 2*sizeInBytes, we only report sizeInBytes as written to hide + // the 8bit mixer restriction from the user of this function + written = sizeInBytes; + } } env->ReleasePrimitiveArrayCritical(javaAudioData, cAudioData, 0); @@ -468,10 +496,12 @@ static jint android_media_AudioTrack_native_write(JNIEnv *env, jobject thiz, // ---------------------------------------------------------------------------- static jint android_media_AudioTrack_native_write_short(JNIEnv *env, jobject thiz, jshortArray javaAudioData, - jint offsetInShorts, jint sizeInShorts) { + jint offsetInShorts, jint sizeInShorts, + jint javaAudioFormat) { return (android_media_AudioTrack_native_write(env, thiz, (jbyteArray) javaAudioData, - offsetInShorts*2, sizeInShorts*2) + offsetInShorts*2, sizeInShorts*2, + javaAudioFormat) / 2); } @@ -676,6 +706,7 @@ static jint android_media_AudioTrack_get_output_sample_rate(JNIEnv *env, jobjec // ---------------------------------------------------------------------------- // returns the minimum required size for the successful creation of a streaming AudioTrack +// returns -1 if there was an error querying the hardware. static jint android_media_AudioTrack_get_min_buff_size(JNIEnv *env, jobject thiz, jint sampleRateInHertz, jint nbChannels, jint audioFormat) { int afSamplingRate; @@ -715,8 +746,8 @@ static JNINativeMethod gMethods[] = { (void *)android_media_AudioTrack_native_setup}, {"native_finalize", "()V", (void *)android_media_AudioTrack_native_finalize}, {"native_release", "()V", (void *)android_media_AudioTrack_native_release}, - {"native_write_byte", "([BII)I", (void *)android_media_AudioTrack_native_write}, - {"native_write_short", "([SII)I", (void *)android_media_AudioTrack_native_write_short}, + {"native_write_byte", "([BIII)I", (void *)android_media_AudioTrack_native_write}, + {"native_write_short", "([SIII)I", (void *)android_media_AudioTrack_native_write_short}, {"native_setVolume", "(FF)V", (void *)android_media_AudioTrack_set_volume}, {"native_get_native_frame_count", "()I", (void *)android_media_AudioTrack_get_native_frame_count}, diff --git a/core/jni/android_media_JetPlayer.cpp b/core/jni/android_media_JetPlayer.cpp index fe6094341886..e345af621314 100644 --- a/core/jni/android_media_JetPlayer.cpp +++ b/core/jni/android_media_JetPlayer.cpp @@ -81,7 +81,7 @@ android_media_JetPlayer_setup(JNIEnv *env, jobject thiz, jobject weak_this, jint maxTracks, jint trackBufferSize) { //LOGV("android_media_JetPlayer_setup(): entering."); - JetPlayer* lpJet = new JetPlayer(weak_this, maxTracks, trackBufferSize); + JetPlayer* lpJet = new JetPlayer(env->NewGlobalRef(weak_this), maxTracks, trackBufferSize); EAS_RESULT result = lpJet->init(); diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index a7a0428a4f6f..a6b63d81f2b9 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -117,22 +117,25 @@ static void read_mapinfo(FILE *fp, stats_t* stats) line[--len] = 0; /* ignore guard pages */ - if (len > 18 && line[18] == '-') skip = true; + if (len > 18 && line[17] == '-') skip = true; start = strtoul(line, 0, 16); - if (strstr("[heap]", line)) { + if (strstr(line, "[heap]")) { isNativeHeap = 1; - } else if (strstr("/dalvik-LinearAlloc", line)) { + } else if (strstr(line, "/dalvik-LinearAlloc")) { isDalvikHeap = 1; - } else if (strstr("/mspace/dalvik-heap", line)) { + } else if (strstr(line, "/mspace/dalvik-heap")) { isDalvikHeap = 1; - } else if (strstr("/dalvik-heap-bitmap/", line)) { + } else if (strstr(line, "/dalvik-heap-bitmap/")) { isDalvikHeap = 1; - } else if (strstr("/tmp/sqlite-heap", line)) { + } else if (strstr(line, "/tmp/sqlite-heap")) { isSqliteHeap = 1; } + //LOGI("native=%d dalvik=%d sqlite=%d: %s\n", isNativeHeap, isDalvikHeap, + // isSqliteHeap, line); + while (true) { if (fgets(line, 1024, fp) == 0) { done = true; diff --git a/core/jni/android_server_BluetoothDeviceService.cpp b/core/jni/android_server_BluetoothDeviceService.cpp index 0936310ba06d..796da1536b1c 100644 --- a/core/jni/android_server_BluetoothDeviceService.cpp +++ b/core/jni/android_server_BluetoothDeviceService.cpp @@ -515,24 +515,39 @@ static jboolean cancelBondingProcessNative(JNIEnv *env, jobject object, static jboolean removeBondingNative(JNIEnv *env, jobject object, jstring address) { LOGV(__FUNCTION__); + jboolean result = JNI_FALSE; #ifdef HAVE_BLUETOOTH native_data_t *nat = get_native_data(env, object); if (nat) { const char *c_address = env->GetStringUTFChars(address, NULL); LOGV("... address = %s", c_address); + DBusError err; + dbus_error_init(&err); DBusMessage *reply = - dbus_func_args_timeout(env, nat->conn, -1, nat->adapter, - DBUS_CLASS_NAME, "RemoveBonding", - DBUS_TYPE_STRING, &c_address, - DBUS_TYPE_INVALID); - env->ReleaseStringUTFChars(address, c_address); - if (reply) { - dbus_message_unref(reply); + dbus_func_args_error(env, nat->conn, &err, nat->adapter, + DBUS_CLASS_NAME, "RemoveBonding", + DBUS_TYPE_STRING, &c_address, + DBUS_TYPE_INVALID); + if (dbus_error_is_set(&err)) { + if (dbus_error_has_name(&err, + BLUEZ_DBUS_BASE_IFC ".Error.DoesNotExist")) { + LOGW("%s: Warning: %s (%s)", __FUNCTION__, err.message, + c_address); + result = JNI_TRUE; + } else { + LOGE("%s: D-Bus error %s (%s)", __FUNCTION__, err.name, + err.message); + } + } else { + result = JNI_TRUE; } - return JNI_TRUE; + + env->ReleaseStringUTFChars(address, c_address); + dbus_error_free(&err); + if (reply) dbus_message_unref(reply); } #endif - return JNI_FALSE; + return result; } static jobjectArray listBondingsNative(JNIEnv *env, jobject object) { diff --git a/core/jni/android_server_BluetoothEventLoop.cpp b/core/jni/android_server_BluetoothEventLoop.cpp index e5ae2ea19bea..9100e81013e7 100644 --- a/core/jni/android_server_BluetoothEventLoop.cpp +++ b/core/jni/android_server_BluetoothEventLoop.cpp @@ -755,17 +755,22 @@ void onCreateBondingResult(DBusMessage *msg, void *user) { // already bonded LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_SUCCESS; + } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.InProgress")) { + // don't make the java callback + LOGV("... error = %s (%s)\n", err.name, err.message); + goto done; } else { LOGE("%s: D-Bus error: %s (%s)\n", __FUNCTION__, err.name, err.message); result = BOND_RESULT_ERROR; } - dbus_error_free(&err); } env->CallVoidMethod(event_loop_nat->me, method_onCreateBondingResult, env->NewStringUTF(address), result); +done: + dbus_error_free(&err); free(user); } diff --git a/core/jni/android_view_Surface.cpp b/core/jni/android_view_Surface.cpp index f09bd4c7be98..8baaa84e6820 100644 --- a/core/jni/android_view_Surface.cpp +++ b/core/jni/android_view_Surface.cpp @@ -212,7 +212,9 @@ static jobject Surface_lockCanvas(JNIEnv* env, jobject clazz, jobject dirtyRect) dirty.top = env->GetIntField(dirtyRect, ro.t); dirty.right = env->GetIntField(dirtyRect, ro.r); dirty.bottom= env->GetIntField(dirtyRect, ro.b); - dirtyRegion.set(dirty); + if (dirty.left < dirty.right && dirty.top < dirty.bottom) { + dirtyRegion.set(dirty); + } } else { dirtyRegion.set(Rect(0x3FFF,0x3FFF)); } @@ -246,6 +248,14 @@ static jobject Surface_lockCanvas(JNIEnv* env, jobject clazz, jobject dirtyRect) int saveCount = nativeCanvas->save(); env->SetIntField(clazz, so.saveCount, saveCount); + if (dirtyRect) { + Rect bounds(dirtyRegion.bounds()); + env->SetIntField(dirtyRect, ro.l, bounds.left); + env->SetIntField(dirtyRect, ro.t, bounds.top); + env->SetIntField(dirtyRect, ro.r, bounds.right); + env->SetIntField(dirtyRect, ro.b, bounds.bottom); + } + return canvas; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0e8d3fd8a03f..2c3b59022944 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -18,7 +18,8 @@ */ --> + package="android" android:sharedUserId="android.uid.system" + android:sharedUserLabel="@string/android_system_label"> @@ -946,7 +947,7 @@ <9b|= z>v27<$Ms{cr>Cd45d8X7FK@p7_iHJwlv6#G^V`46`%-T$?R#^bif=1F;{|=IG5hyq zEbPaQb$#E?-|hkJj;8%yGFRH3-BUF0bQD3Sc|PMG(qYF@d{A_ybQqpc{;M{Gmtp?d zbBWy8*dyN4cA4%NI_YOJN5*S92hrZPW%KFZE1rOENf`)zoVMQqEr(^8*%Ip){SVQf zj324(BzlqAfRw$k1vw7mL&oEQT@L*>;g_ Ava+{ol*S9`Wm+ z?T^yc5?)~Y_U$us?vBTW&uM=}M43Gh zptmjRzto>HuyfGIOfE<} =`GQ%>T4i`cLXR65kbkG|`(` zJLs}xzHy?j)c*~5!t`_2rg6?x&s(k!vocQo?g85m34w?u&n|LboYEzT)R%Y1N}R`T zP*;JP;MSkNIX8UW+gZoFGtLU7kF(#eFFqjK%*W8W)6?m=e6H^fcW>+FM+Dc~wfAuu z{e8S@#xpcoCBODn&AgcdYb2pD-`O$HG3|X@Ifg{S$j$~?D@RPMFn~Bd8(J=N-urj` z?Z#*3z6T5|FPf++=PnW|mJIf1jgB}PD-Rxf-`$W})JxmO>AQUf=)$qCZ9(tRG1|n= z$qCw8{<9Tmml;V!osA0H#WAhju;*=(7JPzz*4RJOf5s1*92niQ@>063bcV@GB~I4X zogN53+S?S*2j{zto?4z+Td{U){hjTL$rF8jw;k)lu&><*qTSibCb1F63-n>;mBt#7 z)!yFfk!xdpESWR&11N9HXH0Od{n>t*UR#-jxFG_6FHI}u4xg(xWOv^ 0d$-b~xWq|QnG6?_l#o#< qbRRk|}fm*IG2otEXSOb`2z zJ7hR3*HH|Zp;gIF+4d*~QQi({IwaQ#e=vOJKp#K-d@D_3cAJ)e+FWM8m$ye{0(XBs zR&GkmoUo#?t1c`Zv$5Q2pU{BWXDA~Xp5V$G_YC?oD|wCyzKzSYtOSIaewbGq^Iz!& z>OlJ3T2lUx%E?$3+{bmiXui^;_{nqyOY69bwDO+i4J|d3(&J27jp{cz8MTc;=iAm{ zH3DUex=goJ+I_l|txz)u?R-enx})}&-S|ycWiovY)x*g$Kl3-LtQo2(tPI|`uR=N7 z@0i|GdtB#yIptF;l-E`V+4ha1pKYf~I~V#F^eEX1m0O1E8g-x0sqE&NIR+VU#fmQ6 zZ+;VAmG;7 Mgl>Q`7Q1rgmD|+m&zPbw%3bg5H=^IrT1UqxkmcKawGDc8E}dF zvw?5IK(GydkNd=(?pAoaJ^5&S- 0(B#=2CcaMS+HG!htYbs4%Ar z2HeEF2YN`8wMt~%xP$3sKf^~_#%~GULg)-S{$P_$V@are6$1XTNe$WVQoaCn)b&yI zc9PmsV>ZmT7XnSK=zVmf@0ngWS)tz{_pVz@49`J)L;xJMB!FSNT6B3Ew^o$@s>CnU z!hI@BoLipY+p@RxV~yA@@$1-!D(_!Q(|`QykJlfLo;%V;1>Ok+<{`A|XU*@gKqLM? zUJ5165RS8G+oL@At9X{od5T9LoyU(rk>0`E5la2=uLX|qZB{1VOV$VY_dSsDf5mlJ z_a^5z^@{?)GyPCMKhRO@_rCo6<+dUgnIT2LH4dDhudD8}zpiUU9g8xvEJZinKSgt| z1DeS9bi7r}JE6ycs7eH+SYU+YEv!aR4hT44Jj&+IKuak1(3YVtSHY*X1mFhF$BhZ) zdxrRwJX;0ur{$ m*upA7hOrT*@B>h+?Q*rGG5~88Z~A z1L(kNLzR8Q{H%N%rzx!3*TipN<*ek@1}E2*_?MQC#kR8;NRX|dGT=|<4>QE}5cw~# z80j`40U1(#YeKe7^B=2BY#YSwskf5)wG@rL3VKlAtnY2Ss_f#D(SfQ;!EOV*8gy8F zH>8g2BgkP{(kr|a(fcqP_lR=tpe985F)+qn8o%9hzgJWJVyUEfl&}RpITW tAkS_M?nX92#HHSnPG6VQKlY=>54^f5pb$i35S0ST3FOYD4d{hG300N)Yxxsx*( zU_hBR7z1Ry)8i1TaF3zXnG77i>l#*|t|Yr7|H(ZI?qG6t$jk~TbCP9u1idl(E4_q2 zSju%s`3k(R+)l%slhuIbuS2UMU?%}CNgF`W4)_sk8+6rTeTLC93JPpQ&de7k_G|q> zir6M)WK6UXm>GO%G5RO&1^$BF;r2wg0bJ$bRtu9SE2G!K{c$aPzSSe8onNT19VCFT z;TI3^9Fu>DHO3DF8V%2!U#Tyy-*2Vq-~V_G`)j21JBtjx7hUTD^8v(Wc5jN7q|7}; z9v*#mR8pTgnP-9y_R|h_|B!r236AHZTL?!H#-FiWF@ySLdU@o!3kiJ?<@yo3J36o6 zi0dTX*%)`+3;HMQWD0p}o*nsynf^vLcSvXKuPN^3^AR~s>dCC%pn7w_KL%YUeH`J( zJ$~kHojG8am(O2*`SL9g|MvTDuWZvi{Z7@$)iG{I_RCbAT?ML^(9)9Ed-f@g^fjWX z9N53g+TSzIjC}9qO-Xcutol<~W^~T9yGK7}_Hk!zq20;9d-eI=z37oXmIHd)68*bJ zH?np|bR+X8Wgx3;J{fJZx^>jwl@I9J$S2f^`ISjt-TU1Pf7(I&DR;>C$e8!g_;&sJ z%dh5SEmV9)6bwlB4Vo^g$TBrCI(fhc(xZ^MrKD(I2mVpUUy9C%vc57!V6-!yz~LKd z` J*QBSRT%AzF=ozGAqg|oC+x3C#COqi47$j+OWHv0Bh;vH;tXSf qU~1K%J0>eilQ>R~g3elUA{T<3Rv|N(KRdC^ z?H#R!9}znNUQF$w6bgb1Hc ;bDg2nuxLAn!(c zx|Vs}FKZ6K%)o3bnNv$Zb8Q0LYISQZ^rvDe5Rv&>_{kMJzLkR6ERWV4_+(EIOXCLd zOI=?3oJ}mDpvblkeSjSw?K|@$&Y!Meo2Fg0MB>J^`z2M1hNdQ6+!)2RG7$^bfHV*@CMoz)snUMlaj+SRS&NqGt9GY7y;C4d43k|7`* zcZ?naN$?~Z 13&``E7zgIjIXDKi}b%3cIh*`bAqAMi?J5~Y|tuOL-xCoYW;64k?6ZRjf zLDd5yyFaQvPS{{ 08F*s`IFRlQ&Tj1A13*YN@ev3z0|qz*$RYrYLjaBgV28uc zPW P$a zNL1)}hyxthfHZ}ZXNvf~4ffKi-oJJyS2cdpkIQbG_zO)np2U^zJtDtLnL2f9WHjn@ ztWbWzlWCHDl+j q2ufsc4A!7zCiiKp$}vJDZx%q`3ta>Yb# L}f>YxPBh5c5U{)V=$8imM^ zdKqsAV1%@91q3OHqO<`JWcKe#3tH6?o>M?1%U=du8Gt}k1}uSxtmIcG an2epLXqp~1E7Z5D)i*p5JVmCs#lfLL5 zrKz9ilaL2NhiCPR=xl29W#AuL;tOWLR4U%gj91qDImWL<4|+*_ yuvq{DbzXzz|)P@Io zWo^spj-ZqJ6y&|4eHO8;RzqNWofaj#dVm&|gk`>Nq(f!kCz{~xXn~?+F%u};z?TVL zhZs{6-@}sp20Ai`8P0}F$ 6 zcRPn5Putno_Wqpe`+C3eAiBTr^B_U@au3_}@3umDMrkdaKlHm6-y=_{kub-XDRk1W zYIB`COHSIED)mr?jDOAOL?%1N>+KwMGvQ5`8+=B2*fukAMT2E+aa@`F5vSVih6Kxz zEUD~ClF<3E&FitWpB8n7`4%Og(RkFSjP$K|eGB$7kjK!T?RRJM`FzkHG~)m6lm+?^ z_PlEk{yFt`8}z-!?=AnBt)}$1B57gq3dam>Hf{$}ZxTC@&$ZnDK075h6S)SNrMQ4| zeceol;Pm!;SBCAAyY=IojR|0hd_dB%8krP(Ne<;~^$>pkq}HhxgJt$2O`|7aU< zmX6j !@t}ZxI$oDE=TY(U4f8-<0*4j&OU8Bc z1l}M`SN&)DP$F-b?K!!kzCfGZ) G zNo;>^Is1eLgYhQx5N#3{SBv?d1X-25goJP*`~{uvH0|~VzQD=NjAPq&pSZ=~*!=|y zq{xSb_zg(P*n-GCx1GXg^y}SrjDLwM;$8S=a?Z)vL4W}mB*H6Z Q(*VIO$b;xQ#(^%=g;Y&gJSjfua8*njycb_dn~4*h7gk1)3}W9- zKg&ta&BU5#n$;19fr58g3d>p-Lg?Z%H@d>ZGREdh=RMGEC$4nMIbW$UM`( ZMDH$*NSoE~H!y^9;$+7RT+nCW4H!M_2%`dC(R761N5 zS)iIAS9>;yZ%M_m 9S6yMj9TR7)+%06wE=yWXQs;v3k|eh!`Y@ZsxCIf| zy<5;=rPD5c3g=(gezblMR@L2hciT2JyuQ9ZRa6$6NrZBi9cVGaT#B!q<0@>6D=*A8 zLk48VbV9a{x}5ZCz|6rJKNuQj<#vzJ?orbHqy@5d*f(~;6_uQn`)#(3!JHbV;e*X$ zQ#r8rP@+VqO80jbPotYR8K)~_iMoUHbuuvW%qklwUrT#f* o3OHHbwButEHNtHpWt3N^&x^zYirIY+O5I``~mp2P&t5PHEegN z%FqUW7<>y&Ac~iN=Kmd^OuRu~En@?bB}=k`4w~wy)d@Go?fvy+Ep @5EJ+8;~ pxE|NzdR&idcwJV$>fZkZ7yzsZp+Y<$23!CD002ovPDHLkV1oNHSq1n#=lK&t*P(*804V8uf`U8h#B zwN1CFX|1)^I;WowasQyT)>`Y7emTSdKpuw}05)-mFR=gsQhzOn_&hJh^EiKuKaZEO z+1uOO+uPf}Z{H8`{lnv<)>><=bNb~F0|0rJLwt$_0FcTd27s*`;^8#(4_a%jwSIs1 zLp&T~af}Z!wboi|ozqW;7y!uS5Cgy_4)Gxt06;2-7yuk%0Ql#Jcz4)+E$&lmt+m!U zy&U3wEcdZAO+?c)O*3tN`-|8#P17{Zoaq;_X_}^KuKSC)^#82&xXxN@t+meS@{891 dfIs{3eFExbQfMhc(&zvH002ovPDHLkV1oRKqmBRo diff --git a/core/res/res/drawable-land/title_bar_tall.png b/core/res/res/drawable-land/title_bar_tall.png index 670decc589c7f78557e1ce29e55fbff3d2fd2ac1..16290fb3a800ff71027274eb308af3b4582b5ff0 100644 GIT binary patch literal 18449 zcmV((K;XZLP) Y>|Evts}3x!_NpPzG;Y|h rCW{2+vg7+1mc*e6LKsQ9F5+ zUt-^$^>x;~t*~>I@D3;SOuZl8Zp9Ru^9Dw3Ifhe-@aiwT;pfG$*r$vQGYt|yxdo^3 z>+kV0JakR_=htD@<9yC_{d&~!G{twVVHc{I9$qcgND4?+XvCUbtU)UEWSUm62R!ii zhxsToWuB3aWWdr49)BH-uQi%asHO=+U8H8s_5Skv*ME31|LT>XeLnnp4x_CRntK~j zL!|oT6C|%Zyb3#pj%}@Y9{fn6(RN}b(>d9Ayz)bosW#@>MJJ29Mwu9yyy|5<(i|gH z=tRma^@ii&EpN>65M_duW`uo(p`oY?e2a_KaBOehHi8NN)VYkpG0n4J;*$sc^WsJ_ zMzRV|`EK9&%kv4>%-^5ShlHwZyaxFUpEDWpd{p~34DlY%^)7iMsqgES`O>`PcqQG% zJU3sl9(4^;rl8YoX)%8Pb6y}*$U_yznl!;D%yl6JigaLMJBNC$^~rysqk2DZT|B>M zX*U s5i%K`qqBqvmZP9RY^1O>iJ9wH+sf{0m+r13pz+eE= zBm8*?OK~>I!tJOua3WU2igX+96;5ch%FBK &dLuagzllt-bV=8PK6OWX&V zRI+@i!sZfYj+u)k*9b?N&%<+`DhO0n?7tvE(5T?6(E}n~IaI9}j*mi-Az{I6(3IIt zUZieHkoETfCTZVhF7qdJgu#@mTsWT*)sXT4_(LK=^DdIs!g7-hU_#MzYHg6c? vz&fD4gSSVgyA1j)2DR2?w#LB1hcEW1)ua_9pl zqntw|i#yjvl<&HDyi6pNG&T7n#ot7q`FSi3M<&UhyM5sNDeI9*Mq2l4Y33Lfk;f}9 zmilz_b>o{X<;#R_*3kYIA)SI%);Mh81bX$*sG^EBMPrD;{<-+2Tv~~zkcEVW?R3R9 zUM}W(y~x!=^@V&c-lt_YF${Kgl69DWf8he0?;FWO8JyEV#% GA>(1E%v)zVqk8N9SSvi_8%jf*{rt4_BpQZw$PiR@CYK)ir# zTF6ZvD~z>rh*O@U5-&0b%cRaYdnn9`jIkDCBpoysF-?BPv6I9=B2@BBlCGgoBi{ba zlk+pKK6>azT-hF@dPVxr39gqmRpRn42@W(*k|!80%q=YIueF|Y6dQRGVF0b#OLe!% zEUfh@i9 H zZ_i0l4*F+3;C+jNBWTWp9xA31X@Ow^g`X{nN_$QBZSou@ozc0Ihhw<$!!(W9Yte36 z#qpY1)c1Rdl;e9vSg )hjU+I@ladyo$QeVz^QB2w!}!{cQMs^a=hj%)Oi_fqz5rN zhDyKw#zi6BeNA%>yMg^cZ)0#~qkT(8buA?b1tnE*BV o9SY9k1PI4ZHQK)C zKeKrwW!1hZDKS((&WB*yom@s<$H|cAq6jk&oe`#AP9MLW?1A_GaCIX(|Iq2}(uz%( z4^q1{U K$R}Zgl=573TGRJ<8bmOSd1jnuRWhwOuw~=pC7h!^{zz6Td4(t^d0W-$=v+d# ztO(3vIq7XNu!zah)Id*Nj;S|ug8nTd?V9Oa>$WuS6s4cZHekr6a&3#=$Tl=B{FvV_ zhHqdX2c_vJlg#muRbVBEN`ts`eCtmBU?xG%Pr=-j{oIYRXdd5K=0U7GdA{kMc}`|d z?IA^Ya~AXVulXkO9NTNI`uu)w?wu=#qu~-CVi!qnaNI;SUZR6iZAoT9v`Z@ojq8!f zLEJFey}f2$Z|Ts+iXKe5*1AWm>*x0UND8-oFGb9Cl*qF95PS}nkxn~tE(f?0XQrib zF+&5*J6#xf`}=8&BL{m2VRQr~yBd3Nd5*X)SNLL@7oai{bx o^nwH)VmuC zs0fwuoT=bFPjo3%vBXPuO&3c`_2HC}AZO*TfBpBr$U(ywaK1Qa3B{z_?E5JX@5pf! zdriqf(*$djW6;m-B^68LC#GvOTXadU%vzlnd}%X|x%KACxzt3yrMwRBgByi75`yht zv{{$UeC>>?WV&I7#9caNoCOoJy4U{6Qz)0OY5JvF-- j_4m02!ZHpPn& zh7u>YqUr;8Cb%R{cU)67A#Lq8-48J3lCt$TXdiyXk&c-uHE(zI2M79%?a&Ajw5kMC zst-)Hu9S6M5f9$`*T4SjUpMyuLzJSl6a9DYs$W-mYM*%4pEz_kqXJUwUB$mgxBI8X zJv>e6EPRMy^=}dfc5clu#w)+N3<73I*LI9ymK$#wzGPlUGPGxe=00|)VU`m%U;YhF z_>==HvRiQ|ro4f~$`AM7ZyC&wXM@AlK2=BZv|G;ncbxZ1%iTURPwJDP^J}GpS+z@9 z3C+{})Aoi~=f#xKaJNP7WUDGVO&4-1K+YvQxkt;mz{3zX*RHR$dzw+@w^|~|?xJtC zfOeSuLzD;!RX`{#4S~5o%Ffg_=9>M3)Cp$W1f-?Pbi1UMC=)!T23L(jKs%d;o$R7= zKbY*rX{$(XeU6YGzfu;)P5TvY1#4mNpx&Z97wto7)V-2hNR$ur!=<4t99j?x#i+Y~ z)Ar_kcxFOu2h*L~2x<3Fj&0gqqU6f09>X`*VxHRc`lj}DZC0xL8?)>E*MI)kf8L`_ zMvaV@oQu-pjSp~wG8Pn@M6Hc&HLOwvH+AQhdS7FYP1?4u;vPJ#rMm{#Bexpgbrgj9 z4B1k?#Me=J&fTRua)ZiUbTy-f*NGa)G5#DaEIy*r&1LT4WOxRZ9;^G4mv1jV_Bq4V zr0-GX;!m(W;{*A<1&@tlrEbw;-7Qj3RIZ3nEdTQ^;<;Vx5*9e_U`4Yy-=XFj8D|;! zo#GJGl?@=?=F4`6L%6|0+%sRKm&SG&l{~~9Or`y^M|ay`pWdRO#@&r}n(+a? bK9IO}=O-n4eqluEp?G8KylhaC_f)R%SRHzt zidjookpgeHSuJ_O2I|vQO3?(z2XYlPWvbOh62%tccF)04qj3)7FRTL3_JR7yE8c3Z z6}Z~is*HAL0>|A!1QG>X2+EOY8xGTTFKLlrUB|t%yu<0Ww7rcuM;) 21csM@e7~@7O;SaQ zix`U4Y11zBkd`>90`_zfv4D=QmkugG#$poPn9d?CIHDsV5H`-2H3jXIU2l($ogk8_ zPdYs4XkH;gsBtgSnKlESX|Okgd*Y=QJ3*TgCo}><73j_k+NnU4b_!7gGlq8>D8W); zy)tbpT6sw84UTXQ(T1u6>FCL;jKn9fkx?Kc56?&fSQrUtrP9~=MrFKgcjE)|5Khty z&~UqUrG1Tr D4JvpUit-28;^RRRRtr+X4ogv_{prz1L#YV)t|2TgV76 zo9!ll?*Po8Y7Z89V0`EOF{;5xoFL^y6-Qa1*9 oya_``#ug$1-qv&fsD1n3$C4-bW~5T8cNT`Vx>C7#t{St+RY`Ogli zLPZdrhrzL$KV{M>)a`g>y k_puz3j-5y`Q138nP=?=K;%;%wMaj#qxD#R`${ z=T@@aDy6!}<`E-loFEujlAOy+srL*R2=|3iTj_@6pzR1dHHjVsktY(QnXT(E-?Wg; z;;?{6u=h>P+({h^3XehyHB-fCDnpvHHzRT}EES 5h4?#?VVDyUh#%Ts9}Zq8g_f(iW@szdqBis*3!IID=Df++R^`9KdO-K=>J4iz z0m#M2F1BP8Bw7gRLu(2huD#Up!WE$^I#}G&;~5D&vIVwEQ3YY+_LaOA!OFFeW2K zU~q=WB&@&;QN=}=DzvOLPFbFO{;mkT02LzRLtPzqQuHFI&`W;Xw QL)CQY2S0kmzfwB6SZrKD}m{Z)nL7B z9hcAvEM)K*83ec^c&F0C)(9X*bJNlek1{Niox~lhGivHzNx4ke8oH(hMWzBI#0c~r zS7>ne@THWOd)Qv6>;yF%Jqv_`5y-05swX&B7EA}oh;31FwwB-_5PT1~=S#G%toOW8 zF%-vw961prn!^7}1}1L;8>Bs0R$)uzhxd9BgjNdG7bX#de4-EJ5s_jsl^(x@)w>J` zMeI?W?Wv$+h(MHrFiC$StSC`+BD7^{BRW$1MrD#wh|jeK2A-o;*am)!!e=68Q_?UJ za}YS!NXc GNMpU4kL&n#@~?; zZz13e0Iy*n#0 Va26evgH*>MBpBv{o_ z?qh%TD|$y*)18$t8}B<)nREsKs~!UZGbRe;s(_o8eMS<`4G7XsCU8sT>sr_)%N8gD zOId&(#s3l+Nl07wa~F}^0#VsNhCOgmwm?)O3mH$%Q(K_N;&qGh6raIbeTW5C;*QlQ zX~LWt-_2+MhcLCDm*t1OYW-2re+RJ2_0s$i2HPi?1G@o4eAo!T@0#B~wrnQ6W19E? z(P9HC@`>P8*aGH(+m1Wi{g|P$ZncSaM%8QQC%!>Zh&u#^4Y_jD q8VrYzH3&aD;-&};8|TX-#fHa(lV7z+pg9nUv_Bj9l*!cQWRI!}ch;x x}W?zf&CJ zt_3;jK1j@qSs;kp4*hly6XE+fs)kPMDn?mVevdE^MRVXM-M~>$FA5xn&9I~HIUIZ( z+#XqFKmD=oUK@Yd=AIiYqgdR1)H2dE+b) rK9%ZuO`KWO=w!BdWVZ)s2rxBs#ZQ3>( zOZY;~H*v 57(kPwpbIDrr3a%4A MHx~|>BpgmfOZ*8L9y9A! zXx?hQn&Xg2z!Y|B;b_}RJEyJl)r#09*tlF4*r%D^YaQw+fL1D;kiw$)6&w!S1Bmmr z3P&>XhBTE&STI&Y0)YZZvt9YtL3KP>^scxH%^(I~D%s;;WZc-wgBXT75ebwaDL8|4 z(PH;a5UL9$F|!k4+y94SWe3ZEQ_xNUK|6^LAj(GV17>s-i-?k@Zypj3j>CGm!e*RV z&JMff@SW _|#aym$tP*Wsac0ve!Vt8;Ps zFTb-wzGo14i*9?gn%NxCxlt+M{Q!z=ENqL7O`Y=1Wx4d%g(b)=r;|-$UvaW-296qv zK^p6ap88hpB PIwvn7b^N-Xn(Xb- z3Ta)e0zrbS78(hMC~d~d)bbMNG74(-N(*5R1DhJ`7I{Wi>WhxfYo_%+!ayh>Oo13r z)3lyC_X7` $iuDBbxxX|rs%quW(~iV4LAT4yP`CB z*PLAy5`}A)75i_nP&ODKjhdCPa;QQc&;v41B@JrXuw=1N4{Pv42X8q+W+6*_uKxJ; zGB6A{K?dAd8&mCNKtF8hNLy#PR4`syk4rmcMWtNcBLbWEpA9++<~{K|Nhh$GVYH;T zW^Y02Sj1^NmX0l;hnaPwCLKJZ9eWhB4l$$~hMmIpE(%qLo>PkFB^uquy@9zB>1(xS zj+c6l*oZva$p&}92X%WdGy{f7!(M*uC8J*Z27P*i;XWAn3cXYXK$#dQO`!lNg)%$n zoAQAP2~!0a##5#r;d1#`x!d6N7>u7V6sZfb9hRvVWCm#rP2dJ}K-W+&V|@V93Ee2u zHxfyKfNK`mT-vzvYNWzv2%jb1!fg}OW!fY?zV)P{5rncbJFuHVvQR%;szMWYa*BgR zPsHV0151r($c^iP?OQPLDmP`39eH59)alMRIUEiWQr#e1PLL);A$Rgw$tzKF{Z9&u z(}3E_FRgupN=^l4L^>fupbH|XmGr8P#r%m-el zceUxZor~+N_vw$;ti&12$Xi=0 U&t|BB7#fKFb@feASG^!8%^9W5Qfb6r1aG7tiDyP2Faicr)XEIL8|mT* z;+bT|R#Rn5ZQMcF*0WoB@r(tXK;)gA+UAaYB%|=;;uG}0QgVE0 C~+|mhds|{sSl~qg79AixrztP?P z1`(1p{kK5W3ORRN(7VV7=grKiKXIBe4EgofhZc;tvM`SXNu+L4TrIP$0BQ Y z&+Y8Fk%^cdi*pgaqrD8yTy91Ot-Ww C~%rkfGN!OIUoKpgdg8(6ZT& &o}(j@24W|bufFk+d|_d+*9 z@YFKAfwVWzX;NkeVAuZo=l}hu-~$-mZRRxGveQx4Y=f}jv{-!Hb}7gk5 +X+uVyW?t5kutlt&A$15)`THYb^ZHCB?tvGwk?K3HHG~jYuoaQTNs2LY)j^W z43ChH%Dn@f$nX@O^GqhohkT+kB?~S5?8GfTMu)I)95$6N!@hx4*)U;Kk8MLqole2D z{@(42D$JQ1K!wZ_4E>RJ0zKO>gMA77C=L!?A#4}4yG#^pUf8UNEhmYj6`?c(&K=UA zd_9dee_AR`!2+`O(rkUxdKYvY2z@P9=qDQmvS}IVFm`*GJZ`~-pkAeIfpRw~w8t7j z-zmX2sJ~h0kkhPdu`pO^-?`zK_P*>^u&Yg^hdUO+hgWCOQ$2>&&XVm-Tku{Bz~0X6 z{@PWBjF;rQ@fzu1&MKP%vi736BR(7g&t_)8UF)XKphP65z+jm75`d~~ShbLpv5py& zu(He7UiJtW(I*IGnnlTWz!Pp9^h}GIvVukw7-cO`1_O}BdQ*KdD8r7l%G>yMlUaiP zzy9$b|IoI@*f#rZ-Jcud@EH`&4n3zhNBthBsXBmtbI$zst_=ll{@hXZ*3tNPNAf2$ zG> w}Vtq*V+z9g_=$eePLVe25+>tkJ?f%i9TXdlf z8-GIEk{Y$t8wU6j@iZKACkUUYtWU-BV+<)F^_LW>P*GabQXVa*O9y}D62fqZB>LD! z6;TN0q8%K?dPvnISyZNGRneS19b=7UQjCU^fqo=erq1>7;YbA|73rKhd5al|`&ACC zQW~Z%34Lr+n4w76G#UW8GH^q&<|~b;uBMeVUrD{RvJ#DL-lCv3&DWkA*dug~THSAz z0)pTJXrh7#Y9%gMSjeymk!+uqM?U=61kp*JpU- t0nzz5Tt`K8%k+g&YdW9-zcj zXJ`184CxYEL 2UJ|zoso`;*rb~}Lpc*v?-m@4U2wO}_DYRoNw#va{tY};g` zKr#yNrTW!$>u50{tqZKzkGTIkV&|hJ2qD4xG0T_xJlhB0N3q4G2)FK)AjjW@GrL`j zqQzgOpt+x73vfE9I4U;*-mhS!yz96DjWT>yiEwcuc#iqL#2_drfm0>fsu |FjRp`27-2ZD8);*WJGiI<-Gb2iTVfv0=^MS%6x7LfZyL~&;6Ci!j_)8} zmb(Y8KnaD>Ove^AD`x3UNCryb=5RAc_;XJ(PDMs>$&`!)3eiFUPV-?#ULjiOrTlR5 zLz_dGNEU+D!s tt6{A%>SMox`fL()PCl-P_m3SR^P0Ird=rMW8bqwzrs z)!|^!rraJSXAiOycLUpiWL6kG8-&b31v(ynB*TWa8lng}uf0kjx6L#g52k_yivmh5 z(l|G+gavG?AYCdvrad&zDBwy2r)2|KXKm!z0_E(lDPTr2(uJ?{Nqf|1$#4}KM(nUO z_u1{JSvnkT+h;PHJV!NSDbHq*#Wu!Zq_V%*NlKPt6~^ZIS tfovCBw6?el9>cjg zt>%?cLuysJukryc23~Bzf3t+N)ONA44WA&CwzS-B3qjrr5H$;w7zLi3rImA+d;nC1 z2pPX^)Vj~g*QhS-3CZ3F)nwJbOQJil;lD@U!cc6qHMLHb4Q*j5?23XN~0xLPe5 zqZXz1fr}6Yv|eQ)GE<4K4O-%BlOBEk`j40S{ATb^*Aw1f(lzh9# )-%m)JnHXR2PtEZw1`=I3?oy*4ER_p>3$dqz)uy972a0E-6ac3 zZ{J9L3thNP+}~jQ4pQ-aLYruIY?t0sdjI|S^&bwa{r=|nKj#Oe$fG^pcfGHFU$Z_8 z7H`WsjkVbI{dye? 6A6%j3Vjt4G&k9-bty{@fUp!t~;pfO+TG@mac`T4%V zbdIMGL@roj$(P|Q-Xs5Y*&0upDUBAjKdwcRYIz-wM<%%JWj56#Xkb~=5@GR{buX|P z^3R&)AOhmwdAZ59xd$S%WdcqPSGx}UCd-i(H_fH3!Heun7Rs|gv8Q1V=P *r&t~jw3nH@=ai3!sGj*vUJm@{(ltO; ztK82S9gnOvvc05ltQH1M_kQLlx187W_|GvP*JV!%u*ifQ&pcpJxFS4nj?JU8qooHP zY6-IK5tc>@dkw|p&Et^=zK5X~bL_RG1DWu7>Xdmsu6tGyqCJ;iGT;7uiol-NKNr7c zZ)Q*MpFhWzGEW*?UUXsFQpw^sPhOWo zjhl{ s&ooQGE7`1xoEbev dffXZ`bQFI4lvqT565$Ai_xK?|YkiK!}Yd4G6MYK;dK(DAekAY>6C zS+jVAk%vRI{~FKn%v?8FOG$ANWOA%E6%{AH7ux8EV`nyy)#U=qR5ZtaBl8=bWyGh$ zD-L4Lb5wneYFWyD+W7T~&h+U-I$|N@rKuudb%^G_7fT3Rai;R55IK1yZ^0D3Y?mI% zBHCZQ-L5f5c6Us$bacy7P2UwBLV!oo{-pq+^X>eRHsQyhZeQ;49~qsuNkL=40pW z=ZE8*{I6f |j*q!gQMTsj08=uQhWToTxN~_8?lnIqYYs z2N}HgsNPcXCaEDEJo&wr>&s5AE-A|6`4+JAX)=K88d8WIWTP)OLi4cxK7g|il7#v2 z^PA&gbvLcxaXIgk)84oAljiH0*Y&5;Jf4*R%+E*=)a>X==M{W^iQk`>!FXiKlljkk z#ReRt)|IIaVpo~eWD=Yad5#4g&@Z6?>i_|Pk(>fDCwQ$sk@S+;De`Cu=;4=@^ 5u c#K*59)u9oSRs z^_J`7r2H32R0lWe`ew;?f38vXrS+y#drQt_EXi=8r?cjE>Bn;c`gy_BLE@7CycaF) zp+ X@#EP zd-^#_UCJ>?N52Pl{__`|kHwC}k+Gyg9I^TLyk^QU`kRZ@Iy3$CaIwmBQfKIfG>->z zrmzai+xj)u1=H)%d3BieBpL)>KZA^i-U8#9x@pm*Uk=gx-fPxx159ciSOQ%HrF;jY z*q=C~<*9+WLN1*=Kx6A9#ke}>7>-yIgeVZL+G+mdIo)Bj?iyfqGK8eff%wEfz!l31 zijac!BGZ2H5Bbae68uA*45d;$kqst^7JrI!DN5$jfm1c1XLH{8`=_aem*;+v7r0g@ zo?lb0rUD`_mFZ3d?H7%D$oJ%kfv-UxR;Rqjlp=A^mW@50_|4Q=q{_jNkl}e7h92mQ zO!-j-D*x<32BVu4;TkjRTyiQwAjMN47qu@VL9x{l$nofIFLv*=3Trz3bnB4ge=c~d zLN cwa5NwtBg#`Quag%YY -K(*Wa)>CCB9Jj(Fr<5t_L~rlb{EMMQCJ5Ib!CdKXNGX zRvair_=@F3^p%s9tSCLLQ*xG6+wl~~d@<#*%+iuQ$dD{1hKOcPR)eEsu@`bG-F{K^ z)Wv;y%`}o@k@S4Uas4wM$V2N_WIl+|mdkuBKWqB?>lNr523be)3Ypr=;-W(AFjNVc zf;e@$W2o;|aPww^NhOHrk#smVSvXZgs7DT7xPFT3=_|xVndt>*haIwW%i994nX(`8 zG~VKytA(A?IwB9BS_4C_-&8`WTp~Pq0Q|?59|NE9eDXn&5ILULI UYb z{TTEs(N#ak32Tkl(0T }Wrb2X?e3)KNs}WmLqZxd zbY(G;Gmgmd%Y#OJaV&?Qe4kjWj$s!KXvURbVaP(prKZ6+Yb=1u2x*t4q9<*8MSP@x zkEgi{0$xeN%t=j+LoxN#6qmEJ;^+tpC&d)?r)F|7L_Nq)U)1jGMqNV^sxEm(MF#2G zDZn@dN_Wg>9xM1luSe*AdWB(_m<$q{+s@3*o>b1WEr 7PZXo41 aSM9l=%iu|(#aux`HpC-h-0q^(rDBUepvtIL3j3W`;+{p>;R%d>7 AgBX^@iVFvoPoi$W{ zD*I+vnt2gdjto2cp_s`6tw%PT6I08n0nJOvK32XMXP|KeS;-<66Y-{)XTWQ#Z?&h-VPy3CNIB9g}UtphR>KOpW9M zR-eWnPpWiG_A)1tmW&9gVLJ*%9z*y=6Np955|N%fFen#Uzh3`i97*A3Xevp(9O6&( z{Eo
CWFv!Xec&Ysz^lKay@HVJd43>X&ag`$yb0(KsF!nFLt zA8uc3nQKe6?4lk|RmV~g)76`%47e4%;~_*`1q>bPle(tubO(m_=tN5z8yjp0`LPl! zV0Q1#L$s+FTr iE#f~#~$R?F}vp!qzYk>X+*2G>X3TU!c4TRgGedcOo+I* zp{@qbMF>LvmtzvDlj)?$aAm!_B-w_8_O5YYvfGTK61Fkk3 &K(`0Q6$u9 zX>lk<;+=Su3i6H>r7H2086e(0bPG|7qIk!GJoJO&07)i@;#*7}DYwAsZ%v(p3%1f? z2ThiENnR&r5_gl(!SAa&h-z|Y{=Gj$3`t&c+ TcBsYMN%IbHcv zW~WXNr-@fz# z1toOyHq@Ca>JQPU)-2@> zI$F8QU{JCw<$QDGwY )U>Xa|1fx(^V!?<`< zJS3TY-v5yVt48j;IV?ubdWh *P2`zg)m{=#9+_hDN6fs9|Sdx+REo*ti@&^Z%> tAjTH zFP*+VTppt6yJesHsx%@aUuD3#vLXkMVy7wYfx=Jes#5W@nStafP{OhMeHF@4*nM5x z8A~}F-4@P%G$v%&jn$OJl_jJauTJ5=)cEV4w0&gK5naC#qr3JV<6(k>?)>y%PYB9y zrWVEoZqAa+fH6m;X)D%5By_<$Pi(HJ`QQjukv%28`a|nI*v#YJ?=t*7Z3(7o0$V5{ z8-3^S`qFHRdMN`T#i_ Hg=M?G7BcVB2vY9uv^_zHfomN9jT?F zLu@NL8krBX&%y3<^5byJ!tNd{LjnP{_n1<$7$@jc?Q^pjRD(EotK5}?dePBpqK~AQ zup~1ygOpiQ^ig(m)E`e8gtFCa8r9L>IyJfQ9D9zmjSxf52-Q|I?{w&%bKXsnmPo6k zX^!1p6R_!@GvGiR JS$F?;z+GTI#M{*pFSr8 z##lF*B9#No (UC_S poF#vQrcZPpi@{!rL_ldTVIQ)i9Aj;OK_m&H(mDwXk`zrL0mj~sJCd!=~!)1 zw_z%5qHfPHX=thq=z)7~t3set2288_J5`IKF{3 1wT~ zxKdX^j%6jw;VO|SrOTybCXWGqRi;*#Gpf`c0+FQxLiEVh3)Q~M3U)V11};z5ve%r3 zhMGme%+bOjOkcpJqN#?G!bf(Ev5XDvhc*Ud(()4RubiLCHKg>J`)ahlGPtO=a}9Rq zO!T3fkW$%7uJn=WCA%iq@ J4BtcL#-2CYx{GSN=TY_@TdcV zM`heJXwVlUs!62WQBoD> GZIiP{T%88#Yv<~Cs_+X}*mo6HQ$1)>C|Esd z(lXMr?6Qt|t4U%n-3LBu!bW0@ +cXnkfS`WuPc(k*Dg;}#ky+19-Yh)^g0r+cGms&*n z8WQqW5|ymQBUikuW`ux%_R|gy`GpR-+clN1iKo1Fhq;)y%7@&Rf=O$B2C1+fE3xm9 z>Ln0GD32kMWwoVR2URRh016GC! IZ=zn_|j09J8R6ZMQ;xF-&1`7 zZWzoy>8frrG@@0fKDe2X* d2wE2qszzEm*cma0+W!@#QuGO$@pkYD95U zm$SQa_@ETC!isg+uj^)ikBX*XF5-GC>cf$?vLGi(J23|vD(MA9H$`e>kC0+t)jvrL z%?4hT!9UE-E!t5bROmrnz1WV2%6Y^d6HgQ2Z;0sqsgB{1whblLi3IGn%xqv!aJK({OHr2}EqYwjs6Dj;>H=h_1wsQr+ z!^c2fy}YtTp4Fl%P@9VFiRC!2D&@u*^REOvlD^Q?BqF<-**TSs3anR?J}(5?Jh3g4 z!N6!qi}0M2OIZlMx5}AN4u| - Hx?w8@>8xxA132JDFI3>A`F$r({V z{ex5%t-F_pdM9S-5v$;hnsBX=_M{zDnrVyhG!a+XlFq
wJJr6|9U9S z&hgZhwq0%A?VdqRrF1{$l2Y}pSp6YvSgx8ddm8bSZa1l^6kRFIltXyiQE8e3WgAc= z5U0=hr3S(x`=|YpI~sg9Jf~|=&1C?;Svk8l{fc&WU)f#dC=Y{eq}yLz@&6=EpYxAK zCK<_u3bRC#69X&3`%{riyDLLJ9#R`8{oD(@IJJSuK5bRyjg}7qu*CP&Ug}UlD8jBf z+&-tp>Isu}(^eODpS8v`BDbykTDo{ Cadq_|K@3?1}}hd0QLi=m*tzA7 #XpZ|Rh+bXRg!G3&KCzeC+6NK&BP^cJmmqkZD2q@v zoRdm&j?NAgG#BJ`W4N%Do~n!w;%vLypcMiR>_$=_T5~Ox{D- Wh6VERA1v*LUN5223bj0yuX z%*cIZNRAcWYE@}&4L=?d!z~~esZ ?Lq$+^{KVeUvBXfM=FPr+T0Lm+<*I@WTOE#hJPP40@;L&6X}wWp*W*N_<~ zoSyoXj;8>zQuRVEhIQVPVc)~R0OUPDnsQ%A`~<$<$v0f(x^jLPC{{%Zbwv?aStJL2 zN#;>ti?wg4hi>1+!<2z1Ko75Ca$VZS%R?KMaR@?B7nIISX;zZ;rBg3L>!xigCfgp# zerU~4I3#g<%mw K69lI^_j&B@;yuzfwIsrr@(B)Y^$ijq@3N zCkzNlZmCkR9;w>tA*dH}=76-i;EsyffD+%9u_;Vjuwbg8Qzmtzp}Ub3&2@K{(qV)| zps^Jy8nTB9`jh+s6K2hpFit5>6-Ex4 qo#ltm` z;ofz2GWxTso+3w8GV; wHrc@#Aln)+g8B?a zNY3X3<;cl2)*Z akE>t@A0I*&KpxlYMwl(wOz0=@vu%?GOtl> zB+$rCWiWC`BRSEwM29Qz3^($rspc^CgY2TFzXZO=oyBg-fsc?QZmn|h+cPd;GR@yT z@YE>8e#%(B>T1ZA8lRY zg{Nug0SZ0kA^|6w3a}7xqc02*4e-;B|H*2y?bbEBqB|Zz+aUJn2Lobq6epN4pc)>1 zyWnZKUqkZgq=NfXyS8|nLiht~hi&6Hwn8Dz&ynOp1uUL{4RmSpq`V%l9UVSBcQ(4O z{H5KV&as7L?Ni2y%ui2y6`;yV-VV*rQcj?!!*P0GXBT1A N{!2{W*_P$O!mgA5rgdov(YZbbJy(xFu_WeUs2ecM^^gA|vpn%W^ zixZG@(0sfGRZ`;d-rYdpYxh?8)-yQ*Mh*ya(Fz#EDAT7}n{Yk?W@AzdLH9Ry7-EhjT*b?jrF-Lg*wCobt=r12(B{f^-?1_WZ zMyVe!(h1Gl1SFbqo-ro)Ed80`3kW;yO2S4E4Y4JR&M)UCIy|co&wl~(2Mze6oj^$D z(zbBt)0gpJY=?47OoE*SN%=-P#d#U{L$~s216C@I^6?*7q15R>abXF?Sv(dLqMkhj zemW`Tc`8`K@ xDGW wKW%#aiq^GcKAG_U6;qaYfN_mcvYin*~h<@xnD6>#1Ebb#Ty%VpDaP1ITA*k-^Wi zhde=97$FPira`Wop|k$b%j1v@j}S&(tO*L}M$!CFgq-aXa6UY3M_^If6icwXsI~~l zoqfrr8iLV#kEcB(bgAah%J-A1Kw&+&UaWps??(5R@ce|d7Pt^<21JXcP8nJ2nHG@P zXic^UEnKJRVE3uIw@GpPU>t$aE^h`yPYo$xOT!%YIHNHK@TLbeU#D@J4lgiHrVF9F zYJzIN3bqI2)b>R;86lX83}2&Fym0as$r0X&mKB_ysR5{I=rHy}>rKITULcDZ!3H3r z{`$xN`G+~kCselaOX1z62(Z1!JyTn;-JeaPtv%7MdV>AN;7e= Sc@b%X$KZCG?O{SRkEd|uDa zJeHg0?Yl}L6+S6cVny>j!7sT@FdGpMVHpr;-Y3WvNZG6w2`IbCZc)G~bF?Xh_#R-L zvXiCHVP9<7Lw08AOH9bX2b>N@#OPz5i}ZNNj@E7fhZ}QHs6}2R<}rGR7|UCx4=Ase zegRC#;4`HKP9GajbZX4(V!=%y#<_hOIzjjl+)F^OC$7`iq(?d5ZfPqEEl;N5@i|`j zE^VjkPn;A!UzU%8OC~s-iw2^UZ`a9op7@byh;QLjCG(S!_lOkC{A=l-Pm%O>&2Zrw zC&zN2hVX&Uhsv7eIx{@{TaE-_POcc{=V_0pY-ou~fv&W!OE>IF@ia3DjsGxu?5}_P z$3LbeG`if`$-Rg5iJ6keu5VgA$Dg=w`8jtNR8IY7BtI9o$1NS`S9ksA4qfRT-_EzD zz%DkjkKMfM_fPjO43Hwzq*@v~!dox?BWAcIb)wbZpz|%=ZpK;sy7#lJ8!Mvpc}TSL zSY>#h%; Nh23b+po!yr{Zs5+<@oM zbf4>VVc|*5CV3g{5dfLD?BM|G4S;%Udd<*lT{#Ci=qIwb?~D0@Uw^*`kAtKqU$^}F z``6&ttcNy*XXk4^>b$;j;1jxgkX7)c?IeOyas -zDiGPBu%WfvZYL@GPKw0X)ef?J}^=kp>Wx35{G z@lT=45x^W3an#UR(8RY~WN15AXQa`#EvxVcR2^%Tk^NA{d!o54Za1=t97QsFX=iy6 z>8|$3pWh$kuJ!A1G H*GHu8q}F_CD2Z`!0X8jMv<0kDm>(CeDZ6n z@5+8h_GKzNpC1xz1Ld*dJVzP-;1BXs#P0gdz%2bE*%kZixT8SZjy65#H2dLrrp1Px z)XQka*joMAv2{!5!`nyGH!H{&tAn%Qy*;Dyjf;TeZhb3Gu81#E`3aHtW8Taf-&NhR zUxMN;an$U4%XJ?qfQ#* kA?xpFEg$*% z=KTL{WU`qWNrT87gf1i9HPrR+@>~h<+YUFrvaKnQ!HmdUBBqrL-~dtt5M+e0-d0}+ zyCjt&9_fEs;K7_d+ sB!8_L=8 %xoCmGagE0^Cs++eJo$LH^Z*(el5yK+^{ zsTF<#ncrj!*K^`CilMNz$1Z15%{}UrMcdkjthNTIaxs}~)x@G3lWZAd!^RT%jmIu; z#&j^ORdAGUD4|J??doJHli^w67m8Vnn>I!CO6xy5DWg@WBr&!@{U}K%bW{Fsb(VS0 zjX@%#xwU#0OZAIMh)0>MnBi}G>ZVIY*gPtx!2cIu09{4!5s-JxeE P`P) _`Hh)?dDyB7TOuDHDLFE$yQ({~0e6(G4}Ocbq^B?c`S*X` _pndVbuGpF}1 L^BIB0|%cDj$ zt+g(ESuVD28(GP#inHw$luR|dPEXd?RaH*=qRn2iv31eax}#+tphT|6jiPH$MstPk ze_4g%&q|Fx)zC6+fl?A15&v6~DzQM;wa#eMguA*ynbm4JBa$nnXs4jOR%<3|U1+@E zx2QYqy_UctRkbu$+7T8XGt-r% 2_vIWtFi&F|H!g~mV_itD$*E7dr=l*;wVcK *VR2(=2Jy zB;(fdCBrxSjW!x4wCt{UJF7#FmakBnNAoOVwnU_jS^T20>DoJx<^xX_Of$DMB@chm zM5 M}eJ+$E=Y+VH90# z3N1XebKKNK^7SJiueqCtMlu6lQGq7r8t-RfRHhdwtX>*=*g25BB7cyoB?f(`UD3Er z2bJ6(?%CG-$UHhinU=zDOD3JVss`a33*m}u^5ysc`<;v@^4_b#kHuoNd@NwEP#`w4 z(j7Dy$(Y{BC|@s6St$6r)agotA0*(C^rnxZil>U6mYJH_Vwp?&U01=105ADYX5N<= zSvRe9gTLxu4>$F&A6<2gMY$F2T=J1Ru8wcekX#g|V~3_YHT4hLaxZZIr!Aai=&>&R zup?nXMq~ua!~%5-w87Xoy%{B$U1K}cJ~9~=Lau3^N^i 3 zFw<%LU2t7DyW%x97u3vF1V!7)nuRJ;n~Abram-9OQ(uNM8Sw>GE3yRvuGnPVmu37G z!~D0$=lgQ}(d(mC=}UhzvbF5^P=rDlMu7isSH&0vjYL_*mXDj7rg$-|3`O4GNA$(M zkUC~V3sz_#$^Lc&lFY8hE2nw=q;>|{h4(4TNF21OHtjq0anq8j@01cK1MO4$;arE1 zR5)sUA*1bNbVkwHE}U4RPQmKFzo>SLExg8dXWx#w*XTRdLYAyWw~8({tW>!?vtUXi zFZDRC2z8b={DL`V76+qdZG&l8S4RMC#JXqH1@E`eDH~(J*s^ix)`_;?wqBZ(m%Y>D z3&fhE%NA<+?`u{%u6pu&lyC3w`P?>Q>5tL#h$DK2w&wWlUEpM3vy5qiMwIk?FA?Y;UI<0h^ui)j=l{0 zVAL4_p4qx!(bvvYndKYy@;o(g}C_ij6DAWaC z+TGWucT;Ss*QA z!;%wVH`8Vq_bZ~D-Wy4Qxlz&Fp@#|JJ+#n!&)zn&F3`Zd9F9ATR2O-D`8Yc_#HEQL zHKJ&)I4}uKRs=QS2CPEm06Y!}pLsy^dZNCQi*HsJ0|>Zkv >?q;FSndH zFMxj42dSF42wmd9by##8K;`FKJ`yi4 OO9^ zAn@3!k|^fvznPR-S)0v!xV~4b?}H-{Q!%tTWd-36>(!B@lNWBO%wR;io=8P{q%u5` zbT=~gWKqaPt?;JkEY_}D?PFo{G7(JCQfCk04qn=<{O`oy@N a=!1|Vacj| zd`2{hwo|#65)cXBF?&bu70G{bf>lj!=w`2bcGV}myu(cl1eWE~21a=DU!0x0He*88 z4C0sEyYAx0haN$i;I|i}Zfi5vH;9FJYF(}Gx&uX6i^A^=?cJ@@vZImbG{#mJIK2JD z^@z>^VX`Oo6-we<4y1wJg+bS@apNiWV^mL#=|>(2ospg~*q?R5yt}VO`j4)g;Fmzl zy9wydoqcd{vTEjwklAX0kwKiH;_^*Txs9cCB5^o-liMh#Vd75tM9pOWP|0kyi?-&6 zo%o{j!h9~s`k_&C))zc-L~59nFWIr4?08f+5oc%iV7>Od7k!V7z!Vk;kW3#NL(Jx5 zhasAh?q((v32vXXZ}p3Vu3v>3Re+Pp%BG8Ri=tgeLD4%ll#f|x&AC>b6LX&_!xS;h zEmMWUY;dtSEQ+FDbHd$3hy~V`;2aQ4lueVyWP;NcTqzbv-7}d>cLW5vQ9pu&mDre- z68oJ|4rXSHp?>w{%kTgDJA9=ozTZusKK7SQ+{Z;HKbn%d*{hPF>x8cMvR-joKK`bK zYepWB)urlmEyvVmK1G|4Nj__5MFO8_oR=hDEE~8aqQ`7|+Q=d?A8cV!DeGQ;)lF9D zxj_lH|B5Zs;ZGG;B4b^@#> X50G;vnW(SG z9V~=M=VSf16lf|sR9uvnI#KR@D;ke0S?MM~$C|!RU4RQJA<}-vDFO?ub(`{1GhS?3 zu^|FH@q*WT_gS$tv^VCt_a9&W^}qj`pc<}RWx0S{Z$y^M%dAJa$k%!mxyYT&Sd)ij zwMet%Bu(Y{urP~B`4u+hiXF SlDJYT=H9uK1xmLpU z{W!_Uhs7li+4k3qThE)xP8RNDFnE__8ke -HwO=n9zwT+SlAuP|$DGaVIvL}k)SG%jI% z)hMP{16KAI1t>FwOIcGiU2i6(d}KMJT1+EkEesa0!xZZ+6u6Dma#?f}=0z(B3ni VygxpIA=N_CAOpE~KcXe8F9 z^9i)^AHn!~U7={136km}g;25PT}l|$OJrOUV^vb;_%$E7PSsS+wHA=G3k`mX=>C}N z1FF0v+oH;fZ%%Yt+rK90MPVMhz^eTe7l?Om65vAUgkuB`ifc;?7@D}q23IerP<)&e z%Z6dfzzdfHM+VfJqzk&{O!9?**}QxK*HVj1))pFPGupJ56%|uMse#s3II3l}Von(n z6SY#;tj^2VaE!75qBg~%s~fbWPQ^&8XL5eP8L9PVOCU1c#aOc<+ivZ3$vP~o=#;LA zFTA@)k6MXh_XL(uTs6&cx%AD&y8zhH@pk~*$gZ1l+ujm{5x4^oKYgMSv9;9iW%AHJ zfE0tMa{BIe-=6o~7J!rZDV`2|eZzfd>c<$0(-a2hLhQKtDsT!2G6YRV-eGYY?F&JI z{MA*ImkfzX7C5%Rp$bd~EYElG*%VnM-<}^Wf zh;mbIHc?`!4+iaII7C!^WS5PSy^bpYK(K&vxM6z%8XyQn1So{!)Au?ML9+v24DcE~ z)Wo;A5knROZ0g$!9m^rkKv)AI+Z~v Kv} zM}$VokgDQ~9+!CrveQkaqg-`BhBj rj-(Nrd0 z& oH*I_Z#LSGP~tcB zM_jsy3t^{sVZ7wHrV*P4Kv3HY3E~mGLET`3nraM}wHrf#*XfJ^5*VU2Dw65|TN3yi zCwWaEFQQpCve3{0jD^(>#HgCX|C4|i zrMHCBo 0L>Hos zfnWG)LPWo6{;mIn^0Dwwut)|Ss)MLl;GMGt#2z`*TdR6_A-3jG9dBrO?5}PJXntm# zfJM_sdW!;tZ&4ON?T#U%1oOx$G@^q+%&~6_U;g<&|CD-js*mWhc96>Yc47Jfw|+Fo zx^B2tsiv^N(yn(ukBSD+Yr0i^v1?BDnd1+TCY=jvME3Sh00GuCINA@)(l6t#uM85C zDSd^8sagwWs}{UG0lYh#Y$z-WOZwEz;xbmBO|8J&ONO>VpFSYonqMlb6@Y;i)l}w` z@1Ca;STWQUaV@!thuD-EtU`1OD)X;5Z}YQ=*ol*LpQuCg3G)ou=Xsey*ktfY)l=n9 zB|lZGmO}=t13)nJU@$6*m9Dv9;aCk#1;wYEm8*%~Np%#M6?Wv0MKyx6ND6@fd9}e5 zYK?F?bF=aClz>A-dtbsNiVKYZ*@rPM4A3VcNzHMlN~5avn}SJ+P#CYNbHTErtV})E zUH#Vg!cZ|vE{Ka7B;L`NI=pb1!22;%#F0T)*$Tk&UI$P4WB#r}w0scg=TnmXOI 3Q+V^7>oOyjBjKE}sF0^$~j`U)!5N7+R6}rH(+Ab+-7x>&3%>UuhY@pk zgUs45RG)sD%*>#L-rL+(0T*o2k&631kXWWm?lRNTC0`(j>IJo)RG0)Knw3N#b<2g; zWWl2HW>*vq H;=brk#mtP5Jaq`pw@KXeXutcGP0TP^@xC*m^b33xI zC3<<|5VUEb_tPpXzzzk7k8rF5$0nPDjv~F5HWvHui?kQ!Dl<@mMWm!?33Rrd8i{;B zxsq(TX0=G~GO>C0^{|zw9dZ$Zhq|83>Uv7%P8T!c46q^FG9CqT6+mVM4k%-s#)0gP zu#m!B+n(%I8F>~Pr*U%T QAFksoBX`8_7i^sKxozLF;R z2Gc51LLX2(hXPKa#s;E7Xl1R)exa9^6wZX-k`PZZYo>W?VVxOfmgG^R0D*!~r6sgN zOGexkPzYhpsS9X6D_UqX=!Wc-g^HpUc!BgJiAhKjOtY*-N*oedfL9ZSXc6UE1878X zWq=- ~>cE=$XB3`22@;H`X1o&K{xeaeDb PB~`V}=(3z; zO*L9_r1?#qi9!=OEPVW#TTYu OPlLxnfJ}VQOj>HxaT^ zK7u^h#jWljB6SN+?ITkASZUezl9gb07Fv93&lBT1oW_na|h^^J{J!lQ+6xhIPTio;(h0? zH>6`}r-rr38roMTG!=?xLhQD^*mJ2 z=*u}!Pp6Y`8ci!Zqg|a&Z(?_XH{qkJK}&FJqR%SF21<)d0GmYHOWbxGch2 7#}8M|F^=d|4p=i7;fG*C_2fI}Hl(w8`!bO_C!_yZY3 zkhw$EY445;)0cySQL@&RzBmuw{Z3zMfgqwTbF_I)R~mHNz^)BnZmvlKr!C-VNP;(V zR(C2k+m};}o#urplcQb+`%O~3l zvnFe&sHh28v!F6-Xi*N2R;MB0g5%2^Sb(kX{h0&(Rk-(SAEKC^aNC#3wL6q!M zKOT?``0%pHdo?}PxMH*lBiE1QM65EK!#Fi;oN0}E(?bTF_dIInAF zGh9g_SX9odJ!S=p9ROGo-*a}{9NF1ac#lC{yy)tFVS}@Q9*FH|(L7X8Bjs`AhS3qQ zvDBkp2!pnWh%xOY?H%maNEo8$&`TdFW0X`OBW|=hF%T~5#TBtozzi5#3-?)z{!vKP zB<2+638Jd?lu@?@WUH7vYRa7pN*XjCutbJ4hWOy51or6>IR;5*9`p73<+p$QmI6wc zNL?XIKq#5*?yry{gg}&Gt-gKU?xTl6CE1c6K>#QhmIkSzGrVy`0mi;4RET5{0Ty{9 ziF8L7{i(-JiF^DfG&8ZpM+AE$VnpOEJA&CCNT9&7sIfk#uq2V4%3>GH7+dJJWT+*^ z8R&=17m7tJ2#3)~fgLE=NMRm{rw~pp4JTq8Fj)9kWp80QO@@83D2fVZq(h@*8u9K1 z# ooYAeVefR!2xrt*uM{R#2mFi`QChJ0l=y`?7fB`o>coW)Mn@upCNKF z;MQi7Ian*KK {&5U>tE-;kkC@IahnJHS&hs3{VVNu11;({I0!ir+w3d@|110sopZ9vvT8 zLp$t-ryLN&om!$P*bCa5CT^n4YhaRqF<8Kij5fWfxkm@%v6Pd!$Dr|vxK1PgmO-c4 z(-mZfhpWqHV0V0a3sVe@MSPFP2&=KXiu7=?)Ag1w$?7q*llAg+Uh68q>uS0rgp5vE z2B3J%%T{#y>5918!pBwiiT57{?wD%);jmD=qu(f^7(Wz)sLjT-+Bi)g(C=3Ob@^ z8Wlex4Q^BfoCF3d5pz14Kuy;*M=~O|{R&4;bjg{7lpX+Bn06YWwkp-DWSYg4LEp2b zsa*?zbdiY{o-Qp4R*AVD!BJz}Aw4R@20V-deN(0@;_1TJB@5TeBno$<{8A4}72%nM z*8<+Ja$@2_=^{ALW>=z07IUL*P=n_Nv(R<1NI$O2PCZD8G2efD&!BZ-zxi}${R$ex zM{ce+E6Gk}DO;9{{i?*{R~}?RFM b~Wp8g~55QYnWAthkWRd=}Rr!uorxu}B27+-$mPRWtv=5gee#T+S+ zfFeMNUJ*TAc~d~QEW(Q@+bk{&f1pMP%C@|>YUfDmibchibR_v{I$=_tcTHuL-C4vo zYRPu?cEw3F0H;Ji11YVg2=|$CwUj`#7D>=urq D>Z>q z8VmJ;W|5Z~iLCKP#*a(J)^gG5d?jCnUXTSM$w5eD%@`VkFw$xP z*i=#qE?S}FF sP#aA6oU z#?{V4r8hMUnFR=H!>yD+hL%-~nzfw>(|8Rhq 70e+_Om?M@bSQN)c_p;}h< zpShmo9@Lh-kFt{?7U>e$Y!#yxC6=4?4Us+~(oF`)Br{CH8emfxCYB_Rv_!&dEE_69 zA 8_P=To`hg _y6^+&w6;2;^OQ}GR|NO7p^$;@|TL0lA_PV;QH zK*iH)PYOz=b_!w9QgSTcHp7Y2iBfJ{sYRkLdD@u8o$1XfYL_g)m}xHb#6{n}{wH~{ zf^5_Ho4#=h*XfsXXj$}`qN%A)v+$(l%ZJPm-P%J;tpf^5GJe!}Z!5%4$8WiUQMclv zMU|G@bKb?UZ3fTw?P&>dH>}zg)-~6YHB~<5dQzIA=HIx?ed74C;3L!Cr;_nEhmiBJ z6X8bhq<01eC-J7b!r8U>@j$VnBHq-Tg_H8vum8zIT G@PEL0PN57rrXk zMQdCPJGIK{?xKxZ?qD>E1k}Vo@B_%G?5drsIKKY7m+ozi`seTXqyDQKp(Phvj_qHi ze7&RfUW)1uNNg&ff|rLi1?4ZPBa_Txn=2w+7yDJgUMjX|9+Ca4^grx|D>k`kt`9dg z74Kr$`K$&bceSk+g%`c1U}qW@?vKt4dY5zr{{9 t_p-lEt3MzyXk +dtTCwa&^?m-X5P6@-b3eqmB8SNSq7tVjl=i!`Y;Lcr zSfyK$65~8Yy~6Lhv!su9=NC2QD{}r;a$ohG_|MvTr+Vx*Q#eB5j8VL{Jlt9IvMkn7 zQ53Sh7dXI@WRuRV4L9HFd*>NGydS!`$M<72*vA!Rv^$Hxw`?DceodQw@aW~?jupqK z;yZoaUO}DXPA4Na&ddcwbo|)fKzcx7zuJ!M6$VHv#)IbFv?gzsciXsYYDe|mX>%0r zMm4)QL2bkLdc`ixVP2u%do5>qyV&w*JY5|dO3Y~_w?L PH8U;IKPa1W%`SbVh59pqefaD>P@2yP({>X=THzvpwTkASJ7DTZPz3PiRizI5#<; z-bNE$(A%4pZ+D)fp;N)bsrJ_qp|-=ItxX)QSuJiylyr}_Gc=oj9ueW*$afm-o&IAq z`@O)3N}Ctx7KWUYksh16<3+jm2JamWMs^g1CAI(4b!6!591VB6o89K60psY|&T-Sr zWXSW*&i2-6a}=KJ49fSjd@zZ7F^nCd-+Qyphc}aQZayNPTDX@V1Ax_Rw?Gj$ifo5Y z4` ax jLR_uYob<%%HarO!pz*F=mQDC_HrB z)w%h>!~}>@w>P+ZOWGY*BTnr@!Ek3JApLJY3UopAaC1=NrD>)Ajes4Q?bg|CW#j@N zf!z&> JBsWytu14fxx?+6T2DMMsm8DobmkKi;bwU<3NiVw1L}_5v<+yxElvHG^5e( zG6wkFOU924ch!I=cZ-5d0yVh5L7O8LGcBMb&%D+8iyM}D@Uj}*anOQ%VtDTU>YRNW z0vX|~D|$~pj{8gw`i;34aZg*zuruOb%qe|(K4fSd CEu0zsH~OjgSIPaAP)l7^M)C_rau}+OiLC2UWzVC*{k1YG z-I%@eYfpSsbnQW0`@P3p4wYL8$pg6hGC6n_Zt{^o`}QZLYK!ZZ$L@aJxx-{S#@seu zWvMk6?$-thycPr1UJ(%OZ+cC`BE4=ikru{na>)yp_eWmAa;^+r)4T?g&~nU3jcq|k zqs;}bm;4kGnve$82|>z>DU&91%e>6#zqsP_6D2miaz~p9jl{b1`mq50DSs=>KNJl= z{#U&{eDJ#c1R-onVeYF{aQV}tbDd@XvDc5->Ut>uWZL#mjZhDakw0x9df@tS*84r> zy2&r`_k(2jhe!NBpQBIr!|RFR^ZLC0bP;Cz8N*)n4f^@_w;PQ ?B^knLaSBh=Hx|6EcZS^V?7{)MmGeDUiiUeApx&&=`piv4^`V7cToDm|}%?)CW% z`uR5fygskb>rZ-p2I!wP&3yhn|DGfF-{+eAJp=S#-3gz6&(HVIzhCy#-!nk}X@7sv zs-WkGyuZvf!WccT&rclBzaNzF{=MSMbN%d3yg&PS{b{ey0R01BPcFa@{pGLEnEt#z zuh*~70R02=XpA+__w-*1=jeI;g|5#G`Z?MCfh}0i_qFHsdHrG6zwnozb|HLxR_Mw9 zJ_q>f&sxCnhY9dgK4-6dlvw|8arLvtkN!OJtAF14I9Bbi$larD3T!D_nK}QnX5BCL zb;E L_6+oXIwi`xNe-wd_pgu~-A=R6R7B79Z0md5NrrmHoBO zX_oZ6h2M|G==G2!;kvs+;;E-h(;bpNXVq;E_wd0N$tic0n_Lo?cavK`yAKS=+7_-1 z3ny*UumT${+=;^HDa-c!A$th4JI8vLcsK yF;1%`H(p3 zurnm_ENz