diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-02 22:54:33 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-02 22:54:33 -0800 |
commit | 3dec7d563a2f3e1eb967ce2054a00b6620e3558c (patch) | |
tree | aa3b0365c47cb3c1607c0dc76c8d32b4046fc287 /core/java/android | |
parent | 15ab3eae2ec3d73b3e8aa60b33ae41445bf83f4b (diff) | |
download | frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.zip frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.tar.gz frameworks_base-3dec7d563a2f3e1eb967ce2054a00b6620e3558c.tar.bz2 |
auto import from //depot/cupcake/@137055
Diffstat (limited to 'core/java/android')
79 files changed, 4321 insertions, 1364 deletions
diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index 394b8e3..3b5ad86 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -1487,7 +1487,7 @@ class ApplicationContext extends Context { static final class ApplicationPackageManager extends PackageManager { @Override public PackageInfo getPackageInfo(String packageName, int flags) - throws NameNotFoundException { + throws NameNotFoundException { try { PackageInfo pi = mPM.getPackageInfo(packageName, flags); if (pi != null) { @@ -1500,6 +1500,43 @@ class ApplicationContext extends Context { throw new NameNotFoundException(packageName); } + public Intent getLaunchIntentForPackage(String packageName) + throws NameNotFoundException { + // First see if the package has an INFO activity; the existence of + // such an activity is implied to be the desired front-door for the + // overall package (such as if it has multiple launcher entries). + Intent intent = getLaunchIntentForPackageCategory(this, packageName, + Intent.CATEGORY_INFO); + if (intent != null) { + return intent; + } + + // Otherwise, try to find a main launcher activity. + return getLaunchIntentForPackageCategory(this, packageName, + Intent.CATEGORY_LAUNCHER); + } + + // XXX This should be implemented as a call to the package manager, + // to reduce the work needed. + static Intent getLaunchIntentForPackageCategory(PackageManager pm, + String packageName, String category) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent intentToResolve = new Intent(Intent.ACTION_MAIN, null); + intentToResolve.addCategory(category); + final List<ResolveInfo> apps = + pm.queryIntentActivities(intentToResolve, 0); + // I wish there were a way to directly get the "main" activity of a + // package but ... + for (ResolveInfo app : apps) { + if (app.activityInfo.packageName.equals(packageName)) { + intent.setClassName(packageName, app.activityInfo.name); + return intent; + } + } + return null; + } + @Override public int[] getPackageGids(String packageName) throws NameNotFoundException { diff --git a/core/java/android/app/ExpandableListActivity.java b/core/java/android/app/ExpandableListActivity.java index 75dfcae..a2e048f 100644 --- a/core/java/android/app/ExpandableListActivity.java +++ b/core/java/android/app/ExpandableListActivity.java @@ -63,21 +63,21 @@ import java.util.Map; * * <pre> * <?xml version="1.0" encoding="UTF-8"?> - * <LinearLayout + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:orientation="vertical" * android:layout_width="fill_parent" * android:layout_height="fill_parent" - * android:paddingLeft="8" - * android:paddingRight="8"> + * android:paddingLeft="8dp" + * android:paddingRight="8dp"> * - * <ExpandableListView id="android:list" + * <ExpandableListView android:id="@id/android:list" * android:layout_width="fill_parent" * android:layout_height="fill_parent" * android:background="#00FF00" * android:layout_weight="1" * android:drawSelectorOnTop="false"/> * - * <TextView id="android:empty" + * <TextView android:id="@id/android:empty" * android:layout_width="fill_parent" * android:layout_height="fill_parent" * android:background="#FF0000" @@ -113,19 +113,19 @@ import java.util.Map; * * <pre> * <?xml version="1.0" encoding="utf-8"?> - * <LinearLayout + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:layout_width="fill_parent" * android:layout_height="wrap_content" * android:orientation="vertical"> * - * <TextView id="text1" - * android:textSize="16" + * <TextView android:id="@+id/text1" + * android:textSize="16sp" * android:textStyle="bold" * android:layout_width="fill_parent" * android:layout_height="wrap_content"/> * - * <TextView id="text2" - * android:textSize="16" + * <TextView android:id="@+id/text2" + * android:textSize="16sp" * android:layout_width="fill_parent" * android:layout_height="wrap_content"/> * </LinearLayout> @@ -162,7 +162,8 @@ public class ExpandableListActivity extends Activity implements /** * Override this to populate the context menu when an item is long pressed. menuInfo - * will contain a {@link AdapterContextMenuInfo} whose position is a packed position + * will contain an {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} + * whose packedPosition is a packed position * that should be used with {@link ExpandableListView#getPackedPositionType(long)} and * the other similar methods. * <p> diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java new file mode 100644 index 0000000..2b12a2a --- /dev/null +++ b/core/java/android/app/IntentService.java @@ -0,0 +1,74 @@ +package android.app; + +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; + +/** + * An abstract {@link Service} that serializes the handling of the Intents passed upon service + * start and handles them on a handler thread. + * + * <p>To use this class extend it and implement {@link #onHandleIntent}. The {@link Service} will + * automatically be stopped when the last enqueued {@link Intent} is handled. + */ +public abstract class IntentService extends Service { + private volatile Looper mServiceLooper; + private volatile ServiceHandler mServiceHandler; + private String mName; + + private final class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + onHandleIntent((Intent)msg.obj); + stopSelf(msg.arg1); + } + } + + public IntentService(String name) { + super(); + mName = name; + } + + @Override + public void onCreate() { + super.onCreate(); + HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); + thread.start(); + + mServiceLooper = thread.getLooper(); + mServiceHandler = new ServiceHandler(mServiceLooper); + } + + @Override + public void onStart(Intent intent, int startId) { + super.onStart(intent, startId); + Message msg = mServiceHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = intent; + mServiceHandler.sendMessage(msg); + } + + @Override + public void onDestroy() { + mServiceLooper.quit(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Invoked on the Handler thread with the {@link Intent} that is passed to {@link #onStart}. + * Note that this will be invoked from a different thread than the one that handles the + * {@link #onStart} call. + */ + protected abstract void onHandleIntent(Intent intent); +} diff --git a/core/java/android/app/ListActivity.java b/core/java/android/app/ListActivity.java index 2818937..5523c18 100644 --- a/core/java/android/app/ListActivity.java +++ b/core/java/android/app/ListActivity.java @@ -53,22 +53,22 @@ import android.widget.ListView; * </p> * * <pre> - * <?xml version="1.0" encoding="UTF-8"?> - * <LinearLayout + * <?xml version="1.0" encoding="utf-8"?> + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:orientation="vertical" * android:layout_width="fill_parent" * android:layout_height="fill_parent" - * android:paddingLeft="8" - * android:paddingRight="8"> + * android:paddingLeft="8dp" + * android:paddingRight="8dp"> * - * <ListView id="android:list" + * <ListView android:id="@id/android:list" * android:layout_width="fill_parent" * android:layout_height="fill_parent" * android:background="#00FF00" * android:layout_weight="1" * android:drawSelectorOnTop="false"/> * - * <TextView id="android:empty" + * <TextView id="@id/android:empty" * android:layout_width="fill_parent" * android:layout_height="fill_parent" * android:background="#FF0000" @@ -99,19 +99,19 @@ import android.widget.ListView; * * <pre> * <?xml version="1.0" encoding="utf-8"?> - * <LinearLayout + * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:layout_width="fill_parent" * android:layout_height="wrap_content" * android:orientation="vertical"> * - * <TextView id="text1" - * android:textSize="16" + * <TextView android:id="@+id/text1" + * android:textSize="16sp" * android:textStyle="bold" * android:layout_width="fill_parent" * android:layout_height="wrap_content"/> * - * <TextView id="text2" - * android:textSize="16" + * <TextView android:id="@+id/text2" + * android:textSize="16sp" * android:layout_width="fill_parent" * android:layout_height="wrap_content"/> * </LinearLayout> @@ -142,8 +142,8 @@ import android.widget.ListView; * public class MyListAdapter extends ListActivity { * * @Override - * protected void onCreate(Bundle icicle){ - * super.onCreate(icicle); + * protected void onCreate(Bundle savedInstanceState){ + * super.onCreate(savedInstanceState); * * // We'll define a custom screen layout here (the one shown above), but * // typically, you could just use the standard ListActivity layout. diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index afb3827..39edab7 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -82,9 +82,7 @@ public class NotificationManager * @param id An identifier for this notification unique within your * application. * @param notification A {@link Notification} object describing how to - * notify the user, other than the view you're providing. If you - * pass null, there will be no persistent notification and no - * flashing, vibration, etc. + * notify the user, other than the view you're providing. Must not be null. */ public void notify(int id, Notification notification) { diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 7b8256c..d447eb2 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -448,6 +448,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } } mSearchTextField.setInputType(inputType); + mSearchTextField.setImeOptions(mSearchable.getImeOptions()); } } @@ -793,7 +794,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // otherwise, dispatch an "edit view" key switch (keyCode) { case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_DPAD_CENTER: if (event.getAction() == KeyEvent.ACTION_UP) { v.cancelLongPress(); launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 2cc6de9..c1d66f4 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -747,6 +747,14 @@ import android.view.KeyEvent; * <a href="../R.attr.html#inputType">inputType</a> attribute.</td> * <td align="center">No</td> * </tr> + * <tr><th>android:imeOptions</th> + * <td>If provided, supplies additional options for the input method. + * For most searches, in which free form text is expected, this attribute + * need not be provided, and will default to "actionSearch". + * Suitable values for this attribute are described in the + * <a href="../R.attr.html#imeOptions">imeOptions</a> attribute.</td> + * <td align="center">No</td> + * </tr> * * </tbody> * </table> diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index 56b231f..1ba1c1e 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -63,8 +63,10 @@ public class BluetoothDevice { public static final int UNBOND_REASON_AUTH_CANCELED = 3; /** A bond attempt failed because we could not contact the remote device */ public static final int UNBOND_REASON_REMOTE_DEVICE_DOWN = 4; + /** A bond attempt failed because a discovery is in progress */ + public static final int UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5; /** An existing bond was explicitly revoked */ - public static final int UNBOND_REASON_REMOVED = 5; + public static final int UNBOND_REASON_REMOVED = 6; private static final String TAG = "BluetoothDevice"; diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 3a64cee..25544de 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -18,6 +18,7 @@ package android.content; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; import android.content.res.Configuration; import android.database.Cursor; import android.database.CursorToBulkCursorAdaptor; @@ -162,6 +163,13 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.openFile(uri, mode); } + public AssetFileDescriptor openAssetFile(Uri uri, String mode) + throws FileNotFoundException { + if (mode != null && mode.startsWith("rw")) checkWritePermission(uri); + else checkReadPermission(uri); + return ContentProvider.this.openAssetFile(uri, mode); + } + public ISyncAdapter getSyncAdapter() { checkWritePermission(null); return ContentProvider.this.getSyncAdapter().getISyncAdapter(); @@ -438,8 +446,9 @@ public abstract class ContentProvider implements ComponentCallbacks { * of this method should create a new ParcelFileDescriptor for each call. * * @param uri The URI whose file is to be opened. - * @param mode Access mode for the file. May be "r" for read-only access - * or "rw" for read and write access. + * @param mode Access mode for the file. May be "r" for read-only access, + * "rw" for read and write access, or "rwt" for read and write access + * that truncates any existing file. * * @return Returns a new ParcelFileDescriptor which you can use to access * the file. @@ -448,19 +457,66 @@ public abstract class ContentProvider implements ComponentCallbacks { * no file associated with the given URI or the mode is invalid. * @throws SecurityException Throws SecurityException if the caller does * not have permission to access the file. - */ + * + * @see #openAssetFile(Uri, String) + * @see #openFileHelper(Uri, String) + */ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { throw new FileNotFoundException("No files supported by provider at " + uri); } + + /** + * This is like {@link #openFile}, but can be implemented by providers + * that need to be able to return sub-sections of files, often assets + * inside of their .apk. Note that when implementing this your clients + * must be able to deal with such files, either directly with + * {@link ContentResolver#openAssetFileDescriptor + * ContentResolver.openAssetFileDescriptor}, or by using the higher-level + * {@link ContentResolver#openInputStream ContentResolver.openInputStream} + * or {@link ContentResolver#openOutputStream ContentResolver.openOutputStream} + * methods. + * + * <p><em>Note: if you are implementing this to return a full file, you + * should create the AssetFileDescriptor with + * {@link AssetFileDescriptor#UNKNOWN_LENGTH} to be compatible with + * applications that can not handle sub-sections of files.</em></p> + * + * @param uri The URI whose file is to be opened. + * @param mode Access mode for the file. May be "r" for read-only access, + * "w" for write-only access (erasing whatever data is currently in + * the file), "wa" for write-only access to append to any existing data, + * "rw" for read and write access on any existing data, and "rwt" for read + * and write access that truncates any existing file. + * + * @return Returns a new AssetFileDescriptor which you can use to access + * the file. + * + * @throws FileNotFoundException Throws FileNotFoundException if there is + * no file associated with the given URI or the mode is invalid. + * @throws SecurityException Throws SecurityException if the caller does + * not have permission to access the file. + * + * @see #openFile(Uri, String) + * @see #openFileHelper(Uri, String) + */ + public AssetFileDescriptor openAssetFile(Uri uri, String mode) + throws FileNotFoundException { + ParcelFileDescriptor fd = openFile(uri, mode); + return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; + } /** * Convenience for subclasses that wish to implement {@link #openFile} * by looking up a column named "_data" at the given URI. * * @param uri The URI to be opened. - * @param mode The file mode. + * @param mode The file mode. May be "r" for read-only access, + * "w" for write-only access (erasing whatever data is currently in + * the file), "wa" for write-only access to append to any existing data, + * "rw" for read and write access on any existing data, and "rwt" for read + * and write access that truncates any existing file. * * @return Returns a new ParcelFileDescriptor that can be used by the * client to access the file. @@ -489,16 +545,7 @@ public abstract class ContentProvider implements ComponentCallbacks { throw new FileNotFoundException("Column _data not found."); } - int modeBits; - if ("r".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_ONLY; - } else if ("rw".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_WRITE - | ParcelFileDescriptor.MODE_CREATE; - } else { - throw new FileNotFoundException("Bad mode for " + uri + ": " - + mode); - } + int modeBits = ContentResolver.modeToMode(uri, mode); return ParcelFileDescriptor.open(new File(path), modeBits); } diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index ede2c9b..e5e3f74 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -16,6 +16,7 @@ package android.content; +import android.content.res.AssetFileDescriptor; import android.database.BulkCursorNative; import android.database.BulkCursorToCursorAdaptor; import android.database.Cursor; @@ -187,6 +188,25 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case OPEN_ASSET_FILE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String mode = data.readString(); + + AssetFileDescriptor fd; + fd = openAssetFile(url, mode); + reply.writeNoException(); + if (fd != null) { + reply.writeInt(1); + fd.writeToParcel(reply, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + reply.writeInt(0); + } + return true; + } + case GET_SYNC_ADAPTER_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -413,6 +433,29 @@ final class ContentProviderProxy implements IContentProvider return fd; } + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(mode); + + mRemote.transact(IContentProvider.OPEN_ASSET_FILE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(reply); + int has = reply.readInt(); + AssetFileDescriptor fd = has != 0 + ? AssetFileDescriptor.CREATOR.createFromParcel(reply) : null; + + data.recycle(); + reply.recycle(); + + return fd; + } + public ISyncAdapter getSyncAdapter() throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 52f55b6..0d886ee 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -17,6 +17,7 @@ package android.content; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; @@ -28,6 +29,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.text.TextUtils; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -170,119 +172,100 @@ public abstract class ContentResolver { * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li> * <li>file ({@link #SCHEME_FILE})</li> * </ul> - * <h5>The android.resource ({@link #SCHEME_ANDROID_RESOURCE}) Scheme</h5> - * <p> - * A Uri object can be used to reference a resource in an APK file. The - * Uri should be one of the following formats: - * <ul> - * <li><code>android.resource://package_name/id_number</code><br/> - * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. - * For example <code>com.example.myapp</code><br/> - * <code>id_number</code> is the int form of the ID.<br/> - * The easiest way to construct this form is - * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/" + R.raw.my_resource");</pre> - * </li> - * <li><code>android.resource://package_name/type/name</code><br/> - * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. - * For example <code>com.example.myapp</code><br/> - * <code>type</code> is the string form of the resource type. For example, <code>raw</code> - * or <code>drawable</code>. - * <code>name</code> is the string form of the resource name. That is, whatever the file - * name was in your res directory, without the type extension. - * The easiest way to construct this form is - * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/raw/my_resource");</pre> - * </li> - * </ul> - * @param uri The desired "content:" URI. + * + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * + * @param uri The desired URI. * @return InputStream * @throws FileNotFoundException if the provided URI could not be opened. + * @see #openAssetFileDescriptor(Uri, String) */ public final InputStream openInputStream(Uri uri) throws FileNotFoundException { String scheme = uri.getScheme(); - if (SCHEME_CONTENT.equals(scheme)) { - ParcelFileDescriptor fd = openFileDescriptor(uri, "r"); - return fd != null ? new ParcelFileDescriptor.AutoCloseInputStream(fd) : null; - } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { - String authority = uri.getAuthority(); - Resources r; - if (TextUtils.isEmpty(authority)) { - throw new FileNotFoundException("No authority: " + uri); - } else { - try { - r = mContext.getPackageManager().getResourcesForApplication(authority); - } catch (NameNotFoundException ex) { - throw new FileNotFoundException("No package found for authority: " + uri); - } - } - List<String> path = uri.getPathSegments(); - if (path == null) { - throw new FileNotFoundException("No path: " + uri); - } - int len = path.size(); - int id; - if (len == 1) { - try { - id = Integer.parseInt(path.get(0)); - } catch (NumberFormatException e) { - throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); - } - } else if (len == 2) { - id = r.getIdentifier(path.get(1), path.get(0), authority); - } else { - throw new FileNotFoundException("More than two path segments: " + uri); - } - if (id == 0) { - throw new FileNotFoundException("No resource found for: " + uri); - } + if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { + // Note: left here to avoid breaking compatibility. May be removed + // with sufficient testing. + OpenResourceIdResult r = getResourceId(uri); try { - InputStream stream = r.openRawResource(id); + InputStream stream = r.r.openRawResource(r.id); return stream; } catch (Resources.NotFoundException ex) { - throw new FileNotFoundException("Resource ID does not exist: " + uri); + throw new FileNotFoundException("Resource does not exist: " + uri); } } else if (SCHEME_FILE.equals(scheme)) { + // Note: left here to avoid breaking compatibility. May be removed + // with sufficient testing. return new FileInputStream(uri.getPath()); } else { - throw new FileNotFoundException("Unknown scheme: " + uri); + AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r"); + try { + return fd != null ? fd.createInputStream() : null; + } catch (IOException e) { + throw new FileNotFoundException("Unable to create stream"); + } } } /** + * Synonym for {@link #openOutputStream(Uri, String) + * openOutputStream(uri, "w")}. + * @throws FileNotFoundException if the provided URI could not be opened. + */ + public final OutputStream openOutputStream(Uri uri) + throws FileNotFoundException { + return openOutputStream(uri, "w"); + } + + /** * Open a stream on to the content associated with a content URI. If there * is no data associated with the URI, FileNotFoundException is thrown. * * <h5>Accepts the following URI schemes:</h5> * <ul> * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>file ({@link #SCHEME_FILE})</li> * </ul> * - * @param uri The desired "content:" URI. + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * + * @param uri The desired URI. + * @param mode May be "w", "wa", "rw", or "rwt". * @return OutputStream + * @throws FileNotFoundException if the provided URI could not be opened. + * @see #openAssetFileDescriptor(Uri, String) */ - public final OutputStream openOutputStream(Uri uri) + public final OutputStream openOutputStream(Uri uri, String mode) throws FileNotFoundException { - String scheme = uri.getScheme(); - if (SCHEME_CONTENT.equals(scheme)) { - ParcelFileDescriptor fd = openFileDescriptor(uri, "rw"); - return fd != null - ? new ParcelFileDescriptor.AutoCloseOutputStream(fd) : null; - } else { - throw new FileNotFoundException("Unknown scheme: " + uri); + AssetFileDescriptor fd = openAssetFileDescriptor(uri, mode); + try { + return fd != null ? fd.createOutputStream() : null; + } catch (IOException e) { + throw new FileNotFoundException("Unable to create stream"); } } /** * Open a raw file descriptor to access data under a "content:" URI. This - * interacts with the underlying {@link ContentProvider#openFile} - * ContentProvider.openFile()} method of the provider associated with the - * given URI, to retrieve any file stored there. + * is like {@link #openAssetFileDescriptor(Uri, String)}, but uses the + * underlying {@link ContentProvider#openFile} + * ContentProvider.openFile()} method, so will <em>not</em> work with + * providers that return sub-sections of files. If at all possible, + * you should use {@link #openAssetFileDescriptor(Uri, String)}. You + * will receive a FileNotFoundException exception if the provider returns a + * sub-section of a file. * * <h5>Accepts the following URI schemes:</h5> * <ul> * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>file ({@link #SCHEME_FILE})</li> * </ul> * + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * * @param uri The desired URI to open. * @param mode The file mode to use, as per {@link ContentProvider#openFile * ContentProvider.openFile}. @@ -290,32 +273,189 @@ public abstract class ContentResolver { * own this descriptor and are responsible for closing it when done. * @throws FileNotFoundException Throws FileNotFoundException of no * file exists under the URI or the mode is invalid. + * @see #openAssetFileDescriptor(Uri, String) */ public final ParcelFileDescriptor openFileDescriptor(Uri uri, String mode) throws FileNotFoundException { - IContentProvider provider = acquireProvider(uri); - if (provider == null) { - throw new FileNotFoundException("No content provider: " + uri); + AssetFileDescriptor afd = openAssetFileDescriptor(uri, mode); + if (afd == null) { + return null; + } + + if (afd.getDeclaredLength() < 0) { + // This is a full file! + return afd.getParcelFileDescriptor(); } + + // Client can't handle a sub-section of a file, so close what + // we got and bail with an exception. try { - ParcelFileDescriptor fd = provider.openFile(uri, mode); - if(fd == null) { + afd.close(); + } catch (IOException e) { + } + + throw new FileNotFoundException("Not a whole file"); + } + + /** + * Open a raw file descriptor to access data under a "content:" URI. This + * interacts with the underlying {@link ContentProvider#openAssetFile} + * ContentProvider.openAssetFile()} method of the provider associated with the + * given URI, to retrieve any file stored there. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li> + * <li>file ({@link #SCHEME_FILE})</li> + * </ul> + * <h5>The android.resource ({@link #SCHEME_ANDROID_RESOURCE}) Scheme</h5> + * <p> + * A Uri object can be used to reference a resource in an APK file. The + * Uri should be one of the following formats: + * <ul> + * <li><code>android.resource://package_name/id_number</code><br/> + * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. + * For example <code>com.example.myapp</code><br/> + * <code>id_number</code> is the int form of the ID.<br/> + * The easiest way to construct this form is + * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/" + R.raw.my_resource");</pre> + * </li> + * <li><code>android.resource://package_name/type/name</code><br/> + * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. + * For example <code>com.example.myapp</code><br/> + * <code>type</code> is the string form of the resource type. For example, <code>raw</code> + * or <code>drawable</code>. + * <code>name</code> is the string form of the resource name. That is, whatever the file + * name was in your res directory, without the type extension. + * The easiest way to construct this form is + * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/raw/my_resource");</pre> + * </li> + * </ul> + * + * @param uri The desired URI to open. + * @param mode The file mode to use, as per {@link ContentProvider#openAssetFile + * ContentProvider.openAssetFile}. + * @return Returns a new ParcelFileDescriptor pointing to the file. You + * own this descriptor and are responsible for closing it when done. + * @throws FileNotFoundException Throws FileNotFoundException of no + * file exists under the URI or the mode is invalid. + */ + public final AssetFileDescriptor openAssetFileDescriptor(Uri uri, + String mode) throws FileNotFoundException { + String scheme = uri.getScheme(); + if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { + if (!"r".equals(mode)) { + throw new FileNotFoundException("Can't write resources: " + uri); + } + OpenResourceIdResult r = getResourceId(uri); + try { + return r.r.openRawResourceFd(r.id); + } catch (Resources.NotFoundException ex) { + throw new FileNotFoundException("Resource does not exist: " + uri); + } + } else if (SCHEME_FILE.equals(scheme)) { + ParcelFileDescriptor pfd = ParcelFileDescriptor.open( + new File(uri.getPath()), modeToMode(uri, mode)); + return new AssetFileDescriptor(pfd, 0, -1); + } else { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + throw new FileNotFoundException("No content provider: " + uri); + } + try { + AssetFileDescriptor fd = provider.openAssetFile(uri, mode); + if(fd == null) { + releaseProvider(provider); + return null; + } + ParcelFileDescriptor pfd = new ParcelFileDescriptorInner( + fd.getParcelFileDescriptor(), provider); + return new AssetFileDescriptor(pfd, fd.getStartOffset(), + fd.getDeclaredLength()); + } catch (RemoteException e) { releaseProvider(provider); - return null; + throw new FileNotFoundException("Dead content provider: " + uri); + } catch (FileNotFoundException e) { + releaseProvider(provider); + throw e; + } catch (RuntimeException e) { + releaseProvider(provider); + throw e; } - return new ParcelFileDescriptorInner(fd, provider); - } catch (RemoteException e) { - releaseProvider(provider); - throw new FileNotFoundException("Dead content provider: " + uri); - } catch (FileNotFoundException e) { - releaseProvider(provider); - throw e; - } catch (RuntimeException e) { - releaseProvider(provider); - throw e; } } + class OpenResourceIdResult { + Resources r; + int id; + } + + OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { + String authority = uri.getAuthority(); + Resources r; + if (TextUtils.isEmpty(authority)) { + throw new FileNotFoundException("No authority: " + uri); + } else { + try { + r = mContext.getPackageManager().getResourcesForApplication(authority); + } catch (NameNotFoundException ex) { + throw new FileNotFoundException("No package found for authority: " + uri); + } + } + List<String> path = uri.getPathSegments(); + if (path == null) { + throw new FileNotFoundException("No path: " + uri); + } + int len = path.size(); + int id; + if (len == 1) { + try { + id = Integer.parseInt(path.get(0)); + } catch (NumberFormatException e) { + throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); + } + } else if (len == 2) { + id = r.getIdentifier(path.get(1), path.get(0), authority); + } else { + throw new FileNotFoundException("More than two path segments: " + uri); + } + if (id == 0) { + throw new FileNotFoundException("No resource found for: " + uri); + } + OpenResourceIdResult res = new OpenResourceIdResult(); + res.r = r; + res.id = id; + return res; + } + + /** @hide */ + static public int modeToMode(Uri uri, String mode) throws FileNotFoundException { + int modeBits; + if ("r".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_ONLY; + } else if ("w".equals(mode) || "wt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else if ("wa".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_APPEND; + } else if ("rw".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE; + } else if ("rwt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else { + throw new FileNotFoundException("Bad mode for " + uri + ": " + + mode); + } + return modeBits; + } + /** * Inserts a row into a table at the given URL. * diff --git a/core/java/android/content/ContentServiceNative.java b/core/java/android/content/ContentServiceNative.java index f050501..364f9ee 100644 --- a/core/java/android/content/ContentServiceNative.java +++ b/core/java/android/content/ContentServiceNative.java @@ -75,6 +75,13 @@ abstract class ContentServiceNative extends Binder implements IContentService { try { switch (code) { + case 5038: { + data.readString(); // ignore the interface token that service generated + Uri uri = Uri.parse(data.readString()); + notifyChange(uri, null, false, false); + return true; + } + case REGISTER_CONTENT_OBSERVER_TRANSACTION: { Uri uri = Uri.CREATOR.createFromParcel(data); boolean notifyForDescendents = data.readInt() != 0; diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index a6ef46f..0606956 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -16,6 +16,7 @@ package android.content; +import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.CursorWindow; import android.database.IBulkCursor; @@ -52,6 +53,8 @@ public interface IContentProvider extends IInterface { String[] selectionArgs) throws RemoteException; public ParcelFileDescriptor openFile(Uri url, String mode) throws RemoteException, FileNotFoundException; + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException; public ISyncAdapter getSyncAdapter() throws RemoteException; /* IPC constants */ @@ -65,4 +68,5 @@ public interface IContentProvider extends IInterface { static final int GET_SYNC_ADAPTER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10; static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12; static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13; + static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c1c3b49..e1c1f64 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -522,6 +522,7 @@ import java.util.Set; * <li> {@link #CATEGORY_ALTERNATIVE} * <li> {@link #CATEGORY_SELECTED_ALTERNATIVE} * <li> {@link #CATEGORY_LAUNCHER} + * <li> {@link #CATEGORY_INFO} * <li> {@link #CATEGORY_HOME} * <li> {@link #CATEGORY_PREFERENCE} * <li> {@link #CATEGORY_GADGET} @@ -1546,6 +1547,13 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; /** + * Provides information about the package it is in; typically used if + * a package does not contain a {@link #CATEGORY_LAUNCHER} to provide + * a front-door to the user without having to be shown in the all apps list. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_INFO = "android.intent.category.INFO"; + /** * This is the home activity, that is the first activity that is displayed * when the device boots. */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 698f27f..7287d9c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -480,6 +480,26 @@ public abstract class PackageManager { throws NameNotFoundException; /** + * Return a "good" intent to launch a front-door activity in a package, + * for use for example to implement an "open" button when browsing through + * packages. The current implementation will look first for a main + * activity in the category {@link Intent#CATEGORY_INFO}, next for a + * main activity in the category {@link Intent#CATEGORY_LAUNCHER}, or return + * null if neither are found. + * + * <p>Throws {@link NameNotFoundException} if a package with the given + * name can not be found on the system. + * + * @param packageName The name of the package to inspect. + * + * @return Returns either a fully-qualified Intent that can be used to + * launch the main activity in the package, or null if the package does + * not contain such an activity. + */ + public abstract Intent getLaunchIntentForPackage(String packageName) + throws NameNotFoundException; + + /** * Return an array of all of the secondary group-ids that have been * assigned to a package. * diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java index 4a073f7..231e3e2 100644 --- a/core/java/android/content/res/AssetFileDescriptor.java +++ b/core/java/android/content/res/AssetFileDescriptor.java @@ -16,9 +16,13 @@ package android.content.res; +import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.Parcelable; import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; /** @@ -26,16 +30,32 @@ import java.io.IOException; * opened FileDescriptor that can be used to read the data, as well as the * offset and length of that entry's data in the file. */ -public class AssetFileDescriptor { +public class AssetFileDescriptor implements Parcelable { + /** + * Length used with {@link #AssetFileDescriptor(ParcelFileDescriptor, long, long)} + * and {@link #getDeclaredLength} when a length has not been declared. This means + * the data extends to the end of the file. + */ + public static final long UNKNOWN_LENGTH = -1; + private final ParcelFileDescriptor mFd; private final long mStartOffset; private final long mLength; /** * Create a new AssetFileDescriptor from the given values. + * @param fd The underlying file descriptor. + * @param startOffset The location within the file that the asset starts. + * This must be 0 if length is UNKNOWN_LENGTH. + * @param length The number of bytes of the asset, or + * {@link #UNKNOWN_LENGTH if it extends to the end of the file. */ public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset, long length) { + if (length < 0 && startOffset != 0) { + throw new IllegalArgumentException( + "startOffset must be 0 when using UNKNOWN_LENGTH"); + } mFd = fd; mStartOffset = startOffset; mLength = length; @@ -66,9 +86,33 @@ public class AssetFileDescriptor { } /** - * Returns the total number of bytes of this asset entry's data. + * Returns the total number of bytes of this asset entry's data. May be + * {@link #UNKNOWN_LENGTH} if the asset extends to the end of the file. + * If the AssetFileDescriptor was constructed with {@link #UNKNOWN_LENGTH}, + * this will use {@link ParcelFileDescriptor#getStatSize() + * ParcelFileDescriptor.getStatSize()} to find the total size of the file, + * returning that number if found or {@link #UNKNOWN_LENGTH} if it could + * not be determined. + * + * @see #getDeclaredLength() */ public long getLength() { + if (mLength >= 0) { + return mLength; + } + long len = mFd.getStatSize(); + return len >= 0 ? len : UNKNOWN_LENGTH; + } + + /** + * Return the actual number of bytes that were declared when the + * AssetFileDescriptor was constructed. Will be + * {@link #UNKNOWN_LENGTH} if the length was not declared, meaning data + * should be read to the end of the file. + * + * @see #getDeclaredLength() + */ + public long getDeclaredLength() { return mLength; } @@ -78,4 +122,227 @@ public class AssetFileDescriptor { public void close() throws IOException { mFd.close(); } + + /** + * Create and return a new auto-close input stream for this asset. This + * will either return a full asset {@link AutoCloseInputStream}, or + * an underlying {@link ParcelFileDescriptor.AutoCloseInputStream + * ParcelFileDescriptor.AutoCloseInputStream} depending on whether the + * the object represents a complete file or sub-section of a file. You + * should only call this once for a particular asset. + */ + public FileInputStream createInputStream() throws IOException { + if (mLength < 0) { + return new ParcelFileDescriptor.AutoCloseInputStream(mFd); + } + return new AutoCloseInputStream(this); + } + + /** + * Create and return a new auto-close output stream for this asset. This + * will either return a full asset {@link AutoCloseOutputStream}, or + * an underlying {@link ParcelFileDescriptor.AutoCloseOutputStream + * ParcelFileDescriptor.AutoCloseOutputStream} depending on whether the + * the object represents a complete file or sub-section of a file. You + * should only call this once for a particular asset. + */ + public FileOutputStream createOutputStream() throws IOException { + if (mLength < 0) { + return new ParcelFileDescriptor.AutoCloseOutputStream(mFd); + } + return new AutoCloseOutputStream(this); + } + + @Override + public String toString() { + return "{AssetFileDescriptor: " + mFd + + " start=" + mStartOffset + " len=" + mLength + "}"; + } + + /** + * An InputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescritor.close()} for you when the stream is closed. + */ + public static class AutoCloseInputStream + extends ParcelFileDescriptor.AutoCloseInputStream { + private long mRemaining; + + public AutoCloseInputStream(AssetFileDescriptor fd) throws IOException { + super(fd.getParcelFileDescriptor()); + super.skip(fd.getStartOffset()); + mRemaining = (int)fd.getLength(); + } + + @Override + public int available() throws IOException { + return mRemaining >= 0 + ? (mRemaining < 0x7fffffff ? (int)mRemaining : 0x7fffffff) + : super.available(); + } + + @Override + public int read() throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + int res = super.read(); + if (res >= 0) mRemaining--; + return res; + } + + return super.read(); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + if (count > mRemaining) count = (int)mRemaining; + int res = super.read(buffer, offset, count); + if (res >= 0) mRemaining -= res; + return res; + } + + return super.read(buffer, offset, count); + } + + @Override + public int read(byte[] buffer) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + int count = buffer.length; + if (count > mRemaining) count = (int)mRemaining; + int res = super.read(buffer, 0, count); + if (res >= 0) mRemaining -= res; + return res; + } + + return super.read(buffer); + } + + @Override + public long skip(long count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + if (count > mRemaining) count = mRemaining; + long res = super.skip(count); + if (res >= 0) mRemaining -= res; + return res; + } + + // TODO Auto-generated method stub + return super.skip(count); + } + + @Override + public void mark(int readlimit) { + if (mRemaining >= 0) { + // Not supported. + return; + } + super.mark(readlimit); + } + + @Override + public boolean markSupported() { + if (mRemaining >= 0) { + return false; + } + return super.markSupported(); + } + + @Override + public synchronized void reset() throws IOException { + if (mRemaining >= 0) { + // Not supported. + return; + } + super.reset(); + } + } + + /** + * An OutputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescritor.close()} for you when the stream is closed. + */ + public static class AutoCloseOutputStream + extends ParcelFileDescriptor.AutoCloseOutputStream { + private long mRemaining; + + public AutoCloseOutputStream(AssetFileDescriptor fd) throws IOException { + super(fd.getParcelFileDescriptor()); + if (fd.getParcelFileDescriptor().seekTo(fd.getStartOffset()) < 0) { + throw new IOException("Unable to seek"); + } + mRemaining = (int)fd.getLength(); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + if (count > mRemaining) count = (int)mRemaining; + super.write(buffer, offset, count); + mRemaining -= count; + return; + } + + super.write(buffer, offset, count); + } + + @Override + public void write(byte[] buffer) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + int count = buffer.length; + if (count > mRemaining) count = (int)mRemaining; + super.write(buffer); + mRemaining -= count; + return; + } + + super.write(buffer); + } + + @Override + public void write(int oneByte) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + super.write(oneByte); + mRemaining--; + return; + } + + super.write(oneByte); + } + } + + + /* Parcelable interface */ + public int describeContents() { + return mFd.describeContents(); + } + + public void writeToParcel(Parcel out, int flags) { + mFd.writeToParcel(out, flags); + out.writeLong(mStartOffset); + out.writeLong(mLength); + } + + AssetFileDescriptor(Parcel src) { + mFd = ParcelFileDescriptor.CREATOR.createFromParcel(src); + mStartOffset = src.readLong(); + mLength = src.readLong(); + } + + public static final Parcelable.Creator<AssetFileDescriptor> CREATOR + = new Parcelable.Creator<AssetFileDescriptor>() { + public AssetFileDescriptor createFromParcel(Parcel in) { + return new AssetFileDescriptor(in); + } + public AssetFileDescriptor[] newArray(int size) { + return new AssetFileDescriptor[size]; + } + }; } diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index 17cb687..0f3f270 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -16,8 +16,6 @@ package android.content.res; -import com.google.android.collect.Lists; - import com.android.internal.util.ArrayUtils; import org.xmlpull.v1.XmlPullParser; @@ -113,7 +111,8 @@ public class ColorStateList implements Parcelable { * Create a ColorStateList from an XML document, given a set of {@link Resources}. */ public static ColorStateList createFromXml(Resources r, XmlPullParser parser) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { + AttributeSet attrs = Xml.asAttributeSet(parser); int type; @@ -125,19 +124,16 @@ public class ColorStateList implements Parcelable { throw new XmlPullParserException("No start tag found"); } - final ColorStateList colorStateList = createFromXmlInner(r, parser, attrs); - - return colorStateList; + return createFromXmlInner(r, parser, attrs); } /* Create from inside an XML document. Called on a parser positioned at * a tag in an XML document, tries to create a ColorStateList from that tag. * Returns null if the tag is not a valid ColorStateList. */ - private static ColorStateList createFromXmlInner(Resources r, - XmlPullParser parser, - AttributeSet attrs) - throws XmlPullParserException, IOException { + private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, + AttributeSet attrs) throws XmlPullParserException, IOException { + ColorStateList colorStateList; final String name = parser.getName(); @@ -146,8 +142,7 @@ public class ColorStateList implements Parcelable { colorStateList = new ColorStateList(); } else { throw new XmlPullParserException( - parser.getPositionDescription() + ": invalid drawable tag " - + name); + parser.getPositionDescription() + ": invalid drawable tag " + name); } colorStateList.inflate(r, parser, attrs); diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 5a0daea..1a963f6 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -46,6 +46,7 @@ public class Resources { static final String TAG = "Resources"; private static final boolean DEBUG_LOAD = false; private static final boolean DEBUG_CONFIG = false; + private static final boolean TRACE_FOR_PRELOAD = false; private static final int sSdkVersion = SystemProperties.getInt( "ro.build.version.sdk", 0); @@ -57,6 +58,8 @@ public class Resources { // single-threaded, and after that these are immutable. private static final SparseArray<Drawable.ConstantState> mPreloadedDrawables = new SparseArray<Drawable.ConstantState>(); + private static final SparseArray<ColorStateList> mPreloadedColorStateLists + = new SparseArray<ColorStateList>(); private static boolean mPreloaded; /*package*/ final TypedValue mTmpValue = new TypedValue(); @@ -78,7 +81,7 @@ public class Resources { private final Configuration mConfiguration = new Configuration(); /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); PluralRules mPluralRule; - + /** * This exception is thrown by the resource APIs when a requested resource * can not be found. @@ -90,7 +93,7 @@ public class Resources { public NotFoundException(String name) { super(name); } - }; + } /** * Create a new Resources object on top of an existing set of assets in an @@ -1229,7 +1232,9 @@ public class Resources { width = mMetrics.widthPixels; height = mMetrics.heightPixels; } else { + //noinspection SuspiciousNameCombination width = mMetrics.heightPixels; + //noinspection SuspiciousNameCombination height = mMetrics.widthPixels; } int keyboardHidden = mConfiguration.keyboardHidden; @@ -1342,6 +1347,7 @@ public class Resources { try { return Integer.parseInt(name); } catch (Exception e) { + // Ignore } return mAssets.getResourceIdentifier(name, defType, defPackage); } @@ -1575,21 +1581,18 @@ public class Resources { /*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT - && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - // Should we be caching these? If we use constant colors much - // at all, most likely... - //System.out.println("Creating drawable for color: #" + - // Integer.toHexString(value.data)); - Drawable dr = new ColorDrawable(value.data); - dr.setChangingConfigurations(value.changingConfigurations); - return dr; + + if (TRACE_FOR_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) android.util.Log.d("PreloadDrawable", name); + } } - final int key = (value.assetCookie<<24)|value.data; + final int key = (value.assetCookie << 24) | value.data; Drawable dr = getCachedDrawable(key); - //System.out.println("Cached drawable @ #" + - // Integer.toHexString(key.intValue()) + ": " + dr); + if (dr != null) { return dr; } @@ -1597,46 +1600,52 @@ public class Resources { Drawable.ConstantState cs = mPreloadedDrawables.get(key); if (cs != null) { dr = cs.newDrawable(); - } else { - if (value.string == null) { - throw new NotFoundException( - "Resource is not a Drawable (color or path): " + value); + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + dr = new ColorDrawable(value.data); } - - String file = value.string.toString(); - - if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " - + value.assetCookie + ": " + file); - - if (file.endsWith(".xml")) { - try { - XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "drawable"); - dr = Drawable.createFromXml(this, rp); - rp.close(); - } catch (Exception e) { - NotFoundException rnf = new NotFoundException( - "File " + file + " from drawable resource ID #0x" - + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; + + if (dr == null) { + if (value.string == null) { + throw new NotFoundException( + "Resource is not a Drawable (color or path): " + value); } - - } else { - try { - InputStream is = mAssets.openNonAsset( - value.assetCookie, file, AssetManager.ACCESS_BUFFER); - // System.out.println("Opened file " + file + ": " + is); - dr = Drawable.createFromResourceStream(this, value, is, file); - is.close(); - // System.out.println("Created stream: " + dr); - } catch (Exception e) { - NotFoundException rnf = new NotFoundException( - "File " + file + " from drawable resource ID #0x" - + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; + + String file = value.string.toString(); + + if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " + + value.assetCookie + ": " + file); + + if (file.endsWith(".xml")) { + try { + XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "drawable"); + dr = Drawable.createFromXml(this, rp); + rp.close(); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + + } else { + try { + InputStream is = mAssets.openNonAsset( + value.assetCookie, file, AssetManager.ACCESS_BUFFER); + // System.out.println("Opened file " + file + ": " + is); + dr = Drawable.createFromResourceStream(this, value, is, file); + is.close(); + // System.out.println("Created stream: " + dr); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } } } } @@ -1647,13 +1656,13 @@ public class Resources { if (cs != null) { if (mPreloading) { mPreloadedDrawables.put(key, cs); - } - synchronized (mTmpValue) { - //Log.i(TAG, "Saving cached drawable @ #" + - // Integer.toHexString(key.intValue()) - // + " in " + this + ": " + cs); - mDrawableCache.put( - key, new WeakReference<Drawable.ConstantState>(cs)); + } else { + synchronized (mTmpValue) { + //Log.i(TAG, "Saving cached drawable @ #" + + // Integer.toHexString(key.intValue()) + // + " in " + this + ": " + cs); + mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); + } } } } @@ -1661,7 +1670,7 @@ public class Resources { return dr; } - private final Drawable getCachedDrawable(int key) { + private Drawable getCachedDrawable(int key) { synchronized (mTmpValue) { WeakReference<Drawable.ConstantState> wr = mDrawableCache.get(key); if (wr != null) { // we have the key @@ -1682,13 +1691,40 @@ public class Resources { /*package*/ ColorStateList loadColorStateList(TypedValue value, int id) throws NotFoundException { - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT - && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - return ColorStateList.valueOf(value.data); + if (TRACE_FOR_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) android.util.Log.d("PreloadColorStateList", name); + } + } + + final int key = (value.assetCookie << 24) | value.data; + + ColorStateList csl; + + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + + csl = mPreloadedColorStateLists.get(key); + if (csl != null) { + return csl; + } + + csl = ColorStateList.valueOf(value.data); + if (mPreloading) { + mPreloadedColorStateLists.put(key, csl); + } + + return csl; } - final int key = (value.assetCookie<<24)|value.data; - ColorStateList csl = getCachedColorStateList(key); + csl = getCachedColorStateList(key); + if (csl != null) { + return csl; + } + + csl = mPreloadedColorStateLists.get(key); if (csl != null) { return csl; } @@ -1720,12 +1756,16 @@ public class Resources { } if (csl != null) { - synchronized (mTmpValue) { - //Log.i(TAG, "Saving cached color state list @ #" + - // Integer.toHexString(key.intValue()) - // + " in " + this + ": " + csl); - mColorStateListCache.put( - key, new WeakReference<ColorStateList>(csl)); + if (mPreloading) { + mPreloadedColorStateLists.put(key, csl); + } else { + synchronized (mTmpValue) { + //Log.i(TAG, "Saving cached color state list @ #" + + // Integer.toHexString(key.intValue()) + // + " in " + this + ": " + csl); + mColorStateListCache.put( + key, new WeakReference<ColorStateList>(csl)); + } } } diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 3df7708..e684cb8 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -141,6 +141,8 @@ final class StringBlock { int type = style[i]; if (localLOGV) Log.v(TAG, "Applying style span id=" + type + ", start=" + style[i+1] + ", end=" + style[i+2]); + + if (type == ids.boldId) { buffer.setSpan(new StyleSpan(Typeface.BOLD), style[i+1], style[i+2]+1, @@ -178,9 +180,8 @@ final class StringBlock { style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (type == ids.listItemId) { - buffer.setSpan(new BulletSpan(10), - style[i+1], style[i+2]+1, - Spannable.SPAN_PARAGRAPH); + addParagraphSpan(buffer, new BulletSpan(10), + style[i+1], style[i+2]+1); } else if (type == ids.marqueeId) { buffer.setSpan(TextUtils.TruncateAt.MARQUEE, style[i+1], style[i+2]+1, @@ -194,9 +195,8 @@ final class StringBlock { sub = subtag(tag, ";height="); if (sub != null) { int size = Integer.parseInt(sub); - buffer.setSpan(new Height(size), - style[i+1], style[i+2]+1, - Spannable.SPAN_PARAGRAPH); + addParagraphSpan(buffer, new Height(size), + style[i+1], style[i+2]+1); } sub = subtag(tag, ";size="); @@ -231,6 +231,28 @@ final class StringBlock { style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + } else if (tag.startsWith("annotation;")) { + int len = tag.length(); + int next; + + for (int t = tag.indexOf(';'); t < len; t = next) { + int eq = tag.indexOf('=', t); + if (eq < 0) { + break; + } + + next = tag.indexOf(';', eq); + if (next < 0) { + next = len; + } + + String key = tag.substring(t + 1, eq); + String value = tag.substring(eq + 1, next); + + buffer.setSpan(new Annotation(key, value), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } } @@ -239,6 +261,34 @@ final class StringBlock { return new SpannedString(buffer); } + /** + * If a translator has messed up the edges of paragraph-level markup, + * fix it to actually cover the entire paragraph that it is attached to + * instead of just whatever range they put it on. + */ + private static void addParagraphSpan(Spannable buffer, Object what, + int start, int end) { + int len = buffer.length(); + + if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { + for (start--; start > 0; start--) { + if (buffer.charAt(start - 1) == '\n') { + break; + } + } + } + + if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { + for (end++; end < len; end++) { + if (buffer.charAt(end - 1) == '\n') { + break; + } + } + } + + buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); + } + private static String subtag(String full, String attribute) { int start = full.indexOf(attribute); if (start < 0) { diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 82a57dd..3a32c03 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -438,6 +438,34 @@ public class TypedArray { throw new RuntimeException(getPositionDescription() + ": You must supply a " + name + " attribute."); } + + /** + * Special version of {@link #getDimensionPixelSize} for retrieving + * {@link android.view.ViewGroup}'s layout_width and layout_height + * attributes. This is only here for performance reasons; applications + * should use {@link #getDimensionPixelSize}. + * + * @param index Index of the attribute to retrieve. + * @param defValue The default value to return if this attribute is not + * default or contains the wrong type of data. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + */ + public int getLayoutDimension(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + return defValue; + } /** * Retrieve a fractional unit attribute at <var>index</var>. diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 87bb277..2af080a 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -778,9 +778,9 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Returns the maximum size the database may grow to. + * Returns the current database page size, in bytes. * - * @return the new maximum database size + * @return the database page size, in bytes */ public long getPageSize() { SQLiteStatement prog = null; diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index d169259..5889ad9 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -17,6 +17,7 @@ package android.database.sqlite; import android.os.SystemClock; +import android.util.Log; /** * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused. @@ -26,6 +27,10 @@ import android.os.SystemClock; */ public class SQLiteStatement extends SQLiteProgram { + private static final String TAG = "SQLiteStatement"; + + private final String mSql; + /** * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} @@ -34,6 +39,11 @@ public class SQLiteStatement extends SQLiteProgram */ /* package */ SQLiteStatement(SQLiteDatabase db, String sql) { super(db, sql); + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + mSql = sql; + } else { + mSql = null; + } } /** @@ -50,6 +60,9 @@ public class SQLiteStatement extends SQLiteProgram acquireReference(); try { + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.v(TAG, "execute() for [" + mSql + "]"); + } native_execute(); if (logStats) { mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); @@ -77,6 +90,9 @@ public class SQLiteStatement extends SQLiteProgram acquireReference(); try { + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.v(TAG, "executeInsert() for [" + mSql + "]"); + } native_execute(); if (logStats) { mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); @@ -103,6 +119,9 @@ public class SQLiteStatement extends SQLiteProgram acquireReference(); try { + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.v(TAG, "simpleQueryForLong() for [" + mSql + "]"); + } long retValue = native_1x1_long(); if (logStats) { mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); @@ -129,6 +148,9 @@ public class SQLiteStatement extends SQLiteProgram acquireReference(); try { + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + Log.v(TAG, "simpleQueryForString() for [" + mSql + "]"); + } String retValue = native_1x1_string(); if (logStats) { mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); diff --git a/core/java/android/gadget/GadgetHost.java b/core/java/android/gadget/GadgetHost.java index 31aed32..3d88b58 100644 --- a/core/java/android/gadget/GadgetHost.java +++ b/core/java/android/gadget/GadgetHost.java @@ -19,6 +19,7 @@ package android.gadget; import android.content.Context; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; @@ -62,7 +63,11 @@ public class GadgetHost { } } - Handler mHandler = new Handler() { + class UpdateHandler extends Handler { + public UpdateHandler(Looper looper) { + super(looper); + } + public void handleMessage(Message msg) { switch (msg.what) { case HANDLE_UPDATE: { @@ -75,7 +80,9 @@ public class GadgetHost { } } } - }; + } + + Handler mHandler; int mHostId; Callbacks mCallbacks = new Callbacks(); @@ -84,6 +91,7 @@ public class GadgetHost { public GadgetHost(Context context, int hostId) { mContext = context; mHostId = hostId; + mHandler = new UpdateHandler(context.getMainLooper()); synchronized (sServiceLock) { if (sService == null) { IBinder b = ServiceManager.getService(Context.GADGET_SERVICE); diff --git a/core/java/android/gadget/GadgetHostView.java b/core/java/android/gadget/GadgetHostView.java index a985bd4..5cbd988 100644 --- a/core/java/android/gadget/GadgetHostView.java +++ b/core/java/android/gadget/GadgetHostView.java @@ -18,7 +18,13 @@ package android.gadget; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; import android.util.Config; import android.util.Log; import android.view.Gravity; @@ -29,16 +35,23 @@ import android.view.animation.Animation; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; -import android.widget.ViewAnimator; /** * Provides the glue to show gadget views. This class offers automatic animation * between updates, and will try recycling old views for each incoming * {@link RemoteViews}. */ -public class GadgetHostView extends ViewAnimator implements Animation.AnimationListener { +public class GadgetHostView extends FrameLayout { static final String TAG = "GadgetHostView"; - static final boolean LOGD = Config.LOGD || true; + static final boolean LOGD = false; + static final boolean CROSSFADE = false; + + static final int VIEW_MODE_NOINIT = 0; + static final int VIEW_MODE_CONTENT = 1; + static final int VIEW_MODE_ERROR = 2; + static final int VIEW_MODE_DEFAULT = 3; + + static final int FADE_DURATION = 1000; // When we're inflating the initialLayout for a gadget, we only allow // views that are allowed in RemoteViews. @@ -47,28 +60,17 @@ public class GadgetHostView extends ViewAnimator implements Animation.AnimationL return clazz.isAnnotationPresent(RemoteViews.RemoteView.class); } }; - - Context mLocalContext; + + Context mContext; int mGadgetId; GadgetProviderInfo mInfo; - - View mActiveView = null; - View mStaleView = null; - - int mActiveLayoutId = -1; - int mStaleLayoutId = -1; - - /** - * Last set of {@link RemoteViews} applied to {@link #mActiveView} - */ - RemoteViews mActiveActions = null; - - /** - * Flag indicating that {@link #mActiveActions} has been applied to - * {@link #mStaleView}, meaning it's readyto recycle. - */ - boolean mStalePrepared = false; + View mView; + int mViewMode = VIEW_MODE_NOINIT; + int mLayoutId = -1; + long mFadeStartTime = -1; + Bitmap mOld; + Paint mOldPaint = new Paint(); /** * Create a host view. Uses default fade animations. @@ -86,27 +88,13 @@ public class GadgetHostView extends ViewAnimator implements Animation.AnimationL */ public GadgetHostView(Context context, int animationIn, int animationOut) { super(context); - mLocalContext = context; - - // Prepare our default transition animations - setAnimateFirstView(true); - setInAnimation(context, animationIn); - setOutAnimation(context, animationOut); - - // Watch for animation events to prepare recycling - Animation inAnimation = getInAnimation(); - if (inAnimation != null) { - inAnimation.setAnimationListener(this); - } + mContext = context; } /** * Set the gadget that will be displayed by this view. */ public void setGadget(int gadgetId, GadgetProviderInfo info) { - if (mInfo != null) { - // TODO: remove the old view, or whatever - } mGadgetId = gadgetId; mInfo = info; } @@ -119,92 +107,141 @@ public class GadgetHostView extends ViewAnimator implements Animation.AnimationL return mInfo; } - public void onAnimationEnd(Animation animation) { - // When our transition animation finishes, we should try bringing our - // newly-stale view up to the current view. - if (mActiveActions != null && - mStaleLayoutId == mActiveActions.getLayoutId()) { - if (LOGD) Log.d(TAG, "after animation, layoutId matched so we're recycling old view"); - mActiveActions.reapply(mLocalContext, mStaleView); - mStalePrepared = true; - } - } - - public void onAnimationRepeat(Animation animation) { - } - - public void onAnimationStart(Animation animation) { - } - /** * Process a set of {@link RemoteViews} coming in as an update from the * gadget provider. Will animate into these new views as needed. */ public void updateGadget(RemoteViews remoteViews) { - if (LOGD) Log.d(TAG, "updateGadget called"); + if (LOGD) Log.d(TAG, "updateGadget called mOld=" + mOld); boolean recycled = false; - View newContent = null; + View content = null; Exception exception = null; - if (remoteViews == null) { - newContent = getDefaultView(); + // Capture the old view into a bitmap so we can do the crossfade. + if (CROSSFADE) { + if (mFadeStartTime < 0) { + if (mView != null) { + final int width = mView.getWidth(); + final int height = mView.getHeight(); + try { + mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError e) { + // we just won't do the fade + mOld = null; + } + if (mOld != null) { + //mView.drawIntoBitmap(mOld); + } + } + } } - // If our stale view has been prepared to match active, and the new - // layout matches, try recycling it - if (newContent == null && mStalePrepared && - remoteViews.getLayoutId() == mStaleLayoutId) { - try { - remoteViews.reapply(mLocalContext, mStaleView); - newContent = mStaleView; - recycled = true; - if (LOGD) Log.d(TAG, "was able to recycled existing layout"); - } catch (RuntimeException e) { - exception = e; + if (remoteViews == null) { + if (mViewMode == VIEW_MODE_DEFAULT) { + // We've already done this -- nothing to do. + return; + } + content = getDefaultView(); + mLayoutId = -1; + mViewMode = VIEW_MODE_DEFAULT; + } else { + int layoutId = remoteViews.getLayoutId(); + + // If our stale view has been prepared to match active, and the new + // layout matches, try recycling it + if (content == null && layoutId == mLayoutId) { + try { + remoteViews.reapply(mContext, mView); + content = mView; + recycled = true; + if (LOGD) Log.d(TAG, "was able to recycled existing layout"); + } catch (RuntimeException e) { + exception = e; + } + } + + // Try normal RemoteView inflation + if (content == null) { + try { + content = remoteViews.apply(mContext, this); + if (LOGD) Log.d(TAG, "had to inflate new layout"); + } catch (RuntimeException e) { + exception = e; + } } + + mLayoutId = layoutId; + mViewMode = VIEW_MODE_CONTENT; } - // Try normal RemoteView inflation - if (newContent == null) { - try { - newContent = remoteViews.apply(mLocalContext, this); - if (LOGD) Log.d(TAG, "had to inflate new layout"); - } catch (RuntimeException e) { - exception = e; + if (content == null) { + if (mViewMode == VIEW_MODE_ERROR) { + // We've already done this -- nothing to do. + return ; } + Log.w(TAG, "updateGadget couldn't find any view, using error view", exception); + content = getErrorView(); + mViewMode = VIEW_MODE_ERROR; } - if (exception != null && LOGD) { - Log.w(TAG, "Error inflating gadget " + getGadgetInfo(), exception); + if (!recycled) { + prepareView(content); + addView(content); } - - if (newContent == null) { - // TODO: Should we throw an exception here for the host activity to catch? - // Maybe we should show a generic error widget. - if (LOGD) Log.d(TAG, "updateGadget couldn't find any view, so inflating error"); - newContent = getErrorView(); + + if (mView != content) { + removeView(mView); + mView = content; } - - if (!recycled) { - prepareView(newContent); - addView(newContent); + + if (CROSSFADE) { + if (mFadeStartTime < 0) { + // if there is already an animation in progress, don't do anything -- + // the new view will pop in on top of the old one during the cross fade, + // and that looks okay. + mFadeStartTime = SystemClock.uptimeMillis(); + invalidate(); + } } - - showNext(); - - if (!recycled) { - removeView(mStaleView); + } + + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (CROSSFADE) { + int alpha; + int l = child.getLeft(); + int t = child.getTop(); + if (mFadeStartTime > 0) { + alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); + if (alpha > 255) { + alpha = 255; + } + Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t + + " w=" + child.getWidth()); + if (alpha != 255 && mOld != null) { + mOldPaint.setAlpha(255-alpha); + //canvas.drawBitmap(mOld, l, t, mOldPaint); + } + } else { + alpha = 255; + } + int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, + Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); + boolean rv = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreTo); + if (alpha < 255) { + invalidate(); + } else { + mFadeStartTime = -1; + if (mOld != null) { + mOld.recycle(); + mOld = null; + } + } + return rv; + } else { + return super.drawChild(canvas, child, drawingTime); } - - mStalePrepared = false; - mActiveActions = remoteViews; - - mStaleView = mActiveView; - mActiveView = newContent; - - mStaleLayoutId = mActiveLayoutId; - mActiveLayoutId = (remoteViews == null) ? -1 : remoteViews.getLayoutId(); } /** @@ -234,7 +271,7 @@ public class GadgetHostView extends ViewAnimator implements Animation.AnimationL try { if (mInfo != null) { - Context theirContext = mLocalContext.createPackageContext( + Context theirContext = mContext.createPackageContext( mInfo.provider.getPackageName(), 0 /* no flags */); LayoutInflater inflater = (LayoutInflater) theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -266,9 +303,9 @@ public class GadgetHostView extends ViewAnimator implements Animation.AnimationL * Inflate and return a view that represents an error state. */ protected View getErrorView() { - TextView tv = new TextView(mLocalContext); - // TODO: move this error string and background color into resources - tv.setText("Error inflating gadget"); + TextView tv = new TextView(mContext); + tv.setText(com.android.internal.R.string.gadget_host_error_inflating); + // TODO: get this color from somewhere. tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); return tv; } diff --git a/core/java/android/gadget/GadgetManager.java b/core/java/android/gadget/GadgetManager.java index a9a2c80..d2c4055 100644 --- a/core/java/android/gadget/GadgetManager.java +++ b/core/java/android/gadget/GadgetManager.java @@ -300,5 +300,21 @@ public class GadgetManager { throw new RuntimeException("system server dead?", e); } } + + /** + * Get the list of gadgetIds that have been bound to the given gadget + * provider. + * + * @param provider The {@link android.content.BroadcastReceiver} that is the + * gadget provider to find gadgetIds for. + */ + public int[] getGadgetIds(ComponentName provider) { + try { + return sService.getGadgetIds(provider); + } + catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + } } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 40a5b47..106c920 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -48,7 +48,6 @@ public class Camera { private static final int ERROR_CALLBACK = 5; private int mNativeContext; // accessed by native methods - private int mListenerContext; private EventHandler mEventHandler; private ShutterCallback mShutterCallback; private PictureCallback mRawImageCallback; diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 52f8209..0295f69 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -98,6 +98,13 @@ public class ExtractEditText extends EditText { } /** + * Return true if the edit text is currently showing a scroll bar. + */ + public boolean hasVerticalScrollBar() { + return computeVerticalScrollRange() > computeVerticalScrollExtent(); + } + + /** * Pretend like the window this view is in always has focus, so its * highlight and cursor will be displayed. */ diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 4be1fc7..1e2e2f3 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -26,7 +26,9 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.IBinder; +import android.os.SystemClock; import android.provider.Settings; +import android.text.InputType; import android.text.Layout; import android.text.Spannable; import android.text.method.MovementMethod; @@ -49,6 +51,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.EditorInfo; +import android.widget.Button; import android.widget.FrameLayout; import java.io.FileDescriptor; @@ -241,6 +244,8 @@ public class InputMethodService extends AbstractInputMethodService { boolean mIsFullscreen; View mExtractView; ExtractEditText mExtractEditText; + ViewGroup mExtractAccessories; + Button mExtractAction; ExtractedText mExtractedText; int mExtractedToken; @@ -271,6 +276,21 @@ public class InputMethodService extends AbstractInputMethodService { } }; + final View.OnClickListener mActionClickListener = new View.OnClickListener() { + public void onClick(View v) { + final EditorInfo ei = getCurrentInputEditorInfo(); + final InputConnection ic = getCurrentInputConnection(); + if (ei != null && ic != null) { + if (ei.actionId != 0) { + ic.performEditorAction(ei.actionId); + } else if ((ei.imeOptions&EditorInfo.IME_MASK_ACTION) + != EditorInfo.IME_ACTION_NONE) { + ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION); + } + } + } + }; + /** * Concrete implementation of * {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides @@ -522,6 +542,8 @@ public class InputMethodService extends AbstractInputMethodService { mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea); mExtractView = null; mExtractEditText = null; + mExtractAccessories = null; + mExtractAction = null; mFullscreenApplied = false; mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea); @@ -703,7 +725,7 @@ public class InputMethodService extends AbstractInputMethodService { setExtractView(v); } } - startExtractingText(); + startExtractingText(false); } } @@ -907,9 +929,17 @@ public class InputMethodService extends AbstractInputMethodService { mExtractEditText = (ExtractEditText)view.findViewById( com.android.internal.R.id.inputExtractEditText); mExtractEditText.setIME(this); - startExtractingText(); + mExtractAction = (Button)view.findViewById( + com.android.internal.R.id.inputExtractAction); + if (mExtractAction != null) { + mExtractAccessories = (ViewGroup)view.findViewById( + com.android.internal.R.id.inputExtractAccessories); + } + startExtractingText(false); } else { mExtractEditText = null; + mExtractAccessories = null; + mExtractAction = null; } } @@ -1166,7 +1196,7 @@ public class InputMethodService extends AbstractInputMethodService { } if (doShowInput) { - startExtractingText(); + startExtractingText(false); } if (!wasVisible) { @@ -1276,7 +1306,7 @@ public class InputMethodService extends AbstractInputMethodService { if (DEBUG) Log.v(TAG, "CALL: onStartInputView"); mInputViewStarted = true; onStartInputView(mInputEditorInfo, restarting); - startExtractingText(); + startExtractingText(true); } else if (mCandidatesVisibility == View.VISIBLE) { if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView"); mCandidatesViewStarted = true; @@ -1453,6 +1483,25 @@ public class InputMethodService extends AbstractInputMethodService { static final int MOVEMENT_DOWN = -1; static final int MOVEMENT_UP = -2; + void reportExtractedMovement(int keyCode, int count) { + int dx = 0, dy = 0; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + dx = -count; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + dx = count; + break; + case KeyEvent.KEYCODE_DPAD_UP: + dy = -count; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + dy = count; + break; + } + onExtractedCursorMovement(dx, dy); + } + boolean doMovementKey(int keyCode, KeyEvent event, int count) { final ExtractEditText eet = mExtractEditText; if (isFullscreenMode() && isInputViewShown() && eet != null) { @@ -1467,6 +1516,7 @@ public class InputMethodService extends AbstractInputMethodService { if (count == MOVEMENT_DOWN) { if (movement.onKeyDown(eet, (Spannable)eet.getText(), keyCode, event)) { + reportExtractedMovement(keyCode, 1); return true; } } else if (count == MOVEMENT_UP) { @@ -1475,7 +1525,9 @@ public class InputMethodService extends AbstractInputMethodService { return true; } } else { - if (!movement.onKeyOther(eet, (Spannable)eet.getText(), event)) { + if (movement.onKeyOther(eet, (Spannable)eet.getText(), event)) { + reportExtractedMovement(keyCode, count); + } else { KeyEvent down = new KeyEvent(event, KeyEvent.ACTION_DOWN); if (movement.onKeyDown(eet, (Spannable)eet.getText(), keyCode, down)) { @@ -1488,6 +1540,7 @@ public class InputMethodService extends AbstractInputMethodService { movement.onKeyUp(eet, (Spannable)eet.getText(), keyCode, up); } + reportExtractedMovement(keyCode, count); } } } @@ -1507,6 +1560,97 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Send the given key event code (as defined by {@link KeyEvent}) to the + * current input connection is a key down + key up event pair. The sent + * events have {@link KeyEvent#FLAG_SOFT_KEYBOARD KeyEvent.FLAG_SOFT_KEYBOARD} + * set, so that the recipient can identify them as coming from a software + * input method, and + * {@link KeyEvent#FLAG_KEEP_TOUCH_MODE KeyEvent.FLAG_KEEP_TOUCH_MODE}, so + * that they don't impact the current touch mode of the UI. + * + * @param keyEventCode The raw key code to send, as defined by + * {@link KeyEvent}. + */ + public void sendDownUpKeyEvents(int keyEventCode) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + long eventTime = SystemClock.uptimeMillis(); + ic.sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); + ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, keyEventCode, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + + /** + * Ask the input target to execute its default action via + * {@link InputConnection#performEditorAction + * InputConnection.performEditorAction()}. + * + * @param fromEnterKey If true, this will be executed as if the user had + * pressed an enter key on the keyboard, that is it will <em>not</em> + * be done if the editor has set {@link EditorInfo#IME_FLAG_NO_ENTER_ACTION + * EditorInfo.IME_FLAG_NO_ENTER_ACTION}. If false, the action will be + * sent regardless of how the editor has set that flag. + * + * @return Returns a boolean indicating whether an action has been sent. + * If false, either the editor did not specify a default action or it + * does not want an action from the enter key. If true, the action was + * sent (or there was no input connection at all). + */ + public boolean sendDefaultEditorAction(boolean fromEnterKey) { + EditorInfo ei = getCurrentInputEditorInfo(); + if (ei != null && + (!fromEnterKey || (ei.imeOptions & + EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) && + (ei.imeOptions & EditorInfo.IME_MASK_ACTION) != + EditorInfo.IME_ACTION_NONE) { + // If the enter key was pressed, and the editor has a default + // action associated with pressing enter, then send it that + // explicit action instead of the key event. + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION); + } + return true; + } + + return false; + } + + /** + * Send the given UTF-16 character to the current input connection. Most + * characters will be delivered simply by calling + * {@link InputConnection#commitText InputConnection.commitText()} with + * the character; some, however, may be handled different. In particular, + * the enter character ('\n') will either be delivered as an action code + * or a raw key event, as appropriate. + * + * @param charCode The UTF-16 character code to send. + */ + public void sendKeyChar(char charCode) { + switch (charCode) { + case '\n': // Apps may be listening to an enter key to perform an action + if (!sendDefaultEditorAction(true)) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER); + } + break; + default: + // Make sure that digits go through any text watcher on the client side. + if (charCode >= '0' && charCode <= '9') { + sendDownUpKeyEvents(charCode - '0' + KeyEvent.KEYCODE_0); + } else { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitText(String.valueOf((char) charCode), 1); + } + } + break; + } + } + + /** * This is called when the user has moved the cursor in the extracted * text view, when running in fullsreen mode. The default implementation * performs the corresponding selection change on the underlying text @@ -1522,11 +1666,36 @@ public class InputMethodService extends AbstractInputMethodService { /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides - * the candidates view when this happens. Re-implement this to provide - * whatever behavior you want. + * the candidates view when this happens, but only if the extracted text + * editor has a vertical scroll bar because its text doesn't fit. + * Re-implement this to provide whatever behavior you want. */ public void onExtractedTextClicked() { - setCandidatesViewShown(false); + if (mExtractEditText == null) { + return; + } + if (mExtractEditText.hasVerticalScrollBar()) { + setCandidatesViewShown(false); + } + } + + /** + * This is called when the user has performed a cursor movement in the + * extracted text view, when it is running in fullscreen mode. The default + * implementation hides the candidates view when a vertical movement + * happens, but only if the extracted text editor has a vertical scroll bar + * because its text doesn't fit. + * Re-implement this to provide whatever behavior you want. + * @param dx The amount of cursor movement in the x dimension. + * @param dy The amount of cursor movement in the y dimension. + */ + public void onExtractedCursorMovement(int dx, int dy) { + if (mExtractEditText == null || dy == 0) { + return; + } + if (mExtractEditText.hasVerticalScrollBar()) { + setCandidatesViewShown(false); + } } /** @@ -1545,7 +1714,74 @@ public class InputMethodService extends AbstractInputMethodService { return true; } - void startExtractingText() { + /** + * Return text that can be used as a button label for the given + * {@link EditorInfo#imeOptions EditorInfo.imeOptions}. Returns null + * if there is no action requested. Note that there is no guarantee that + * the returned text will be relatively short, so you probably do not + * want to use it as text on a soft keyboard key label. + * + * @param imeOptions The value from @link EditorInfo#imeOptions EditorInfo.imeOptions}. + * + * @return Returns a label to use, or null if there is no action. + */ + public CharSequence getTextForImeAction(int imeOptions) { + switch (imeOptions&EditorInfo.IME_MASK_ACTION) { + case EditorInfo.IME_ACTION_NONE: + return null; + case EditorInfo.IME_ACTION_GO: + return getText(com.android.internal.R.string.ime_action_go); + case EditorInfo.IME_ACTION_SEARCH: + return getText(com.android.internal.R.string.ime_action_search); + case EditorInfo.IME_ACTION_SEND: + return getText(com.android.internal.R.string.ime_action_send); + case EditorInfo.IME_ACTION_NEXT: + return getText(com.android.internal.R.string.ime_action_next); + default: + return getText(com.android.internal.R.string.ime_action_default); + } + } + + /** + * Called when it is time to update the actions available from a full-screen + * IME. You do not need to deal with this if you are using the standard + * full screen extract UI. If replacing it, you will need to re-implement + * this to put the action in your own UI and handle it. + */ + public void onUpdateExtractingAccessories(EditorInfo ei) { + if (mExtractAccessories == null) { + return; + } + final boolean hasAction = ei.actionLabel != null || ( + (ei.imeOptions&EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE && + (ei.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0); + if (hasAction) { + mExtractAccessories.setVisibility(View.VISIBLE); + if (ei.actionLabel != null) { + mExtractAction.setText(ei.actionLabel); + } else { + mExtractAction.setText(getTextForImeAction(ei.imeOptions)); + } + mExtractAction.setOnClickListener(mActionClickListener); + } else { + mExtractAccessories.setVisibility(View.GONE); + mExtractAction.setOnClickListener(null); + } + } + + /** + * This is called when, while currently displayed in extract mode, the + * current input target changes. The default implementation will + * auto-hide the IME if the new target is not a full editor, since this + * can be an confusing experience for the user. + */ + public void onExtractingInputChanged(EditorInfo ei) { + if (ei.inputType == InputType.TYPE_NULL) { + dismissSoftInput(InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + void startExtractingText(boolean inputChanged) { final ExtractEditText eet = mExtractEditText; if (eet != null && getCurrentInputStarted() && isFullscreenMode()) { @@ -1557,9 +1793,13 @@ public class InputMethodService extends AbstractInputMethodService { req.hintMaxChars = 10000; mExtractedText = getCurrentInputConnection().getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); + + final EditorInfo ei = getCurrentInputEditorInfo(); + try { eet.startInternalChanges(); - int inputType = getCurrentInputEditorInfo().inputType; + onUpdateExtractingAccessories(ei); + int inputType = ei.inputType; if ((inputType&EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { if ((inputType&EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE) != 0) { @@ -1567,7 +1807,7 @@ public class InputMethodService extends AbstractInputMethodService { } } eet.setInputType(inputType); - eet.setHint(mInputEditorInfo.hintText); + eet.setHint(ei.hintText); if (mExtractedText != null) { eet.setEnabled(true); eet.setExtractedText(mExtractedText); @@ -1578,6 +1818,10 @@ public class InputMethodService extends AbstractInputMethodService { } finally { eet.finishInternalChanges(); } + + if (inputChanged) { + onExtractingInputChanged(ei); + } } } diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 886e688..c838779 100755 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -74,7 +74,6 @@ public class KeyboardView extends View implements View.OnClickListener { * For keys that repeat, this is only called once. * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid * key, the value will be zero. - * @hide Pending API Council approval */ void onPress(int primaryCode); @@ -82,7 +81,6 @@ public class KeyboardView extends View implements View.OnClickListener { * Called when the user releases a key. This is sent after the {@link #onKey} is called. * For keys that repeat, this is only called once. * @param primaryCode the code of the key that was released - * @hide Pending API Council approval */ void onRelease(int primaryCode); @@ -99,6 +97,12 @@ public class KeyboardView extends View implements View.OnClickListener { void onKey(int primaryCode, int[] keyCodes); /** + * Sends a sequence of characters to the listener. + * @param text the sequence of characters to be displayed. + */ + void onText(CharSequence text); + + /** * Called when the user quickly moves the finger from right to left. */ void swipeLeft(); @@ -394,6 +398,7 @@ public class KeyboardView extends View implements View.OnClickListener { requestLayout(); invalidate(); computeProximityThreshold(keyboard); + mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views } /** @@ -699,9 +704,7 @@ public class KeyboardView extends View implements View.OnClickListener { 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++) { - mKeyboardActionListener.onKey(key.text.charAt(i), key.codes); - } + mKeyboardActionListener.onText(key.text); mKeyboardActionListener.onRelease(NOT_A_KEY); } else { int code = key.codes[0]; @@ -792,7 +795,7 @@ public class KeyboardView extends View implements View.OnClickListener { mPreviewText.setCompoundDrawables(null, null, null, null); mPreviewText.setText(getPreviewText(key)); if (key.label.length() > 1 && key.codes.length < 2) { - mPreviewText.setTextSize(mLabelTextSize); + mPreviewText.setTextSize(mKeyTextSize); mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); } else { mPreviewText.setTextSize(mPreviewTextSizeLarge); @@ -896,6 +899,11 @@ public class KeyboardView extends View implements View.OnClickListener { dismissPopupKeyboard(); } + public void onText(CharSequence text) { + mKeyboardActionListener.onText(text); + dismissPopupKeyboard(); + } + public void swipeLeft() { } public void swipeRight() { } public void swipeUp() { } @@ -1102,6 +1110,8 @@ public class KeyboardView extends View implements View.OnClickListener { mHandler.removeMessages(MSG_SHOW_PREVIEW); dismissPopupKeyboard(); + + mMiniKeyboardCache.clear(); } @Override diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index f816caa..ccef97e 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -16,39 +16,46 @@ package android.net; -import android.util.Log; -import android.util.Config; +import android.content.Context; import android.net.http.DomainNameChecker; import android.os.SystemProperties; +import android.util.Config; +import android.util.Log; -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; +import com.android.internal.net.SSLSessionCache; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import java.security.NoSuchAlgorithmException; +import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; -import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * SSLSocketFactory that allows skipping the certificate chain validation + * based on system setting (socket.relaxsslcheck=yes, ro.secure=1 - for + * testing only). + * + * It also adds a readTimeout that will be set on each created socket. + * The factory will use SSL session persistence if enabled by config. + */ public class SSLCertificateSocketFactory extends SSLSocketFactory { - private static final boolean DBG = true; private static final String LOG_TAG = "SSLCertificateSocketFactory"; private static X509TrustManager sDefaultTrustManager; - private final int socketReadTimeoutForSslHandshake; - static { try { TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); @@ -83,14 +90,36 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { } }; - private SSLSocketFactory factory; + private static SSLSocketFactory factory; + + /** + * Initialize a single default factory to be used for all returned + * sockets. + * + * Because of the signature of getDefault(int timeout) it needs to create + * a new instance which encapsulates the timeout on each call. We want + * to share a single SSLContext and SSLSessionCache. + * + * Can be called multiple times - but only the first will initialize the factory. + * + * @param androidContext will be used for SSL session persistence. Null for backward + * compatibility, no SSL persistence. + * @hide + */ + public static synchronized void setupDefaultFactory(Context androidContext) { + if ( factory != null) { + // Can only be initialized once, to avoid having multiple caches. + return; + } + factory = SSLSessionCache.getSocketFactory(androidContext, TRUST_MANAGER); + } + + private final int socketReadTimeoutForSslHandshake; public SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake) throws NoSuchAlgorithmException, KeyManagementException { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, TRUST_MANAGER, new java.security.SecureRandom()); - factory = (SSLSocketFactory) context.getSocketFactory(); - this.socketReadTimeoutForSslHandshake = socketReadTimeoutForSslHandshake; + this.socketReadTimeoutForSslHandshake + = socketReadTimeoutForSslHandshake; } /** @@ -103,6 +132,11 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { */ public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake) { try { + if (factory == null) { + // The delegated factory was not initialized explicitely with a context. + // Use a default one. + setupDefaultFactory(null); + } return new SSLCertificateSocketFactory(socketReadTimeoutForSslHandshake); } catch (NoSuchAlgorithmException e) { Log.e(LOG_TAG, diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java index 4fb1499..0c4fcda 100644 --- a/core/java/android/net/http/AndroidHttpClient.java +++ b/core/java/android/net/http/AndroidHttpClient.java @@ -47,6 +47,8 @@ import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.BasicHttpProcessor; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.BasicHttpContext; +import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; +import org.apache.harmony.xnet.provider.jsse.SSLContextImpl; import java.io.IOException; import java.io.InputStream; @@ -55,6 +57,7 @@ import java.io.OutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.net.URI; +import java.security.KeyManagementException; import android.util.Log; import android.content.ContentResolver; @@ -98,10 +101,13 @@ public final class AndroidHttpClient implements HttpClient { /** * Create a new HttpClient with reasonable defaults (which you can update). + * * @param userAgent to report in your HTTP requests. + * @param sessionCache persistent session cache * @return AndroidHttpClient for you to use for all your requests. */ - public static AndroidHttpClient newInstance(String userAgent) { + public static AndroidHttpClient newInstance(String userAgent, + SSLClientSessionCache sessionCache) { HttpParams params = new BasicHttpParams(); // Turn off stale checking. Our connections break all the time anyway, @@ -123,7 +129,8 @@ public final class AndroidHttpClient implements HttpClient { schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("https", - SSLSocketFactory.getSocketFactory(), 443)); + socketFactoryWithCache(sessionCache), 443)); + ClientConnectionManager manager = new ThreadSafeClientConnManager(params, schemeRegistry); @@ -132,6 +139,41 @@ public final class AndroidHttpClient implements HttpClient { return new AndroidHttpClient(manager, params); } + /** + * Returns a socket factory backed by the given persistent session cache. + * + * @param sessionCache to retrieve sessions from, null for no cache + */ + private static SSLSocketFactory socketFactoryWithCache( + SSLClientSessionCache sessionCache) { + if (sessionCache == null) { + // Use the default factory which doesn't support persistent + // caching. + return SSLSocketFactory.getSocketFactory(); + } + + // Create a new SSL context backed by the cache. + // TODO: Keep a weak *identity* hash map of caches to engines. In the + // mean time, if we have two engines for the same cache, they'll still + // share sessions but will have to do so through the persistent cache. + SSLContextImpl sslContext = new SSLContextImpl(); + try { + sslContext.engineInit(null, null, null, sessionCache, null); + } catch (KeyManagementException e) { + throw new AssertionError(e); + } + return new SSLSocketFactory(sslContext.engineGetSocketFactory()); + } + + /** + * Create a new HttpClient with reasonable defaults (which you can update). + * @param userAgent to report in your HTTP requests. + * @return AndroidHttpClient for you to use for all your requests. + */ + public static AndroidHttpClient newInstance(String userAgent) { + return newInstance(userAgent, null /* session cache */); + } + private final HttpClient delegate; private RuntimeException mLeakedException = new IllegalStateException( diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index b7f7368..0edbe5b 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -16,8 +16,6 @@ package android.net.http; -import android.os.SystemClock; - import java.io.IOException; import java.security.cert.Certificate; @@ -28,23 +26,13 @@ import java.security.cert.X509Certificate; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.util.Arrays; -import java.util.Date; -import java.util.Enumeration; - -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.apache.http.HttpHost; - -import org.bouncycastle.asn1.x509.X509Name; - /** * Class responsible for all server certificate validation functionality * @@ -52,9 +40,6 @@ import org.bouncycastle.asn1.x509.X509Name; */ class CertificateChainValidator { - private static long sTotal = 0; - private static long sTotalReused = 0; - /** * The singleton instance of the certificate chain validator */ @@ -110,91 +95,42 @@ class CertificateChainValidator { * @return An SSL error object if there is an error and null otherwise */ public SslError doHandshakeAndValidateServerCertificates( - HttpsConnection connection, SSLSocket sslSocket, String domain) - throws SSLHandshakeException, IOException { - - ++sTotal; - - SSLContext sslContext = HttpsConnection.getContext(); - if (sslContext == null) { - closeSocketThrowException(sslSocket, "SSL context is null"); - } - + HttpsConnection connection, SSLSocket sslSocket, String domain) + throws IOException { X509Certificate[] serverCertificates = null; - long sessionBeforeHandshakeLastAccessedTime = 0; - byte[] sessionBeforeHandshakeId = null; - - SSLSession sessionAfterHandshake = null; - - synchronized(sslContext) { - // get SSL session before the handshake - SSLSession sessionBeforeHandshake = - getSSLSession(sslContext, connection.getHost()); - if (sessionBeforeHandshake != null) { - sessionBeforeHandshakeLastAccessedTime = - sessionBeforeHandshake.getLastAccessedTime(); + // start handshake, close the socket if we fail + try { + sslSocket.setUseClientMode(true); + sslSocket.startHandshake(); + } catch (IOException e) { + closeSocketThrowException( + sslSocket, e.getMessage(), + "failed to perform SSL handshake"); + } - sessionBeforeHandshakeId = - sessionBeforeHandshake.getId(); - } + // retrieve the chain of the server peer certificates + Certificate[] peerCertificates = + sslSocket.getSession().getPeerCertificates(); - // start handshake, close the socket if we fail - try { - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - } catch (IOException e) { - closeSocketThrowException( - sslSocket, e.getMessage(), - "failed to perform SSL handshake"); + if (peerCertificates == null || peerCertificates.length <= 0) { + closeSocketThrowException( + sslSocket, "failed to retrieve peer certificates"); + } else { + serverCertificates = + new X509Certificate[peerCertificates.length]; + for (int i = 0; i < peerCertificates.length; ++i) { + serverCertificates[i] = + (X509Certificate)(peerCertificates[i]); } - // retrieve the chain of the server peer certificates - Certificate[] peerCertificates = - sslSocket.getSession().getPeerCertificates(); - - if (peerCertificates == null || peerCertificates.length <= 0) { - closeSocketThrowException( - sslSocket, "failed to retrieve peer certificates"); - } else { - serverCertificates = - new X509Certificate[peerCertificates.length]; - for (int i = 0; i < peerCertificates.length; ++i) { - serverCertificates[i] = - (X509Certificate)(peerCertificates[i]); - } - - // update the SSL certificate associated with the connection - if (connection != null) { - if (serverCertificates[0] != null) { - connection.setCertificate( - new SslCertificate(serverCertificates[0])); - } + // update the SSL certificate associated with the connection + if (connection != null) { + if (serverCertificates[0] != null) { + connection.setCertificate( + new SslCertificate(serverCertificates[0])); } } - - // get SSL session after the handshake - sessionAfterHandshake = - getSSLSession(sslContext, connection.getHost()); - } - - if (sessionBeforeHandshakeLastAccessedTime != 0 && - sessionAfterHandshake != null && - Arrays.equals( - sessionBeforeHandshakeId, sessionAfterHandshake.getId()) && - sessionBeforeHandshakeLastAccessedTime < - sessionAfterHandshake.getLastAccessedTime()) { - - if (HttpLog.LOGV) { - HttpLog.v("SSL session was reused: total reused: " - + sTotalReused - + " out of total of: " + sTotal); - - ++sTotalReused; - } - - // no errors!!! - return null; } // check if the first certificate in the chain is for this site @@ -216,7 +152,6 @@ class CertificateChainValidator { } } - // // first, we validate the chain using the standard validation // solution; if we do not find any errors, we are done; if we // fail the standard validation, we re-validate again below, @@ -393,14 +328,14 @@ class CertificateChainValidator { } private void closeSocketThrowException( - SSLSocket socket, String errorMessage, String defaultErrorMessage) - throws SSLHandshakeException, IOException { + SSLSocket socket, String errorMessage, String defaultErrorMessage) + throws IOException { closeSocketThrowException( socket, errorMessage != null ? errorMessage : defaultErrorMessage); } - private void closeSocketThrowException(SSLSocket socket, String errorMessage) - throws SSLHandshakeException, IOException { + private void closeSocketThrowException(SSLSocket socket, + String errorMessage) throws IOException { if (HttpLog.LOGV) { HttpLog.v("validation error: " + errorMessage); } @@ -416,29 +351,4 @@ class CertificateChainValidator { throw new SSLHandshakeException(errorMessage); } - - /** - * @param sslContext The SSL context shared accross all the SSL sessions - * @param host The host associated with the session - * @return A suitable SSL session from the SSL context - */ - private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) { - if (sslContext != null && host != null) { - Enumeration en = sslContext.getClientSessionContext().getIds(); - while (en.hasMoreElements()) { - byte[] id = (byte[]) en.nextElement(); - if (id != null) { - SSLSession session = - sslContext.getClientSessionContext().getSession(id); - if (session.isValid() && - host.getHostName().equals(session.getPeerHost()) && - host.getPort() == session.getPeerPort()) { - return session; - } - } - } - } - - return null; - } } diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 6f9d6c6..7590bfe 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -16,6 +16,8 @@ import android.util.SparseArray; */ public abstract class BatteryStats implements Parcelable { + private static final boolean LOCAL_LOGV = false; + /** * A constant indicating a partial wake lock timer. */ @@ -72,6 +74,7 @@ public abstract class BatteryStats implements Parcelable { private static final String WAKELOCK_DATA = "wakelock"; private static final String NETWORK_DATA = "network"; private static final String BATTERY_DATA = "battery"; + private static final String MISC_DATA = "misc"; private final StringBuilder mFormatBuilder = new StringBuilder(8); private final Formatter mFormatter = new Formatter(mFormatBuilder); @@ -93,11 +96,11 @@ public abstract class BatteryStats implements Parcelable { * Returns the total time in microseconds associated with this Timer for the * selected type of statistics. * - * @param now system uptime time in microseconds + * @param batteryRealtime system realtime on battery in microseconds * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT * @return a time in microseconds */ - public abstract long getTotalTime(long now, int which); + public abstract long getTotalTime(long batteryRealtime, int which); /** * Temporary for debugging. @@ -222,11 +225,11 @@ public abstract class BatteryStats implements Parcelable { /** * Returns the amount of time spent started. * - * @param now elapsed realtime in microseconds. + * @param batteryUptime elapsed uptime on battery in microseconds. * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. * @return */ - public abstract long getStartTime(long now, int which); + public abstract long getStartTime(long batteryUptime, int which); /** * Returns the total number of times startService() has been called. @@ -256,16 +259,16 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getBatteryScreenOnTime(); + public abstract long getScreenOnTime(long batteryRealtime, int which); /** - * Returns the time in milliseconds that the screen has been on while the device was - * plugged in. + * Returns the time in milliseconds that the phone has been on while the device was + * running on battery. * * {@hide} */ - public abstract long getPluggedScreenOnTime(); - + public abstract long getPhoneOnTime(long batteryRealtime, int which); + /** * Return whether we are currently running on battery. */ @@ -382,18 +385,18 @@ public abstract class BatteryStats implements Parcelable { * * @param sb a StringBuilder object. * @param timer a Timer object contining the wakelock times. - * @param now the current time in microseconds. + * @param batteryRealtime the current on-battery time in microseconds. * @param name the name of the wakelock. * @param which which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. * @param linePrefix a String to be prepended to each line of output. * @return the line prefix */ - private static final String printWakeLock(StringBuilder sb, Timer timer, long now, - String name, int which, String linePrefix) { + private static final String printWakeLock(StringBuilder sb, Timer timer, + long batteryRealtime, String name, int which, String linePrefix) { if (timer != null) { // Convert from microseconds to milliseconds with rounding - long totalTimeMicros = timer.getTotalTime(now, which); + long totalTimeMicros = timer.getTotalTime(batteryRealtime, which); long totalTimeMillis = (totalTimeMicros + 500) / 1000; int count = timer.getCount(which); @@ -470,26 +473,30 @@ public abstract class BatteryStats implements Parcelable { * @param which */ private final void dumpCheckinLocked(PrintWriter pw, int which) { - long uSecTime = SystemClock.elapsedRealtime() * 1000; - final long uSecNow = getBatteryUptime(uSecTime); + final long rawUptime = SystemClock.uptimeMillis() * 1000; + final long rawRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryUptime = getBatteryUptime(rawUptime); + final long batteryRealtime = getBatteryRealtime(rawRealtime); + final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); + final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); + final long totalRealtime = computeRealtime(rawRealtime, which); + final long totalUptime = computeUptime(rawUptime, which); + final long screenOnTime = getScreenOnTime(batteryRealtime, which); + final long phoneOnTime = getPhoneOnTime(batteryRealtime, which); StringBuilder sb = new StringBuilder(128); - long batteryUptime = computeBatteryUptime(uSecNow, which); - long batteryRealtime = computeBatteryRealtime(getBatteryRealtime(uSecTime), which); - long elapsedRealtime = computeRealtime(uSecTime, which); - long uptime = computeUptime(SystemClock.uptimeMillis() * 1000, which); String category = STAT_NAMES[which]; // Dump "battery" stat dumpLine(pw, 0 /* uid */, category, BATTERY_DATA, which == STATS_TOTAL ? getStartCount() : "N/A", - batteryUptime / 1000, - formatRatioLocked(batteryUptime, elapsedRealtime), - batteryRealtime / 1000, - formatRatioLocked(batteryRealtime, elapsedRealtime), - uptime / 1000, - elapsedRealtime / 1000); + whichBatteryUptime / 1000, whichBatteryRealtime / 1000, + totalUptime / 1000, totalRealtime / 1000); + + // Dump misc stats + dumpLine(pw, 0 /* uid */, category, MISC_DATA, + screenOnTime / 1000, phoneOnTime / 1000); SparseArray<? extends Uid> uidStats = getUidStats(); final int NU = uidStats.size(); @@ -508,11 +515,11 @@ public abstract class BatteryStats implements Parcelable { Uid.Wakelock wl = ent.getValue(); String linePrefix = ""; sb.setLength(0); - linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_FULL), uSecNow, + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_FULL), batteryRealtime, "full", which, linePrefix); - linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), uSecNow, + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), batteryRealtime, "partial", which, linePrefix); - linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), uSecNow, + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), batteryRealtime, "window", which, linePrefix); // Only log if we had at lease one wakelock... @@ -531,7 +538,7 @@ public abstract class BatteryStats implements Parcelable { Timer timer = se.getSensorTime(); if (timer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (timer.getTotalTime(uSecNow, which) + 500) / 1000; + long totalTime = (timer.getTotalTime(batteryRealtime, which) + 500) / 1000; int count = timer.getCount(which); if (totalTime != 0) { dumpLine(pw, uid, category, SENSOR_DATA, sensorNumber, totalTime, count); @@ -571,7 +578,7 @@ public abstract class BatteryStats implements Parcelable { for (Map.Entry<String, ? extends BatteryStats.Uid.Pkg.Serv> sent : serviceStats.entrySet()) { BatteryStats.Uid.Pkg.Serv ss = sent.getValue(); - long startTime = ss.getStartTime(uSecNow, which); + long startTime = ss.getStartTime(batteryUptime, which); int starts = ss.getStarts(which); int launches = ss.getLaunches(which); if (startTime != 0 || starts != 0 || launches != 0) { @@ -591,29 +598,40 @@ public abstract class BatteryStats implements Parcelable { @SuppressWarnings("unused") private final void dumpLocked(Printer pw, String prefix, int which) { - long uSecTime = SystemClock.elapsedRealtime() * 1000; - final long uSecNow = getBatteryUptime(uSecTime); - + final long rawUptime = SystemClock.uptimeMillis() * 1000; + final long rawRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryUptime = getBatteryUptime(rawUptime); + final long batteryRealtime = getBatteryUptime(rawRealtime); + + final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); + final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); + final long totalRealtime = computeRealtime(rawRealtime, which); + final long totalUptime = computeUptime(rawUptime, which); + StringBuilder sb = new StringBuilder(128); - 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 - + " Time on battery: " + formatTimeMs(batteryUptime / 1000) + "(" - + formatRatioLocked(batteryUptime, elapsedRealtime) + + " Time on battery: " + formatTimeMs(whichBatteryUptime / 1000) + + "(" + formatRatioLocked(whichBatteryUptime, totalRealtime) + ") uptime, " - + formatTimeMs(batteryRealtime / 1000) + "(" - + formatRatioLocked(batteryRealtime, elapsedRealtime) + + formatTimeMs(whichBatteryRealtime / 1000) + "(" + + formatRatioLocked(whichBatteryRealtime, totalRealtime) + ") realtime"); pw.println(prefix + " Total: " - + formatTimeMs(uptime / 1000) + + formatTimeMs(totalUptime / 1000) + "uptime, " - + formatTimeMs(elapsedRealtime / 1000) + + formatTimeMs(totalRealtime / 1000) + "realtime"); + long screenOnTime = getScreenOnTime(batteryRealtime, which); + long phoneOnTime = getPhoneOnTime(batteryRealtime, which); + pw.println(prefix + + " Time with screen on: " + formatTimeMs(screenOnTime / 1000) + + "(" + formatRatioLocked(screenOnTime, whichBatteryRealtime) + + "), time with phone on: " + formatTimeMs(phoneOnTime / 1000) + + "(" + formatRatioLocked(phoneOnTime, whichBatteryRealtime) + ")"); + pw.println(" "); SparseArray<? extends Uid> uidStats = getUidStats(); @@ -641,11 +659,11 @@ public abstract class BatteryStats implements Parcelable { sb.append(prefix); sb.append(" Wake lock "); sb.append(ent.getKey()); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), uSecNow, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), batteryRealtime, "full", which, linePrefix); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), uSecNow, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), batteryRealtime, "partial", which, linePrefix); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), uSecNow, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), batteryRealtime, "window", which, linePrefix); if (!linePrefix.equals(": ")) { sb.append(" realtime"); @@ -677,7 +695,7 @@ public abstract class BatteryStats implements Parcelable { Timer timer = se.getSensorTime(); if (timer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (timer.getTotalTime(uSecNow, which) + 500) / 1000; + long totalTime = (timer.getTotalTime(batteryRealtime, which) + 500) / 1000; int count = timer.getCount(which); //timer.logState(); if (totalTime != 0) { @@ -737,7 +755,7 @@ public abstract class BatteryStats implements Parcelable { for (Map.Entry<String, ? extends BatteryStats.Uid.Pkg.Serv> sent : serviceStats.entrySet()) { BatteryStats.Uid.Pkg.Serv ss = sent.getValue(); - long startTime = ss.getStartTime(uSecNow, which); + long startTime = ss.getStartTime(batteryUptime, which); int starts = ss.getStarts(which); int launches = ss.getLaunches(which); if (startTime != 0 || starts != 0 || launches != 0) { @@ -787,9 +805,24 @@ public abstract class BatteryStats implements Parcelable { @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); + boolean isUnpluggedOnly = false; + + for (String arg : args) { + if ("-u".equals(arg)) { + if (LOCAL_LOGV) Log.v("BatteryStats", "Dumping unplugged data"); + isUnpluggedOnly = true; + } + } + + if (isUnpluggedOnly) { + dumpCheckinLocked(pw, STATS_UNPLUGGED); + } + else { + dumpCheckinLocked(pw, STATS_TOTAL); + dumpCheckinLocked(pw, STATS_LAST); + dumpCheckinLocked(pw, STATS_UNPLUGGED); + dumpCheckinLocked(pw, STATS_CURRENT); + } } + } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index ed138cb..3fcb18e 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -76,6 +76,11 @@ public class ParcelFileDescriptor implements Parcelable { public static final int MODE_TRUNCATE = 0x04000000; /** + * For use with {@link #open}: append to end of file while writing. + */ + public static final int MODE_APPEND = 0x02000000; + + /** * Create a new ParcelFileDescriptor accessing a given file. * * @param file The file to be opened. @@ -138,6 +143,19 @@ public class ParcelFileDescriptor implements Parcelable { } /** + * Return the total size of the file representing this fd, as determined + * by stat(). Returns -1 if the fd is not a file. + */ + public native long getStatSize(); + + /** + * This is needed for implementing AssetFileDescriptor.AutoCloseOutputStream, + * and I really don't think we want it to be public. + * @hide + */ + public native long seekTo(long pos); + + /** * Close the ParcelFileDescriptor. This implementation closes the underlying * OS resources allocated to represent this stream. * diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java index 4a5d7e4..cc0f45e 100644 --- a/core/java/android/pim/ICalendar.java +++ b/core/java/android/pim/ICalendar.java @@ -405,13 +405,15 @@ public class ICalendar { // TODO: get rid of this -- handle all of the parsing in one pass through // the text. private static String normalizeText(String text) { - // first we deal with line folding, by replacing all "\r\n " strings - // with nothing - text = text.replaceAll("\r\n ", ""); - // it's supposed to be \r\n, but not everyone does that text = text.replaceAll("\r\n", "\n"); text = text.replaceAll("\r", "\n"); + + // we deal with line folding, by replacing all "\n " strings + // with nothing. The RFC specifies "\r\n " to be folded, but + // we handle "\n " and "\r " too because we can get those. + text = text.replaceAll("\n ", ""); + return text; } @@ -440,7 +442,7 @@ public class ICalendar { current = parseLine(line, state, current); // if the provided component was null, we will return the root // NOTE: in this case, if the first line is not a BEGIN, a - // FormatException will get thrown. + // FormatException will get thrown. if (component == null) { component = current; } @@ -524,8 +526,7 @@ public class ICalendar { private static String extractValue(ParserState state) throws FormatException { String line = state.line; - char c = line.charAt(state.index); - if (c != ':') { + if (state.index >= line.length() || line.charAt(state.index) != ':') { throw new FormatException("Expected ':' before end of line in " + line); } diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 0cdac53..1454089 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -62,6 +62,9 @@ public final class Checkin { AUTOTEST_FAILURE, AUTOTEST_SEQUENCE_BEGIN, AUTOTEST_SUITE_BEGIN, + AUTOTEST_TCPDUMP_BEGIN, + AUTOTEST_TCPDUMP_DATA, + AUTOTEST_TCPDUMP_END, AUTOTEST_TEST_BEGIN, AUTOTEST_TEST_FAILURE, AUTOTEST_TEST_SUCCESS, @@ -98,14 +101,14 @@ public final class Checkin { SETUP_SERVER_ERROR, SETUP_SERVER_TIMEOUT, SETUP_NO_DATA_NETWORK, - SYSTEM_APP_NOT_RESPONDING, SYSTEM_BOOT, SYSTEM_LAST_KMSG, SYSTEM_RECOVERY_LOG, SYSTEM_RESTART, SYSTEM_SERVICE_LOOPING, SYSTEM_TOMBSTONE, - TEST, + TEST, + BATTERY_DISCHARGE_INFO, } } @@ -190,6 +193,9 @@ public final class Checkin { // The category is used for GTalk service messages public static final String CATEGORY = "android.server.checkin.CHECKIN"; + + // If true indicates that the checkin should only transfer market related data + public static final String EXTRA_MARKET_ONLY = "market_only"; } private static final String TAG = "Checkin"; diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java index bea857f..19ad158 100644 --- a/core/java/android/provider/Im.java +++ b/core/java/android/provider/Im.java @@ -2044,4 +2044,37 @@ public class Im { */ public static final Uri CONTENT_URI = Uri.parse("content://im/lastRmqId"); } + + /** + * Columns for IM branding resource map cache table. This table caches the result of + * loading the branding resources to speed up IM landing page start. + */ + public interface BrandingResourceMapCacheColumns { + /** + * The provider ID + * <P>Type: INTEGER</P> + */ + String PROVIDER_ID = "provider_id"; + /** + * The application resource ID + * <P>Type: INTEGER</P> + */ + String APP_RES_ID = "app_res_id"; + /** + * The plugin resource ID + * <P>Type: INTEGER</P> + */ + String PLUGIN_RES_ID = "plugin_res_id"; + } + + /** + * The table for caching the result of loading IM branding resources. + */ + public static final class BrandingResourceMapCache + implements BaseColumns, BrandingResourceMapCacheColumns { + /** + * The content:// style URL for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://im/brandingResMapCache"); + } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4a784c8..10ca5d5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2183,6 +2183,12 @@ public final class Settings { public static final String CHECKIN_INTERVAL = "checkin_interval"; /** + * Boolean indicating if the market app should force market only checkins on + * install/uninstall. Any non-0 value is considered true. + */ + public static final String MARKET_FORCE_CHECKIN = "market_force_checkin"; + + /** * How frequently (in seconds) to check the memory status of the * device. */ @@ -2455,6 +2461,14 @@ public final class Settings { "gtalk_ssl_handshake_timeout_ms"; /** + * Enable use of ssl session caching. + * 'db' - save each session in a per-process database + * not set or any other value - normal java in-memory caching. + * Other cache types may be added. + */ + public static final String SSL_SESSION_CACHE = "ssl_session_cache"; + + /** * How many bytes long a message has to be, in order to be gzipped. */ public static final String SYNC_MIN_GZIP_BYTES = @@ -2766,11 +2780,29 @@ public final class Settings { public static final String VOICE_SEARCH_ENCODING_WIFI = "voice_search_encoding_wifi"; /** - * Prefix for rules that launch automatic instrumentation test cycles. - * The format is the InstrumentationTestRunner (or compatible) package and class, - * followed optionally by space-separated key value pairs to pass to it. + * Whether to use automatic gain control in voice search (0 = disable, 1 = enable). + * To be factored out of this class. */ - public static final String AUTOTEST_PREFIX = "autotest:"; + public static final String VOICE_SEARCH_ENABLE_AGC = "voice_search_enable_agc"; + + /** + * Whether to use noise suppression in voice search (0 = disable, 1 = enable). + * To be factored out of this class. + */ + public static final String VOICE_SEARCH_ENABLE_NS = "voice_search_enable_ns"; + + /** + * Whether to use the IIR filter in voice search (0 = disable, 1 = enable). + * To be factored out of this class. + */ + public static final String VOICE_SEARCH_ENABLE_IIR = "voice_search_enable_iir"; + + /** + * List of test suites (local disk filename) for the automatic instrumentation test runner. + * The file format is similar to automated_suites.xml, see AutoTesterService. + * If this setting is missing or empty, the automatic test runner will not start. + */ + public static final String AUTOTEST_SUITES_FILE = "autotest_suites_file"; /** * Interval between synchronous checkins forced by the automatic test runner. @@ -2785,6 +2817,15 @@ public final class Settings { */ public static final String AUTOTEST_REBOOT_SECONDS = "autotest_reboot_seconds"; + + /** + * Threshold values for the duration and level of a discharge cycle, under + * which we log discharge cycle info. + */ + public static final String BATTERY_DISCHARGE_DURATION_THRESHOLD = + "battery_discharge_duration_threshold"; + public static final String BATTERY_DISCHARGE_THRESHOLD = "battery_discharge_threshold"; + /** * @deprecated * @hide diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 58f9491..f8bc765 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -15,8 +15,7 @@ */ /** - * TODO: Move this to - * java/services/com/android/server/BluetoothA2dpService.java + * TODO: Move this to services.jar * and make the contructor package private again. * @hide */ @@ -35,15 +34,16 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioManager; import android.os.Binder; +import android.os.Handler; +import android.os.Message; import android.provider.Settings; import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.List; import java.util.HashMap; -import java.util.Iterator; +import java.util.List; public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String TAG = "BluetoothA2dpService"; @@ -57,6 +57,8 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String A2DP_SINK_ADDRESS = "a2dp_sink_address"; private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; + private static final int MESSAGE_CONNECT_TO = 1; + private final Context mContext; private final IntentFilter mIntentFilter; private HashMap<String, SinkState> mAudioDevices; @@ -86,6 +88,7 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { mIntentFilter = new IntentFilter(BluetoothIntent.ENABLED_ACTION); mIntentFilter.addAction(BluetoothIntent.DISABLED_ACTION); mIntentFilter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION); + mIntentFilter.addAction(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); mContext.registerReceiver(mReceiver, mIntentFilter); if (device.isEnabled()) { @@ -123,6 +126,37 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { setSinkPriority(address, BluetoothA2dp.PRIORITY_OFF); break; } + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION)) { + if (getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF) { + // This device is a preferred sink. Make an A2DP connection + // after a delay. We delay to avoid connection collisions, + // and to give other profiles such as HFP a chance to + // connect first. + Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, address); + mHandler.sendMessageDelayed(msg, 6000); + } + } + } + }; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_CONNECT_TO: + String address = (String)msg.obj; + // check device is still preferred, and nothing is currently + // connected + if (getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF && + lookupSinksMatchingStates(new int[] { + BluetoothA2dp.STATE_CONNECTING, + BluetoothA2dp.STATE_CONNECTED, + BluetoothA2dp.STATE_PLAYING, + BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { + log("Auto-connecting A2DP to sink " + address); + connectSink(address); + } + break; } } }; @@ -142,7 +176,10 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private synchronized void onBluetoothDisable() { if (mAudioDevices != null) { - for (String path : mAudioDevices.keySet()) { + // copy to allow modification during iteration + String[] paths = new String[mAudioDevices.size()]; + paths = mAudioDevices.keySet().toArray(paths); + for (String path : paths) { switch (mAudioDevices.get(path).state) { case BluetoothA2dp.STATE_CONNECTING: case BluetoothA2dp.STATE_CONNECTED: @@ -234,17 +271,8 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { public synchronized List<String> listConnectedSinks() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - List<String> connectedSinks = new ArrayList<String>(); - if (mAudioDevices == null) { - return connectedSinks; - } - for (SinkState sink : mAudioDevices.values()) { - if (sink.state == BluetoothA2dp.STATE_CONNECTED || - sink.state == BluetoothA2dp.STATE_PLAYING) { - connectedSinks.add(sink.address); - } - } - return connectedSinks; + return lookupSinksMatchingStates(new int[] {BluetoothA2dp.STATE_CONNECTED, + BluetoothA2dp.STATE_PLAYING}); } public synchronized int getSinkState(String address) { @@ -298,7 +326,11 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { // bluez 3.36 quietly disconnects the previous sink when a new sink // is connected, so we need to mark all previously connected sinks as // disconnected - for (String oldPath : mAudioDevices.keySet()) { + + // copy to allow modification during iteration + String[] paths = new String[mAudioDevices.size()]; + paths = mAudioDevices.keySet().toArray(paths); + for (String oldPath : paths) { if (path.equals(oldPath)) { continue; } @@ -350,6 +382,22 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return null; } + private synchronized List<String> lookupSinksMatchingStates(int[] states) { + List<String> sinks = new ArrayList<String>(); + if (mAudioDevices == null) { + return sinks; + } + for (SinkState sink : mAudioDevices.values()) { + for (int state : states) { + if (sink.state == state) { + sinks.add(sink.address); + break; + } + } + } + return sinks; + } + private synchronized void updateState(String path, int state) { if (mAudioDevices == null) return; diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java index fa53a60..950ff3a 100644 --- a/core/java/android/server/BluetoothDeviceService.java +++ b/core/java/android/server/BluetoothDeviceService.java @@ -141,6 +141,20 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); } + + // Remove remoteServiceChannelCallbacks + HashMap<String, IBluetoothDeviceCallback> callbacksMap = + mEventLoop.getRemoteServiceChannelCallbacks(); + IBluetoothDeviceCallback callback; + + for (String address : callbacksMap.keySet()) { + callback = callbacksMap.get(address); + try { + callback.onGetRemoteServiceChannelResult(address, BluetoothError.ERROR_DISABLED); + } catch (RemoteException e) {} + callbacksMap.remove(address); + } + // update mode Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); intent.putExtra(BluetoothIntent.SCAN_MODE, BluetoothDevice.SCAN_MODE_NONE); @@ -569,10 +583,18 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { } address = address.toUpperCase(); + String[] bonding = mBondState.listInState(BluetoothDevice.BOND_BONDING); + if (bonding.length > 0 && !bonding[0].equals(address)) { + log("Ignoring createBond(): another device is bonding"); + // a different device is currently bonding, fail + return false; + } + // Check for bond state only if we are not performing auto // pairing exponential back-off attempts. if (!mBondState.isAutoPairingAttemptsInProgress(address) && - mBondState.getBondState(address) != BluetoothDevice.BOND_NOT_BONDED) { + mBondState.getBondState(address) != BluetoothDevice.BOND_NOT_BONDED) { + log("Ignoring createBond(): this device is already bonding or bonded"); return false; } diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java index c18675e..0c04839 100644 --- a/core/java/android/server/search/SearchableInfo.java +++ b/core/java/android/server/search/SearchableInfo.java @@ -35,6 +35,7 @@ import android.text.InputType; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; +import android.view.inputmethod.EditorInfo; import java.io.IOException; import java.util.ArrayList; @@ -77,6 +78,7 @@ public final class SearchableInfo implements Parcelable { private int mIconId = 0; private int mSearchButtonText = 0; private int mSearchInputType = 0; + private int mSearchImeOptions = 0; private String mSuggestAuthority = null; private String mSuggestPath = null; private String mSuggestSelection = null; @@ -429,8 +431,9 @@ public final class SearchableInfo implements Parcelable { com.android.internal.R.styleable.Searchable_searchButtonText, 0); mSearchInputType = a.getInt(com.android.internal.R.styleable.Searchable_inputType, InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_FLAG_SEARCH | InputType.TYPE_TEXT_VARIATION_NORMAL); + mSearchImeOptions = a.getInt(com.android.internal.R.styleable.Searchable_imeOptions, + EditorInfo.IME_ACTION_SEARCH); setSearchModeFlags(); if (DBG_INHIBIT_SUGGESTIONS == 0) { @@ -743,6 +746,17 @@ public final class SearchableInfo implements Parcelable { } /** + * Return the input method options specified in the searchable attributes. + * This will default to EditorInfo.ACTION_SEARCH if not specified (which is + * appropriate for a search box). + * + * @return the input type + */ + public int getImeOptions() { + return mSearchImeOptions; + } + + /** * Return the list of searchable activities, for use in the drop-down. */ public static ArrayList<SearchableInfo> getSearchablesList() { @@ -782,6 +796,7 @@ public final class SearchableInfo implements Parcelable { mIconId = in.readInt(); mSearchButtonText = in.readInt(); mSearchInputType = in.readInt(); + mSearchImeOptions = in.readInt(); setSearchModeFlags(); mSuggestAuthority = in.readString(); @@ -818,6 +833,7 @@ public final class SearchableInfo implements Parcelable { dest.writeInt(mIconId); dest.writeInt(mSearchButtonText); dest.writeInt(mSearchInputType); + dest.writeInt(mSearchImeOptions); dest.writeString(mSuggestAuthority); dest.writeString(mSuggestPath); diff --git a/core/java/android/speech/srec/package.html b/core/java/android/speech/srec/package.html index 723b30b..9a99df8 100644 --- a/core/java/android/speech/srec/package.html +++ b/core/java/android/speech/srec/package.html @@ -1,5 +1,6 @@ <HTML> <BODY> Simple, synchronous SREC speech recognition API. +@hide </BODY> </HTML> diff --git a/core/java/android/text/InputType.java b/core/java/android/text/InputType.java index a073cf4..f643f92 100644 --- a/core/java/android/text/InputType.java +++ b/core/java/android/text/InputType.java @@ -128,11 +128,6 @@ public interface InputType { */ public static final int TYPE_TEXT_FLAG_IME_MULTI_LINE = 0x00040000; - /** - * Flag for {@link #TYPE_CLASS_TEXT}: flags any text being used as a search string - */ - public static final int TYPE_TEXT_FLAG_SEARCH = 0x00080000; - // ---------------------------------------------------------------------- /** diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java index 05c27ea..0aa2004 100644 --- a/core/java/android/text/Styled.java +++ b/core/java/android/text/Styled.java @@ -16,25 +16,26 @@ package android.text; -import android.graphics.Paint; import android.graphics.Canvas; -import android.graphics.Path; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.graphics.MaskFilter; -import android.graphics.Rasterizer; -import android.graphics.LayerRasterizer; -import android.text.style.*; - -/* package */ class Styled +import android.graphics.Paint; +import android.text.style.CharacterStyle; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; + +/** + * This class provides static methods for drawing and measuring styled texts, like + * {@link android.text.Spanned} object with {@link android.text.style.ReplacementSpan}. + * @hide + */ +public class Styled { private static float each(Canvas canvas, Spanned text, int start, int end, int dir, boolean reverse, float x, int top, int y, int bottom, - Paint.FontMetricsInt fm, - TextPaint realPaint, + Paint.FontMetricsInt fmi, TextPaint paint, + TextPaint workPaint, boolean needwid) { boolean havewid = false; @@ -43,9 +44,9 @@ import android.text.style.*; ReplacementSpan replacement = null; - realPaint.bgColor = 0; - realPaint.baselineShift = 0; - paint.set(realPaint); + paint.bgColor = 0; + paint.baselineShift = 0; + workPaint.set(paint); if (spans.length > 0) { for (int i = 0; i < spans.length; i++) { @@ -55,7 +56,7 @@ import android.text.style.*; replacement = (ReplacementSpan)span; } else { - span.updateDrawState(paint); + span.updateDrawState(workPaint); } } } @@ -74,66 +75,66 @@ import android.text.style.*; tmpend = end; } - if (fm != null) { - paint.getFontMetricsInt(fm); + if (fmi != null) { + workPaint.getFontMetricsInt(fmi); } if (canvas != null) { - if (paint.bgColor != 0) { - int c = paint.getColor(); - Paint.Style s = paint.getStyle(); - paint.setColor(paint.bgColor); - paint.setStyle(Paint.Style.FILL); + if (workPaint.bgColor != 0) { + int c = workPaint.getColor(); + Paint.Style s = workPaint.getStyle(); + workPaint.setColor(workPaint.bgColor); + workPaint.setStyle(Paint.Style.FILL); if (!havewid) { - ret = paint.measureText(tmp, tmpstart, tmpend); + ret = workPaint.measureText(tmp, tmpstart, tmpend); havewid = true; } if (dir == Layout.DIR_RIGHT_TO_LEFT) - canvas.drawRect(x - ret, top, x, bottom, paint); + canvas.drawRect(x - ret, top, x, bottom, workPaint); else - canvas.drawRect(x, top, x + ret, bottom, paint); + canvas.drawRect(x, top, x + ret, bottom, workPaint); - paint.setStyle(s); - paint.setColor(c); + workPaint.setStyle(s); + workPaint.setColor(c); } if (dir == Layout.DIR_RIGHT_TO_LEFT) { if (!havewid) { - ret = paint.measureText(tmp, tmpstart, tmpend); + ret = workPaint.measureText(tmp, tmpstart, tmpend); havewid = true; } canvas.drawText(tmp, tmpstart, tmpend, - x - ret, y + paint.baselineShift, paint); + x - ret, y + workPaint.baselineShift, workPaint); } else { if (needwid) { if (!havewid) { - ret = paint.measureText(tmp, tmpstart, tmpend); + ret = workPaint.measureText(tmp, tmpstart, tmpend); havewid = true; } } canvas.drawText(tmp, tmpstart, tmpend, - x, y + paint.baselineShift, paint); + x, y + workPaint.baselineShift, workPaint); } } else { if (needwid && !havewid) { - ret = paint.measureText(tmp, tmpstart, tmpend); + ret = workPaint.measureText(tmp, tmpstart, tmpend); havewid = true; } } } else { - ret = replacement.getSize(paint, text, start, end, fm); + ret = replacement.getSize(workPaint, text, start, end, fmi); if (canvas != null) { if (dir == Layout.DIR_RIGHT_TO_LEFT) replacement.draw(canvas, text, start, end, - x - ret, top, y, bottom, paint); + x - ret, top, y, bottom, workPaint); else replacement.draw(canvas, text, start, end, - x, top, y, bottom, paint); + x, top, y, bottom, workPaint); } } @@ -143,15 +144,29 @@ import android.text.style.*; return ret; } - public static int getTextWidths(TextPaint realPaint, - TextPaint paint, - Spanned text, int start, int end, - float[] widths, Paint.FontMetricsInt fm) { - + /** + * Return the advance widths for the characters in the string. + * See also {@link android.graphics.Paint#getTextWidths(CharSequence, int, int, float[])}. + * + * @param paint The main {@link TextPaint} object. + * @param workPaint The {@link TextPaint} object used for temporal workspace. + * @param text The text to measure + * @param start The index of the first char to to measure + * @param end The end of the text slice to measure + * @param widths Array to receive the advance widths of the characters. + * Must be at least a large as (end - start). + * @param fmi FontMetrics information. Can be null. + * @return The actual number of widths returned. + */ + public static int getTextWidths(TextPaint paint, + TextPaint workPaint, + Spanned text, int start, int end, + float[] widths, Paint.FontMetricsInt fmi) { + // Keep workPaint as is so that developers reuse the workspace. MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class); ReplacementSpan replacement = null; - paint.set(realPaint); + workPaint.set(paint); for (int i = 0; i < spans.length; i++) { MetricAffectingSpan span = spans[i]; @@ -159,15 +174,15 @@ import android.text.style.*; replacement = (ReplacementSpan)span; } else { - span.updateMeasureState(paint); + span.updateMeasureState(workPaint); } } if (replacement == null) { - paint.getFontMetricsInt(fm); - paint.getTextWidths(text, start, end, widths); + workPaint.getFontMetricsInt(fmi); + workPaint.getTextWidths(text, start, end, widths); } else { - int wid = replacement.getSize(paint, text, start, end, fm); + int wid = replacement.getSize(workPaint, text, start, end, fmi); if (end > start) { widths[0] = wid; @@ -183,10 +198,10 @@ import android.text.style.*; CharSequence text, int start, int end, int dir, boolean reverse, float x, int top, int y, int bottom, - Paint.FontMetricsInt fm, + Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, - boolean needwid) { + boolean needWidth) { if (! (text instanceof Spanned)) { float ret = 0; @@ -194,22 +209,22 @@ import android.text.style.*; CharSequence tmp = TextUtils.getReverse(text, start, end); int tmpend = end - start; - if (canvas != null || needwid) + if (canvas != null || needWidth) ret = paint.measureText(tmp, 0, tmpend); if (canvas != null) canvas.drawText(tmp, 0, tmpend, x - ret, y, paint); } else { - if (needwid) + if (needWidth) ret = paint.measureText(text, start, end); if (canvas != null) canvas.drawText(text, start, end, x, y, paint); } - if (fm != null) { - paint.getFontMetricsInt(fm); + if (fmi != null) { + paint.getFontMetricsInt(fmi); } return ret * dir; //Layout.DIR_RIGHT_TO_LEFT == -1 @@ -232,67 +247,129 @@ import android.text.style.*; next = sp.nextSpanTransition(i, end, division); x += each(canvas, sp, i, next, dir, reverse, - x, top, y, bottom, fm, paint, workPaint, - needwid || next != end); - - if (fm != null) { - if (fm.ascent < asc) - asc = fm.ascent; - if (fm.descent > desc) - desc = fm.descent; - - if (fm.top < ftop) - ftop = fm.top; - if (fm.bottom > fbot) - fbot = fm.bottom; + x, top, y, bottom, fmi, paint, workPaint, + needWidth || next != end); + + if (fmi != null) { + if (fmi.ascent < asc) + asc = fmi.ascent; + if (fmi.descent > desc) + desc = fmi.descent; + + if (fmi.top < ftop) + ftop = fmi.top; + if (fmi.bottom > fbot) + fbot = fmi.bottom; } } - if (fm != null) { + if (fmi != null) { if (start == end) { - paint.getFontMetricsInt(fm); + paint.getFontMetricsInt(fmi); } else { - fm.ascent = asc; - fm.descent = desc; - fm.top = ftop; - fm.bottom = fbot; + fmi.ascent = asc; + fmi.descent = desc; + fmi.top = ftop; + fmi.bottom = fbot; } } return x - ox; } - public static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean reverse, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needwid) { - if ((dir == Layout.DIR_RIGHT_TO_LEFT && !reverse)||(reverse && dir == Layout.DIR_LEFT_TO_RIGHT)) { + + /* package */ static float drawText(Canvas canvas, + CharSequence text, int start, int end, + int direction, boolean reverse, + float x, int top, int y, int bottom, + TextPaint paint, + TextPaint workPaint, + boolean needWidth) { + if ((direction == Layout.DIR_RIGHT_TO_LEFT && !reverse) || + (reverse && direction == Layout.DIR_LEFT_TO_RIGHT)) { float ch = foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, workPaint, true); - ch *= dir; // DIR_RIGHT_TO_LEFT == -1 - foreach(canvas, text, start, end, -dir, + ch *= direction; // DIR_RIGHT_TO_LEFT == -1 + foreach(canvas, text, start, end, -direction, reverse, x + ch, top, y, bottom, null, paint, workPaint, true); return ch; } - return foreach(canvas, text, start, end, dir, reverse, + return foreach(canvas, text, start, end, direction, reverse, x, top, y, bottom, null, paint, workPaint, - needwid); + needWidth); } - + + /** + * Draw the specified range of text, specified by start/end, with its origin at (x,y), + * in the specified Paint. The origin is interpreted based on the Align setting in the + * Paint. + * + * This method considers style information in the text + * (e.g. Even when text is an instance of {@link android.text.Spanned}, this method + * correctly draws the text). + * See also + * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, float, Paint)} + * and + * {@link android.graphics.Canvas#drawRect(float, float, float, float, Paint)}. + * + * @param canvas The target canvas. + * @param text The text to be drawn + * @param start The index of the first character in text to draw + * @param end (end - 1) is the index of the last character in text to draw + * @param direction The direction of the text. This must be + * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or + * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. + * @param x The x-coordinate of origin for where to draw the text + * @param top The top side of the rectangle to be drawn + * @param y The y-coordinate of origin for where to draw the text + * @param bottom The bottom side of the rectangle to be drawn + * @param paint The main {@link TextPaint} object. + * @param workPaint The {@link TextPaint} object used for temporal workspace. + * @param needWidth If true, this method returns the width of drawn text. + * @return Width of the drawn text if needWidth is true. + */ + public static float drawText(Canvas canvas, + CharSequence text, int start, int end, + int direction, + float x, int top, int y, int bottom, + TextPaint paint, + TextPaint workPaint, + boolean needWidth) { + // For safety. + direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; + /* + * Hided "reverse" parameter since it is meaningless for external developers. + * Kept workPaint as is so that developers reuse the workspace. + */ + return drawText(canvas, text, start, end, direction, false, + x, top, y, bottom, paint, workPaint, needWidth); + } + + /** + * Return the width of the text, considering style information in the text + * (e.g. Even when text is an instance of {@link android.text.Spanned}, this method + * correctly mesures the width of the text). + * + * @param paint The main {@link TextPaint} object. + * @param workPaint The {@link TextPaint} object used for temporal workspace. + * @param text The text to measure + * @param start The index of the first character to start measuring + * @param end 1 beyond the index of the last character to measure + * @param fmi FontMetrics information. Can be null + * @return The width of the text + */ public static float measureText(TextPaint paint, TextPaint workPaint, CharSequence text, int start, int end, - Paint.FontMetricsInt fm) { + Paint.FontMetricsInt fmi) { + // Keep workPaint as is so that developers reuse the workspace. return foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, false, - 0, 0, 0, 0, fm, paint, workPaint, true); + 0, 0, 0, 0, fmi, paint, workPaint, true); } } diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index feae6cf..8a7cdd9 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -595,6 +595,17 @@ public class DateUtils * @param elapsedSeconds the elapsed time in seconds. */ public static String formatElapsedTime(long elapsedSeconds) { + return formatElapsedTime(null, elapsedSeconds); + } + + /** + * Formats an elapsed time in the form "MM:SS" or "H:MM:SS" + * for display on the call-in-progress screen. + * + * @param recycle {@link StringBuilder} to recycle, if possible + * @param elapsedSeconds the elapsed time in seconds. + */ + public static String formatElapsedTime(StringBuilder recycle, long elapsedSeconds) { initFormatStrings(); long hours = 0; @@ -613,18 +624,24 @@ public class DateUtils String result; if (hours > 0) { - return formatElapsedTime(sElapsedFormatHMMSS, hours, minutes, seconds); + return formatElapsedTime(recycle, sElapsedFormatHMMSS, hours, minutes, seconds); } else { - return formatElapsedTime(sElapsedFormatMMSS, minutes, seconds); + return formatElapsedTime(recycle, sElapsedFormatMMSS, minutes, seconds); } } /** * Fast formatting of h:mm:ss */ - private static String formatElapsedTime(String format, long hours, long minutes, long seconds) { + private static String formatElapsedTime(StringBuilder recycle, String format, long hours, + long minutes, long seconds) { if (FAST_FORMAT_HMMSS.equals(format)) { - StringBuffer sb = new StringBuffer(16); + StringBuilder sb = recycle; + if (sb == null) { + sb = new StringBuilder(8); + } else { + sb.setLength(0); + } sb.append(hours); sb.append(TIME_SEPARATOR); if (minutes < 10) { @@ -649,9 +666,15 @@ public class DateUtils /** * Fast formatting of m:ss */ - private static String formatElapsedTime(String format, long minutes, long seconds) { + private static String formatElapsedTime(StringBuilder recycle, String format, long minutes, + long seconds) { if (FAST_FORMAT_MMSS.equals(format)) { - StringBuffer sb = new StringBuffer(16); + StringBuilder sb = recycle; + if (sb == null) { + sb = new StringBuilder(8); + } else { + sb.setLength(0); + } if (minutes < 10) { sb.append(TIME_PADDING); } else { @@ -1028,8 +1051,9 @@ public class DateUtils * If FORMAT_NO_YEAR is set, then the year is not shown. * If neither FORMAT_SHOW_YEAR nor FORMAT_NO_YEAR are set, then the year * is shown only if it is different from the current year, or if the start - * and end dates fall on different years. - * + * and end dates fall on different years. If both are set, + * FORMAT_SHOW_YEAR takes precedence. + * * <p> * Normally the date is shown unless the start and end day are the same. * If FORMAT_SHOW_DATE is set, then the date is always shown, even for @@ -1120,24 +1144,28 @@ public class DateUtils boolean abbrevMonth = (flags & (FORMAT_ABBREV_MONTH | FORMAT_ABBREV_ALL)) != 0; boolean noMonthDay = (flags & FORMAT_NO_MONTH_DAY) != 0; boolean numericDate = (flags & FORMAT_NUMERIC_DATE) != 0; - - Time startDate; + + // If we're getting called with a single instant in time (from + // e.g. formatDateTime(), below), then we can skip a lot of + // computation below that'd otherwise be thrown out. + boolean isInstant = (startMillis == endMillis); + + Time startDate = useUTC ? new Time(Time.TIMEZONE_UTC) : new Time(); + startDate.set(startMillis); + Time endDate; - - if (useUTC) { - startDate = new Time(Time.TIMEZONE_UTC); - endDate = new Time(Time.TIMEZONE_UTC); + int dayDistance; + if (isInstant) { + endDate = startDate; + dayDistance = 0; } else { - startDate = new Time(); - endDate = new Time(); + endDate = useUTC ? new Time(Time.TIMEZONE_UTC) : new Time(); + endDate.set(endMillis); + int startJulianDay = Time.getJulianDay(startMillis, startDate.gmtoff); + int endJulianDay = Time.getJulianDay(endMillis, endDate.gmtoff); + dayDistance = endJulianDay - startJulianDay; } - - startDate.set(startMillis); - endDate.set(endMillis); - int startJulianDay = Time.getJulianDay(startMillis, startDate.gmtoff); - int endJulianDay = Time.getJulianDay(endMillis, endDate.gmtoff); - int dayDistance = endJulianDay - startJulianDay; - + // If the end date ends at 12am at the beginning of a day, // then modify it to make it look like it ends at midnight on // the previous day. This will allow us to display "8pm - midnight", @@ -1152,20 +1180,21 @@ public class DateUtils // and an end date of Nov 12 at 00:00. // If the start and end time are the same, then skip this and don't // adjust the date. - if ((endDate.hour | endDate.minute | endDate.second) == 0 - && (!showTime || dayDistance <= 1) && (startMillis != endMillis)) { + if (!isInstant + && (endDate.hour | endDate.minute | endDate.second) == 0 + && (!showTime || dayDistance <= 1)) { endDate.monthDay -= 1; endDate.normalize(true /* ignore isDst */); } - + int startDay = startDate.monthDay; int startMonthNum = startDate.month; int startYear = startDate.year; - + int endDay = endDate.monthDay; int endMonthNum = endDate.month; int endYear = endDate.year; - + String startWeekDayString = ""; String endWeekDayString = ""; if (showWeekDay) { @@ -1176,9 +1205,9 @@ public class DateUtils weekDayFormat = WEEKDAY_FORMAT; } startWeekDayString = startDate.format(weekDayFormat); - endWeekDayString = endDate.format(weekDayFormat); + endWeekDayString = isInstant ? startWeekDayString : endDate.format(weekDayFormat); } - + String startTimeString = ""; String endTimeString = ""; if (showTime) { @@ -1204,7 +1233,7 @@ public class DateUtils boolean capNoon = (flags & FORMAT_CAP_NOON) != 0; boolean noMidnight = (flags & FORMAT_NO_MIDNIGHT) != 0; boolean capMidnight = (flags & FORMAT_CAP_MIDNIGHT) != 0; - + boolean startOnTheHour = startDate.minute == 0 && startDate.second == 0; boolean endOnTheHour = endDate.minute == 0 && endDate.second == 0; if (abbrevTime && startOnTheHour) { @@ -1220,20 +1249,41 @@ public class DateUtils startTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm); } } - if (abbrevTime && endOnTheHour) { - if (capAMPM) { - endTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm); + + // Don't waste time on setting endTimeFormat when + // we're dealing with an instant, where we'll never + // need the end point. (It's the same as the start + // point) + if (!isInstant) { + if (abbrevTime && endOnTheHour) { + if (capAMPM) { + endTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm); + } else { + endTimeFormat = res.getString(com.android.internal.R.string.hour_ampm); + } } else { - endTimeFormat = res.getString(com.android.internal.R.string.hour_ampm); + if (capAMPM) { + endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm); + } else { + endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm); + } } - } else { - if (capAMPM) { - endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm); - } else { - endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm); + + if (endDate.hour == 12 && endOnTheHour && !noNoon) { + if (capNoon) { + endTimeFormat = res.getString(com.android.internal.R.string.Noon); + } else { + endTimeFormat = res.getString(com.android.internal.R.string.noon); + } + } else if (endDate.hour == 0 && endOnTheHour && !noMidnight) { + if (capMidnight) { + endTimeFormat = res.getString(com.android.internal.R.string.Midnight); + } else { + endTimeFormat = res.getString(com.android.internal.R.string.midnight); + } } } - + if (startDate.hour == 12 && startOnTheHour && !noNoon) { if (capNoon) { startTimeFormat = res.getString(com.android.internal.R.string.Noon); @@ -1243,37 +1293,32 @@ public class DateUtils // Don't show the start time starting at midnight. Show // 12am instead. } - - if (endDate.hour == 12 && endOnTheHour && !noNoon) { - if (capNoon) { - endTimeFormat = res.getString(com.android.internal.R.string.Noon); - } else { - endTimeFormat = res.getString(com.android.internal.R.string.noon); - } - } else if (endDate.hour == 0 && endOnTheHour && !noMidnight) { - if (capMidnight) { - endTimeFormat = res.getString(com.android.internal.R.string.Midnight); - } else { - endTimeFormat = res.getString(com.android.internal.R.string.midnight); - } - } } + startTimeString = startDate.format(startTimeFormat); - endTimeString = endDate.format(endTimeFormat); + endTimeString = isInstant ? startTimeString : endDate.format(endTimeFormat); } - - // Get the current year - long millis = System.currentTimeMillis(); - Time time = new Time(); - time.set(millis); - int currentYear = time.year; - + // Show the year if the user specified FORMAT_SHOW_YEAR or if // the starting and end years are different from each other // or from the current year. But don't show the year if the - // user specified FORMAT_NO_YEAR; - showYear = showYear || (!noYear && (startYear != endYear || startYear != currentYear)); - + // user specified FORMAT_NO_YEAR. + if (showYear) { + // No code... just a comment for clarity. Keep showYear + // on, as they enabled it with FORMAT_SHOW_YEAR. This + // takes precedence over them setting FORMAT_NO_YEAR. + } else if (noYear) { + // They explicitly didn't want a year. + showYear = false; + } else if (startYear != endYear) { + showYear = true; + } else { + // Show the year if it's not equal to the current year. + Time currentTime = new Time(); + currentTime.setToNow(); + showYear = startYear != currentTime.year; + } + String defaultDateFormat, fullFormat, dateRange; if (numericDate) { defaultDateFormat = res.getString(com.android.internal.R.string.numeric_date); @@ -1306,7 +1351,7 @@ public class DateUtils } } } - + if (showWeekDay) { if (showTime) { fullFormat = res.getString(com.android.internal.R.string.wday1_date1_time1_wday2_date2_time2); @@ -1320,20 +1365,20 @@ public class DateUtils fullFormat = res.getString(com.android.internal.R.string.date1_date2); } } - + if (noMonthDay && startMonthNum == endMonthNum) { // Example: "January, 2008" String startDateString = startDate.format(defaultDateFormat); return startDateString; } - + if (startYear != endYear || noMonthDay) { // Different year or we are not showing the month day number. // Example: "December 31, 2007 - January 1, 2008" // Or: "January - February, 2008" String startDateString = startDate.format(defaultDateFormat); String endDateString = endDate.format(defaultDateFormat); - + // The values that are used in a fullFormat string are specified // by position. dateRange = String.format(fullFormat, @@ -1341,7 +1386,7 @@ public class DateUtils endWeekDayString, endDateString, endTimeString); return dateRange; } - + // Get the month, day, and year strings for the start and end dates String monthFormat; if (numericDate) { @@ -1354,16 +1399,17 @@ public class DateUtils String startMonthString = startDate.format(monthFormat); String startMonthDayString = startDate.format(MONTH_DAY_FORMAT); String startYearString = startDate.format(YEAR_FORMAT); - String endMonthString = endDate.format(monthFormat); - String endMonthDayString = endDate.format(MONTH_DAY_FORMAT); - String endYearString = endDate.format(YEAR_FORMAT); - + + String endMonthString = isInstant ? null : endDate.format(monthFormat); + String endMonthDayString = isInstant ? null : endDate.format(MONTH_DAY_FORMAT); + String endYearString = isInstant ? null : endDate.format(YEAR_FORMAT); + if (startMonthNum != endMonthNum) { // Same year, different month. // Example: "October 28 - November 3" // or: "Wed, Oct 31 - Sat, Nov 3, 2007" // or: "Oct 31, 8am - Sat, Nov 3, 2007, 5pm" - + int index = 0; if (showWeekDay) index = 1; if (showYear) index += 2; @@ -1371,7 +1417,7 @@ public class DateUtils if (numericDate) index += 8; int resId = sameYearTable[index]; fullFormat = res.getString(resId); - + // The values that are used in a fullFormat string are specified // by position. dateRange = String.format(fullFormat, @@ -1381,7 +1427,7 @@ public class DateUtils endYearString, endTimeString); return dateRange; } - + if (startDay != endDay) { // Same month, different day. int index = 0; @@ -1391,7 +1437,7 @@ public class DateUtils if (numericDate) index += 8; int resId = sameMonthTable[index]; fullFormat = res.getString(resId); - + // The values that are used in a fullFormat string are specified // by position. dateRange = String.format(fullFormat, @@ -1401,19 +1447,19 @@ public class DateUtils endYearString, endTimeString); return dateRange; } - + // Same start and end day boolean showDate = (flags & FORMAT_SHOW_DATE) != 0; - + // If nothing was specified, then show the date. if (!showTime && !showDate && !showWeekDay) showDate = true; - + // Compute the time string (example: "10:00 - 11:00 am") String timeString = ""; if (showTime) { // If the start and end time are the same, then just show the // start time. - if (startMillis == endMillis) { + if (isInstant) { // Same start and end time. // Example: "10:15 AM" timeString = startTimeString; @@ -1423,7 +1469,7 @@ public class DateUtils timeString = String.format(timeFormat, startTimeString, endTimeString); } } - + // Figure out which full format to use. fullFormat = ""; String dateString = ""; @@ -1457,7 +1503,7 @@ public class DateUtils } else if (showTime) { return timeString; } - + // The values that are used in a fullFormat string are specified // by position. dateRange = String.format(fullFormat, timeString, startWeekDayString, dateString); diff --git a/core/java/android/text/method/NumberKeyListener.java b/core/java/android/text/method/NumberKeyListener.java index e500fae..9270ca5 100644 --- a/core/java/android/text/method/NumberKeyListener.java +++ b/core/java/android/text/method/NumberKeyListener.java @@ -101,6 +101,11 @@ public abstract class NumberKeyListener extends BaseKeyListener selEnd = Math.max(a, b); } + if (selStart < 0 || selEnd < 0) { + selStart = selEnd = 0; + Selection.setSelection(content, 0); + } + int i = event != null ? lookup(event, content) : 0; int repeatCount = event != null ? event.getRepeatCount() : 0; if (repeatCount == 0) { diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 679c683..e0231a7 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -113,20 +113,48 @@ public class GestureDetector { } /** - * @hide pending API council + * The listener that is used to notify when a double-tap or a confirmed + * single-tap occur. */ public interface OnDoubleTapListener { + /** + * Notified when a single-tap occurs. + * <p> + * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this + * will only be called after the detector is confident that the user's + * first tap is not followed by a second tap leading to a double-tap + * gesture. + * + * @param e The down motion event of the single-tap. + * @return true if the event is consumed, else false + */ boolean onSingleTapConfirmed(MotionEvent e); + + /** + * Notified when a double-tap occurs. + * + * @param e The down motion event of the first tap of the double-tap. + * @return true if the event is consumed, else false + */ + boolean onDoubleTap(MotionEvent e); + + /** + * Notified when an event within a double-tap gesture occurs, including + * the down, move, and up events. + * + * @param e The motion event that occurred during the double-tap gesture. + * @return true if the event is consumed, else false + */ boolean onDoubleTapEvent(MotionEvent e); } - + /** - * A convenience class to extend when you only want to listen for a - * subset of all the gestures. This implements all methods in the - * {@link OnGestureListener} but does nothing and return {@code false} - * for all applicable methods. + * A convenience class to extend when you only want to listen for a subset + * of all the gestures. This implements all methods in the + * {@link OnGestureListener} and {@link OnDoubleTapListener} but does + * nothing and return {@code false} for all applicable methods. */ - public static class SimpleOnGestureListener implements OnGestureListener { + public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener { public boolean onSingleTapUp(MotionEvent e) { return false; } @@ -150,13 +178,25 @@ public class GestureDetector { public boolean onDown(MotionEvent e) { return false; } + + public boolean onDoubleTap(MotionEvent e) { + return false; + } + + public boolean onDoubleTapEvent(MotionEvent e) { + return false; + } + + public boolean onSingleTapConfirmed(MotionEvent e) { + return false; + } } // TODO: ViewConfiguration private int mBiggerTouchSlopSquare = 20 * 20; - + private int mTouchSlopSquare; - private int mDoubleTapSlopSquare; + private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); @@ -164,7 +204,7 @@ public class GestureDetector { // 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.getDoubleTapTimeout(); - + // constants for Message.what used by GestureHandler below private static final int SHOW_PRESS = 1; private static final int LONG_PRESS = 2; @@ -181,13 +221,13 @@ public class GestureDetector { private MotionEvent mCurrentDownEvent; private MotionEvent mPreviousUpEvent; - + /** * True when the user is still touching for the second tap (down, move, and * up events). Can only be true if there is a double tap listener attached. */ private boolean mIsDoubleTapping; - + private float mLastMotionY; private float mLastMotionX; @@ -226,7 +266,7 @@ public class GestureDetector { break; default: - throw new RuntimeException("Unknown message " + msg); //never + throw new RuntimeException("Unknown message " + msg); //never } } } @@ -303,6 +343,9 @@ public class GestureDetector { mHandler = new GestureHandler(); } mListener = listener; + if (listener instanceof OnDoubleTapListener) { + setOnDoubleTapListener((OnDoubleTapListener) listener); + } init(context); } @@ -331,8 +374,11 @@ public class GestureDetector { } /** - * @hide pending API council - * @param onDoubleTapListener + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param onDoubleTapListener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. */ public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { mDoubleTapListener = onDoubleTapListener; @@ -387,7 +433,10 @@ public class GestureDetector { isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { // This is a second tap mIsDoubleTapping = true; - handled = mDoubleTapListener.onDoubleTapEvent(ev); + // Give a callback with the first tap of the double-tap + handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); + // Give a callback with down event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); } else { // This is a first tap mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); @@ -418,7 +467,8 @@ public class GestureDetector { final float scrollX = mLastMotionX - x; final float scrollY = mLastMotionY - y; if (mIsDoubleTapping) { - handled = mDoubleTapListener.onDoubleTapEvent(ev); + // Give the move events of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); } else if (mAlwaysInTapRegion) { final int deltaX = (int) (x - mCurrentDownEvent.getX()); final int deltaY = (int) (y - mCurrentDownEvent.getY()); @@ -446,7 +496,8 @@ public class GestureDetector { mStillDown = false; MotionEvent currentUpEvent = MotionEvent.obtain(ev); if (mIsDoubleTapping) { - handled = mDoubleTapListener.onDoubleTapEvent(ev); + // Finally, give the up event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); mIsDoubleTapping = false; break; } else if (mInLongPress) { @@ -495,7 +546,7 @@ public class GestureDetector { if (!mAlwaysInBiggerTapRegion) { return false; } - + if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { return false; } @@ -504,7 +555,7 @@ public class GestureDetector { int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); } - + private void dispatchLongPress() { mHandler.removeMessages(TAP); mInLongPress = true; diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index d6ea91c..430cc71 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -229,6 +229,12 @@ public class KeyEvent implements Parcelable { public static final int FLAG_SOFT_KEYBOARD = 0x2; /** + * This mask is set if we don't want the key event to cause us to leave + * touch mode. + */ + public static final int FLAG_KEEP_TOUCH_MODE = 0x4; + + /** * Returns the maximum keycode. */ public static int getMaxKeyCode() { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5ed3a7e..406fad8 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -57,6 +57,7 @@ import com.android.internal.view.menu.MenuBuilder; import java.util.ArrayList; import java.util.Arrays; +import java.lang.ref.SoftReference; /** * <p> @@ -1563,7 +1564,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private int[] mDrawableState = null; - private Bitmap mDrawingCache; + private SoftReference<Bitmap> mDrawingCache; /** * When this view has focus and the next focus is {@link #FOCUS_LEFT}, @@ -3950,25 +3951,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } if ((changed & WILL_NOT_CACHE_DRAWING) != 0) { - if (mDrawingCache != null) { - mDrawingCache.recycle(); - } - mDrawingCache = null; + destroyDrawingCache(); } if ((changed & DRAWING_CACHE_ENABLED) != 0) { - if (mDrawingCache != null) { - mDrawingCache.recycle(); - } - mDrawingCache = null; + destroyDrawingCache(); mPrivateFlags &= ~DRAWING_CACHE_VALID; } if ((changed & DRAWING_CACHE_QUALITY_MASK) != 0) { - if (mDrawingCache != null) { - mDrawingCache.recycle(); - } - mDrawingCache = null; + destroyDrawingCache(); mPrivateFlags &= ~DRAWING_CACHE_VALID; } @@ -5415,11 +5407,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; } - if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED && - ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null)) { + if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) { buildDrawingCache(); } - return mDrawingCache; + return mDrawingCache == null ? null : mDrawingCache.get(); } /** @@ -5434,7 +5425,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ public void destroyDrawingCache() { if (mDrawingCache != null) { - mDrawingCache.recycle(); + final Bitmap bitmap = mDrawingCache.get(); + if (bitmap != null) bitmap.recycle(); mDrawingCache = null; } } @@ -5474,7 +5466,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @see #destroyDrawingCache() */ public void buildDrawingCache() { - if ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null) { + if ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null || + mDrawingCache.get() == null) { + if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.BUILD_CACHE); } @@ -5492,15 +5486,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (width <= 0 || height <= 0 || (width * height * (opaque ? 2 : 4) >= // Projected bitmap size in bytes ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize())) { - if (mDrawingCache != null) { - mDrawingCache.recycle(); - } - mDrawingCache = null; + destroyDrawingCache(); return; } boolean clear = true; - Bitmap bitmap = mDrawingCache; + Bitmap bitmap = mDrawingCache == null ? null : mDrawingCache.get(); if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { @@ -5525,12 +5516,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } // Try to cleanup memory - if (mDrawingCache != null) { - mDrawingCache.recycle(); - } + if (bitmap != null) bitmap.recycle(); try { - mDrawingCache = bitmap = Bitmap.createBitmap(width, height, quality); + bitmap = Bitmap.createBitmap(width, height, quality); + mDrawingCache = new SoftReference<Bitmap>(bitmap); } catch (OutOfMemoryError e) { // If there is not enough memory to create the bitmap cache, just // ignore the issue as bitmap caches are not required to draw the @@ -8060,7 +8050,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { shader = new LinearGradient(0, 0, 0, 1, color, 0, Shader.TileMode.CLAMP); paint.setShader(shader); - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); + // Restore the default transfer mode (src_over) + paint.setXfermode(null); } } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 2f7b0d1..d3f48c6 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -56,7 +56,7 @@ public class ViewConfiguration { /** * Defines the duration in milliseconds we will wait to see if a touch event - * is a top or a scroll. If the user does not move within this interval, it is + * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ private static final int TAP_TIMEOUT = 100; @@ -213,7 +213,7 @@ public class ViewConfiguration { } /** - * @return Defines the length of the fading edges in pixels + * @return the length of the fading edges in pixels * * @deprecated Use {@link #getScaledFadingEdgeLength()} instead. */ @@ -223,14 +223,14 @@ public class ViewConfiguration { } /** - * @return Defines the length of the fading edges in pixels + * @return the length of the fading edges in pixels */ public int getScaledFadingEdgeLength() { return mFadingEdgeLength; } /** - * @return Defines the duration in milliseconds of the pressed state in child + * @return the duration in milliseconds of the pressed state in child * components. */ public static int getPressedStateDuration() { @@ -238,7 +238,7 @@ public class ViewConfiguration { } /** - * @return Defines the duration in milliseconds before a press turns into + * @return the duration in milliseconds before a press turns into * a long press */ public static int getLongPressTimeout() { @@ -246,8 +246,8 @@ public class ViewConfiguration { } /** - * @return Defines the duration in milliseconds we will wait to see if a touch event - * is a top or a scroll. If the user does not move within this interval, it is + * @return the duration in milliseconds we will wait to see if a touch event + * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ public static int getTapTimeout() { @@ -255,7 +255,7 @@ public class ViewConfiguration { } /** - * @return Defines the duration in milliseconds we will wait to see if a touch event + * @return the duration in milliseconds we will wait to see if a touch event * is a jump tap. If the user does not move within this interval, it is * considered to be a tap. */ @@ -264,7 +264,7 @@ public class ViewConfiguration { } /** - * @return Defines the duration in milliseconds between the first tap's up event and + * @return 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 diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 3cfaf1b..de64d0e 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -143,6 +143,7 @@ public final class ViewRoot extends Handler implements ViewParent, boolean mFullRedrawNeeded; boolean mNewSurfaceNeeded; boolean mHasHadWindowFocus; + boolean mLastWasImTarget; boolean mWindowAttributesChanged = false; @@ -998,6 +999,21 @@ public final class ViewRoot extends Handler implements ViewParent, mNewSurfaceNeeded = false; mViewVisibility = viewVisibility; + if (mAttachInfo.mHasWindowFocus) { + final boolean imTarget = WindowManager.LayoutParams + .mayUseInputMethod(mWindowAttributes.flags); + if (imTarget != mLastWasImTarget) { + mLastWasImTarget = imTarget; + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imTarget) { + imm.startGettingWindowFocus(mView); + imm.onWindowFocus(mView, mView.findFocus(), + mWindowAttributes.softInputMode, + !mHasHadWindowFocus, mWindowAttributes.flags); + } + } + } + boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw(); if (!cancelDraw && !newSurface) { @@ -1176,7 +1192,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(0xff0000ff, PorterDuff.Mode.CLEAR); + canvas.drawColor(0x00000000, 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 @@ -1608,10 +1624,13 @@ public final class ViewRoot extends Handler implements ViewParent, } } + mLastWasImTarget = WindowManager.LayoutParams + .mayUseInputMethod(mWindowAttributes.flags); + InputMethodManager imm = InputMethodManager.peekInstance(); if (mView != null) { - if (hasWindowFocus && imm != null) { - imm.startGettingWindowFocus(); + if (hasWindowFocus && imm != null && mLastWasImTarget) { + imm.startGettingWindowFocus(mView); } mView.dispatchWindowFocusChanged(hasWindowFocus); } @@ -1619,7 +1638,7 @@ public final class ViewRoot extends Handler implements ViewParent, // Note: must be done after the focus change callbacks, // so all of the view state is set up correctly. if (hasWindowFocus) { - if (imm != null) { + if (imm != null && mLastWasImTarget) { imm.onWindowFocus(mView, mView.findFocus(), mWindowAttributes.softInputMode, !mHasHadWindowFocus, mWindowAttributes.flags); @@ -1976,6 +1995,9 @@ public final class ViewRoot extends Handler implements ViewParent, if (event.getAction() != KeyEvent.ACTION_DOWN) { return false; } + if ((event.getFlags()&KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) { + return false; + } // only relevant if we are in touch mode if (!mAttachInfo.mInTouchMode) { @@ -2095,8 +2117,7 @@ public final class ViewRoot extends Handler implements ViewParent, // If it is possible for this window to interact with the input // method window, then we want to first dispatch our key events // to the input method. - if (WindowManager.LayoutParams.mayUseInputMethod( - mWindowAttributes.flags)) { + if (mLastWasImTarget) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && mView != null) { int seq = enqueuePendingEvent(event, sendDone); @@ -2126,6 +2147,10 @@ public final class ViewRoot extends Handler implements ViewParent, sWindowSession.finishKey(mWindow); } catch (RemoteException e) { } + } else { + Log.w("ViewRoot", "handleFinishedEvent(seq=" + seq + + " handled=" + handled + " ev=" + event + + ") neither delivering nor finishing key"); } } } @@ -2448,6 +2473,8 @@ public final class ViewRoot extends Handler implements ViewParent, final ViewRoot viewRoot = mViewRoot.get(); if (viewRoot != null) { viewRoot.dispatchKey(event); + } else { + Log.w("ViewRoot.W", "Key event " + event + " but no ViewRoot available!"); } } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 6fbc174..52b4107 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -341,6 +341,13 @@ public class BaseInputConnection implements InputConnection { /** * The default implementation does nothing. */ + public boolean performEditorAction(int actionCode) { + return false; + } + + /** + * The default implementation does nothing. + */ public boolean performContextMenuAction(int id) { return false; } diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index b2f26d7..0405371 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -25,17 +25,100 @@ public class EditorInfo implements InputType, Parcelable { public int inputType = TYPE_NULL; /** - * A string supplying additional information about the content type that - * is private to a particular IME implementation. The string must be + * Set of bits in {@link #imeOptions} that provide alternative actions + * associated with the "enter" key. This both helps the IME provide + * better feedback about what the enter key will do, and also allows it + * to provide alternative mechanisms for providing that command. + */ + public static final int IME_MASK_ACTION = 0x000000ff; + + /** + * Bits of {@link #IME_MASK_ACTION}: there is no special action + * associated with this editor. + */ + public static final int IME_ACTION_NONE = 0x00000000; + + /** + * Bits of {@link #IME_MASK_ACTION}: the action key performs a "go" + * operation to take the user to the target of the text they typed. + * Typically used, for example, when entering a URL. + */ + public static final int IME_ACTION_GO = 0x00000001; + + /** + * Bits of {@link #IME_MASK_ACTION}: the action key performs a "search" + * operation, taking the user to the results of searching for the text + * the have typed (in whatever context is appropriate). + */ + public static final int IME_ACTION_SEARCH = 0x00000002; + + /** + * Bits of {@link #IME_MASK_ACTION}: the action key performs a "send" + * operation, delivering the text to its target. This is typically used + * when composing a message. + */ + public static final int IME_ACTION_SEND = 0x00000003; + + /** + * Bits of {@link #IME_MASK_ACTION}: the action key performs a "next" + * operation, taking the user to the next field that will accept text. + */ + public static final int IME_ACTION_NEXT = 0x00000004; + + /** + * Flag of {@link #imeOptions}: used in conjunction with + * {@link #IME_MASK_ACTION}, this indicates that the action should not + * be available in-line as the same as a "enter" key. Typically this is + * because the action has such a significant impact or is not recoverable + * enough that accidentally hitting it should be avoided, such as sending + * a message. + */ + public static final int IME_FLAG_NO_ENTER_ACTION = 0x40000000; + + /** + * Generic non-special type for {@link #imeOptions}. + */ + public static final int IME_NORMAL = 0x00000000; + + /** + * Special code for when the ime option has been undefined. This is not + * used with the EditorInfo structure, but can be used elsewhere. + */ + public static final int IME_UNDEFINED = 0x80000000; + + /** + * Extended type information for the editor, to help the IME better + * integrate with it. + */ + public int imeOptions = IME_NORMAL; + + /** + * A string supplying additional information options that are + * private to a particular IME implementation. The string must be * scoped to a package owned by the implementation, to ensure there are * no conflicts between implementations, but other than that you can put * whatever you want in it to communicate with the IME. For example, * you could have a string that supplies an argument like * <code>"com.example.myapp.SpecialMode=3"</code>. This field is can be - * filled in from the {@link android.R.attr#editorPrivateContentType} + * filled in from the {@link android.R.attr#privateImeOptions} * attribute of a TextView. */ - public String privateContentType = null; + public String privateImeOptions = null; + + /** + * In some cases an IME may be able to display an arbitrary label for + * a command the user can perform, which you can specify here. You can + * not count on this being used. + */ + public CharSequence actionLabel = null; + + /** + * If {@link #actionLabel} has been given, this is the id for that command + * when the user presses its button that is delivered back with + * {@link InputConnection#performEditorAction(int) + * InputConnection.performEditorAction()}. + */ + public int actionId = 0; /** * The text offset of the start of the selection at the time editing @@ -106,7 +189,10 @@ public class EditorInfo implements InputType, Parcelable { */ public void dump(Printer pw, String prefix) { pw.println(prefix + "inputType=0x" + Integer.toHexString(inputType) - + " privateContentType=" + privateContentType); + + " imeOptions=0x" + Integer.toHexString(imeOptions) + + " privateImeOptions=" + privateImeOptions); + pw.println(prefix + "actionLabel=" + actionLabel + + " actionId=" + actionId); pw.println(prefix + "initialSelStart=" + initialSelStart + " initialSelEnd=" + initialSelEnd + " initialCapsMode=0x" @@ -127,7 +213,10 @@ public class EditorInfo implements InputType, Parcelable { */ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(inputType); - dest.writeString(privateContentType); + dest.writeInt(imeOptions); + dest.writeString(privateImeOptions); + TextUtils.writeToParcel(actionLabel, dest, flags); + dest.writeInt(actionId); dest.writeInt(initialSelStart); dest.writeInt(initialSelEnd); dest.writeInt(initialCapsMode); @@ -146,7 +235,10 @@ public class EditorInfo implements InputType, Parcelable { public EditorInfo createFromParcel(Parcel source) { EditorInfo res = new EditorInfo(); res.inputType = source.readInt(); - res.privateContentType = source.readString(); + res.imeOptions = source.readInt(); + res.privateImeOptions = source.readString(); + res.actionLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.actionId = source.readInt(); res.initialSelStart = source.readInt(); res.initialSelEnd = source.readInt(); res.initialCapsMode = source.readInt(); diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 530127d..32cce35 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -208,10 +208,24 @@ public interface InputConnection { /** * Set the selection of the text editor. To set the cursor position, * start and end should have the same value. + * @return Returns true on success, false if the input connection is no longer + * valid. */ public boolean setSelection(int start, int end); /** + * Have the editor perform an action it has said it can do. + * + * @param editorAction This must be one of the action constants for + * {@link EditorInfo#imeOptions EditorInfo.editorType}, such as + * {@link EditorInfo#IME_ACTION_GO EditorInfo.EDITOR_ACTION_GO}. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean performEditorAction(int editorAction); + + /** * Perform a context menu action on the field. The given id may be one of: * {@link android.R.id#selectAll}, * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText}, diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 91fa211..e9e4703 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -222,6 +222,11 @@ public final class InputMethodManager { // ----------------------------------------------------------- /** + * This is the root view of the overall window that currently has input + * method focus. + */ + View mCurRootView; + /** * This is the view that should currently be served by an input method, * regardless of the state of setting that up. */ @@ -840,6 +845,13 @@ public final class InputMethodManager { void focusInLocked(View view) { if (DEBUG) Log.v(TAG, "focusIn: " + view); + + if (mCurRootView != view.getRootView()) { + // This is a request from a window that isn't in the window with + // IME focus, so ignore it. + return; + } + // Okay we have a new view that is being served. if (mServedView != view) { mCurrentTextBoxAttribute = null; @@ -913,7 +925,7 @@ public final class InputMethodManager { } /** - * Called by ViewRoot the first time it gets window focus. + * Called by ViewRoot when its window gets input focus. * @hide */ public void onWindowFocus(View rootView, View focusedView, int softInputMode, @@ -946,9 +958,10 @@ public final class InputMethodManager { } /** @hide */ - public void startGettingWindowFocus() { + public void startGettingWindowFocus(View rootView) { synchronized (mH) { mWindowFocusedView = null; + mCurRootView = rootView; } } @@ -1165,6 +1178,7 @@ public final class InputMethodManager { + " mBindSequence=" + mBindSequence + " mCurId=" + mCurId); p.println(" mCurMethod=" + mCurMethod); + p.println(" mCurRootView=" + mCurRootView); p.println(" mServedView=" + mServedView); p.println(" mLastServedView=" + mLastServedView); p.println(" mServedConnecting=" + mServedConnecting); diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 4f8e5e4..84aeb83 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -907,10 +907,12 @@ class CallbackProxy extends Handler { } public void onReceivedIcon(Bitmap icon) { - if (Config.DEBUG && mBackForwardList.getCurrentItem() == null) { - throw new AssertionError(); + // The current item might be null if the icon was already stored in the + // database and this is a new WebView. + WebHistoryItem i = mBackForwardList.getCurrentItem(); + if (i != null) { + i.setFavicon(icon); } - mBackForwardList.getCurrentItem().setFavicon(icon); // Do an unsynchronized quick check to avoid posting if no callback has // been set. if (mWebChromeClient == null) { diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index c2620a5..8a82411 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -25,8 +25,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; -import android.os.Handler; -import android.os.Message; import android.text.Editable; import android.text.InputFilter; import android.text.Selection; @@ -43,7 +41,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewConfiguration; import android.widget.AbsoluteLayout.LayoutParams; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; @@ -82,22 +79,6 @@ import java.util.ArrayList; // FIXME: This can be replaced with TextView.NO_FILTERS if that // is made public/protected. private static final InputFilter[] NO_FILTERS = new InputFilter[0]; - // The time of the last enter down, so we know whether to perform a long - // press. - private long mDownTime; - - private boolean mTrackballDown = false; - private static int LONGPRESS = 1; - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - if (msg.what == LONGPRESS) { - if (mTrackballDown) { - performLongClick(); - mTrackballDown = false; - } - } - } - }; /** * Create a new TextDialog. @@ -135,6 +116,13 @@ import java.util.ArrayList; } @Override + protected boolean shouldAdvanceFocusOnEnter() { + // In the browser, single line textfields use enter as a form submit, + // so we never want to advance the focus on enter. + return false; + } + + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.isSystem()) { return super.dispatchKeyEvent(event); @@ -152,43 +140,33 @@ import java.util.ArrayList; return true; } - // For single-line textfields, return key should not be handled - // here. Instead, the WebView is passed the key up, so it may fire a - // submit/onClick. - // Center key should always be passed to a potential onClick - if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode) - || KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { + if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode)) { if (isPopupShowing()) { - super.dispatchKeyEvent(event); - return true; + return super.dispatchKeyEvent(event); } - if (down) { - if (event.getRepeatCount() == 0) { - mGotEnterDown = true; - mDownTime = event.getEventTime(); - // Send the keydown when the up comes, so that we have - // a chance to handle a long press. - } else if (mGotEnterDown && event.getEventTime() - mDownTime > - ViewConfiguration.getLongPressTimeout()) { - performLongClick(); - mGotEnterDown = false; - } - } else if (mGotEnterDown) { - mGotEnterDown = false; - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { - mWebView.shortPressOnTextField(); - return true; - } - // If we reached here, then this is a single line textfield, and - // the user pressed ENTER. In this case, we want to hide the - // soft input method. + if (!down) { + // Hide the keyboard, since the user has just submitted this + // form. The submission happens thanks to the two calls + // to sendDomEvent. InputMethodManager.getInstance(mContext) .hideSoftInputFromWindow(getWindowToken(), 0); sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); sendDomEvent(event); } - return true; + return super.dispatchKeyEvent(event); + } else if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { + // Note that this handles center key and trackball. + if (isPopupShowing()) { + return super.dispatchKeyEvent(event); + } + // Center key should be passed to a potential onClick + if (!down) { + mWebView.shortPressOnTextField(); + } + // Pass to super to handle longpress. + return super.dispatchKeyEvent(event); } + // Ensure there is a layout so arrow keys are handled properly. if (getLayout() == null) { measure(mWidthSpec, mHeightSpec); @@ -225,9 +203,8 @@ import java.util.ArrayList; case KeyEvent.KEYCODE_DPAD_DOWN: isArrowKey = true; break; - case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: - // For multi-line text boxes, newlines and dpad center will + // For multi-line text boxes, newlines will // trigger onTextChanged for key down (which will send both // key up and key down) but not key up. mGotEnterDown = true; @@ -269,7 +246,7 @@ import java.util.ArrayList; // with WebCore's notion of the current selection, reset the selection // to what it was before the key event. Selection.setSelection(text, oldStart, oldEnd); - // Ignore the key up event for newlines or dpad center. This prevents + // Ignore the key up event for newlines. This prevents // multiple newlines in the native textarea. if (mGotEnterDown && !down) { return true; @@ -391,27 +368,8 @@ import java.util.ArrayList; if (isPopupShowing()) { return super.onTrackballEvent(event); } - int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (!mTrackballDown) { - mTrackballDown = true; - mHandler.sendEmptyMessageDelayed(LONGPRESS, - ViewConfiguration.getLongPressTimeout()); - } - return true; - case MotionEvent.ACTION_UP: - if (mTrackballDown) { - mWebView.shortPressOnTextField(); - mTrackballDown = false; - mHandler.removeMessages(LONGPRESS); - } - return true; - case MotionEvent.ACTION_CANCEL: - mTrackballDown = false; - return true; - case MotionEvent.ACTION_MOVE: - // fall through + if (event.getAction() != MotionEvent.ACTION_MOVE) { + return false; } Spannable text = (Spannable) getText(); MovementMethod move = getMovementMethod(); @@ -442,7 +400,6 @@ import java.util.ArrayList; // hide the soft keyboard when the edit text is out of focus InputMethodManager.getInstance(mContext).hideSoftInputFromWindow( getWindowToken(), 0); - mHandler.removeMessages(LONGPRESS); mWebView.removeView(this); mWebView.requestFocus(); mScrollToAccommodateCursor = false; diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index c59a5fc..5126ef0 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -71,6 +71,7 @@ import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.Scroller; import android.widget.Toast; +import android.widget.ZoomButtonsController; import android.widget.ZoomControls; import android.widget.ZoomRingController; import android.widget.FrameLayout; @@ -284,8 +285,10 @@ public class WebView extends AbsoluteLayout /** * Customizable constant */ - // pre-computed square of ViewConfiguration.getTouchSlop() + // pre-computed square of ViewConfiguration.getScaledTouchSlop() private int mTouchSlopSquare; + // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() + private int mDoubleTapSlopSquare; // This should be ViewConfiguration.getTapTimeout() // But system time out is 100ms, which is too short for the browser. // In the browser, if it switches out of tap too soon, jump tap won't work. @@ -321,6 +324,8 @@ public class WebView extends AbsoluteLayout private int mContentHeight; // cache of value from WebViewCore static int MAX_FLOAT_CONTENT_WIDTH = 480; + // the calculated minimum content width for calculating the minimum scale. + // If it is 0, it means don't use it. private int mMinContentWidth; // Need to have the separate control for horizontal and vertical scrollbar @@ -553,7 +558,9 @@ public class WebView extends AbsoluteLayout return mExtra; } } - + + private ZoomButtonsController mZoomButtonsController; + private ZoomRingController mZoomRingController; private ImageView mZoomRingOverview; private Animation mZoomRingOverviewExitAnimation; @@ -617,6 +624,9 @@ public class WebView extends AbsoluteLayout / ZOOM_RING_STEPS; } mZoomRingController.setThumbAngle(angle * MAX_ZOOM_RING_ANGLE); + + // Don't show a thumb if the user cannot zoom + mZoomRingController.setThumbVisible(mMinZoomScale != mMaxZoomScale); // Show the zoom overview tab on the ring setZoomOverviewVisible(true); @@ -733,6 +743,26 @@ public class WebView extends AbsoluteLayout mZoomRingController.setPannerAcceleration(160); mZoomRingController.setPannerStartAcceleratingDuration(700); createZoomRingOverviewTab(); + mZoomButtonsController = new ZoomButtonsController(context, this); + mZoomButtonsController.setOverviewVisible(true); + mZoomButtonsController.setCallback(new ZoomButtonsController.OnZoomListener() { + public void onCenter(int x, int y) { + mZoomListener.onCenter(x, y); + } + + public void onOverview() { + mZoomButtonsController.setVisible(false); + zoomScrollOut(); + } + + public void onVisibilityChanged(boolean visible) { + mZoomListener.onVisibilityChanged(visible); + } + + public void onZoom(boolean zoomIn) { + mZoomListener.onSimpleZoom(zoomIn); + } + }); } private void init() { @@ -745,6 +775,9 @@ public class WebView extends AbsoluteLayout final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); mTouchSlopSquare = slop * slop; mMinLockSnapReverseDistance = slop; + final int doubleTapslop = ViewConfiguration.get(getContext()) + .getScaledDoubleTapSlop(); + mDoubleTapSlopSquare = doubleTapslop * doubleTapslop; } private void createZoomRingOverviewTab() { @@ -763,7 +796,7 @@ public class WebView extends AbsoluteLayout FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER); // TODO: magic constant that's based on the zoom ring radius + some offset - lp.topMargin = 208; + lp.topMargin = 200; mZoomRingOverview.setLayoutParams(lp); mZoomRingOverview.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { @@ -2305,8 +2338,19 @@ public class WebView extends AbsoluteLayout /** * Use this function to bind an object to Javascript so that the * methods can be accessed from Javascript. - * IMPORTANT, the object that is bound runs in another thread and - * not in the thread that it was constructed in. + * <p><strong>IMPORTANT:</strong> + * <ul> + * <li> Using addJavascriptInterface() allows JavaScript to control your + * application. This can be a very useful feature or a dangerous security + * issue. When the HTML in the WebView is untrustworthy (for example, part + * or all of the HTML is provided by some person or process), then an + * attacker could inject HTML that will execute your code and possibly any + * code of the attacker's choosing.<br> + * Do not use addJavascriptInterface() unless all of the HTML in this + * WebView was written by you.</li> + * <li> The Java object that is bound runs in another thread and not in + * the thread that it was constructed in.</li> + * </ul></p> * @param obj The class instance to bind to Javascript * @param interfaceName The name to used to expose the class in Javascript */ @@ -2969,8 +3013,8 @@ public class WebView extends AbsoluteLayout 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; + float x = mLastTouchX + (float) (mScrollX - lp.x); + float y = mLastTouchY + (float) (mScrollY - lp.y); mTextEntry.fakeTouchEvent(x, y); } } @@ -3164,6 +3208,9 @@ public class WebView extends AbsoluteLayout mSelectX = mScrollX + (int) mLastTouchX; mSelectY = mScrollY + (int) mLastTouchY; } + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + nativeClearFocus(contentX, contentY); } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP @@ -3355,6 +3402,9 @@ public class WebView extends AbsoluteLayout public void emulateShiftHeld() { mExtendSelection = false; mShiftIsPressed = true; + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + nativeClearFocus(contentX, contentY); } private boolean commitCopy() { @@ -3401,6 +3451,7 @@ public class WebView extends AbsoluteLayout // Clean up the zoom ring mZoomRingController.setVisible(false); + mZoomButtonsController.setVisible(false); } // Implementation for OnHierarchyChangeListener @@ -3449,8 +3500,17 @@ public class WebView extends AbsoluteLayout // false for the first parameter } } else { - // If our window has lost focus, stop drawing the focus ring - mDrawFocusRing = false; + if (!mZoomButtonsController.isVisible()) { + /* + * The zoom controls come in their own window, so our window + * loses focus. Our policy is to not draw the focus ring if + * our window is not focused, but this is an exception since + * the user can still navigate the web page with the zoom + * controls showing. + */ + // If our window has lost focus, stop drawing the focus ring + mDrawFocusRing = false; + } mGotKeyDown = false; mShiftIsPressed = false; if (mNativeClass != 0) { @@ -3592,7 +3652,8 @@ public class WebView extends AbsoluteLayout + mTouchMode); } - if (mZoomRingController.isVisible() && mInZoomTapDragMode) { + if ((mZoomRingController.isVisible() || mZoomButtonsController.isVisible()) + && mInZoomTapDragMode) { if (ev.getAction() == MotionEvent.ACTION_UP) { // Just released the second tap, no longer in tap-drag mode mInZoomTapDragMode = false; @@ -3630,6 +3691,9 @@ public class WebView extends AbsoluteLayout mLastSentTouchTime = eventTime; } + int deltaX = (int) (mLastTouchX - x); + int deltaY = (int) (mLastTouchY - y); + switch (action) { case MotionEvent.ACTION_DOWN: { if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN @@ -3655,16 +3719,23 @@ public class WebView extends AbsoluteLayout , viewToContent(mSelectY), false); mTouchSelection = mExtendSelection = true; } else if (!ZoomRingController.useOldZoom(mContext) && - mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { + mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP) && + (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare)) { // Found doubletap, invoke the zoom controller mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); - mZoomRingController.setVisible(true); + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + if (inEditingMode()) { + mTextEntry.updateCachedTextfield(); + } + nativeClearFocus(contentX, contentY); mInZoomTapDragMode = true; if (mLogEvent) { EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } - return mZoomRingController.handleDoubleTapEvent(ev); + return mZoomRingController.handleDoubleTapEvent(ev) || + mZoomButtonsController.handleDoubleTapEvent(ev); } else { mTouchMode = TOUCH_INIT_MODE; mPreventDrag = mForwardTouchEvents; @@ -3701,9 +3772,6 @@ public class WebView extends AbsoluteLayout } mVelocityTracker.addMovement(ev); - int deltaX = (int) (mLastTouchX - x); - int deltaY = (int) (mLastTouchY - y); - if (mTouchMode != TOUCH_DRAG_MODE) { if (mTouchMode == TOUCH_SELECT_MODE) { mSelectX = mScrollX + (int) x; @@ -5092,6 +5160,8 @@ public class WebView extends AbsoluteLayout }); if (mSelection != -1) { listView.setSelection(mSelection); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setItemChecked(mSelection, true); } } dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index a7261c5..6ab088d 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -1294,7 +1294,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, nativeGetContentMinPrefWidth(), + WebView.NEW_PICTURE_MSG_ID, + mViewportMinimumScale == 0 ? nativeGetContentMinPrefWidth() + : 0, 0, draw).sendToTarget(); nativeCheckNavCache(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index f362e22..9da78d0 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -17,7 +17,6 @@ package android.widget; import android.content.Context; -import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -28,6 +27,7 @@ import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.Gravity; @@ -893,25 +893,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mSyncMode = SYNC_FIRST_POSITION; } - // Don't restore the type filter window when there is no keyboard - if (acceptFilter()) { - String filterText = ss.filter; - setFilterText(filterText); - } + setFilterText(ss.filter); 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()); + return !inputManager.isFullscreenMode(); } /** @@ -922,7 +913,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ public void setFilterText(String filterText) { // TODO: Should we check for acceptFilter()? - if (mTextFilterEnabled && filterText != null && filterText.length() > 0) { + if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { createTextFilter(false); // This is going to call our listener onTextChanged, but we might not // be ready to bring up a window yet @@ -942,6 +933,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** + * Returns the list's text filter, if available. + * @return the list's text filter or null if filtering isn't enabled + * @hide pending API Council approval + */ + public CharSequence getTextFilter() { + if (mTextFilterEnabled && mTextFilter != null) { + return mTextFilter.getText(); + } + return null; + } + @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index b046a6b..1d553f1 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -40,9 +40,15 @@ public abstract class AbsSeekBar extends ProgressBar { * Whether this is user seekable. */ boolean mIsUserSeekable = true; + + /** + * On key presses (right or left), the amount to increment/decrement the + * progress. + */ + private int mKeyProgressIncrement = 1; private static final int NO_ALPHA = 0xFF; - float mDisabledAlpha; + private float mDisabledAlpha; public AbsSeekBar(Context context) { super(context); @@ -101,6 +107,39 @@ public abstract class AbsSeekBar extends ProgressBar { invalidate(); } + /** + * Sets the amount of progress changed via the arrow keys. + * + * @param increment The amount to increment or decrement when the user + * presses the arrow keys. + */ + public void setKeyProgressIncrement(int increment) { + mKeyProgressIncrement = increment < 0 ? -increment : increment; + } + + /** + * Returns the amount of progress changed via the arrow keys. + * <p> + * By default, this will be a value that is derived from the max progress. + * + * @return The amount to increment or decrement when the user presses the + * arrow keys. This will be positive. + */ + public int getKeyProgressIncrement() { + return mKeyProgressIncrement; + } + + @Override + public synchronized void setMax(int max) { + super.setMax(max); + + if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) { + // It will take the user too long to change this via keys, change it + // to something more reasonable + setKeyProgressIncrement(Math.max(1, Math.round((float) getMax() / 20))); + } + } + @Override protected boolean verifyDrawable(Drawable who) { return who == mThumb || super.verifyDrawable(who); @@ -321,12 +360,12 @@ public abstract class AbsSeekBar extends ProgressBar { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (progress <= 0) break; - setProgress(progress - 1, true); + setProgress(progress - mKeyProgressIncrement, true); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (progress >= getMax()) break; - setProgress(progress + 1, true); + setProgress(progress + mKeyProgressIncrement, true); return true; } diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index cf9c588..f847bc3 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -48,7 +48,6 @@ public class AnalogClock extends View { private int mDialHeight; private boolean mAttached; - private long mLastTime; private final Handler mHandler = new Handler(); private float mMinutes; @@ -96,7 +95,6 @@ public class AnalogClock extends View { protected void onAttachedToWindow() { super.onAttachedToWindow(); - onTimeChanged(); if (!mAttached) { mAttached = true; IntentFilter filter = new IntentFilter(); @@ -107,6 +105,15 @@ public class AnalogClock extends View { getContext().registerReceiver(mIntentReceiver, filter, null, mHandler); } + + // NOTE: It's safe to do these after registering the receiver since the receiver always runs + // in the main thread, therefore the receiver can't run before this method returns. + + // The time zone may have changed while the receiver wasn't registered, so update the Time + mCalendar = new Time(); + + // Make sure we update to the current time + onTimeChanged(); } @Override @@ -212,9 +219,7 @@ public class AnalogClock extends View { } private void onTimeChanged() { - long time = System.currentTimeMillis(); - mCalendar.set(time); - mLastTime = time; + mCalendar.setToNow(); int hour = mCalendar.hour; int minute = mCalendar.minute; @@ -231,8 +236,6 @@ public class AnalogClock extends View { if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { String tz = intent.getStringExtra("time-zone"); mCalendar = new Time(TimeZone.getTimeZone(tz).getID()); - } else { - mCalendar = new Time(); } onTimeChanged(); diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 7a51676..0c1c72a 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -78,6 +78,8 @@ import com.android.internal.R; * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth */ public class AutoCompleteTextView extends EditText implements Filter.FilterListener { static final boolean DEBUG = false; @@ -96,6 +98,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private DropDownListView mDropDownList; private int mDropDownVerticalOffset; private int mDropDownHorizontalOffset; + private int mDropDownAnchorId; + private View mDropDownAnchorView; // view is retrieved lazily from id once needed + private int mDropDownWidth; private Drawable mDropDownListHighlight; @@ -147,6 +152,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); mDropDownHorizontalOffset = (int) a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); + + // Get the anchor's id now, but the view won't be ready, so wait to actually get the + // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. + // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return + // this TextView, as a default anchoring point. + mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, + View.NO_ID); + + // For dropdown width, the developer can specify a specific width, or FILL_PARENT + // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). + mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, + ViewGroup.LayoutParams.WRAP_CONTENT); mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); @@ -187,6 +204,49 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void setCompletionHint(CharSequence hint) { mHintText = hint; } + + /** + * <p>Returns the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> + * + * @return the width for the drop down list + */ + public int getDropDownWidth() { + return mDropDownWidth; + } + + /** + * <p>Sets the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> + * + * @param width the width to use + */ + public void setDropDownWidth(int width) { + mDropDownWidth = width; + } + + /** + * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> + * + * @return the view's id, or {@link View#NO_ID} if none specified + */ + public int getDropDownAnchor() { + return mDropDownAnchorId; + } + + /** + * <p>Sets the view to which the auto-complete drop down list should anchor. The view + * corresponding to this id will not be loaded until the next time it is needed to avoid + * loading a view which is not yet instantiated.</p> + * + * @param id the id to anchor the drop down list view to + */ + public void setDropDownAnchor(int id) { + mDropDownAnchorId = id; + mDropDownAnchorView = null; + } /** * <p>Returns the number of characters the user must type before the drop @@ -741,6 +801,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return result; } + + /** + * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of + * the id is NO_ID or we can't find a view for the given id, we return this TextView as + * the default anchoring point.</p> + */ + private View getDropDownAnchorView() { + if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { + mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); + } + return mDropDownAnchorView == null ? this : mDropDownAnchorView; + } /** * <p>Displays the drop down on screen.</p> @@ -748,16 +820,37 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void showDropDown() { int height = buildDropDown(); if (mPopup.isShowing()) { - mPopup.update(this, mDropDownHorizontalOffset, mDropDownVerticalOffset, - getWidth(), height); + int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getDropDownAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, widthSpec, height); } else { - mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); - mPopup.setWidth(getWidth()); + int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { + mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } else { + mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setWidth(getDropDownAnchorView().getWidth()); + } else { + mPopup.setWidth(mDropDownWidth); + } + } mPopup.setHeight(height); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); mPopup.setOutsideTouchable(true); mPopup.setTouchInterceptor(new PopupTouchIntercepter()); - mPopup.showAsDropDown(this, mDropDownHorizontalOffset, mDropDownVerticalOffset); + mPopup.showAsDropDown(getDropDownAnchorView(), + mDropDownHorizontalOffset, mDropDownVerticalOffset); mDropDownList.setSelection(ListView.INVALID_POSITION); mDropDownList.hideSelector(); mDropDownList.requestFocus(); diff --git a/core/java/android/widget/BaseAdapter.java b/core/java/android/widget/BaseAdapter.java index 1921d73..532fd76 100644 --- a/core/java/android/widget/BaseAdapter.java +++ b/core/java/android/widget/BaseAdapter.java @@ -42,6 +42,10 @@ public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { mDataSetObservable.unregisterObserver(observer); } + /** + * Notifies the attached View that the underlying data has been changed + * and it should refresh itself. + */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 369221e..91add58 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -69,7 +69,10 @@ public class Chronometer extends TextView { private Object[] mFormatterArgs = new Object[1]; private StringBuilder mFormatBuilder; private OnChronometerTickListener mOnChronometerTickListener; - + private StringBuilder mRecycle = new StringBuilder(8); + + private static final int TICK_WHAT = 2; + /** * Initialize this Chronometer object. * Sets the base to the current time. @@ -115,6 +118,7 @@ public class Chronometer extends TextView { @android.view.RemotableViewMethod public void setBase(long base) { mBase = base; + dispatchChronometerTick(); updateText(SystemClock.elapsedRealtime()); } @@ -216,10 +220,10 @@ public class Chronometer extends TextView { updateRunning(); } - private void updateText(long now) { + private synchronized void updateText(long now) { long seconds = now - mBase; seconds /= 1000; - String text = DateUtils.formatElapsedTime(seconds); + String text = DateUtils.formatElapsedTime(mRecycle, seconds); if (mFormat != null) { Locale loc = Locale.getDefault(); @@ -247,7 +251,10 @@ public class Chronometer extends TextView { if (running != mRunning) { if (running) { updateText(SystemClock.elapsedRealtime()); - mHandler.sendMessageDelayed(Message.obtain(), 1000); + dispatchChronometerTick(); + mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); + } else { + mHandler.removeMessages(TICK_WHAT); } mRunning = running; } @@ -255,10 +262,10 @@ public class Chronometer extends TextView { private Handler mHandler = new Handler() { public void handleMessage(Message m) { - if (mStarted) { + if (mRunning) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); - sendMessageDelayed(Message.obtain(), 1000); + sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); } } }; diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index 3d758e7..898e501 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -348,6 +348,21 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, mFilterQueryProvider = filterQueryProvider; } + /** + * Called when the {@link ContentObserver} on the cursor receives a change notification. + * The default implementation provides the auto-requery logic, but may be overridden by + * sub classes. + * + * @see ContentObserver#onChange(boolean) + * @hide pending API Council approval + */ + protected void onContentChanged() { + if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { + if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + mDataValid = mCursor.requery(); + } + } + private class ChangeObserver extends ContentObserver { public ChangeObserver() { super(new Handler()); @@ -360,10 +375,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, @Override public void onChange(boolean selfChange) { - if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); - mDataValid = mCursor.requery(); - } + onContentChanged(); } } diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java index a2316cf..1d0fd5e 100644 --- a/core/java/android/widget/Filter.java +++ b/core/java/android/widget/Filter.java @@ -45,8 +45,6 @@ public abstract class Filter { private Handler mThreadHandler; private Handler mResultHandler; - private String mConstraint; - private boolean mConstraintIsValid = false; /** * <p>Creates a new asynchronous filter.</p> @@ -84,13 +82,6 @@ public abstract class Filter { */ public final void filter(CharSequence constraint, FilterListener listener) { synchronized (this) { - String constraintAsString = constraint != null ? constraint.toString() : null; - if (mConstraintIsValid && ( - (constraintAsString == null && mConstraint == null) || - (constraintAsString != null && constraintAsString.equals(mConstraint)))) { - // nothing to do - return; - } if (mThreadHandler == null) { HandlerThread thread = new HandlerThread(THREAD_NAME); @@ -103,16 +94,13 @@ public abstract class Filter { RequestArguments args = new RequestArguments(); // make sure we use an immutable copy of the constraint, so that // it doesn't change while the filter operation is in progress - args.constraint = constraintAsString; + args.constraint = constraint != null ? constraint.toString() : null; args.listener = listener; message.obj = args; mThreadHandler.removeMessages(FILTER_TOKEN); mThreadHandler.removeMessages(FINISH_TOKEN); mThreadHandler.sendMessage(message); - - mConstraint = constraintAsString; - mConstraintIsValid = true; } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 38bfc7c..6bbf062 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -924,32 +924,23 @@ public class GridView extends AbsListView { final int count = mItemCount; if (count > 0) { final View child = obtainView(0); - final int childViewType = mAdapter.getItemViewType(0); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; - - final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, - mListPadding.left + mListPadding.right, lp.width); - - int lpHeight = lp.height; - - int childHeightSpec; - if (lpHeight > 0) { - childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } + p.viewType = mAdapter.getItemViewType(0); + int childHeightSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + int childWidthSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); - if (mRecycler.shouldRecycleViewType(childViewType)) { + if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 96fe595..652e30c 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -875,7 +875,7 @@ public class HorizontalScrollView extends FrameLayout { int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - final int childHeightMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 94d1bd1..a4523b9 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -840,7 +840,7 @@ public class ImageView extends View { @Override public int getBaseline() { - return mBaselineAligned ? getHeight() : -1; + return mBaselineAligned ? getMeasuredHeight() : -1; } /** diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 4e5989c..6df72d4 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -1011,34 +1011,13 @@ public class ListView extends AbsListView { if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) { final View child = obtainView(0); - final int childViewType = mAdapter.getItemViewType(0); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; - - final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, - mListPadding.left + mListPadding.right, lp.width); - - int lpHeight = lp.height; - - int childHeightSpec; - if (lpHeight > 0) { - childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } - - child.measure(childWidthSpec, childHeightSpec); + measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); - if (mRecycler.shouldRecycleViewType(childViewType)) { + if (recycleOnMeasure()) { mRecycler.addScrapView(child); } } @@ -1055,13 +1034,40 @@ public class ListView extends AbsListView { if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 - heightSize = measureHeightOfChildren( - MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), - 0, NO_POSITION, heightSize, -1); + heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); - mWidthMeasureSpec = widthMeasureSpec; + mWidthMeasureSpec = widthMeasureSpec; + } + + private void measureScrapChild(View child, int position, int widthMeasureSpec) { + LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p == null) { + p = new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * @return True to recycle the views used to measure this ListView in + * UNSPECIFIED/AT_MOST modes, false otherwise. + * @hide + */ + protected boolean recycleOnMeasure() { + return true; } /** @@ -1090,8 +1096,8 @@ public class ListView extends AbsListView { * startPosition is 0). * @return The height of this ListView with the given children. */ - final int measureHeightOfChildren(final int widthMeasureSpec, final int startPosition, - int endPosition, final int maxHeight, int disallowPartialChildPosition) { + final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, + final int maxHeight, int disallowPartialChildPosition) { final ListAdapter adapter = mAdapter; if (adapter == null) { @@ -1110,29 +1116,20 @@ public class ListView extends AbsListView { // mItemCount - 1 since endPosition parameter is inclusive endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; final AbsListView.RecycleBin recycleBin = mRecycler; + final boolean recyle = recycleOnMeasure(); + for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i); - final int childViewType = adapter.getItemViewType(i); - AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, 0); - child.setLayoutParams(lp); - } - lp.viewType = childViewType; + measureScrapChild(child, i, widthMeasureSpec); if (i > 0) { // Count the divider for all but one child returnedHeight += dividerHeight; } - child.measure(widthMeasureSpec, lp.height >= 0 - ? MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) - : MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - // Recycle the view before we possibly return from the method - if (recycleBin.shouldRecycleViewType(childViewType)) { + if (recyle) { recycleBin.addScrapView(child); } @@ -1656,7 +1653,7 @@ public class ListView extends AbsListView { // Respect layout params that are already in the view. Otherwise make some up... // noinspection unchecked - AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); @@ -1675,7 +1672,7 @@ public class ListView extends AbsListView { if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { if (child instanceof Checkable) { - ((Checkable)child).setChecked(mCheckStates.get(position)); + ((Checkable) child).setChecked(mCheckStates.get(position)); } } diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 59a9310..05abc26 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -126,7 +126,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { Editable text = getText(); int end = getSelectionEnd(); - if (end < 0) { + if (end < 0 || mTokenizer == null) { return false; } @@ -147,7 +147,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { public void performValidation() { Validator v = getValidator(); - if (v == null) { + if (v == null || mTokenizer == null) { return; } diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 4a5cea1..53db77e 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -30,6 +30,7 @@ import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.os.IBinder; import android.content.Context; import android.content.res.TypedArray; @@ -102,6 +103,8 @@ public class PopupWindow { private Rect mTempRect = new Rect(); private Drawable mBackground; + private Drawable mAboveAnchorBackgroundDrawable; + private Drawable mBelowAnchorBackgroundDrawable; private boolean mAboveAnchor; @@ -164,6 +167,43 @@ public class PopupWindow { mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); + // If this is a StateListDrawable, try to find and store the drawable to be + // used when the drop-down is placed above its anchor view, and the one to be + // used when the drop-down is placed below its anchor view. We extract + // the drawables ourselves to work around a problem with using refreshDrawableState + // that it will take into account the padding of all drawables specified in a + // StateListDrawable, thus adding superfluous padding to drop-down views. + // + // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and + // at least one other drawable, intended for the 'below-anchor state'. + if (mBackground instanceof StateListDrawable) { + StateListDrawable background = (StateListDrawable) mBackground; + + // Find the above-anchor view - this one's easy, it should be labeled as such. + int aboveAnchorStateIndex = background.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); + + // Now, for the below-anchor view, look for any other drawable specified in the + // StateListDrawable which is not for the above-anchor state and use that. + int count = background.getStateCount(); + int belowAnchorStateIndex = -1; + for (int i = 0; i < count; i++) { + if (i != aboveAnchorStateIndex) { + belowAnchorStateIndex = i; + break; + } + } + + // Store the drawables we found, if we found them. Otherwise, set them both + // to null so that we'll just use refreshDrawableState. + if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { + mAboveAnchorBackgroundDrawable = background.getStateDrawable(aboveAnchorStateIndex); + mBelowAnchorBackgroundDrawable = background.getStateDrawable(belowAnchorStateIndex); + } else { + mBelowAnchorBackgroundDrawable = null; + mAboveAnchorBackgroundDrawable = null; + } + } + a.recycle(); } @@ -661,7 +701,18 @@ public class PopupWindow { mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); if (mBackground != null) { - mPopupView.refreshDrawableState(); + // If the background drawable provided was a StateListDrawable with above-anchor + // and below-anchor states, use those. Otherwise rely on refreshDrawableState to + // do the job. + if (mAboveAnchorBackgroundDrawable != null) { + if (mAboveAnchor) { + mPopupView.setBackgroundDrawable(mAboveAnchorBackgroundDrawable); + } else { + mPopupView.setBackgroundDrawable(mBelowAnchorBackgroundDrawable); + } + } else { + mPopupView.refreshDrawableState(); + } } if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; @@ -697,12 +748,18 @@ public class PopupWindow { */ private void preparePopup(WindowManager.LayoutParams p) { if (mBackground != null) { + final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); + int height = ViewGroup.LayoutParams.FILL_PARENT; + if (layoutParams != null && + layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } + // when a background is available, we embed the content view // within another view that owns the background drawable PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT + ViewGroup.LayoutParams.FILL_PARENT, height ); popupViewContainer.setBackgroundDrawable(mBackground); popupViewContainer.addView(mContentView, listParams); diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index dd2570a..f646ab5 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -526,6 +526,7 @@ public class ProgressBar extends View { * @see #getProgress() * @see #incrementProgressBy(int) */ + @android.view.RemotableViewMethod public synchronized void setProgress(int progress) { setProgress(progress, false); } diff --git a/core/java/android/widget/ResourceCursorAdapter.java b/core/java/android/widget/ResourceCursorAdapter.java index 9052ae3..a5dbd98 100644 --- a/core/java/android/widget/ResourceCursorAdapter.java +++ b/core/java/android/widget/ResourceCursorAdapter.java @@ -46,10 +46,30 @@ public abstract class ResourceCursorAdapter extends CursorAdapter { public ResourceCursorAdapter(Context context, int layout, Cursor c) { super(context, c); mLayout = mDropDownLayout = layout; - mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. + * @hide Pending API Council approval + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** * Inflates view(s) from the specified XML file. * * @see android.widget.CursorAdapter#newView(android.content.Context, diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index c852be5..88b2a01 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -84,6 +84,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; +import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; import android.view.animation.AnimationUtils; @@ -215,7 +216,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; int mDrawablePadding; - }; + } private Drawables mDrawables; private CharSequence mError; @@ -239,8 +240,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mMarqueeRepeatLimit = 3; class InputContentType { - String privateContentType; + int imeOptions = EditorInfo.IME_UNDEFINED; + String privateImeOptions; + CharSequence imeActionLabel; + int imeActionId; Bundle extras; + OnEditorActionListener onEditorActionListener; + boolean enterDown; } InputContentType mInputContentType; @@ -268,6 +274,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener p.measureText("H"); } + /** + * Interface definition for a callback to be invoked when an action is + * performed on the editor. + */ + public interface OnEditorActionListener { + /** + * Called when an action is being performed. + * + * @param v The view that was clicked. + * @param actionId Identifier of the action. This will be either the + * identifier you supplied, or {@link EditorInfo#IME_UNDEFINED + * EditorInfo.IME_UNDEFINED} if being called due to the enter key + * being pressed. + * @param event If triggered by an enter key, this is the event; + * otherwise, this is null. + * @return Return true if you have consumed the action, else false. + */ + boolean onEditorAction(TextView v, int actionId, KeyEvent event); + } + public TextView(Context context) { this(context, null); } @@ -376,7 +402,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean password = false; - int contentType = EditorInfo.TYPE_NULL; + int inputType = EditorInfo.TYPE_NULL; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { @@ -610,11 +636,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextView_inputType: - contentType = a.getInt(attr, mInputType); + inputType = a.getInt(attr, mInputType); break; - case com.android.internal.R.styleable.TextView_editorPrivateContentType: - setPrivateContentType(a.getString(attr)); + case com.android.internal.R.styleable.TextView_imeOptions: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeOptions = a.getInt(attr, + mInputContentType.imeOptions); + break; + + case com.android.internal.R.styleable.TextView_imeActionLabel: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionLabel = a.getText(attr); + break; + + case com.android.internal.R.styleable.TextView_imeActionId: + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionId = a.getInt(attr, + mInputContentType.imeActionId); + break; + + case com.android.internal.R.styleable.TextView_privateImeOptions: + setPrivateImeOptions(a.getString(attr)); break; case com.android.internal.R.styleable.TextView_editorExtras: @@ -632,7 +681,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener BufferType bufferType = BufferType.EDITABLE; - if ((contentType&(EditorInfo.TYPE_MASK_CLASS + if ((inputType&(EditorInfo.TYPE_MASK_CLASS |EditorInfo.TYPE_MASK_VARIATION)) == (EditorInfo.TYPE_CLASS_TEXT |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { @@ -656,57 +705,57 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener throw new RuntimeException(ex); } try { - mInputType = contentType != EditorInfo.TYPE_NULL - ? contentType + mInputType = inputType != EditorInfo.TYPE_NULL + ? inputType : mInput.getInputType(); } catch (IncompatibleClassChangeError e) { mInputType = EditorInfo.TYPE_CLASS_TEXT; } } else if (digits != null) { mInput = DigitsKeyListener.getInstance(digits.toString()); - mInputType = contentType; - } else if (contentType != EditorInfo.TYPE_NULL) { - setInputType(contentType, true); - singleLine = (contentType&(EditorInfo.TYPE_MASK_CLASS + mInputType = inputType; + } else if (inputType != EditorInfo.TYPE_NULL) { + setInputType(inputType, true); + singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) != (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); } else if (phone) { mInput = DialerKeyListener.getInstance(); - contentType = EditorInfo.TYPE_CLASS_PHONE; + inputType = EditorInfo.TYPE_CLASS_PHONE; } else if (numeric != 0) { mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, (numeric & DECIMAL) != 0); - contentType = EditorInfo.TYPE_CLASS_NUMBER; + inputType = EditorInfo.TYPE_CLASS_NUMBER; if ((numeric & SIGNED) != 0) { - contentType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; + inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; } if ((numeric & DECIMAL) != 0) { - contentType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; + inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; } - mInputType = contentType; + mInputType = inputType; } else if (autotext || autocap != -1) { TextKeyListener.Capitalize cap; - contentType = EditorInfo.TYPE_CLASS_TEXT; + inputType = EditorInfo.TYPE_CLASS_TEXT; if (!singleLine) { - contentType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } switch (autocap) { case 1: cap = TextKeyListener.Capitalize.SENTENCES; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; break; case 2: cap = TextKeyListener.Capitalize.WORDS; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; break; case 3: cap = TextKeyListener.Capitalize.CHARACTERS; - contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; + inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; break; default: @@ -715,7 +764,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mInput = TextKeyListener.getInstance(autotext, cap); - mInputType = contentType; + mInputType = inputType; } else if (editable) { mInput = TextKeyListener.getInstance(); mInputType = EditorInfo.TYPE_CLASS_TEXT; @@ -1075,6 +1124,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_singleLine */ public final void setTransformationMethod(TransformationMethod method) { + if (method == mTransformation) { + // Avoid the setText() below if the transformation is + // the same. + return; + } if (mTransformation != null) { if (mText instanceof Spannable) { ((Spannable) mText).removeSpan(mTransformation); @@ -2778,7 +2832,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Directly change the content type integer of the text view, without * modifying any other state. - * @see #setContentType + * @see #setInputType(int) * @see android.text.InputType * @attr ref android.R.styleable#TextView_inputType */ @@ -2842,28 +2896,159 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Change the editor type integer associated with the text view, which + * will be reported to an IME with {@link EditorInfo#imeOptions} when it + * has focus. + * @see #getImeOptions + * @see android.view.inputmethod.EditorInfo + * @attr ref android.R.styleable#TextView_imeOptions + */ + public void setImeOptions(int imeOptions) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeOptions = imeOptions; + } + + /** + * Get the type of the IME editor. + * + * @see #setImeOptions(int) + * @see android.view.inputmethod.EditorInfo + */ + public int getImeOptions() { + return mInputContentType != null + ? mInputContentType.imeOptions : EditorInfo.IME_UNDEFINED; + } + + /** + * Change the custom IME action associated with the text view, which + * will be reported to an IME with {@link EditorInfo#actionLabel} + * and {@link EditorInfo#actionId} when it has focus. + * @see #getImeActionLabel + * @see #getImeActionId + * @see android.view.inputmethod.EditorInfo + * @attr ref android.R.styleable#TextView_imeActionLabel + * @attr ref android.R.styleable#TextView_imeActionId + */ + public void setImeActionLabel(CharSequence label, int actionId) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.imeActionLabel = label; + mInputContentType.imeActionId = actionId; + } + + /** + * Get the IME action label previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see android.view.inputmethod.EditorInfo + */ + public CharSequence getImeActionLabel() { + return mInputContentType != null + ? mInputContentType.imeActionLabel : null; + } + + /** + * Get the IME action ID previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see android.view.inputmethod.EditorInfo + */ + public int getImeActionId() { + return mInputContentType != null + ? mInputContentType.imeActionId : 0; + } + + /** + * Set a special OnClickListener to be called when an action is performed + * on the text view. This will be called when the enter key is pressed, + * or when an action supplied to the IME is selected by the user. + */ + public void setOnEditorActionListener(OnEditorActionListener l) { + if (mInputContentType == null) { + mInputContentType = new InputContentType(); + } + mInputContentType.onEditorActionListener = l; + } + + /** + * Called when an attached input method calls + * {@link InputConnection#performEditorAction(int) + * InputConnection.performEditorAction()} + * for this text view. The default implementation will call your click + * listener supplied to {@link #setOnEditorActionListener}, + * or generate an enter key down/up pair to invoke the action if not. + * + * @param actionCode The code of the action being performed. + * + * @see #setOnEditorActionListener + */ + public void onEditorAction(int actionCode) { + final InputContentType ict = mInputContentType; + if (ict != null) { + if (ict.onEditorActionListener != null) { + if (ict.onEditorActionListener.onEditorAction(this, + actionCode, null)) { + return; + } + } + } + + if (actionCode == EditorInfo.IME_ACTION_NEXT && + (ict != null || !shouldAdvanceFocusOnEnter())) { + // This is the default handling for the NEXT action, to advance + // focus. Note that for backwards compatibility we don't do this + // default handling if explicit ime options have not been given, + // and we do not advance by default on an enter key -- in that + // case, we want to turn this into the normal enter key codes that + // an app may be expecting. + View v = focusSearch(FOCUS_DOWN); + if (v != null) { + if (!v.requestFocus(FOCUS_DOWN)) { + throw new IllegalStateException("focus search returned a view " + + "that wasn't able to take focus!"); + } + } + return; + } + + Handler h = getHandler(); + long eventTime = SystemClock.uptimeMillis(); + h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + } + + /** * Set the private content type of the text, which is the - * {@link EditorInfo#privateContentType TextBoxAttribute.privateContentType} + * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions} * field that will be filled in when creating an input connection. * - * @see #getPrivateContentType() - * @see EditorInfo#privateContentType - * @attr ref android.R.styleable#TextView_editorPrivateContentType + * @see #getPrivateImeOptions() + * @see EditorInfo#privateImeOptions + * @attr ref android.R.styleable#TextView_privateImeOptions */ - public void setPrivateContentType(String type) { + public void setPrivateImeOptions(String type) { if (mInputContentType == null) mInputContentType = new InputContentType(); - mInputContentType.privateContentType = type; + mInputContentType.privateImeOptions = type; } /** * Get the private type of the content. * - * @see #setPrivateContentType(String) - * @see EditorInfo#privateContentType + * @see #setPrivateImeOptions(String) + * @see EditorInfo#privateImeOptions */ - public String getPrivateContentType() { + public String getPrivateImeOptions() { return mInputContentType != null - ? mInputContentType.privateContentType : null; + ? mInputContentType.privateImeOptions : null; } /** @@ -3807,7 +3992,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * but also in mail addresses and subjects which will display on multiple * lines but where it doesn't make sense to insert newlines. */ - private boolean advanceFocusOnEnter() { + protected boolean shouldAdvanceFocusOnEnter() { if (mInput == null) { return false; } @@ -3828,15 +4013,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } + private boolean isInterestingEnter(KeyEvent event) { + if ((event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 && + mInputContentType != null && + (mInputContentType.imeOptions & + EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { + // If this enter key came from a soft keyboard, and the + // text editor has been configured to not do a default + // action for software enter keys, then we aren't interested. + return false; + } + return true; + } + private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) { if (!isEnabled()) { return 0; } switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: - if (advanceFocusOnEnter()) { + if (!isInterestingEnter(event)) { + // Ignore enter key we aren't interested in. + return -1; + } + if (mInputContentType != null + && mInputContentType.onEditorActionListener != null) { + mInputContentType.enterDown = true; + } + // fall through... + case KeyEvent.KEYCODE_DPAD_CENTER: + if (shouldAdvanceFocusOnEnter()) { return 0; } } @@ -3939,7 +4146,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); case KeyEvent.KEYCODE_ENTER: - if (advanceFocusOnEnter()) { + if (mInputContentType != null + && mInputContentType.onEditorActionListener != null + && mInputContentType.enterDown) { + mInputContentType.enterDown = false; + if (mInputContentType.onEditorActionListener.onEditorAction( + this, EditorInfo.IME_UNDEFINED, event)) { + return true; + } + } + + if (shouldAdvanceFocusOnEnter()) { /* * If there is a click listener, just call through to * super, which will invoke it. @@ -3994,11 +4211,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInputMethodState = new InputMethodState(); } outAttrs.inputType = mInputType; - outAttrs.hintText = mHint; if (mInputContentType != null) { - outAttrs.privateContentType = mInputContentType.privateContentType; + outAttrs.imeOptions = mInputContentType.imeOptions; + outAttrs.privateImeOptions = mInputContentType.privateImeOptions; + outAttrs.actionLabel = mInputContentType.imeActionLabel; + outAttrs.actionId = mInputContentType.imeActionId; outAttrs.extras = mInputContentType.extras; + } else { + outAttrs.imeOptions = EditorInfo.IME_UNDEFINED; + } + if (outAttrs.imeOptions == EditorInfo.IME_UNDEFINED) { + if (focusSearch(FOCUS_DOWN) != null) { + // An action has not been set, but the enter key will move to + // the next focus, so set the action to that. + outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; + if (!shouldAdvanceFocusOnEnter()) { + outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; + } + } } + outAttrs.hintText = mHint; if (mText instanceof Editable) { InputConnection ic = new EditableInputConnection(this); outAttrs.initialSelStart = Selection.getSelectionStart(mText); @@ -5787,6 +6019,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Don't leave us in the middle of a batch edit. onEndBatchEdit(); + if (mInputContentType != null) { + mInputContentType.enterDown = false; + } } startStopMarquee(hasWindowFocus); @@ -5880,8 +6115,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mScroller = s; } - private static class Blink extends Handler - implements Runnable { + private static class Blink extends Handler implements Runnable { private WeakReference<TextView> mView; private boolean mCancelled; @@ -6139,23 +6373,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } int start = end; - char c; int len = mText.length(); - while (start > 0 && (((c = mTransformed.charAt(start - 1)) == '\'') || - (Character.isLetterOrDigit(c)))) { - start--; + for (; start > 0; start--) { + char c = mTransformed.charAt(start - 1); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } } - while (end < len && (((c = mTransformed.charAt(end)) == '\'') || - (Character.isLetterOrDigit(c)))) { - end++; + for (; end < len; end++) { + char c = mTransformed.charAt(end); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } } if (start == end) { return null; } + if (end - start > 48) { + return null; + } + return TextUtils.substring(mTransformed, start, end); } diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java new file mode 100644 index 0000000..ec45e23 --- /dev/null +++ b/core/java/android/widget/ZoomButtonsController.java @@ -0,0 +1,478 @@ +/* + * 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.widget; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.view.WindowManager.LayoutParams; + +// TODO: make sure no px values exist, only dip (scale if necessary from Viewconfiguration) + +/** + * 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 ZoomButtonsController implements View.OnTouchListener { + + private static final String TAG = "ZoomButtonsController"; + + private static final int ZOOM_CONTROLS_TIMEOUT = + (int) ViewConfiguration.getZoomControlsTimeout(); + + // TODO: scaled to density + private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; + + private Context mContext; + private WindowManager mWindowManager; + + /** + * The view that is being zoomed by this zoom ring. + */ + private View mOwnerView; + + /** + * The bounds of the owner view in global coordinates. This is recalculated + * each time the zoom ring is shown. + */ + private Rect mOwnerViewBounds = new Rect(); + + /** + * The container that is added as a window. + */ + private FrameLayout mContainer; + private LayoutParams mContainerLayoutParams; + private int[] mContainerLocation = new int[2]; + + private ZoomControls mControls; + + /** + * 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; + + private boolean mIsVisible; + + private Rect mTempRect = new Rect(); + + private OnZoomListener mCallback; + + /** + * When showing the zoom, we add the view as a new window. However, there is + * logic that needs to know the size of the zoom which is determined after + * it's laid out. Therefore, we must post this logic onto the UI thread so + * it will be exceuted AFTER the layout. This is the logic. + */ + private Runnable mPostedVisibleInitializer; + + private IntentFilter mConfigurationChangedFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mIsVisible) return; + + mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); + mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); + } + }; + + /** When configuration changes, this is called after the UI thread is idle. */ + private static final int MSG_POST_CONFIGURATION_CHANGED = 2; + /** Used to delay the zoom ring dismissal. */ + private static final int MSG_DISMISS_ZOOM_RING = 3; + /** + * If setVisible(true) is called and the owner view's window token is null, + * we delay the setVisible(true) call until it is not null. + */ + private static final int MSG_POST_SET_VISIBLE = 4; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_POST_CONFIGURATION_CHANGED: + onPostConfigurationChanged(); + break; + + case MSG_DISMISS_ZOOM_RING: + setVisible(false); + break; + + case MSG_POST_SET_VISIBLE: + if (mOwnerView.getWindowToken() == null) { + // Doh, it is still null, throw an exception + throw new IllegalArgumentException( + "Cannot make the zoom ring visible if the owner view is " + + "not attached to a window."); + } + setVisible(true); + break; + } + + } + }; + + public ZoomButtonsController(Context context, View ownerView) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mOwnerView = ownerView; + + mContainer = createContainer(); + } + + private FrameLayout createContainer() { + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.BOTTOM | Gravity.CENTER; + lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | + LayoutParams.FLAG_LAYOUT_NO_LIMITS; + lp.height = LayoutParams.WRAP_CONTENT; + lp.width = LayoutParams.FILL_PARENT; + lp.type = LayoutParams.TYPE_APPLICATION_PANEL; + lp.format = PixelFormat.TRANSPARENT; + // TODO: make a new animation for this + lp.windowAnimations = com.android.internal.R.style.Animation_InputMethodFancy; + mContainerLayoutParams = lp; + + FrameLayout container = new FrameLayout(mContext); + container.setLayoutParams(lp); + container.setMeasureAllChildren(true); + + LayoutInflater inflater = (LayoutInflater) mContext + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(com.android.internal.R.layout.zoom_magnify, container); + + mControls = (ZoomControls) container.findViewById(com.android.internal.R.id.zoomControls); + mControls.setOnZoomInClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onZoom(true); + } + }); + mControls.setOnZoomOutClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onZoom(false); + } + }); + + View overview = container.findViewById(com.android.internal.R.id.zoomMagnify); + overview.setVisibility(View.GONE); + overview.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + if (mCallback != null) mCallback.onOverview(); + } + }); + + return container; + } + + public void setCallback(OnZoomListener callback) { + mCallback = callback; + } + + public void setFocusable(boolean focusable) { + if (focusable) { + mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; + } + + if (mIsVisible) { + mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); + } + } + + public void setOverviewVisible(boolean visible) { + mContainer.findViewById(com.android.internal.R.id.zoomMagnify) + .setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public boolean isVisible() { + return mIsVisible; + } + + public void setVisible(boolean visible) { + + if (!useThisZoom(mContext)) return; + + if (visible) { + if (mOwnerView.getWindowToken() == null) { + /* + * We need a window token to show ourselves, maybe the owner's + * window hasn't been created yet but it will have been by the + * time the looper is idle, so post the setVisible(true) call. + */ + if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { + mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); + } + return; + } + + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + } + + if (mIsVisible == visible) { + return; + } + mIsVisible = visible; + + if (visible) { + if (mContainerLayoutParams.token == null) { + mContainerLayoutParams.token = mOwnerView.getWindowToken(); + } + + mWindowManager.addView(mContainer, mContainerLayoutParams); + + if (mPostedVisibleInitializer == null) { + mPostedVisibleInitializer = new Runnable() { + public void run() { + refreshPositioningVariables(); + + if (mCallback != null) { + mCallback.onVisibilityChanged(true); + } + } + }; + } + + mHandler.post(mPostedVisibleInitializer); + + // Handle configuration changes when visible + mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); + + // Steal touches events from the owner + mOwnerView.setOnTouchListener(this); + mReleaseTouchListenerOnUp = false; + + } else { + // Don't want to steal any more touches + 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); + mHandler.removeCallbacks(mPostedVisibleInitializer); + + if (mCallback != null) { + mCallback.onVisibilityChanged(false); + } + } + + } + + /** + * TODO: docs + * + * Notes: + * - Please ensure you set your View to INVISIBLE not GONE when hiding it. + * + * @return TODO + */ + public FrameLayout getContainer() { + return mContainer; + } + + public int getZoomRingId() { + return mControls.getId(); + } + + private void dismissControlsDelayed(int delay) { + mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); + mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); + } + + /** + * Should be called by the client for each event belonging to the second tap + * (the down, move, up, and cancel events). + * + * @param event The event belonging to the second tap. + * @return Whether the event was consumed. + */ + public boolean handleDoubleTapEvent(MotionEvent event) { + if (!useThisZoom(mContext)) return false; + + int action = event.getAction(); + + if (action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + setVisible(true); + centerPoint(x, y); + } + + return true; + } + + private void refreshPositioningVariables() { + // Calculate the owner view's bounds + mOwnerView.getGlobalVisibleRect(mOwnerViewBounds); + mContainer.getLocationOnScreen(mContainerLocation); + } + + /** + * Centers the point (in owner view's coordinates). + */ + private void centerPoint(int x, int y) { + if (mCallback != null) { + mCallback.onCenter(x, y); + } + } + + public boolean onTouch(View v, MotionEvent event) { + 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); + setTouchTargetView(null); + mReleaseTouchListenerOnUp = false; + } + + // Eat this event + return true; + } + + // TODO: optimize this (it ends up removing message and queuing another) + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + + View targetView = mTouchTargetView; + + switch (action) { + case MotionEvent.ACTION_DOWN: + targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); + setTouchTargetView(targetView); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + setTouchTargetView(null); + break; + } + + if (targetView != null) { + // The upperleft corner of the target view in raw coordinates + int targetViewRawX = mContainerLocation[0] + mTouchTargetLocationInWindow[0]; + int targetViewRawY = mContainerLocation[1] + 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 { + return false; + } + } + + private void setTouchTargetView(View view) { + mTouchTargetView = view; + if (view != null) { + view.getLocationInWindow(mTouchTargetLocationInWindow); + } + } + + /** + * 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) { + // Reverse order so the child drawn on top gets first dibs. + int containerCoordsX = rawX - mContainerLocation[0]; + int containerCoordsY = rawY - mContainerLocation[1]; + Rect frame = mTempRect; + for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { + View child = mContainer.getChildAt(i); + if (child.getVisibility() != View.VISIBLE) { + continue; + } + + child.getHitRect(frame); + // Expand the touch region + frame.top -= ZOOM_CONTROLS_TOUCH_PADDING; + if (frame.contains(containerCoordsX, containerCoordsY)) { + return child; + } + } + + return null; + } + + private void onPostConfigurationChanged() { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + refreshPositioningVariables(); + } + + public static boolean useThisZoom(Context context) { + return ZoomRingController.getZoomType(context) == 2; + } + + public interface OnZoomListener { + void onCenter(int x, int y); + void onVisibilityChanged(boolean visible); + void onZoom(boolean zoomIn); + void onOverview(); + } +} diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java index a29e1a0..a5a867b 100644 --- a/core/java/android/widget/ZoomRing.java +++ b/core/java/android/widget/ZoomRing.java @@ -77,7 +77,7 @@ public class ZoomRing extends View { private int mPreviousWidgetDragX; private int mPreviousWidgetDragY; - private boolean mDrawThumb = true; + private boolean mThumbVisible = true; private Drawable mThumbDrawable; /** Shown beneath the thumb if we can still zoom in. */ @@ -91,6 +91,13 @@ public class ZoomRing extends View { private static final int THUMB_ARROWS_FADE_DURATION = 300; private long mThumbArrowsFadeStartTime; private int mThumbArrowsAlpha = 255; + + private static final int THUMB_PLUS_MINUS_DISTANCE = 69; + private static final int THUMB_PLUS_MINUS_OFFSET_ANGLE = TWO_PI_INT_MULTIPLIED / 11; + /** Drawn (without rotation) on top of the arrow. */ + private Drawable mThumbPlusDrawable; + /** Drawn (without rotation) on top of the arrow. */ + private Drawable mThumbMinusDrawable; private static final int MODE_IDLE = 0; @@ -99,7 +106,7 @@ public class ZoomRing extends View { * 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_WAITING_FOR_DRAG_THUMB_AFTER_JUMP = 5; private static final int MODE_DRAG_THUMB = 1; /** * User has his finger down, but we are waiting for him to pass the touch @@ -109,11 +116,14 @@ 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; + /** Ignore the touch interaction until the user touches the thumb again. */ + private static final int MODE_IGNORE_UNTIL_TOUCHES_THUMB = 6; private int mMode; - - private long mPreviousUpTime; + + /** Records the last mode the user was in. */ + private int mPreviousMode; + + private long mPreviousCenterUpTime; private int mPreviousDownX; private int mPreviousDownY; @@ -122,7 +132,9 @@ public class ZoomRing extends View { private OnZoomRingCallback mCallback; private int mPreviousCallbackAngle; private int mCallbackThreshold = Integer.MAX_VALUE; - + /** If the user drags to within __% of a tick, snap to that tick. */ + private int mFuzzyCallbackThreshold = Integer.MAX_VALUE; + private boolean mResetThumbAutomatically = true; private int mThumbDragStartAngle; @@ -133,6 +145,8 @@ public class ZoomRing extends View { private Scroller mThumbScroller; + private boolean mVibration = true; + private static final int MSG_THUMB_SCROLLER_TICK = 1; private static final int MSG_THUMB_ARROWS_FADE_TICK = 2; private Handler mHandler = new Handler() { @@ -163,6 +177,8 @@ public class ZoomRing extends View { mutate(); mThumbMinusArrowDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus_arrow_rotatable). mutate(); + mThumbPlusDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_plus); + mThumbMinusDrawable = res.getDrawable(R.drawable.zoom_ring_thumb_minus); if (DRAW_TRAIL) { mTrail = res.getDrawable(R.drawable.zoom_ring_trail).mutate(); } @@ -175,7 +191,7 @@ public class ZoomRing extends View { mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2; mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2; - mCallbackThreshold = PI_INT_MULTIPLIED / 6; + setCallbackThreshold(PI_INT_MULTIPLIED / 6); } public ZoomRing(Context context, AttributeSet attrs) { @@ -193,8 +209,20 @@ public class ZoomRing extends View { // TODO: rename public void setCallbackThreshold(int callbackThreshold) { mCallbackThreshold = callbackThreshold; + mFuzzyCallbackThreshold = (int) (callbackThreshold * 0.65f); } + public void setVibration(boolean vibrate) { + mVibration = vibrate; + } + + public void setThumbVisible(boolean thumbVisible) { + if (mThumbVisible != thumbVisible) { + mThumbVisible = thumbVisible; + invalidate(); + } + } + // TODO: from XML too public void setRingBounds(int innerRadius, int outerRadius) { mBoundInnerRadiusSquared = innerRadius * innerRadius; @@ -306,15 +334,7 @@ public class ZoomRing extends View { 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); + setThumbAngleAnimated(angle, duration, deltaAngle > 0); } public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) { @@ -354,14 +374,10 @@ public class ZoomRing extends View { return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED; } - public void resetThumbAngle(int angle) { - mPreviousCallbackAngle = angle; - setThumbAngleInt(angle); - } - public void resetThumbAngle() { if (mResetThumbAutomatically) { - resetThumbAngle(0); + mPreviousCallbackAngle = 0; + setThumbAngleInt(0); } } @@ -394,101 +410,119 @@ public class ZoomRing extends View { mTrail.setBounds(0, 0, right - left, bottom - top); } + // These drawables are the same size as the track mThumbPlusArrowDrawable.setBounds(0, 0, right - left, bottom - top); mThumbMinusArrowDrawable.setBounds(0, 0, right - left, bottom - top); } @Override public boolean onTouchEvent(MotionEvent event) { +// Log.d(TAG, "History size: " + event.getHistorySize()); + return handleTouch(event.getAction(), event.getEventTime(), (int) event.getX(), (int) event.getY(), (int) event.getRawX(), (int) event.getRawY()); } - private void resetState() { - mMode = MODE_IDLE; + private void resetToIdle() { + setMode(MODE_IDLE); mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE; mAcculumalatedTrailAngle = 0.0; } public void setTapDragMode(boolean tapDragMode, int x, int y) { - resetState(); - mMode = tapDragMode ? MODE_TAP_DRAG : MODE_IDLE; - + resetToIdle(); if (tapDragMode) { + setMode(MODE_TAP_DRAG); + mCallback.onUserInteractionStarted(); onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY)); + } else { + onTouchUp(SystemClock.elapsedRealtime(), true); } } public boolean handleTouch(int action, long time, int x, int y, int rawX, int rawY) { - switch (action) { + // local{X,Y} will be where the center of the widget is (0,0) + int localX = x - mCenterX; + int localY = y - mCenterY; + + /* + * If we are not drawing the thumb, there is no way for the user to be + * touching the thumb. Also, if this is the case, assume they are not + * touching the ring (so the user cannot absolute set the thumb, and + * there will be a larger touch region for going into the move-ring + * mode). + */ + boolean isTouchingThumb = mThumbVisible; + boolean isTouchingRing = mThumbVisible; + + int touchAngle = getAngle(localX, localY); +// printAngle("touchAngle", touchAngle); +// printAngle("mThumbAngle", mThumbAngle); +// printAngle("mPreviousCallbackAngle", mPreviousCallbackAngle); +// Log.d(TAG, ""); + + + int radiusSquared = localX * localX + localY * localY; + if (radiusSquared < mBoundInnerRadiusSquared || + radiusSquared > mBoundOuterRadiusSquared) { + // Out-of-bounds + isTouchingThumb = false; + isTouchingRing = false; + } + + if (isTouchingThumb) { + int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); + int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? + deltaThumbAndTouch : -deltaThumbAndTouch; + if (absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) { + // Didn't grab close enough to the thumb + isTouchingThumb = false; + } + } + switch (action) { case MotionEvent.ACTION_DOWN: - if (time - mPreviousUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT) { + if (!isTouchingRing && + (time - mPreviousCenterUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT)) { + // Make sure the double-tap is in the center of the widget (and not on the ring) mCallback.onZoomRingDismissed(true); - onTouchUp(time); + onTouchUp(time, isTouchingRing); // Dismissing, so halt here return true; } + resetToIdle(); mCallback.onUserInteractionStarted(); mPreviousDownX = x; mPreviousDownY = y; - resetState(); // 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: - onTouchUp(time); + onTouchUp(time, isTouchingRing); return true; default: return false; } - // local{X,Y} will be where the center of the widget is (0,0) - int localX = x - mCenterX; - int localY = y - mCenterY; - boolean isTouchingThumb = 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; - isInRingBounds = false; - } - - int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); - int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? - deltaThumbAndTouch : -deltaThumbAndTouch; - if (isTouchingThumb && - absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) { - // Didn't grab close enough to the thumb - isTouchingThumb = false; - } - if (mMode == MODE_IDLE) { if (isTouchingThumb) { // They grabbed the thumb - mMode = MODE_DRAG_THUMB; + setMode(MODE_DRAG_THUMB); onThumbDragStarted(touchAngle); - } else if (isInRingBounds) { + } else if (isTouchingRing) { // They tapped somewhere else on the ring int tickAngle = getClosestTickAngle(touchAngle); - int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle); int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); @@ -497,12 +531,12 @@ public class ZoomRing extends View { 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; + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); return true; } - // Make sure we only let them jump within bounds if (boundAngle != Integer.MIN_VALUE) { + // Cap the user's jump to the bound tickAngle = boundAngle; } } else { @@ -515,47 +549,59 @@ public class ZoomRing extends View { deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw); boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); if (boundAngle != Integer.MIN_VALUE) { - // Not allowed to be here, it is between two bounds - mMode = MODE_IGNORE_UNTIL_UP; + // Cannot get to the tapped location because it is out-of-bounds + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); return true; } } } - mMode = MODE_WAITING_FOR_DRAG_THUMB; + setMode(MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP); mWaitingForDragThumbDownAngle = touchAngle; boolean ccw = deltaThumbAndTick > 0; setThumbAngleAnimated(tickAngle, 0, ccw); - // Our thumb scrolling animation takes us from mThumbAngle to tickAngle + /* + * Our thumb scrolling animation takes us from mThumbAngle to + * tickAngle, so manifest that as the user dragging the thumb + * there. + */ onThumbDragStarted(mThumbAngle); + // We know which direction we want to go onThumbDragged(tickAngle, true, ccw); } else { - // They tapped somewhere else - mMode = MODE_WAITING_FOR_MOVE_ZOOM_RING; + // They tapped somewhere else on the widget + setMode(MODE_WAITING_FOR_MOVE_ZOOM_RING); mCallback.onZoomRingSetMovableHintVisible(true); } - } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB) { + } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { int deltaDownAngle = getDelta(mWaitingForDragThumbDownAngle, touchAngle); if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) && isDeltaInBounds(mWaitingForDragThumbDownAngle, deltaDownAngle)) { - mMode = MODE_DRAG_THUMB; + setMode(MODE_DRAG_THUMB); + + // No need to call onThumbDragStarted, since that was done when they tapped-to-jump } } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { if (Math.abs(x - mPreviousDownX) > mTouchSlop || Math.abs(y - mPreviousDownY) > mTouchSlop) { /* Make sure the user has moved the slop amount before going into that mode. */ - mMode = MODE_MOVE_ZOOM_RING; + setMode(MODE_MOVE_ZOOM_RING); mCallback.onZoomRingMovingStarted(); } + } else if (mMode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) { + if (isTouchingThumb) { + // The user is back on the thumb, let's go back to the previous mode + setMode(mPreviousMode); + } } // Purposefully not an "else if" if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - if (isInRingBounds) { + if (isTouchingRing) { onThumbDragged(touchAngle, false, false); } } else if (mMode == MODE_MOVE_ZOOM_RING) { @@ -565,24 +611,39 @@ public class ZoomRing extends View { return true; } - private void onTouchUp(long time) { - if (mMode == MODE_MOVE_ZOOM_RING || mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { + private void onTouchUp(long time, boolean isTouchingRing) { + int mode = mMode; + if (mode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) { + // For cleaning up, pretend like the user was still in the previous mode + mode = mPreviousMode; + } + + if (mode == MODE_MOVE_ZOOM_RING || mode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { mCallback.onZoomRingSetMovableHintVisible(false); - if (mMode == MODE_MOVE_ZOOM_RING) { + if (mode == MODE_MOVE_ZOOM_RING) { mCallback.onZoomRingMovingStopped(); } - } else if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG || - mMode == MODE_WAITING_FOR_DRAG_THUMB) { + } else if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG || + mode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { onThumbDragStopped(); - if (mMode == MODE_DRAG_THUMB) { + if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG) { // Animate back to a tick setThumbAngleAnimated(mPreviousCallbackAngle, 0); } } - - mPreviousUpTime = time; mCallback.onUserInteractionStopped(); + + if (!isTouchingRing) { + mPreviousCenterUpTime = time; + } + } + + private void setMode(int mode) { + if (mode != mMode) { + mPreviousMode = mMode; + mMode = mode; + } } private boolean isDeltaInBounds(int startAngle, int deltaAngle) { @@ -681,9 +742,8 @@ public class ZoomRing extends View { int totalDeltaAngle; totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); - int fuzzyCallbackThreshold = (int) (mCallbackThreshold * 0.65f); - if (totalDeltaAngle >= fuzzyCallbackThreshold - || totalDeltaAngle <= -fuzzyCallbackThreshold) { + if (totalDeltaAngle >= mFuzzyCallbackThreshold + || totalDeltaAngle <= -mFuzzyCallbackThreshold) { if (!useDirection) { // Set ccw to match the direction found by getDelta @@ -737,7 +797,7 @@ public class ZoomRing extends View { // We bounded the touch angle totalDeltaAngle = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); animateThumbToNewAngle = true; - mMode = MODE_IGNORE_UNTIL_UP; + setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); } @@ -764,11 +824,13 @@ public class ZoomRing extends View { 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); + if (mVibration) { + // 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( @@ -791,6 +853,134 @@ public class ZoomRing extends View { setThumbAngleAuto(touchAngle, useDirection, ccw); } } +// private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) { +// int deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); +// +// if (!useDirection) { +// // Set ccw to match the direction found by getDelta +// ccw = deltaPrevCbAndTouch > 0; +// useDirection = true; +// } +// +// boolean animateThumbToNewAngle = false; +// boolean animationCcw = ccw; +// +// if (deltaPrevCbAndTouch >= mFuzzyCallbackThreshold +// || deltaPrevCbAndTouch <= -mFuzzyCallbackThreshold) { +// +// /* +// * 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, true, ccw); +// if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) { +// // The touch has past far enough from the bound +// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, +// touchAngle, true, ccw); +// if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { +// // The bound is between the previous callback angle and the touch +// // Cap to the bound +// touchAngle = mThumbCcwBound; +// /* +// * We're moving the touch BACK to the bound, so animate +// * back in the opposite direction that passed the bound. +// */ +// animationCcw = false; +// } +// } +// } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) { +// // See block above for general comments +// int deltaCwBoundAndTouch = +// getDelta(mThumbCwBound, touchAngle, true, ccw); +// if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) { +// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, +// touchAngle, true, ccw); +// /* +// * 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; +// animationCcw = true; +// } +// } +// } +// if (touchAngle != oldTouchAngle) { +// // We bounded the touch angle +// deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, true, ccw); +// // Animate back to the bound +// animateThumbToNewAngle = true; +// // Disallow movement now +// setMode(MODE_IGNORE_UNTIL_UP); +// } +// +// +// /* +// * Prevent it from jumping too far (this could happen if the user +// * goes through the center) +// */ +// +// if (mEnforceMaxAbsJump) { +// if (deltaPrevCbAndTouch <= -MAX_ABS_JUMP_DELTA_ANGLE) { +// deltaPrevCbAndTouch = -MAX_ABS_JUMP_DELTA_ANGLE; +// animateThumbToNewAngle = true; +// } else if (deltaPrevCbAndTouch >= MAX_ABS_JUMP_DELTA_ANGLE) { +// deltaPrevCbAndTouch = MAX_ABS_JUMP_DELTA_ANGLE; +// animateThumbToNewAngle = true; +// } +// } +// +// /* +// * 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) deltaPrevCbAndTouch / mCallbackThreshold); +// if (deltaLevels != 0) { +// boolean canStillZoom = mCallback.onZoomRingThumbDragged( +// deltaLevels, mThumbDragStartAngle, touchAngle); +// +// if (mVibration) { +// // 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)); +// } +// } +// +// if (DRAW_TRAIL) { +// int deltaAngle = getDelta(mThumbAngle, touchAngle, true, ccw); +// mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); +// } +// +// if (animateThumbToNewAngle) { +// setThumbAngleAnimated(touchAngle, 0, animationCcw); +// } else { +// /* +// * Use regular ccw here because animationCcw will never have been +// * changed if animateThumbToNewAngle is false +// */ +// setThumbAngleAuto(touchAngle, true, ccw); +// } +// } private int getValidAngle(int invalidAngle) { if (invalidAngle < 0) { @@ -818,16 +1008,16 @@ public class ZoomRing extends View { mCallback.onZoomRingThumbDraggingStopped(); } - private void onZoomRingMoved(int x, int y) { + private void onZoomRingMoved(int rawX, int rawY) { if (mPreviousWidgetDragX != Integer.MIN_VALUE) { - int deltaX = x - mPreviousWidgetDragX; - int deltaY = y - mPreviousWidgetDragY; + int deltaX = rawX - mPreviousWidgetDragX; + int deltaY = rawY - mPreviousWidgetDragY; - mCallback.onZoomRingMoved(deltaX, deltaY); + mCallback.onZoomRingMoved(deltaX, deltaY, rawX, rawY); } - mPreviousWidgetDragX = x; - mPreviousWidgetDragY = y; + mPreviousWidgetDragX = rawX; + mPreviousWidgetDragY = rawY; } @Override @@ -859,15 +1049,17 @@ public class ZoomRing extends View { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mDrawThumb) { + if (mThumbVisible) { if (DRAW_TRAIL) { mTrail.draw(canvas); } if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) { mThumbPlusArrowDrawable.draw(canvas); + mThumbPlusDrawable.draw(canvas); } if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { mThumbMinusArrowDrawable.draw(canvas); + mThumbMinusDrawable.draw(canvas); } mThumbDrawable.draw(canvas); } @@ -877,6 +1069,28 @@ public class ZoomRing extends View { int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED; mThumbPlusArrowDrawable.setLevel(level); mThumbMinusArrowDrawable.setLevel(level); + + // Assume it is a square + int halfSideLength = mThumbPlusDrawable.getIntrinsicHeight() / 2; + int unoffsetAngle = angle + mZeroAngle; + + int plusCenterX = (int) (Math.cos(1f * (unoffsetAngle - THUMB_PLUS_MINUS_OFFSET_ANGLE) + / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) + mCenterX; + int plusCenterY = (int) (Math.sin(1f * (unoffsetAngle - THUMB_PLUS_MINUS_OFFSET_ANGLE) + / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) * -1 + mCenterY; + mThumbPlusDrawable.setBounds(plusCenterX - halfSideLength, + plusCenterY - halfSideLength, + plusCenterX + halfSideLength, + plusCenterY + halfSideLength); + + int minusCenterX = (int) (Math.cos(1f * (unoffsetAngle + THUMB_PLUS_MINUS_OFFSET_ANGLE) + / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) + mCenterX; + int minusCenterY = (int) (Math.sin(1f * (unoffsetAngle + THUMB_PLUS_MINUS_OFFSET_ANGLE) + / RADIAN_INT_MULTIPLIER) * THUMB_PLUS_MINUS_DISTANCE) * -1 + mCenterY; + mThumbMinusDrawable.setBounds(minusCenterX - halfSideLength, + minusCenterY - halfSideLength, + minusCenterX + halfSideLength, + minusCenterY + halfSideLength); } public void setThumbArrowsVisible(boolean visible) { @@ -886,6 +1100,7 @@ public class ZoomRing extends View { if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR || callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) { mThumbPlusArrowDrawable.setAlpha(255); + mThumbPlusDrawable.setAlpha(255); mThumbArrowsToDraw |= THUMB_ARROW_PLUS; } else { mThumbArrowsToDraw &= ~THUMB_ARROW_PLUS; @@ -893,6 +1108,7 @@ public class ZoomRing extends View { if (callbackAngle < mThumbCcwBound - RADIAN_INT_ERROR || callbackAngle > mThumbCcwBound + RADIAN_INT_ERROR) { mThumbMinusArrowDrawable.setAlpha(255); + mThumbMinusDrawable.setAlpha(255); mThumbArrowsToDraw |= THUMB_ARROW_MINUS; } else { mThumbArrowsToDraw &= ~THUMB_ARROW_MINUS; @@ -917,10 +1133,14 @@ public class ZoomRing extends View { if (mThumbArrowsAlpha < 0) mThumbArrowsAlpha = 0; if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) { mThumbPlusArrowDrawable.setAlpha(mThumbArrowsAlpha); + mThumbPlusDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mThumbPlusDrawable); invalidateDrawable(mThumbPlusArrowDrawable); } if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { mThumbMinusArrowDrawable.setAlpha(mThumbArrowsAlpha); + mThumbMinusDrawable.setAlpha(mThumbArrowsAlpha); + invalidateDrawable(mThumbMinusDrawable); invalidateDrawable(mThumbMinusArrowDrawable); } @@ -941,7 +1161,7 @@ public class ZoomRing extends View { void onZoomRingSetMovableHintVisible(boolean visible); void onZoomRingMovingStarted(); - boolean onZoomRingMoved(int deltaX, int deltaY); + boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY); void onZoomRingMovingStopped(); void onZoomRingThumbDraggingStarted(); diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java index 2e97fda..19f66a0 100644 --- a/core/java/android/widget/ZoomRingController.java +++ b/core/java/android/widget/ZoomRingController.java @@ -33,6 +33,7 @@ import android.provider.Settings; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -47,30 +48,36 @@ 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 ZOOM_RING_RADIUS_INSET = 24; private static final int ZOOM_RING_RECENTERING_DURATION = 500; private static final String TAG = "ZoomRing"; - public static final boolean USE_OLD_ZOOM = false; + public static final boolean USE_OLD_ZOOM = false; + static int getZoomType(Context context) { + return Settings.System.getInt(context.getContentResolver(), "zoom", 1); + } public static boolean useOldZoom(Context context) { - return Settings.System.getInt(context.getContentResolver(), "zoom", 1) == 0; + return getZoomType(context) == 0; } - + private static boolean useThisZoom(Context context) { + return getZoomType(context) == 1; + } + private static final int ZOOM_CONTROLS_TIMEOUT = (int) ViewConfiguration.getZoomControlsTimeout(); - + // TODO: move these to ViewConfiguration or re-use existing ones // TODO: scale px values based on latest from ViewConfiguration private static final int SECOND_TAP_TIMEOUT = 500; @@ -80,12 +87,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private static final int MAX_INITIATE_PAN_GAP = 10; // TODO view config private static final int INITIATE_PAN_DELAY = 300; - + private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; - + private Context mContext; private WindowManager mWindowManager; - + /** * The view that is being zoomed by this zoom ring. */ @@ -111,15 +118,15 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * The {@link #mTouchTargetView}'s location in window, set on touch down. */ - private int[] mTouchTargetLocationInWindow = new int[2]; + 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, @@ -132,30 +139,40 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, */ private int mTapDragStartX; private int mTapDragStartY; - + private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_WAITING_FOR_SECOND_TAP = 1; private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2; private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3; private int mTouchMode; - + private boolean mIsZoomRingVisible; - + private ZoomRing mZoomRing; private int mZoomRingWidth; private int mZoomRingHeight; - + /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner; private long mTouchingEdgeStartTime; private boolean mPanningEnabledForThisInteraction; - + + /** + * When the finger moves the zoom ring to an edge, this is the horizontal + * accumulator for how much the finger has moved off of its original touch + * point on the zoom ring (OOB = out-of-bounds). If < 0, the finger has + * moved that many px to the left of its original touch point on the ring. + */ + private int mMovingZoomRingOobX; + /** Vertical accumulator, see {@link #mMovingZoomRingOobX} */ + private int mMovingZoomRingOobY; + private ImageView mPanningArrows; private Animation mPanningArrowsEnterAnimation; private Animation mPanningArrowsExitAnimation; - + private Rect mTempRect = new Rect(); - + private OnZoomListener mCallback; private ViewConfiguration mViewConfig; @@ -171,7 +188,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * for the container's layout params. */ private int mCenteredContainerY = Integer.MIN_VALUE; - + /** * Scroller used to re-center the zoom ring if the user had dragged it to a * corner and then double-taps any point on the owner view (the owner view @@ -181,7 +198,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * The (x,y) of the scroller is the (x,y) of the container's layout params. */ private Scroller mScroller; - + /** * When showing the zoom ring, we add the view as a new window. However, * there is logic that needs to know the size of the zoom ring which is @@ -189,7 +206,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * the UI thread so it will be exceuted AFTER the layout. This is the logic. */ private Runnable mPostedVisibleInitializer; - + /** * Only touch from the main thread. */ @@ -199,23 +216,29 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - + private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!mIsZoomRingVisible) return; - + mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); } }; - + /** Keeps the scroller going (or starts it). */ private static final int MSG_SCROLLER_TICK = 1; /** When configuration changes, this is called after the UI thread is idle. */ private static final int MSG_POST_CONFIGURATION_CHANGED = 2; /** Used to delay the zoom ring dismissal. */ private static final int MSG_DISMISS_ZOOM_RING = 3; + + /** + * If setVisible(true) is called and the owner view's window token is null, + * we delay the setVisible(true) call until it is not null. + */ + private static final int MSG_POST_SET_VISIBLE = 4; private Handler mHandler = new Handler() { @Override @@ -224,26 +247,36 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case MSG_SCROLLER_TICK: onScrollerTick(); break; - + case MSG_POST_CONFIGURATION_CHANGED: onPostConfigurationChanged(); break; - + case MSG_DISMISS_ZOOM_RING: setVisible(false); break; + + case MSG_POST_SET_VISIBLE: + if (mOwnerView.getWindowToken() == null) { + // Doh, it is still null, throw an exception + throw new IllegalArgumentException( + "Cannot make the zoom ring visible if the owner view is " + + "not attached to a window."); + } + setVisible(true); + break; } - - } + + } }; - + public ZoomRingController(Context context, View ownerView) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mPanner = new Panner(); mOwnerView = ownerView; - + mZoomRing = new ZoomRing(context); mZoomRing.setId(com.android.internal.R.id.zoomControls); mZoomRing.setLayoutParams(new FrameLayout.LayoutParams( @@ -251,7 +284,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mZoomRing.setCallback(this); - + createPanningArrows(); mContainerLayoutParams = new LayoutParams(); @@ -269,12 +302,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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); } @@ -287,7 +320,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mPanningArrows.setVisibility(View.INVISIBLE); - + mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_in); mPanningArrowsExitAnimation = AnimationUtils.loadAnimation(mContext, @@ -299,7 +332,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * get a callback. Once there is a callback, the accumulator resets. For * example, if you set this to PI/6, it will give a callback every time the * user moves PI/6 amount on the ring. - * + * * @param callbackThreshold The angle for the callback threshold, in radians */ public void setZoomCallbackThreshold(float callbackThreshold) { @@ -308,7 +341,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * 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 @@ -316,11 +349,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setZoomRingTrack(int drawable) { mZoomRing.setBackgroundResource(drawable); } - + public void setCallback(OnZoomListener callback) { mCallback = callback; } - + public void setThumbAngle(float angle) { mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } @@ -328,13 +361,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setThumbAngleAnimated(float angle) { mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0); } - + public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mZoomRing.setResetThumbAutomatically(resetThumbAutomatically); } + + public void setVibration(boolean vibrate) { + mZoomRing.setVibration(vibrate); + } + + public void setThumbVisible(boolean thumbVisible) { + mZoomRing.setThumbVisible(thumbVisible); + } public void setThumbClockwiseBound(float angle) { - mZoomRing.setThumbClockwiseBound(angle >= 0 ? + mZoomRing.setThumbClockwiseBound(angle >= 0 ? (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : Integer.MIN_VALUE); } @@ -351,14 +392,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void setVisible(boolean visible) { - if (useOldZoom(mContext)) return; + if (!useThisZoom(mContext)) return; if (visible) { + if (mOwnerView.getWindowToken() == null) { + /* + * We need a window token to show ourselves, maybe the owner's + * window hasn't been created yet but it will have been by the + * time the looper is idle, so post the setVisible(true) call. + */ + if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { + mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); + } + return; + } + dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); } else { mPanner.stop(); } - + if (mIsZoomRingVisible == visible) { return; } @@ -368,40 +421,40 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (mContainerLayoutParams.token == null) { mContainerLayoutParams.token = mOwnerView.getWindowToken(); } - + mWindowManager.addView(mContainer, mContainerLayoutParams); - + if (mPostedVisibleInitializer == null) { mPostedVisibleInitializer = new Runnable() { public void run() { refreshPositioningVariables(); resetZoomRing(); - + // TODO: remove this 'update' and just center zoom ring before the // 'add', but need to make sure we have the width and height (which // 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); } } - }; + }; } - + mPanningArrows.setAnimation(null); - + mHandler.post(mPostedVisibleInitializer); - + // Handle configuration changes when visible mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); - + // Steal key/touches events from the owner mOwnerView.setOnKeyListener(this); mOwnerView.setOnTouchListener(this); mReleaseTouchListenerOnUp = false; - + } else { // Don't want to steal any more keys/touches mOwnerView.setOnKeyListener(null); @@ -415,45 +468,45 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // No longer care about configuration changes mContext.unregisterReceiver(mConfigurationChangedReceiver); - + mWindowManager.removeView(mContainer); mHandler.removeCallbacks(mPostedVisibleInitializer); - + 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) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); } - + private void resetZoomRing() { mScroller.abortAnimation(); - + mContainerLayoutParams.x = mCenteredContainerX; mContainerLayoutParams.y = mCenteredContainerY; - + // Reset the thumb mZoomRing.resetThumbAngle(); } @@ -461,13 +514,15 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, /** * Should be called by the client for each event belonging to the second tap * (the down, move, up, and cancel events). - * + * * @param event The event belonging to the second tap. * @return Whether the event was consumed. */ public boolean handleDoubleTapEvent(MotionEvent event) { - int action = event.getAction(); + if (!useThisZoom(mContext)) return false; + int action = event.getAction(); + // TODO: make sure this works well with the // ownerView.setOnTouchListener(this) instead of window receiving // touches @@ -475,19 +530,19 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mTouchMode = TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT; int x = (int) event.getX(); int y = (int) event.getY(); - + refreshPositioningVariables(); setVisible(true); centerPoint(x, y); - ensureZoomRingIsCentered(); - + ensureZoomRingIsCentered(); + // Tap drag mode stuff mTapDragStartX = x; mTapDragStartY = y; } else if (action == MotionEvent.ACTION_CANCEL) { mTouchMode = TOUCH_MODE_IDLE; - + } else { // action is move or up switch (mTouchMode) { case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: { @@ -503,29 +558,29 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, setTouchTargetView(mZoomRing); } return true; - + case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; break; } break; } - + case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: { switch (action) { case MotionEvent.ACTION_MOVE: giveTouchToZoomRing(event); return true; - + case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; - + /* * This is a power-user feature that only shows the * zoom while the user is performing the tap-drag. * That means once it is released, the zoom ring * should disappear. - */ + */ mZoomRing.setTapDragMode(false, (int) event.getX(), (int) event.getY()); dismissZoomRingDelayed(0); break; @@ -534,13 +589,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } } - + return true; } - + private void ensureZoomRingIsCentered() { LayoutParams lp = mContainerLayoutParams; - + if (lp.x != mCenteredContainerX || lp.y != mCenteredContainerY) { int width = mContainer.getWidth(); int height = mContainer.getHeight(); @@ -549,21 +604,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } } - + private void refreshPositioningVariables() { mZoomRingWidth = mZoomRing.getWidth(); mZoomRingHeight = mZoomRing.getHeight(); - + // Calculate the owner view's bounds mOwnerView.getGlobalVisibleRect(mOwnerViewBounds); - + // Get the center Gravity.apply(Gravity.CENTER, mContainer.getWidth(), mContainer.getHeight(), mOwnerViewBounds, mTempRect); mCenteredContainerX = mTempRect.left; mCenteredContainerY = mTempRect.top; } - + /** * Centers the point (in owner view's coordinates). */ @@ -572,7 +627,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mCallback.onCenter(x, y); } } - + private void giveTouchToZoomRing(MotionEvent event) { int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); @@ -580,11 +635,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, int y = rawY - mContainerLayoutParams.y - mZoomRing.getTop(); mZoomRing.handleTouch(event.getAction(), event.getEventTime(), x, y, rawX, rawY); } - + public void onZoomRingSetMovableHintVisible(boolean visible) { - setPanningArrowsVisible(visible); + setPanningArrowsVisible(visible); } - + public void onUserInteractionStarted() { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); } @@ -596,24 +651,62 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingMovingStarted() { mScroller.abortAnimation(); mTouchingEdgeStartTime = 0; + mMovingZoomRingOobX = 0; + mMovingZoomRingOobY = 0; if (mCallback != null) { mCallback.onBeginPan(); } } - + private void setPanningArrowsVisible(boolean visible) { mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation : mPanningArrowsExitAnimation); mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } - - public boolean onZoomRingMoved(int deltaX, int deltaY) { + + public boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY) { + + if (mMovingZoomRingOobX != 0) { + /* + * The finger has moved off the point where it originally touched + * the zidget. + */ + boolean wasOobLeft = mMovingZoomRingOobX < 0; + mMovingZoomRingOobX += deltaX; + if ((wasOobLeft && mMovingZoomRingOobX > 0) || + (!wasOobLeft && mMovingZoomRingOobX < 0)) { + /* + * Woot, the finger is back on the original point. Infact, it + * went PAST its original point, so take the amount it passed + * and use that as the delta to move the zoom ring. + */ + deltaX = mMovingZoomRingOobX; + // No longer out-of-bounds, reset + mMovingZoomRingOobX = 0; + } else { + // The finger is still not back, eat this movement + deltaX = 0; + } + } + + if (mMovingZoomRingOobY != 0) { + // See above for comments + boolean wasOobUp = mMovingZoomRingOobY < 0; + mMovingZoomRingOobY += deltaY; + if ((wasOobUp && mMovingZoomRingOobY > 0) || (!wasOobUp && mMovingZoomRingOobY < 0)) { + deltaY = mMovingZoomRingOobY; + mMovingZoomRingOobY = 0; + } else { + deltaY = 0; + } + } + WindowManager.LayoutParams lp = mContainerLayoutParams; Rect ownerBounds = mOwnerViewBounds; - + int zoomRingLeft = mZoomRing.getLeft(); int zoomRingTop = mZoomRing.getTop(); - + int newX = lp.x + deltaX; int newZoomRingX = newX + zoomRingLeft; newZoomRingX = (newZoomRingX <= ownerBounds.left) ? ownerBounds.left : @@ -627,19 +720,26 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, (newZoomRingY + mZoomRingHeight > ownerBounds.bottom) ? ownerBounds.bottom - mZoomRingHeight : newZoomRingY; lp.y = newZoomRingY - zoomRingTop; - + mWindowManager.updateViewLayout(mContainer, lp); - + // Check for pan boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; if (leftGap < MAX_PAN_GAP) { + if (leftGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { + // Future moves in this direction should be accumulated in mMovingZoomRingOobX + mMovingZoomRingOobX = deltaX / Math.abs(deltaX); + } if (shouldPan(leftGap)) { mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); if (rightGap < MAX_PAN_GAP) { + if (rightGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { + mMovingZoomRingOobX = deltaX / Math.abs(deltaX); + } if (shouldPan(rightGap)) { mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); } @@ -648,15 +748,21 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, horizontalPanning = false; } } - + int topGap = newZoomRingY - ownerBounds.top; if (topGap < MAX_PAN_GAP) { + if (topGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { + mMovingZoomRingOobY = deltaY / Math.abs(deltaY); + } if (shouldPan(topGap)) { mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); if (bottomGap < MAX_PAN_GAP) { + if (bottomGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { + mMovingZoomRingOobY = deltaY / Math.abs(deltaY); + } if (shouldPan(bottomGap)) { mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); } @@ -670,13 +776,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } } } - + return true; } - + private boolean shouldPan(int gap) { if (mPanningEnabledForThisInteraction) return true; - + if (gap < MAX_INITIATE_PAN_GAP) { long time = SystemClock.elapsedRealtime(); if (mTouchingEdgeStartTime != 0 && @@ -693,7 +799,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } return false; } - + public void onZoomRingMovingStopped() { mPanner.stop(); setPanningArrowsVisible(false); @@ -701,18 +807,18 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mCallback.onEndPan(); } } - + private int getStrengthFromGap(int gap) { return gap > MAX_PAN_GAP ? 0 : (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; } - + public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { mCallback.onBeginDrag(); } } - + public boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle) { if (mCallback != null) { int deltaZoomLevel = -numLevels; @@ -720,17 +826,17 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mZoomRingWidth / 2; int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() + mZoomRingHeight / 2; - + return mCallback.onDragZoom(deltaZoomLevel, globalZoomCenterX - mOwnerViewBounds.left, globalZoomCenterY - mOwnerViewBounds.top, (float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER, (float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER); } - + return false; } - + public void onZoomRingThumbDraggingStopped() { if (mCallback != null) { mCallback.onEndDrag(); @@ -740,35 +846,35 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void onZoomRingDismissed(boolean dismissImmediately) { if (dismissImmediately) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); - setVisible(false); + setVisible(false); } else { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } } - + public void onRingDown(int tickAngle, int touchAngle) { } - + public boolean onTouch(View v, MotionEvent event) { 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 + // 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); setTouchTargetView(null); mReleaseTouchListenerOnUp = false; } - + // Eat this event return true; } - + View targetView = mTouchTargetView; switch (action) { @@ -776,7 +882,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); setTouchTargetView(targetView); break; - + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setTouchTargetView(null); @@ -787,7 +893,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, // 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) @@ -796,32 +902,32 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, boolean retValue = targetView.dispatchTouchEvent(containerEvent); containerEvent.recycle(); return retValue; - + } else { if (action == MotionEvent.ACTION_DOWN) { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } - + return false; } } - + private void setTouchTargetView(View view) { mTouchTargetView = view; if (view != null) { view.getLocationInWindow(mTouchTargetLocationInWindow); } } - + /** * 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 + // 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; @@ -832,7 +938,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, 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; @@ -844,13 +950,13 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, !child.isClickable()) { continue; } - + child.getHitRect(frame); if (frame.contains(containerCoordsX, containerCoordsY)) { return child; } } - + return null; } @@ -861,34 +967,34 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, case KeyEvent.KEYCODE_DPAD_RIGHT: // Eat these return true; - + case KeyEvent.KEYCODE_DPAD_UP: 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); } - + return true; } - + return false; } private void onScrollerTick() { if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return; - + mContainerLayoutParams.x = mScroller.getCurrX(); mContainerLayoutParams.y = mScroller.getCurrY(); mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } - + private void onPostConfigurationChanged() { dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); refreshPositioningVariables(); @@ -908,7 +1014,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, * before. Furthermore, if the application does not have privilege to write * to the system settings, it will store this bit locally in a shared * preference. - * + * * @hide This should only be used by our main apps--browser, maps, and * gallery */ @@ -917,53 +1023,65 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } - + SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); if (sp.getInt(SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } - + if (sTutorialDialog != null && sTutorialDialog.isShowing()) { sTutorialDialog.dismiss(); } + + LayoutInflater layoutInflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + TextView textView = (TextView) layoutInflater.inflate( + com.android.internal.R.layout.alert_dialog_simple_text, null) + .findViewById(android.R.id.text1); + textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short); sTutorialDialog = new AlertDialog.Builder(context) - .setMessage( - com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short) + .setView(textView) .setIcon(0) .create(); - + Window window = sTutorialDialog.getWindow(); window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | + 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() { + + public static void finishZoomTutorial(Context context, boolean userNotified) { if (sTutorialDialog == null) return; - + sTutorialDialog.dismiss(); sTutorialDialog = null; - + // Record that they have seen the tutorial - try { - 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(); + if (userNotified) { + try { + Settings.System.putInt(context.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 = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); + sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); + } } } - + + public void finishZoomTutorial() { + finishZoomTutorial(mContext, true); + } + public void setPannerStartVelocity(float startVelocity) { mPanner.mStartVelocity = startVelocity; } @@ -983,27 +1101,27 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, private class Panner implements Runnable { private static final int RUN_DELAY = 15; private static final float STOP_SLOWDOWN = 0.8f; - + private final Handler mUiHandler = new Handler(); - + private int mVerticalStrength; private int mHorizontalStrength; private boolean mStopping; - + /** The time this current pan started. */ private long mStartTime; - + /** The time of the last callback to pan the map/browser/etc. */ private long mPreviousCallbackTime; - + // TODO Adjust to be DPI safe private float mStartVelocity = 135; private float mAcceleration = 160; private float mMaxVelocity = 1000; private int mStartAcceleratingDuration = 700; private float mVelocity; - + /** -100 (full left) to 0 (none) to 100 (full right) */ public void setHorizontalStrength(int horizontalStrength) { if (mHorizontalStrength == 0 && mVerticalStrength == 0 && horizontalStrength != 0) { @@ -1011,7 +1129,7 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } else if (mVerticalStrength == 0 && horizontalStrength == 0) { stop(); } - + mHorizontalStrength = horizontalStrength; mStopping = false; } @@ -1023,11 +1141,11 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, } else if (mHorizontalStrength == 0 && verticalStrength == 0) { stop(); } - - mVerticalStrength = verticalStrength; + + mVerticalStrength = verticalStrength; mStopping = false; } - + private void start() { mUiHandler.post(this); mPreviousCallbackTime = 0; @@ -1037,37 +1155,37 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, public void stop() { mStopping = true; } - + public void run() { if (mStopping) { mHorizontalStrength *= STOP_SLOWDOWN; mVerticalStrength *= STOP_SLOWDOWN; } - + if (mHorizontalStrength == 0 && mVerticalStrength == 0) { return; } - + boolean firstRun = mPreviousCallbackTime == 0; long curTime = SystemClock.elapsedRealtime(); int panAmount = getPanAmount(mPreviousCallbackTime, curTime); mPreviousCallbackTime = curTime; - + if (firstRun) { mStartTime = curTime; mVelocity = mStartVelocity; } else { int panX = panAmount * mHorizontalStrength / 100; int panY = panAmount * mVerticalStrength / 100; - + if (mCallback != null) { mCallback.onPan(panX, panY); } } - + mUiHandler.postDelayed(this, RUN_DELAY); } - + private int getPanAmount(long previousTime, long currentTime) { if (mVelocity > mMaxVelocity) { mVelocity = mMaxVelocity; @@ -1077,14 +1195,12 @@ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, mVelocity += (currentTime - previousTime) * mAcceleration / 1000; } } - + return (int) ((currentTime - previousTime) * mVelocity) / 1000; } } - - public interface OnZoomListener { void onBeginDrag(); boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, |